The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Webservice::InterMine::ResultRow;

=head1 NAME

Webservice::InterMine::ResultRow

a class for providing a unified hash and array reference style interface
for result rows.

=head1 SYNOPSIS

    # Rows should be requested as results for a query.

    use Webservice::InterMine;

    my $query = resultset("Gene")->select("symbol", "proteins.name");

    while (my $row = <$query>) {
        # The following are equivalent:
        print $row->{symbol}, $row->{proteins.name}, "\n";
        print $row->[0], $row->[1];
        print @$row;
    }

=head1 DESCRIPTION

This class exists to unify access to row based data regardless 
of whether you want to use key or column index based lookups for the 
data. This means you do not have to decide in advance which 
mechanism you want to use for retrieving data from a webservice.

=cut

use strict;
use warnings;

use Scalar::Util qw/looks_like_number/;
use Class::InsideOut qw(private readonly new id);

use overload (
    '""' => 'to_string',
    '@{}' => 'to_aref',
    '%{}' => 'to_href',
    fallback => 1,
);

readonly cells => my %cells;
private views => my %views;
private root => my %root;
private aref => my %aref;
private href => my %href;
private key_to_index => my %key_to_index;

=head1 METHODS

=head2 keys

Return the keys for this row, ie. the columns selected for out-put.

=cut

sub keys {
    my $self = shift;
    return @{$views{id $self}};
}

sub _head_and_tail {
    my $in = shift;
    return split(/\./, $in, 2);
}

=head2 to_string

Provides a readable representation of the data in this row.

=cut

sub to_string {
    my $self = shift;
    my $id = id $self;
    my $string = $root{$id};
    unless ($string) {
        ($string) = _head_and_tail($views{$id}[0]);
        $root{$id} = $string;
    }
    for my $view ($self->keys) {
        my (undef, $headless) = _head_and_tail($view);
        my $value = $self->get_value($view);
        $value = 'undef' unless (defined $value);
        $string .= "\t$headless: $value"
    }
    return $string;
}

=head2 get_value($key | $index)

If a string is provided, than a hash-style key-lookup will return
the value for the matching column, and if an integer is provided, then 
the appropriate value will be selected via a array based lookup.

=cut

sub get_value {
    my ($self, $idx) = @_;
    unless (looks_like_number($idx)) {
        $idx = $self->_index_for($idx);
    } 
    my $len = @{ $self->cells };
    if ($idx <= -$len || $idx >= $len) {
        # We don't want the default behaviour, 
        # because we want "not in array" and "undef" to 
        # be distinct.
        die "Index Error: $idx out of range";
    }
    my $cell = $self->cells->[$idx];
    return ((ref $cell eq "HASH") ? $cell->{value} : $cell);
}

=head2 to_aref

Return this row as an array-reference. This returns a copy
of the data in the row.

=cut

sub to_aref { 
    my $self = shift; 
    my $id = id $self;
    if (my $aref = $aref{$id}) {
        return $aref;
    } else {
        my $aref = [map {ref($_) eq 'HASH' ? $_->{value} : $_} @{$self->cells}];
        return $aref{$id} = $aref;
    }
}

=head2 to_href($style)

return this row as a hash-reference. This returns a copy of the
data in the row.

The three available styles are "full", "short" and "long", depending
on whether you want "long" keys ("Gene.proteins.name") or "short" ones
("proteins.name"). "full" gives both, and is the default.

=cut

sub to_href { 
    my $self = shift; 
    my $style = lc(shift || "full");
    my $id = id $self;
    my $href;
    unless ($href = $href{$id}) {
        $href = {map {$_ => $self->get_value($_)} $self->_available_keys};
        $href{$id} = $href;
    }
    if ($style eq "short") {
        return {map {substr($_, index($_, ".") + 1) => $self->get_value($_)} $self->keys};
    } elsif ($style eq "long") {
        return {map {$_ => $self->get_value($_)} $self->keys};
    } else {
        return $href;
    }
}

## Internal logic ##

sub _available_keys {
    my $self = shift;
    my $h = $key_to_index{id $self} || $self->_build_key_to_index_map;
    return CORE::keys %$h;
}

sub _index_for {
    my $self = shift;
    my $key  = shift;
    my $h = $key_to_index{id $self} || $self->_build_key_to_index_map;
    return $h->{$key};
}

sub _build_key_to_index_map {
    my $self = shift;
    my $h = {};
    my $c = 0;
    for my $v ($self->keys) {
        my (undef, $headless) = _head_and_tail($v);
        $h->{$v} = $c;
        $h->{$headless} = $c;
        $c++;
    }
    return $key_to_index{id $self} = $h
}

1;

__END__

=head1 SEE ALSO

=over 4

=item * L<Webservice::InterMine::Cookbook> - A guide to using the Webservice::InterMine Perl API

=item * L<Webservice::InterMine::Query>

=item * L<Webservice::InterMine>

=item * L<Webservice::InterMine::Service>

=back

=head1 AUTHOR

Alex Kalderimis C<dev@intermine.org>

=head1 BUGS

Please report any bugs or feature requests to C<dev@intermine.org>.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Webservice::InterMine::ResultObject

You can also look for information at:

=over 4

=item * InterMine

L<http://www.intermine.org>

=item * Documentation

L<http://www.intermine.org/perlapi>

=back

=head1 COPYRIGHT AND LICENSE

Copyright 2006 - 2011 FlyMine, all rights reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.