The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Games::Lacuna::Task::Action::Trade;

use 5.010;
our $VERSION = $Games::Lacuna::Task::VERSION;

use Moose;
extends qw(Games::Lacuna::Task::Action);
with qw(Games::Lacuna::Task::Role::Ships
    Games::Lacuna::Task::Role::PlanetRun);

use Games::Lacuna::Task::Utils qw(parse_ship_type);

has 'trades' => (
    is              => 'rw',
    isa             => 'HashRef',
    required        => 1,
    documentation   => 'Automatic trades per planet [Required in config]',
);

=pod

Usually you will need to set up automatic trades in your config file to 
use this action. The trade will only be created if you have the needed goods
on stock. Ships that are not on stock will be built.

trade:
  trades:
    "[PLANET NAME OR ID]":
      -
        ask: [ESSENTIA ASKING]
        offers:
          -
            class: "[ship|glyph|resource|plan|prisoner]"
            type: "[NAME OF ITEM]"
            quantity: [QUANTITY]
            level: [PLAN LEVEL]
            extra_build_level: [PLAN EXTRA BUILD LEVEL]
          -
            ...
      -
        ...

Some example configurations:

trade:
  trades:
    "Home Sweet Home":
      -
        ask: 3
        offers:
          -
            class: "ship"
            type: "Galleon"
            quantity: 3
      -
        ask: 10
        offers:
          -
            class: "plan"
            type: "Geo Thermal Vent"
            level: 1
            extra_build_level: 1
          -
            class: "plan"
            type: "Vulcano"
            level: 1
          -
            class: "plan"
            type: "Natural Spring"
            level: 1
      -
        ask: 0.5
        offers:
          -
            class: "resouce"
            type: "trona"
            quantity: 100000
          -
            class: "resouce"
            type: "sulphur"
            quantity: 100000

=cut

sub description {
    return q[Add recurring trades to Trade Ministry];
}

sub process_planet {
    my ($self,$planet_stats) = @_;
    
    return
        unless defined $self->trades->{$planet_stats->{name}}
        || defined $self->trades->{$planet_stats->{id}};
        
    # Get trade ministry
    my $tradeministry = $self->find_building($planet_stats->{id},'Trade');
    return 
        unless $tradeministry;
    
    # Get trade ministry
    my $tradeministry_object = $self->build_object($tradeministry);
    
    my $trades = $self->trades->{$planet_stats->{name}} || $self->trades->{$planet_stats->{id}};
    
    # Check if we have trades
    return 
        unless scalar @{$trades};
    
    # Get current trade
    my $trade_data = $self->paged_request(
        object  => $tradeministry_object,
        method  => 'view_my_market',
        total   => 'trade_count',
        data    => 'trades',
    )->{trades};
    
    my @current_trades = _trade_serialize_response($trade_data);
    
    return
        if scalar @current_trades >= $tradeministry->{level};
    
    my ($stored_resources,$stored_plans,$stored_glyphs,);
    
    # Loop all trades
    TRADE:
    foreach my $trade (@{$trades}) {
        my @offer_data;
        my $trade_complete = 1;
        my $trade_identifier;
        my %trade_identifier_parts;
        
        # Check offers
        unless (defined $trade->{offers}
            && ref $trade->{offers} eq 'ARRAY') {
            $self->log('error','Invalid trade setting: Offers missing or invalid (%s)',$trade->{offers});
            next TRADE;
        }
        unless (defined $trade->{ask}
            && $trade->{ask} =~ m/^\d+(\.\d)?$/
            && $trade->{ask} > 0) {
            $self->log('error','Invalid trade setting: Ask missing or invalid (%s)',$trade->{ask});
            next TRADE;
        }
        
        # Build trade identifier
        foreach my $offer (@{$trade->{offers}}) {
            unless (defined $offer->{type}) {
                $self->log('error','Invalid trade setting: Ask missing or invalid (%s)',$offer->{ask});
                next TRADE;
            }
            $offer->{quantity} ||= 1;
            my $trade_identifier_part;
            if ($offer->{class} eq 'plan') {
                $offer->{level} //= 1;
                $offer->{extra_build_level} //= 0;
                $trade_identifier_part = $offer->{class}.':'.$offer->{type}.':'.$offer->{level};
                $trade_identifier_part .= '+'.$offer->{extra_build_level}
                    if $offer->{extra_build_level} > 0;
            } elsif ($offer->{class} eq 'ship') {
                $trade_identifier_part = $offer->{class}.':'.parse_ship_type($offer->{type});
            } else {
                $trade_identifier_part = $offer->{class}.':'.lc($offer->{type});
            }
            $trade_identifier_part = lc($trade_identifier_part);
            $trade_identifier_parts{$trade_identifier_part} = $offer->{quantity};
        }
        
        $trade_identifier = _trade_serialize($trade->{ask},%trade_identifier_parts);
        
        next TRADE
            if $trade_identifier ~~ \@current_trades;
        
        # Check offer items
        foreach my $offer (@{$trade->{offers}}) {
            
            given ($offer->{class}) {
                when('ship') {
                    my @avaliable_ships = $self->get_ships(
                        planet          => $planet_stats,
                        quantity        => $offer->{quantity},
                        type            => $offer->{type},
                        name_prefix     => 'Trade',
                    );
                    
                    if (scalar @avaliable_ships == $offer->{quantity}) {
                        foreach my $ship (@avaliable_ships) {
                            push (@offer_data,{
                                "type"      => "ship",
                                "ship_id"   => $ship,
                            });
                        }
                    } else {
                        $trade_complete = 0;
                    }
                }
                when ('plan') {
                    $stored_plans ||= $self->request(
                        object  => $tradeministry_object,
                        method  => 'get_plans',
                    )->{plans};
                    
                    my $needed_quantity = $offer->{quantity};
                    PLAN:
                    foreach my $plan (@{$stored_plans}) {
                        if (lc($plan->{name}) eq lc($offer->{type})
                            && $plan->{level} == $offer->{level}
                            && $plan->{extra_build_level} == $offer->{extra_build_level}) {
                            push (@offer_data,{
                                "type"      => "plan",
                                "plan_id"  => $plan->{id},
                            });
                            $needed_quantity --;
                            last PLAN
                                if $needed_quantity == 0;
                        }
                    }
                    $trade_complete = 0
                        unless ($needed_quantity == 0);
                }
                when ('glyph') {
                    $stored_glyphs ||= $self->request(
                        object  => $tradeministry_object,
                        method  => 'get_glyphs',
                    )->{glyphs};
                    
                    my $needed_quantity = $offer->{quantity};
                    GLYPH:
                    foreach my $glyph (@{$stored_glyphs}) {
                        if (lc($glyph->{type}) eq lc($offer->{type})) {
                            push (@offer_data,{
                                "type"      => "glyph",
                                "glyph_id"  => $glyph->{id},
                            });
                            $needed_quantity --;
                            last GLYPH
                                if $needed_quantity == 0;
                        }
                    }
                    $trade_complete = 0
                        unless ($needed_quantity == 0);
                }
                when ('resource') {
                    $stored_resources ||= $self->request(
                        object  => $tradeministry_object,
                        method  => 'get_stored_resources',
                    )->{resources};
                    
                    unless (defined $stored_resources->{$offer->{type}}) {
                        $self->log('error','Invalid trade setting: Unknown resource type (%s)',$trade->{type});
                        next TRADE;
                    }
                    
                    if ($stored_resources->{$offer->{type}} > $offer->{quantity}) {
                        push (@offer_data,{
                            "type"      => $offer->{type},
                            "quantity"  => $offer->{quantity},
                        });
                    } else {
                        $trade_complete = 0;
                    }
                }
                when ('prisoner') {
                    $self->log('warn','Prisoner trade class not implemented yet');
                }
                default {
                    $self->log('error','Invalid trade setting: Unknown offer class (%s)',$_);
                    next TRADE;
                }
            }
        }
        
        # Add trade to market
        if ($trade_complete) {
            
            # Get trade ship
            my $trade_ships = $self->trade_ships($planet_stats->{id},\@offer_data);
            my @trade_ships = keys %{$trade_ships};
            
            next TRADE
                unless scalar @trade_ships == 1;
            
            my $response = $self->request(
                object  => $tradeministry_object,
                method  => 'add_to_market',
                params  => [ \@offer_data, $trade->{ask}, { ship_id => $trade_ships[0] } ]
            );
            $self->log('notice','Adding trade on %s',$planet_stats->{name});
        }
    }
}

