######################################################################
package Net::Amazon::Request;
######################################################################

use Log::Log4perl qw(:easy get_logger);
use Net::Amazon::Validate::Type;
use Net::Amazon::Validate::ItemSearch;

use Data::Dumper;

use warnings;
use strict;
use constant DEFAULT_MODE          => 'books';
use constant DEFAULT_TYPE          => 'Large';
use constant DEFAULT_PAGE_COUNT    => 1;
use constant DEFAULT_FORMAT        => 'xml';
use constant PAGE_NOT_VALID        => qw(TextStream);

# Attempt to provide backward compatability for AWS3 types.
use constant AWS3_VALID_TYPES_MAP => {
	'heavy' => 'Large',
	'lite'  => 'Medium',
};

# Each key represents the REST operation used to execute the action.
use constant SEARCH_TYPE_OPERATION_MAP => {
    Actor        => 'ItemSearch',
    Artist       => 'ItemSearch',
    Author       => 'ItemSearch',
    ASIN         => 'ItemLookup',
    Blended      => 'ItemSearch',
    BrowseNode   => 'ItemSearch',
    Director     => 'ItemSearch',
    EAN          => 'ItemLookup',
    Exchange     => 'SellerListingLookup',
    ISBN         => 'ItemLookup',
    Keyword      => 'ItemSearch',
    # XXX: are there really two types?!?
    Keywords     => 'ItemSearch',
    Manufacturer => 'ItemSearch',
    MusicLabel   => 'ItemSearch',
    Power        => 'ItemSearch',
    Publisher    => 'ItemSearch',
    Seller       => 'SellerListingSearch',
    Similar      => 'SimilarityLookup',
    TextStream   => 'ItemSearch',
    Title        => 'ItemSearch',
    UPC          => 'ItemLookup',
    Wishlist     => 'ListLookup',
};

# if it isn't defined it defaults to salesrank
use constant DEFAULT_SORT_CRITERIA_MAP => {
    Wishlist     => 'DateAdded',
    Blended      => '',
    Seller       => '',
    Exchange     => '',
};

# if it isn't defined it defaults to ItemPage
use constant DEFAULT_ITEM_PAGE_MAP => {
    Seller   => 'ListingPage',
    Wishlist => 'ProductPage',
};

our $AMZN_XML_URL  = 'http://webservices.amazon.com/onca/xml?Service=AWSECommerceService';

##################################################
sub amzn_xml_url {
##################################################
    return $AMZN_XML_URL;
}

##################################################
sub new {
##################################################
    my($class, %options) = @_;

    my ($operation) = $class =~ m/([^:]+)$/;

    my $self = {
        Operation  => SEARCH_TYPE_OPERATION_MAP->{$operation},
        %options,
    };

    $self->{page} = DEFAULT_PAGE_COUNT unless exists $self->{page};

    # TextStream doesn't allow a page (ItemPage) parameter
    delete $self->{page} if grep{$operation eq $_} (PAGE_NOT_VALID);

    # salesrank isn't a valid sort criteria for all operations
    if (! exists $self->{sort}) {
        my $sort = (defined DEFAULT_SORT_CRITERIA_MAP->{$operation}) 
            ? DEFAULT_SORT_CRITERIA_MAP->{$operation} : 'salesrank';
        $self->{sort} = $sort if length($sort);
    }

    my $valid = Net::Amazon::Validate::Type::factory(operation => $self->{Operation});

    # There is no initial default type (ResponseGroup) defined, 
    # if there is, then attempt to map the AWS3 type to the
    # AWS4 type.
    if ($self->{type}) {
        if ( ref $self->{type} eq 'ARRAY' ) {
            my @types;
            for (@{$self->{type}}) {
                push @types, _get_valid_response_group($_, $valid);
            }
            $self->{type} = join(',', @types);
        } else {
            $self->{type} = _get_valid_response_group($self->{type}, $valid);
        }
    } 
    # If no type was defined then try to default to Large, which is a good
    # all around response group.  If Large is not a valid response group
    # let Amazon pick.
    else {
        eval { $valid->ResponseGroup(DEFAULT_TYPE) };
        $self->{type} = DEFAULT_TYPE unless $@;
    }

    my $item_page = (defined DEFAULT_ITEM_PAGE_MAP->{$operation}) 
        ? DEFAULT_ITEM_PAGE_MAP->{$operation} : 'ItemPage';
    
    __PACKAGE__->_convert_option($self, 'page', $item_page);
    __PACKAGE__->_convert_option($self, 'sort', 'Sort');
    __PACKAGE__->_convert_option($self, 'type', 'ResponseGroup') if defined $self->{type};

    # Convert all of the normal user input into Amazon's expected input.  Do it
    # here to allow a user to narrow down there based on any field that is valid
    # for a search operation.
    #
    # One could add all of the different qualifiers for an ItemSearch for free.
    if (SEARCH_TYPE_OPERATION_MAP->{$operation} eq 'ItemSearch' ) {
        for (keys %{(SEARCH_TYPE_OPERATION_MAP)}) {
            __PACKAGE__->_convert_option($self, lc($_), $_) if defined $self->{lc($_)};
        }
    }

    bless $self, $class;
}

