package WebService::EveOnline::Base;

our $VERSION = "0.61";

use LWP::UserAgent;
use HTTP::Request;
use XML::Simple;
use Data::Dumper;

use WebService::EveOnline::Cache;

use WebService::EveOnline::API::Character;
use WebService::EveOnline::API::Corporation;
use WebService::EveOnline::API::Skills;
use WebService::EveOnline::API::Transactions;
use WebService::EveOnline::API::Journal;
use WebService::EveOnline::API::Account;
use WebService::EveOnline::API::Map;

# U.G.L.Y. You ain't got no alibi (this is where we set up the API mappings, sort out the internal symbol conversion and set max cache times)
# max_cache overrides the cache time set in the default EVE webservice response XML (e.g. shorter for wallet, longer for bloodline which
# probably won't update every hour...)

our $API_MAP = {
    # Character
    skills            => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 900     },
    balance           => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 60      },
    race              => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 604800  },
    bloodline         => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 604800  },
    attributes        => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ],                      },
    enhancers         => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ],                      },
    gender            => { endpoint => 'char/CharacterSheet',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 604800  },
    training          => { endpoint => 'char/SkillInTraining',    params => [ [ 'character_id',    'characterID'   ] ],                      },
    accounts          => { endpoint => 'char/AccountBalance',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 60      },
    transactions      => { endpoint => 'char/WalletTransactions', params => [ 
                                                                              [ 'character_id',    'characterID'   ], 
                                                                              [ 'before_trans_id', 'beforeTransID' ], 
                                                                              [ 'account_key',     'accountKey'    ],  
                                                                                                                     ], max_cache => 3600    },
    kills             => { endpoint => 'char/Killlog',            params => [ [ 'character_id',    'characterID'   ] ],                      },
    orders            => { endpoint => 'char/MarketOrders',       params => [ [ 'character_id',    'characterID'   ] ],                      },
    assets            => { endpoint => 'char/AssetList',          params => [ [ 'character_id',    'characterID'   ] ],                      },

    # Corporation
    corp_accounts     => { endpoint => 'corp/AccountBalance',     params => [ [ 'character_id',    'characterID'   ] ], max_cache => 60      },
    corp_members      => { endpoint => 'corp/MemberTracking',     params => [ [ 'character_id',    'characterID'   ] ],                      },
    corp_assets       => { endpoint => 'corp/AssetList',          params => [ [ 'character_id',    'characterID'   ] ],                      },
    corp_sheet        => { endpoint => 'corp/CorporationSheet',   params => [ [ 'character_id',    'characterID'   ] ],                      },
    corp_transactions => { endpoint => 'corp/WalletTransactions', params => [ 
                                                                              [ 'character_id',    'characterID'   ], 
                                                                              [ 'before_trans_id', 'beforeTransID' ], 
                                                                              [ 'account_key',     'accountKey'    ],  
                                                                                                                     ], max_cache => 3600    },
    corp_kills        => { endpoint => 'corp/Killlog',            params => [ [ 'character_id',    'characterID'   ] ],                      },
    corp_orders       => { endpoint => 'corp/MarketOrders',       params => [ [ 'character_id',    'characterID'   ] ],                      },
    corp_baselist     => { endpoint => 'corp/StarbaseList',       params => [ [ 'character_id',    'characterID'   ] ],                      },
    corp_base         => { endpoint => 'corp/StarbaseDetail',     params => [ [ 'character_id',    'characterID'   ] ],                      },

    # Map
    map_jumps         => { endpoint => 'map/Jumps',               params => [ [ 'character_id',    'characterID'   ] ],                      },
    map_kills         => { endpoint => 'map/Kills',               params => [ [ 'character_id',    'characterID'   ] ],                      },
    map               => { endpoint => 'map/Sovereignty',         params => undef                                     ,                      },

    # Global/Misc
    character         => { endpoint => 'account/Characters',      params => undef,                                      max_cache => 3600    },
    all_skills        => { endpoint => 'eve/SkillTree',           params => undef,                                      max_cache => 86400   },
    all_reftypes      => { endpoint => 'eve/RefTypes',            params => undef,                                                           },
};

=head2 new

Called by WebService::EveOnline->new -- sets things up at the backend without cluttering things up.
Doesn't die if not passed an api_key/user_id combination, unlike the latter.

=cut

