package Vee::Controller::Dex::Search;

use strict;
use warnings;
use base 'Catalyst::Controller';

use Vee::Form;
use Vee::Dex;

use List::MoreUtils qw/uniq/;

__PACKAGE__->config->{namespace} = 'dex';

=head1 NAME

Vee::Controller::Dex::Search - Pokedex search Controller

=head1 SYNOPSIS

See L<Vee>

=head1 DESCRIPTION

Catalyst Controller for searching for thingers in the Pokedex.

=cut

=head1 METHODS

=cut

=head2 pokemon_search

Search for pokemachus.

=cut

my %endpoints = (
    lb => { english => 'Minimum', operator => '>=', polarity => -1 },
    ub => { english => 'Maximum', operator => '<=', polarity =>  1 },
);    

# Pokemon search form
our $pokemon_search_fields = {
    name        => { type => 'text', size => 15, title => 'Enter a name or part of a name' },
    habitat     => { type => 'select', options => ['any', 'cave', 'forest', 'grassland', 'mountain', 'rare', 'rough terrain', 'sea', 'urban', 'water\'s edge'] },
    generation  => { type => 'checkbox', options => [ map { [ $_ => $Generations[$_]{games} ] } 0 .. $#Generations ] },
    basedex     => { type => 'checkbox', options => [ map { [ $_ => $Generations[$_]{games} ] } 1 .. $#Generations ] },
    breed       => { type => 'select', options => [ [ 0 => 'n/a' ], map { [ $_ => ($_ ? "$_: " : '') . $BreedingGroups[$_] ] } 1 .. $#BreedingGroups ], count => 2 },
    breed_mode  => { type => 'select', options => [ [ and => 'exactly' ], [ or => 'either of' ] ], default => 'or' },
    gender_rate => { type => 'select', options => [ [ any => 'anything' ], [ 255 => 'no gender' ], [ not255 => 'any gender' ], map { [ $_ => lc gender_text($_) ] } qw/0 31 63 127 191 254/ ] },
    ability     => { type => 'text', size => 20, title => 'Enter the name or number of an ability' },
    color       => { type => 'select', options => [qw/any black blue brown gray green pink purple red white yellow/], title => 'I have no explanation for why this is here' },
    type_mode   => { type => 'radio', options => [[ ignore => 'Ignore types' ], [ any => 'Find Pok&eacute;mon with any type that matches one selected' ], [ all => 'Find Pok&eacute;mon with exactly the selected type combination' ]], default => 'ignore', title => 'Select how types must be matched' },
    type        => { type => 'checkbox', options => [ @TypeNames ], title => 'Select types for the Pokemon to match' },
    move        => { type => 'text', count => 4, class => 'js-dexsuggest js-dexsuggest-moves', title => 'Enter moves the Pokemon must have' },
    move_method => { type => 'checkbox', options => { level => 'Level up', machine => 'TM or HM', egg => 'Egg', tutor => 'Move Tutor' }, title => 'Select ways the Pokemon may learn the move' },
    move_version=> { type => 'checkbox', options => [ map { [ $_ => $Icons{$_} ] } qw/rb y gs c rusa frlg e dp/ ], title => 'Select ways the Pokemon may learn the move' },
    evo_stage    => { type => 'checkbox', options => ['base', 'final'] },

    view        => { type => 'select', options => [[ list => 'boring list' ], [ icons => "small compact icons" ], [ sprites => "sprites" ], [ table => "detailed table" ]], default => 'table', title => 'Select how you want the results displayed' },
    sort        => { type => 'select', options => [[ id => 'National ID' ], [ name => 'Name' ], [ height => 'Height' ], [ weight => 'Weight' ], ( map { [ $StatColumns[$_] => $StatNames[$_] ] } 0 .. $#StatColumns ), [ stat_avg => 'Average stats' ] ], default => 'name', title => 'Select how you want the results ordered' },
    sort_desc   => { type => 'checkbox', title => 'Check this for descending order' },
};

for my $stat (0 .. $#StatColumns) {
    for my $endpoint (keys %endpoints) {
        $pokemon_search_fields->{ $StatColumns[$stat] . '_' . $endpoint } = { type => 'text', size => 4, maxlength => 3, title => "\u$endpoints{$endpoint}{english} $StatNames[$stat]" };
    }
}
for my $stat (0 .. $#StatColumns) {
    for my $endpoint (keys %endpoints) {
        $pokemon_search_fields->{ $StatColumns[$stat] . '_effort_' . $endpoint } = { type => 'text', size => 4, maxlength => 3, title => "\u$endpoints{$endpoint}{english} $StatNames[$stat] effort" };
    }
}
for my $nonstat (qw/height weight/) {
    for my $endpoint (keys %endpoints) {
        $pokemon_search_fields->{ $nonstat . '_' . $endpoint } = { type => 'text', size => 4, maxlength => 8, title => "\u$endpoints{$endpoint}{english} $nonstat" };
    }
}

my %view_sizes = (
    icons   => 250,
    sprites => 100,
    default => 50,
);

sub pokemon_search : Path('pokemon/search') : Args(0) {
    my ($self, $c) = @_;
    my $s = $c->stash;
    my $p = $c->req->params;
    my $skip = $p->{skip} || 0;
    $s->{before} = {%{ $c->req->params }};
    
    my $form = $s->{form} = Vee::Form->new(
        id => 'pokemon_search',
        fields => $pokemon_search_fields,
        params => $c->req->params,
    );

    my $PAGESIZE = $s->{PAGESIZE} = $view_sizes{ $p->{view} } || $view_sizes{default} || 10;

    if ($form->submitted) {
        my %criteria;
        $criteria{'-and'} = [ { 'me.id' => { '<=', $Generations[-1]{maxid} } } ];
        my (%joins, %clauses);
        $clauses{columns} = ['me.id'] unless $p->{view} eq 'table';
        
        # BASIC
        if ($p->{name}) {
            my $name = $p->{name};
            if ($name =~ /[*?]/) {
                $name =~ tr/*?/%_/;
                $criteria{'me.name'} = { like => $name };
            } else {
                $criteria{'me.name'} = { like => "%$name%" };
            }
        }
        if ($p->{habitat} ne 'any') { $criteria{'me.habitat'} = $p->{habitat} }
        if ($p->{color} and $p->{color} ne 'any') { $criteria{'me.color'} = $p->{color} };
        if ($p->{ability}) {
            my $ability = $c->model('DBIC::Abilities')->single( [ id => $p->{ability}, name => $p->{ability} ], { columns => ['id'] } );
            if ($ability) {
                $joins{pokemon_abilities} = 1;
                $criteria{'pokemon_abilities.ability_id'} = $ability->id;
            }
        }
        
        # GENERATIONS
        if (defined $p->{generation}) {
            my $gen_ref = [];
            for my $gen (Vee::Utils::array($p->{generation})) {
                push @$gen_ref, { '-between' => [ ($gen > 0 ? $Generations[$gen - 1]{maxid} : 0) + 1, $Generations[$gen]{maxid} ] };
            }
            push @{$criteria{'-and'}}, { 'me.id' => $gen_ref };
        }
        if (defined $p->{basedex}) {
            for my $gen (Vee::Utils::array($p->{basedex})) {
                $criteria{ 'me.id_' . lc $Generations[$gen]{region} } = { '!=' => 0 };
            }
        }

        # BREEDING AND STUFF
        if (defined $p->{gender_rate} and $p->{gender_rate} ne 'any') {
            if ($p->{gender_rate} eq 'not255') {
                $criteria{'me.gender_rate'} = { '!=' => 255 };
            } else {
                $criteria{'me.gender_rate'} = $p->{gender_rate};
            }
        }
        my @breeds = uniq grep { $_ } Vee::Utils::array($p->{breed});
        if (@breeds) {
            $joins{breeds} = 1;
            $criteria{'breeds.breed'} = \@breeds;
            if ($p->{breed_mode} eq 'and') {
                $clauses{having}{'COUNT(DISTINCT breeds.pokemon_id, breeds.breed)'} = scalar @breeds;

                if (@breeds == 1) {
                    # special hackery here to say one and ONLY one.
                    # I have to use SUM() because only grouped columns are recognized in HAVING.
                    delete $criteria{'breeds.breed'};
                    $clauses{having}{'SUM(breeds.breed)'} = $breeds[0];
                }
            }

            # TODO: do..  something..  if someone sticks in 3+?
        }

        # TYPES        
        if ($p->{type_mode} eq 'any') {
            push @{$criteria{'-and'}}, [
                '-or',
                { 'me.type1' => $p->{type}, 'me.type2' => $p->{type} },
            ];
        } elsif ($p->{type_mode} eq 'all') {
            if (not ref $p->{type}) {
                push @{$criteria{'-and'}}, { 'me.type1' => $p->{type}, 'me.type2' => undef };
            } elsif (scalar @{ $p->{type} } == 2) {
                push @{$criteria{'-and'}}, { -or => [
                    { 'me.type1' => $p->{type}[0], 'me.type2' => $p->{type}[1] },
                    { 'me.type1' => $p->{type}[1], 'me.type2' => $p->{type}[0] },
                ] };
            } else {
                # TODO: this should probably be better and throw an error of some sort
                push @{$criteria{'-and'}}, \ 0;
            }
        }

        # MOVES
        if (ref $p->{move} eq 'ARRAY') {
            my @moveids;
            for my $move (@{ $p->{move} }) {
                # XXX: show some message if a move is invalid?
                my $move_id = get_move($move);
                push @moveids, $move_id if defined $move_id;
            }
            if (@moveids) {
                $joins{pokemon_moves} = 1;
                $criteria{'pokemon_moves.move_id'} = \@moveids;
                $clauses{having}{'COUNT(DISTINCT pokemon_moves.pokemon_id, pokemon_moves.move_id)'} = scalar @moveids;

                if ($p->{move_method}) {
                    $criteria{'pokemon_moves.method'} = $p->{move_method};
                }
                if ($p->{move_version}) {
                    push @{$criteria{'-and'}}, { '-or', [ map { \ "FIND_IN_SET('$_', pokemon_moves.versions)" } Vee::Utils::array($p->{move_version}) ] };
                }
            }
        }
        
        # NUMBERS
        for my $numbar (@StatColumns, qw/height weight/) {
            for my $endpoint (keys %endpoints) {
                my $value = $p->{$numbar . '_' . $endpoint};
                if ($numbar eq 'height') {
                    $value = parse_height($value, $endpoints{$endpoint}{polarity});
                } elsif ($numbar eq 'weight') {
                    $value = parse_weight($value, $endpoints{$endpoint}{polarity});
                }
                # TODO: umm, make this throw an error for invalid height/weight?
                next unless Vee::Utils::isnum($value) and $value > 0;
                $criteria{"me.$numbar"}{ $endpoints{$endpoint}{operator} } = $value;
            }
        }
        for my $stat (0 .. $#StatColumns) {
            for my $endpoint (keys %endpoints) {
                my $value = $p->{ $StatColumns[$stat] . '_effort_' . $endpoint };
                next unless Vee::Utils::isnum($value) and $value > 0;
                my $op = $endpoints{$endpoint}{operator};
                push @{$criteria{'-and'}}, \ "SUBSTR(effort, $stat + 1, 1) $op $value";
            }
        }

        # EVOLUTION STAGE
        if ($p->{evo_stage}) {
            my %evo_stages = map { $_ => 1 } Vee::Utils::array($p->{evo_stage});

            if ($evo_stages{base}) {
                $criteria{'me.evo_parent_id'} = 0;
            }

            if ($evo_stages{final}) {
                $joins{descendants} = 1;
                $criteria{'descendants.id'} = undef;
            }
        }

        # SORTING
        if ($p->{sort} eq 'stat_avg') {
            $clauses{order_by} = '(' . join(' + ', map { "me.$_" } @StatColumns) . ')';
        } else {
            $clauses{order_by} = 'me.' . $p->{sort};
        }
        $clauses{order_by} .= $p->{sort_desc} ? ' DESC' : ' ASC';

        $s->{sql} = SQL::Abstract->new->where(\%criteria) if $c->debug;
        $s->{criteria} = \%criteria;

        my $rs = $c->model('DBIC::Pokemon')->search(
            \%criteria,
            { group_by => 'me.id', join => [ keys %joins ], %clauses }
        );

        my @results;
        # presence of a HAVING clause will break COUNT(*) so we have to do this nonsense instead
        if ($clauses{having}) {
            @results = $rs->all;
            $s->{total} = scalar @results;
            @results = @results[$skip .. $#results];
            @results = @results[0 .. $PAGESIZE - 1] if @results > $PAGESIZE;
        } else {
            @results = $rs->search(undef, { rows => $PAGESIZE, offset => $skip });
            $s->{total} = $rs->count;
        }
        $s->{results} = \@results;
    }
    
    $s->{page_title } = "Pok&eacute;mon Search";
    $s->{link_name  } = 'dex/search';
    $s->{crumbs     } = [
        '<a href="' . $c->uri('Dex') . '">Pok&eacute;dex</a>',
        '<a href="' . $c->uri('Dex', 'pokemon_list') . '">Pok&eacute;mon</a>',
        'Search',
    ];

    $s->{template} = 'dex/search/pokemon.tt';
}

=head2 move_search

Search for moves.

=cut

# move search form
our $move_search_fields = {
    name        => { type => 'text', size => 15, title => 'Enter a name or part of a name' },
    class       => { type => 'select', options => [qw/any physical special/], default => 'any' },
    generation  => { type => 'checkbox', options => [ map { [ $_ => $Generations[$_]{games} ] } 0 .. $#Generations ] },
    type        => { type => 'select', options => ['any', @TypeNames], default => 'any' },
    pokemon     => { type => 'text', count => 6, class => 'js-dexsuggest js-dexsuggest-pokemon', title => 'Enter Pokemon that must be able to learn the move' },
    move_method => { type => 'checkbox', options => { level => 'Level up', machine => 'TM or HM', egg => 'Egg', tutor => 'Move Tutor' }, title => 'Select ways the Pokemon may learn the move' },
    move_version=> { type => 'checkbox', options => [ map { [ $_ => $Icons{$_} ] } qw/rb y gs c rusa frlg e dp/ ], title => 'Select ways the Pokemon may learn the move' },

    view        => { type => 'select', options => [[ list => 'boring list' ], [ table => 'detailed table' ], [ contest => 'contest table' ]], title => 'Select how you want the results displayed' },
    sort        => { type => 'select', options => [qw/name id/], default => 'name', title => 'Select how you want the results ordered' },
    sort_desc   => { type => 'checkbox', title => 'Check this for descending order' },
};

for my $col (qw/power accuracy pp effect_chance priority/) {
    for my $endpoint (keys %endpoints) {
        $move_search_fields->{ $col . '_' . $endpoint } = { type => 'text', size => 4, maxlength => 3, title => "\u$endpoints{$endpoint}{english} $col" };
    }
}

sub move_search : Path('moves/search') : Args(0) {
    my ($self, $c) = @_;
    my $s = $c->stash;
    my $p = $c->req->params;
    my $skip = $p->{skip} || 0;
    $s->{before} = {%{ $c->req->params }};
    
    my $form = $s->{form} = Vee::Form->new(
        id => 'move_search',
        fields => $move_search_fields,
        params => $c->req->params,
    );
    
    if ($form->submitted) {
        my %criteria;
        $criteria{'-and'} = [ { 'me.id' => { '<=', $Generations[-1]{maxmoveid} } } ];
        my (%joins, %clauses);
        
        # BASIC
        if ($p->{name}) {
            my $name = $p->{name};
            if ($name =~ /[*?]/) {
                $name =~ tr/*?/%_/;
                $criteria{'me.name'} = { like => $name };
            } else {
                $criteria{'me.name'} = { like => "%$name%" };
            }
        }
        if ($p->{class} ne 'any') { $criteria{class} = $p->{class} }
        
        # TYPE
        if ($p->{type} ne 'any') { $criteria{type} = $p->{type} }


        # GENERATIONS
        if (defined $p->{generation}) {
            my $gen_ref = [];
            for my $gen (Vee::Utils::array($p->{generation})) {
                push @$gen_ref, { '-between' => [ ($gen > 0 ? $Generations[$gen - 1]{maxmoveid} : 0) + 1, $Generations[$gen]{maxmoveid} ] };
            }
            push @{$criteria{'-and'}}, { 'me.id' => $gen_ref };
        }

        # POKEMON
        if (ref $p->{pokemon} eq 'ARRAY') {
            my @pokemonids;
            for my $pokemon (@{ $p->{pokemon} }) {
                # XXX: show some message if a pokemon is invalid?
                my $pokemonid = get_pokemon($pokemon);
                push @pokemonids, $pokemonid if defined $pokemonid;
            }
            if (@pokemonids) {
                $joins{pokemon_moves} = 1;
                $criteria{'pokemon_moves.pokemon_id'} = \@pokemonids;
                $clauses{having}{'COUNT(DISTINCT pokemon_moves.pokemon_id, pokemon_moves.move_id)'} = scalar @pokemonids;

                if ($p->{move_method}) {
                    $criteria{'pokemon_moves.method'} = $p->{move_method};
                }
                if ($p->{move_version}) {
                    push @{$criteria{'-and'}}, { '-or', [ map { \ "FIND_IN_SET('$_', pokemon_moves.versions)" } Vee::Utils::array($p->{move_version}) ] };
                }
            }
        }

        # NUMBERS
        for my $numbar (qw/power accuracy pp effect_chance priority/) {
            for my $endpoint (keys %endpoints) {
                my $value = $p->{$numbar . '_' . $endpoint};
                next unless Vee::Utils::isnum($value) and ($numbar eq 'priority' or $value > 0);

                my $column = $numbar;
                if ($numbar eq 'priority') {
                    $joins{effect} = 1;
                    $column = 'effect.' . $numbar;
                }
                $criteria{$column}{ $endpoints{$endpoint}{operator} } = $value;
            }
        }

        # SORTING
        $clauses{order_by} = 'me.' . $p->{sort} . ' ';
        $clauses{order_by} .= $criteria{sort_desc} ? 'DESC' : 'ASC';

        if ($p->{view} eq 'contest') {
            push @{ $clauses{prefetch} }, 'contest';
        } else {
            $clauses{columns} = ['me.id'];
        }

        $s->{sql} = SQL::Abstract->new->where(\%criteria) if $c->debug;
        $s->{criteria} = \%criteria;

        my $rs = $c->model('DBIC::Moves')->search(
            \%criteria,
            { order_by => 'me.name ASC', group_by => 'me.id', join => [ keys %joins ], %clauses }
        );

        my @results;
        # presence of a HAVING clause will break COUNT(*) so we have to do this nonsense instead
        if ($clauses{having}) {
            @results = $rs->all;
            $s->{total} = scalar @results;
            @results = @results[$skip .. $#results];
            @results = @results[0 .. 49] if @results > 50;
        } else {
            @results = $rs->search(undef, { rows => 50, offset => $skip });
            $s->{total} = $rs->count;
        }
        $s->{results} = \@results;
    }
    
    $s->{page_title } = "Move Search";
    $s->{link_name  } = 'dex/movesearch';
    $s->{crumbs     } = [
        '<a href="' . $c->uri('Dex') . '">Pok&eacute;dex</a>',
        '<a href="' . $c->uri('Dex', 'move_list') . '">Moves</a>',
        'Search',
    ];

    $s->{template} = 'dex/search/moves.tt';
}

=head2 parse_height($height, $polarity)

Figure out what a height is supposed to be and convert it to decimeters.

C<$polarity> is either -1 (if this is a lower bound) or 1 (if this is an upper
bound), and is used to add an error margin to Imperial heights to compensate for
the database's not storing exact numbers of inches.

=cut

sub parse_height {
    my ($input, $polarity) = @_;
    $input =~ s/^\s+|\s+$//g;
    return if not $input;

    my $number = qr/ (?: \d* \.)? \d+ /x;
    if ($input =~ /^ ($number) \s* m $/x) {
        return $1 * 10;
    } elsif ($input =~ /^ ($number) \s* dm $/x) {
        return $1;
    } elsif ($input =~ /^ ($number) \s* cm $/x) {
        return $1 / 10;
    } elsif ($input =~ /^ (?: ($number) \s* (?:ft|'))? \s* (?: ($number) \s* (?:in|")?)? $/x) {
        # NOTE: with the above regex, just a number will give inches
        return +(($1 || 0) * 12 + ($2 || 0) + $polarity * 0.5) * 0.254;
    } else {
        # no idea, sorry
        return;
    }
}

=head2 parse_weight($height, $polarity)

Figure out what a weight is supposed to be and convert it to kilograms.

C<$polarity> is either -1 (if this is a lower bound) or 1 (if this is an upper
bound), and is used to add an error margin to Imperial weights to compensate for
the database's not storing exact numbers of ounces.

=cut

sub parse_weight {
    my ($input, $polarity) = @_;
    $input =~ s/^\s+|\s+$//g;
    return if not $input;

    my $number = qr/ (?: \d* \.)? \d+ /x;
    if ($input =~ /^ ($number) \s* kg $/x) {
        return $1;
    } elsif ($input =~ /^ ($number) \s* gm? $/x) {
        return $1 / 1000;
    } elsif ($input =~ /^ (?: ($number) \s* (?:lbs?)?)? \s* (?: ($number) \s* oz)? $/x) {
        # NOTE: with the above regex, just a number will give pounds
        return +(($1 || 0) * 16 + ($2 || 0) + $polarity * 0.5) * 0.02835;
    } else {
        # no idea, sorry
        return;
    }
}

=head1 AUTHOR

Maintainer: Alex "Eevee" Munroe (C<veekun@veekun.com>)

See the included F<AUTHORS> file for a full list of contributers.

=head1 LICENSE

See the included F<LICENSE> file.

=cut

1;