##################################################
sub page {
##################################################
    my($self) = @_;
    return $self->{$self->_page_type};
}

##################################################
sub params {
##################################################
    my ($self, %options) = @_;

    my $class = ref $self;
    my ($operation) = $class =~ m/([^:]+)$/;

    unless (grep{$operation eq $_} (PAGE_NOT_VALID)) {
        my $type = $self->_page_type;
        $self->{$type} = $options{page};
    }

    return(%$self);
}

##################################################
# Figure out the Response class to a given Request
# class. To be used by sub classes.
##################################################
sub response_class {
##################################################
    my($self) = @_;

    my $response_class = ref($self);
    $response_class =~ s/Request/Response/;
    return $response_class;
}

##
## 'PRIVATE' METHODS
##

# A subroutine (not a class method), to map a response group
# to from AWS3 to AWS4, or validate that a response group
# is valid for AWS4.
sub _get_valid_response_group {
    my ($response_group, $valid) = @_;

    if (defined AWS3_VALID_TYPES_MAP->{$response_group}) {
        return AWS3_VALID_TYPES_MAP->{$response_group};
    } elsif ($valid->ResponseGroup($response_group)) {
        return $response_group;
    }

    # never reached, valid-> will die if the response group
    # is not valid for AWS4.
    return undef;
}

# CLASS->_convert_option( OPTIONS, ORIGINAL, TARGET [, CALLBACK] )
#
# Takes a reference to a hash of OPTIONS and renames the
# ORIGINAL key name to the TARGET key name. If the optional
# CALLBACK subroutine reference is defined, that subroutine
# is invoked with two arguments:
#
#     CALLBACK->( OPTIONS, TARGET )
#
# The result of the CALLBACK's execution is then returned to
# the caller. No assumptions are made about what the CALLBACK
# should return (or even *if* is should return)--that's the
# caller's responsibility.
#
# Returns 1 in the absensence of a CALLBACK.
#
sub _convert_option {
    my ($class, $options, $original, $target, $callback) = @_;

    if ( exists $options->{$original} ) {
        $options->{$target} = $options->{$original};
        delete $options->{$original};
    }

    return 1 unless ( $callback );
    
    # The key name is explicitly passed-in so that the caller doesn't
    # have think "Hrmm..  now which key am I working on, the original
    # or the target key?" Confusion is bad.
    return $callback->($options, $target);
}

# CLASS->_assert_options_defined( OPTIONS, KEYS )
#
# Takes a reference to a hash of OPTIONS and a list of
# one or more KEYS. Tests to see if each key in KEYS
# has a defined value. Calls die() upon the first
# missing key. Otherwise, returns undef.
#
sub _assert_options_defined {
    my ($class, $options, @keys) = @_;
    
    foreach my $key ( @keys ) {
        die "Mandatory parameter '$key' not defined"
            unless ( defined $options->{$key} );
    }
}