sub new {
    my ($class, $params) = @_;

    $params ||= {};
    $params->{cache_type}   ||= "SQLite";
    $params->{cache_user}   ||= "";
    $params->{cache_pass}   ||= "";
    $params->{cache_dbname} ||= ($^O =~ /MSWin/) ? "c:/windows/temp/webservice_eveonline.db" : "/tmp/webservice_eveonline.db";
    $params->{cache_init}   ||= "yes";
    $params->{cache_maxage} ||= (86400 * 7 * 4); # time (s) between cache rebuilds. 28 days, for now.
    
    my $evecache = WebService::EveOnline::Cache->new( { eve_user_id => $params->{user_id}, cache_type => $params->{cache_type}, cache_dbname => $params->{cache_dbname} } ) if $params->{cache_init} eq "yes";
    if ($evecache && $evecache->cache_age >= $params->{cache_maxage}) {
        $evecache->repopulate( { skills => call_api('all_skills'), map => call_api('map') } );
    } else {
        $evecache ||= WebService::EveOnline::Cache->new( { cache_type => "no_cache" } );
    }
    
    return bless({ _user_id => $params->{user_id}, _api_key => $params->{api_key}, _evecache => $evecache }, $class);
}

=head2 character, characters

Pull back character objects based on your API key -- see examples/show_characters 

Singlar and plural are provided so as to allow grammatically correct usage given
the appropriate context (they both do exactly the same thing under the hood and
can be used interchangeably -- handy for contractors... ;-) )

=cut

sub characters {
    return WebService::EveOnline::API::Character->new(@_);
}

sub character {
    return WebService::EveOnline::API::Character->new(@_);
}

=head2 corporation

Pull back a corporation information object -- use on a character object for best effect.
See examples/show_corporation

=cut

sub corporation {
    return WebService::EveOnline::API::Corporation->new(@_);
}

=head2 skill, skills

Pull back skill objects on a character. See examples/skills_overview for more
details.

Singlar and plural are provided so as to allow grammatically correct usage given
the appropriate context (they both do exactly the same thing under the hood and
can be used interchangeably).

=cut

sub skill {
    return WebService::EveOnline::API::Skills->new(@_);
}

sub skills {
    return WebService::EveOnline::API::Skills->new(@_);
}

=head2 transaction, transactions

Returns transaction objects for a particular character/corporation. Singular/plural as above;
See examples/show_transactions for more details.

=cut

sub transaction {
    return WebService::EveOnline::API::Transactions->new(@_);
}

sub transactions {
    return WebService::EveOnline::API::Transactions->new(@_);
}

=head2 journal

Placeholder, for the moment. 

=cut

sub journal {
    return WebService::EveOnline::API::Journal->new(@_);
}

=head2 account, accounts

Return detailed account objects for a particular character, including corporate
account info. The first member of the array ALWAYS returns the selected character's 
personal account object -- subsequent accounts are from the corporation the
character belongs to. See examples/show_character for an example of how to use this.

=cut

sub account {
    return WebService::EveOnline::API::Account->new(@_);    
}

sub accounts {
    return WebService::EveOnline::API::Account->new(@_);    
}

=head2 map

Another placeholder.

=cut

sub map {
    return WebService::EveOnline::API::Map->new(@_);
}


=head2 $eve->user_id

Returns the current user_id.

=cut

sub user_id {
    my ($self, $user_id) = @_;
    $self->{_user_id} = $user_id if $user_id;
    return $self->{_user_id};
}

=head2 $eve->api_key

Returns the current api_key.

=cut

sub api_key {
    my ($self, $api_key) = @_;
    $self->{_api_key} = $api_key if $api_key;
    return $self->{_api_key};
}

=head2 $eve->call_api(<command>, <params>)

Call the Eve API and retrieve the results. Look in the cache first. Cache results according to API map settings.

=cut