sub _trade_serialize {
    my ($ask,%offer) = @_;
    
    my @trade_identifier_parts = 
        map { lc($_).'='.$offer{$_} }
        grep { $offer{$_} > 0 }
        sort
        keys %offer;
        
    push(@trade_identifier_parts,'ask='.sprintf('%.1f',$ask));
    
    return join(';',@trade_identifier_parts);
}

sub _trade_serialize_response {
    my ($trades) = @_;
    
    my @trade_identifiers;
    
    foreach my $trade (@{$trades}) {
        my %trade_serialize;
        foreach my $offer (@{$trade->{offer}}) {
            my ($moniker,$quantity);
            given ($offer) {
                when (/^(?<quantity>[0-9,]+)\s(?<type>\w+)$/) {
                    $moniker = 'resource:'.$+{type};
                    $quantity = $+{quantity};
                    $quantity =~ s/,//g;
                }
                when (/^(?<type>\w+)\sglyph$/) {
                    $moniker = 'glyph:'.$+{type};
                    $quantity = 1;
                }
                when (/^(?<type>[[:alpha:][:space:]]+)\s\(.+\)$/) {
                    $moniker = 'ship:'.parse_ship_type($+{type});
                    $quantity = 1;
                }
                when (/^(?<type>[[:alpha:][:space:]]+)\s\((?<level>[^\)]+)\)\splan$/) {
                    $moniker = 'plan:'.lc($+{type}).':'.$+{level};
                    $quantity = 1;
                }
                when (/^Level\s(?<level>\d+)\sspy\snamed\s[^(]\(prisoner\)/) {
                    $moniker = 'prisoner:'.lc($+{level});
                    $quantity = 1;
                }
                default {
                    warn("Unkown offer: $_");
                }
            }
            $trade_serialize{$moniker} ||= 0;
            $trade_serialize{$moniker} += $quantity;
        }
        
        push(@trade_identifiers,_trade_serialize($trade->{ask},%trade_serialize));
    }
    
    return @trade_identifiers;
}

__PACKAGE__->meta->make_immutable;
no Moose;
1;