# CLASS->_option_or_default( OPTIONS, DEFAULT, USER )
#
# Takes a list of options, a default option, and a 
# possibly supplied user option.  If the user option
# is defined, it is verified that the option is valid.
# If no user option is supplied, the default option is
# used.
sub _option_or_default {
    my ($self, $options, $default, $user) = @_;
#     if(defined $user) {
#         unless(grep {$user eq $_} @$options) {
#            die "User supplied value, $user, is not a valid option" 
#         }
#         return $user;
#     }
    return $default;
}

# CLASS->_itemsearch_factory()
#
# Create an instance of an ItemSearch validator based on the
# Request class.  This class is used to validate user input
# against valid options for a given mode, and the type of 
# Request.
sub _itemsearch_factory {
    my($self) = @_;

    my $request_class = ref($self);
    my $request_type = (split(/::/, $request_class))[-1];

    # XXX: I'm not sure what to do here.  The ItemSearch validate class
    # is called Keywords, but the Request/Response class is called
    # Keyword.  For now I'm going to special case Keywords to map
    # to Keyword.
    $request_type = 'Keywords' if $request_type eq 'Keyword'; 

    return Net::Amazon::Validate::ItemSearch::factory(search_index => $request_type); 
}

sub _convert_itemsearch {
    my($self) = @_;

    my $is = $self->_itemsearch_factory();
    $self->{mode} = $is->user_or_default($self->{mode});

    __PACKAGE__->_convert_option($self, 'mode', 'SearchIndex');
}

sub _page_type {
    my ($self, %options) = @_;

    my $class = ref $self;
    my ($operation) = $class =~ m/([^:]+)$/;

    my $type = (defined DEFAULT_ITEM_PAGE_MAP->{$operation}) 
            ? DEFAULT_ITEM_PAGE_MAP->{$operation} : 'ItemPage';

    return $type;
}


1;

__END__

=head1 NAME

Net::Amazon::Request - Baseclass for requests to Amazon's web service

=head1 SYNOPSIS

    my $req = Net::Amazon::Request::XXX->new(
                     [ type  => 'Large', ]
                     [ page  => $start_page, ]
                     [ mode  => $mode, ]
                     [ offer => 'All', ]
                     [ sort => $sort_type, ]
    );

=head1 DESCRIPTION

Don't use this class directly, use derived classes 
(like C<Net::Amazon::Request::ASIN>, C<Net::Amazon::Request::Wishlist>
etc.) instead to specify the type of request and its parameters.

However, there's a bunch of parameters to the constructor
that all request types have in common, here they are:

=over 4

=item type

Defaults to C<Large>, but can also be set to C<Medium>, or C<Small>.

=over 8

=item Large

The C<Large> type provides everything in C<Medium> as well as music track
information, customer reviews, similar products, offers, and accessory data,
i.e. the kitchen sink.

=item Medium

The C<Medium> type provides everything in C<Small> as well as sales rank,
editorial reviews, and image URLs.

=item Small

The C<Small> type provies ASIN, product title, creator (author, artist, etc.),
product group, URL, and manufacturer.

=back

=item mode

Defaults to C<books>, but can be set to other catalog values.

=item page

Defaults to C<1>, but can be set to a different number to 
start with a different result page. Used in conjunction with the
C<max_pages> parameter of the C<Net::Amazon> object. C<page> is the
offset, C<max_pages> is the maximum number of pages pulled in starting
at C<page>.

=item sort

Defaults to C<salesrank>, but search results can be sorted in various
ways, depending on the type of product returned by the search.  Search
results may be sorted by the following criteria:

=over 8

=item *
Featured Items                                                           

=item *
Bestselling                                                              

=item *
Alphabetical (A-Z and Z-A)                                               

=item *
Price (High to Low and Low to High)                                      

=item *
Publication or Release Date                                              

=item *
Manufacturer                                                             

=item *
Average Customer Review                                                  

=item *
Artist Name                                   

=back

Consult L<Net::Amazon::Request::Sort> for details.

=item offer

To receive values for the fields
C<CollectibleCount>, C<NumberOfOfferings>, C<UsedCount>, 
specify C<offer =E<gt> "All">.

=back

=head1 SEE ALSO

=head1 AUTHOR

Mike Schilli, E<lt>m@perlmeister.comE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright 2003 by Mike Schilli E<lt>m@perlmeister.comE<gt>

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

=cut