sub call_api {
    my ($self, $command, $params, $base) = @_;
    
    my $auth = { user_id => "", api_key => "" };

    if (ref($base)) {
        $auth = { user_id => $base->user_id, api_key => $base->api_key };
    } else {
        $command = $self;
    }

    if ( defined($API_MAP->{$command}) ) {
        my $cache = ref($self) ? $self->{_evecache} : $base->{_evecache};
        
        my $gen_params = _gen_params($self, $API_MAP->{$command}->{params}, $params);
        
        my $cached_response = $cache->retrieve( { command => "$command", params => $gen_params } ) if ref($cache);
        return $cached_response if $cached_response;
        
        my $ua = LWP::UserAgent->new;
        $ua->agent("$WebService::EveOnline::AGENT/$WebService::EveOnline::VERSION");

        my $req = HTTP::Request->new( POST => $WebService::EveOnline::EVE_API . $API_MAP->{$command}->{endpoint} . '.xml.aspx' );
        $req->content_type("application/x-www-form-urlencoded");

        my $content = 'userid=' . $auth->{user_id} . '&apikey=' . $auth->{api_key} . $gen_params;
        
        $req->content($content) ;
    
        my $res = $ua->request($req);
        if ($res->is_success) {
            my $xs = XML::Simple->new();
            my $xml = $res->content;

            my $pre = $xs->XMLin($xml);
            my $data = {};
            my $in_error_state = undef;

            # print out any error content if it's set.
            if ($pre->{error}) {
                # error 206 is returned on characters without corp permissions. ignore. FIXME: nasty hack
                if ($pre->{error}->{code} != "206") {
                    $in_error_state = 1;
                    $data->{error} = "EVE API Error: " . $pre->{error}->{content} . " (" . $pre->{error}->{code} . ")";
                }
            }

            # at the moment, we deal in hashrefs. one day, these will be objects (like everything else will be ;-P)
            unless ($in_error_state) {
                if ($command eq "character") {
                    $data = $pre->{result}->{rowset}->{row};
                } elsif ($command eq "skills") {
                    $data->{skills} = $pre->{result}->{rowset}->{row} if $pre->{result}->{rowset}->{row};
                } elsif ($command eq "attributes") {
                    $data = $pre->{result}->{attributes};
                } elsif ($command eq "enhancers") {
                    $data = $pre->{result}->{attributeEnhancers};
                } elsif ($command eq "gender") {
                    $data = $pre->{result};
                } elsif ($command eq "race") {
                    $data = $pre->{result};
                } elsif ($command eq "bloodline") {
                    $data = $pre->{result};
                } elsif ($command eq "balance") {
                    $data = $pre->{result};
                } elsif ($command eq "training") {
                    $data = $pre->{result};             
                } elsif ($command eq "kills") {
                    $data = $pre->{result}->{rowset}->{row};             
                } elsif ($command eq "orders") {
                    $data = $pre->{result}->{rowset}->{row};             
                } elsif ($command eq "corp_kills") {
                    $data = $pre->{result}->{rowset}->{row};             
                } elsif ($command eq "corp_members") {
                    $data = $pre->{result}->{rowset}->{row};             
                } elsif ($command eq "corp_orders") {
                    $data = $pre->{result}->{rowset}->{row};             
                } elsif ($command eq "assets") {
                    $data = $pre->{result}->{rowset}->{row};             
                } elsif ($command eq "transactions") {
                    $data->{transactions} = $pre->{result}->{rowset}->{row} if $pre->{result}->{rowset}->{row};
                } elsif ($command =~ /accounts/) {
                    my $acc = $pre->{result}->{rowset}->{row};
                    $data->{accounts} = ref($acc) eq "HASH" ? [ $acc ] : $acc; 
                } else {
                    $data = $pre;
                    return $data;
                }
            }

            $data->{_status} ||= "ok";
            $data->{_xml} = $xml;
            $data->{_parsed_as} = $pre;

            my $stripped_data = undef;
            
            unless ($WebService::EveOnline::DEBUG_MODE) {
                $stripped_data = {};
                foreach my $strip_debug (keys %{$data}) {
                    next if $strip_debug =~ /^_/; # skip meta keys
                    $stripped_data->{$strip_debug} = $data->{$strip_debug};
                }
            }


            if ($cache && ($stripped_data || $data) && !$in_error_state) {
                # error results are not cached
                return $cache->store( { command => $command, obj => $self, data => $stripped_data || $data, params => $gen_params, cache_until => $pre->{cachedUntil}, max_cache => $API_MAP->{$command}->{max_cache}  } );
            } elsif ($in_error_state) {
                warn $data->{error} . "\n";
                return undef; # better error handling is required...;
            } else {
                return $stripped_data || $data;
            }
        } else {
            return { _status => "error", message => $res->status_line, _raw => undef };     
        }
    } else {
        return { _status => "error", message => "Bad command", _raw => undef };     
    }
    
}

=head2 $character->before_trans_id

Set to return transactions older than a particular trans id for character/corp transactions.

=cut

sub before_trans_id {
    my ($self, $before_trans_id) = @_;
    $self->{_before_trans_id} = $before_trans_id if $before_trans_id;
    return $self->{_before_trans_id} || undef;
}

=head2 id

This will not return anything useful on the base class; call id on characters, accounts, transactions, etc.
where appropriate.

=cut

sub id {
    return undef;
}

sub _gen_params {
    my ($self, $keys, $passed) = @_;
    return "" unless defined $keys;

    my @kvp = ();
    foreach my $param (@{$keys}) {
        my ($intkey, $evekey) = @{$param};
        if ($self->can($intkey)) {
            push(@kvp, "$evekey=" . ($self->$intkey || $passed->{$intkey})) if ($self->$intkey || $passed->{$intkey});
        } else {
            push(@kvp, "$evekey=" . ($passed->{$evekey} || $passed->{$intkey})) if ($passed->{$evekey} || $passed->{$intkey});
        }
    }

    return '&' . (join('&', @kvp));
}

1;
