package Perl::PrereqScanner::NotQuiteLite::Context;

use strict;
use warnings;
use CPAN::Meta::Requirements;
use Regexp::Trie;
use Perl::PrereqScanner::NotQuiteLite::Util;

my %defined_keywords = _keywords();

my %default_op_keywords = map {$_ => 1} qw(
  x eq ne and or xor cmp ge gt le lt not
);

my %default_conditional_keywords = map {$_ => 1} qw(
  if elsif unless else
);

my %default_expects_expr_block = map {$_ => 1} qw(
  if elsif unless given when
  for foreach while until
);

my %default_expects_block_list = map {$_ => 1} qw(
  map grep sort
);

my %default_expects_fh_list = map {$_ => 1} qw(
  print printf say
);

my %default_expects_fh_or_block_list = (
  %default_expects_block_list,
  %default_expects_fh_list,
);

my %default_expects_block = map {$_ => 1} qw(
  else default
  eval sub do while until continue
  BEGIN END INIT CHECK
  if elsif unless given when
  for foreach while until
  map grep sort
);

my %default_expects_word = map {$_ => 1} qw(
  use require no sub
);

my %enables_utf8 = map {$_ => 1} qw(
  utf8
  Mojo::Base
  Mojo::Base::Che
);

my %new_keyword_since = (
  say => '5.010',
  state => '5.010',
  given => '5.010',
  when => '5.010',
  default => '5.010',
);

my $default_g_re_prototype = qr{\G(\([^\)]*?\))};

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

  my %context = (
    requires => CPAN::Meta::Requirements->new,
    noes => CPAN::Meta::Requirements->new,
    file => $args{file},
    stash => {},
  );

  if ($args{suggests} or $args{recommends}) {
    $context{recommends} = CPAN::Meta::Requirements->new;
  }
  if ($args{suggests}) {
    $context{suggests} = CPAN::Meta::Requirements->new;
  }
  if ($args{perl_minimum_version}) {
    $context{perl} = CPAN::Meta::Requirements->new;
  }
  for my $type (qw/use no method keyword sub/) {
    if (exists $args{_}{$type}) {
      for my $key (keys %{$args{_}{$type}}) {
        $context{$type}{$key} = [@{$args{_}{$type}{$key}}];
      }
    }
  }

  bless \%context, $class;
}

sub stash { shift->{stash} }

sub register_keyword_parser {
  my ($self, $keyword, $parser_info) = @_;
  $self->{keyword}{$keyword} = $parser_info;
  $self->{defined_keywords}{$keyword} = 0;
}

sub remove_keyword_parser {
  my ($self, $keyword) = @_;
  delete $self->{keyword}{$keyword};
  delete $self->{keyword} if !%{$self->{keyword}};
  delete $self->{defined_keywords}{$keyword};
}

sub register_method_parser {
  my ($self, $method, $parser_info) = @_;
  $self->{method}{$method} = $parser_info;
}

*register_keyword = \&register_keyword_parser;
*remove_keyword = \&remove_keyword_parser;
*register_method = \&register_method_parser;

sub register_sub_parser {
  my ($self, $keyword, $parser_info) = @_;
  $self->{sub}{$keyword} = $parser_info;
  $self->{defined_keywords}{$keyword} = 0;
}

sub requires { shift->{requires} }
sub recommends { shift->_optional('recommends') }
sub suggests { shift->_optional('suggests') }
sub noes { shift->{noes} }

sub _optional {
  my ($self, $key) = @_;
  my $optional = $self->{$key} or return;

  # no need to recommend/suggest what are listed as requires
  if (my $requires = $self->{requires}) {
    my $hash = $optional->as_string_hash;
    for my $module (keys %$hash) {
      if (defined $requires->requirements_for_module($module) and
          $requires->accepts_module($module, $hash->{$module})
      ) {
        $optional->clear_requirement($module);
      }
    }
  }
  $optional;
}

sub add {
  shift->_add('requires', @_);
}

sub add_recommendation {
  shift->_add('recommends', @_);
}

sub add_suggestion {
  shift->_add('suggests', @_);
}

sub add_conditional {
  shift->_add('conditional', @_);
}

sub add_no {
  shift->_add('noes', @_);
}

sub add_perl {
  my ($self, $perl, $reason) = @_;
  return unless $self->{perl};
  $self->_add('perl', 'perl', $perl);
  $self->{perl_minimum_version}{$reason} = $perl;
}

sub _add {
  my ($self, $type, $module, $version) = @_;
  return unless is_module_name($module);

  my $CMR = $self->_object($type) or return;
  $version = 0 unless defined $version;
  $CMR->add_minimum($module, "$version");
}

sub has_added {
  shift->_has_added('requires', @_);
}

sub has_added_recommendation {
  shift->_has_added('recommends', @_);
}

sub has_added_suggestion {
  shift->_has_added('suggests', @_);
}

sub has_added_conditional {
  shift->_has_added('conditional', @_);
}

sub has_added_no {
  shift->_has_added('no', @_);
}

sub _has_added {
  my ($self, $type, $module) = @_;
  return unless is_module_name($module);

  my $CMR = $self->_object($type) or return;
  defined $CMR->requirements_for_module($module) ? 1 : 0;
}

sub _object {
  my ($self, $key) = @_;
  if ($self->{eval}) {
    $key = 'suggests';
  } elsif ($self->{force_cond}) {
    $key = 'recommends';
  } elsif ($key && $key eq 'conditional') {
    if ($self->{cond}) {
      $key = 'recommends';
    } elsif (grep {$_->[0] eq '{' and $_->[2] ne 'BEGIN'} @{$self->{stack} || []}) {
      $key = 'recommends';
    } else {
      $key = 'requires';
    }
  } elsif (!$key) {
    $key = 'requires';
  }
  $self->{$key} or return;
}

sub has_callbacks {
  my ($self, $type) = @_;
  exists $self->{$type};
}

sub has_callback_for {
  my ($self, $type, $name) = @_;
  exists $self->{$type}{$name};
}

sub run_callback_for {
  my ($self, $type, $name, @args) = @_;
  return unless $self->_object;
  my ($parser, $method, @cb_args) = @{$self->{$type}{$name}};
  $parser->$method($self, @cb_args, @args);
}

sub prototype_re {
  my $self = shift;
  if (@_) {
    $self->{prototype_re} = shift;
  }
  return $default_g_re_prototype unless exists $self->{prototype_re};
  $self->{prototype_re};
}

sub quotelike_re {
  my $self = shift;
  return qr/qq?/ unless exists $self->{quotelike_re};
  $self->{quotelike_re};
}

sub register_quotelike_keywords {
  my ($self, @keywords) = @_;
  push @{$self->{quotelike}}, @keywords;
  $self->{defined_keywords}{$_} = 0 for @keywords;

  my $trie = Regexp::Trie->new;
  $trie->add($_) for 'q', 'qq', @{$self->{quotelike} || []};
  $self->{quotelike_re} = $trie->regexp;
}

sub token_expects_block_list {
  my ($self, $token) = @_;
  return 1 if exists $default_expects_block_list{$token};
  return 0 if !exists $self->{expects_block_list};
  return 1 if exists $self->{expects_block_list}{$token};
  return 0;
}

sub token_expects_fh_list {
  my ($self, $token) = @_;
  return 1 if exists $default_expects_fh_list{$token};
  return 0 if !exists $self->{expects_fh_list};
  return 1 if exists $self->{expects_fh_list}{$token};
  return 0;
}

sub token_expects_fh_or_block_list {
  my ($self, $token) = @_;
  return 1 if exists $default_expects_fh_or_block_list{$token};
  return 0 if !exists $self->{expects_fh_or_block_list};
  return 1 if exists $self->{expects_fh_or_block_list}{$token};
  return 0;
}

sub token_expects_expr_block {
  my ($self, $token) = @_;
  return 1 if exists $default_expects_expr_block{$token};
  return 0 if !exists $self->{expects_expr_block};
  return 1 if exists $self->{expects_expr_block}{$token};
  return 0;
}

sub token_expects_block {
  my ($self, $token) = @_;
  return 1 if exists $default_expects_block{$token};
  return 0 if !exists $self->{expects_block};
  return 1 if exists $self->{expects_block}{$token};
  return 0;
}

sub token_expects_word {
  my ($self, $token) = @_;
  return 1 if exists $default_expects_word{$token};
  return 0 if !exists $self->{expects_word};
  return 1 if exists $self->{expects_word}{$token};
  return 0;
}

sub token_is_conditional {
  my ($self, $token) = @_;
  return 1 if exists $default_conditional_keywords{$token};
  return 0 if !exists $self->{is_conditional_keyword};
  return 1 if exists $self->{is_conditional_keyword}{$token};
  return 0;
}

sub token_is_keyword {
  my ($self, $token) = @_;
  return 1 if exists $defined_keywords{$token};
  return 0 if !exists $self->{defined_keywords};
  return 1 if exists $self->{defined_keywords}{$token};
  return 0;
}

sub token_is_op_keyword {
  my ($self, $token) = @_;
  return 1 if exists $default_op_keywords{$token};
  return 0 if !exists $self->{defined_op_keywords};
  return 1 if exists $self->{defined_op_keywords}{$token};
  return 0;
}

sub check_new_keyword {
  my ($self, $token) = @_;
  if (exists $new_keyword_since{$token}) {
    $self->add_perl($new_keyword_since{$token}, $token);
  }
}

sub register_keywords {
  my ($self, @keywords) = @_;
  for my $keyword (@keywords) {
    $self->{defined_keywords}{$keyword} = 0;
  }
}

sub register_op_keywords {
  my ($self, @keywords) = @_;
  for my $keyword (@keywords) {
    $self->{defined_op_keywords}{$keyword} = 0;
  }
}

sub remove_keywords {
  my ($self, @keywords) = @_;
  for my $keyword (@keywords) {
    delete $self->{defined_keywords}{$keyword} if exists $self->{defined_keywords}{$keyword} and !$self->{defined_keywords}{$keyword};
  }
}

sub register_sub_keywords {
  my ($self, @keywords) = @_;
  for my $keyword (@keywords) {
    $self->{defines_sub}{$keyword} = 1;
    $self->{expects_block}{$keyword} = 1;
    $self->{expects_word}{$keyword} = 1;
    $self->{defined_keywords}{$keyword} = 0;
  }
}

sub token_defines_sub {
  my ($self, $token) = @_;
  return 1 if $token eq 'sub';
  return 0 if !exists $self->{defines_sub};
  return 1 if exists $self->{defines_sub}{$token};
  return 0;
}

sub enables_utf8 {
  my ($self, $module) = @_;
  exists $enables_utf8{$module} ? 1 : 0;
}

sub add_package {
  my ($self, $package) = @_;
  $self->{packages}{$package} = 1;
}

sub packages {
  my $self = shift;
  keys %{$self->{packages} || {}};
}

sub remove_inner_packages_from_requirements {
  my $self = shift;
  for my $package ($self->packages) {
    for my $rel (qw/requires recommends suggests noes/) {
      next unless $self->{$rel};
      $self->{$rel}->clear_requirement($package);
    }
  }
}

sub merge_perl {
  my $self = shift;
  return unless $self->{perl};

  my $perl = $self->{requires}->requirements_for_module('perl');
  if ($self->{perl}->accepts_module('perl', $perl)) {
    delete $self->{perl_minimum_version};
  } else {
    $self->add(perl => $self->{perl}->requirements_for_module('perl'));
  }
}

sub _keywords {(
    '__FILE__' => 1,
    '__LINE__' => 2,
    '__PACKAGE__' => 3,
    '__DATA__' => 4,
    '__END__' => 5,
    '__SUB__' => 6,
    AUTOLOAD => 7,
    BEGIN => 8,
    UNITCHECK => 9,
    DESTROY => 10,
    END => 11,
    INIT => 12,
    CHECK => 13,
    abs => 14,
    accept => 15,
    alarm => 16,
    and => 17,
    atan2 => 18,
    bind => 19,
    binmode => 20,
    bless => 21,
    break => 22,
    caller => 23,
    chdir => 24,
    chmod => 25,
    chomp => 26,
    chop => 27,
    chown => 28,
    chr => 29,
    chroot => 30,
    close => 31,
    closedir => 32,
    cmp => 33,
    connect => 34,
    continue => 35,
    cos => 36,
    crypt => 37,
    dbmclose => 38,
    dbmopen => 39,
    default => 40,
    defined => 41,
    delete => 42,
    die => 43,
    do => 44,
    dump => 45,
    each => 46,
    else => 47,
    elsif => 48,
    endgrent => 49,
    endhostent => 50,
    endnetent => 51,
    endprotoent => 52,
    endpwent => 53,
    endservent => 54,
    eof => 55,
    eq => 56,
    eval => 57,
    evalbytes => 58,
    exec => 59,
    exists => 60,
    exit => 61,
    exp => 62,
    fc => 63,
    fcntl => 64,
    fileno => 65,
    flock => 66,
    for => 67,
    foreach => 68,
    fork => 69,
    format => 70,
    formline => 71,
    ge => 72,
    getc => 73,
    getgrent => 74,
    getgrgid => 75,
    getgrnam => 76,
    gethostbyaddr => 77,
    gethostbyname => 78,
    gethostent => 79,
    getlogin => 80,
    getnetbyaddr => 81,
    getnetbyname => 82,
    getnetent => 83,
    getpeername => 84,
    getpgrp => 85,
    getppid => 86,
    getpriority => 87,
    getprotobyname => 88,
    getprotobynumber => 89,
    getprotoent => 90,
    getpwent => 91,
    getpwnam => 92,
    getpwuid => 93,
    getservbyname => 94,
    getservbyport => 95,
    getservent => 96,
    getsockname => 97,
    getsockopt => 98,
    given => 99,
    glob => 100,
    gmtime => 101,
    goto => 102,
    grep => 103,
    gt => 104,
    hex => 105,
    if => 106,
    index => 107,
    int => 108,
    ioctl => 109,
    join => 110,
    keys => 111,
    kill => 112,
    last => 113,
    lc => 114,
    lcfirst => 115,
    le => 116,
    length => 117,
    link => 118,
    listen => 119,
    local => 120,
    localtime => 121,
    lock => 122,
    log => 123,
    lstat => 124,
    lt => 125,
    m => 126,
    map => 127,
    mkdir => 128,
    msgctl => 129,
    msgget => 130,
    msgrcv => 131,
    msgsnd => 132,
    my => 133,
    ne => 134,
    next => 135,
    no => 136,
    not => 137,
    oct => 138,
    open => 139,
    opendir => 140,
    or => 141,
    ord => 142,
    our => 143,
    pack => 144,
    package => 145,
    pipe => 146,
    pop => 147,
    pos => 148,
    print => 149,
    printf => 150,
    prototype => 151,
    push => 152,
    q => 153,
    qq => 154,
    qr => 155,
    quotemeta => 156,
    qw => 157,
    qx => 158,
    rand => 159,
    read => 160,
    readdir => 161,
    readline => 162,
    readlink => 163,
    readpipe => 164,
    recv => 165,
    redo => 166,
    ref => 167,
    rename => 168,
    require => 169,
    reset => 170,
    return => 171,
    reverse => 172,
    rewinddir => 173,
    rindex => 174,
    rmdir => 175,
    s => 176,
    say => 177,
    scalar => 178,
    seek => 179,
    seekdir => 180,
    select => 181,
    semctl => 182,
    semget => 183,
    semop => 184,
    send => 185,
    setgrent => 186,
    sethostent => 187,
    setnetent => 188,
    setpgrp => 189,
    setpriority => 190,
    setprotoent => 191,
    setpwent => 192,
    setservent => 193,
    setsockopt => 194,
    shift => 195,
    shmctl => 196,
    shmget => 197,
    shmread => 198,
    shmwrite => 199,
    shutdown => 200,
    sin => 201,
    sleep => 202,
    socket => 203,
    socketpair => 204,
    sort => 205,
    splice => 206,
    split => 207,
    sprintf => 208,
    sqrt => 209,
    srand => 210,
    stat => 211,
    state => 212,
    study => 213,
    sub => 214,
    substr => 215,
    symlink => 216,
    syscall => 217,
    sysopen => 218,
    sysread => 219,
    sysseek => 220,
    system => 221,
    syswrite => 222,
    tell => 223,
    telldir => 224,
    tie => 225,
    tied => 226,
    time => 227,
    times => 228,
    tr => 229,
    truncate => 230,
    uc => 231,
    ucfirst => 232,
    umask => 233,
    undef => 234,
    unless => 235,
    unlink => 236,
    unpack => 237,
    unshift => 238,
    untie => 239,
    until => 240,
    use => 241,
    utime => 242,
    values => 243,
    vec => 244,
    wait => 245,
    waitpid => 246,
    wantarray => 247,
    warn => 248,
    when => 249,
    while => 250,
    write => 251,
    x => 252,
    xor => 253,
    y => 254 || 255,
)}

1;

__END__

=encoding utf-8

=head1 NAME

Perl::PrereqScanner::NotQuiteLite::Context

=head1 DESCRIPTION

This is typically used to keep callbacks, an eval state, and
found prerequisites for a processing file.

=head1 METHODS

=head2 add

  $c->add($module);
  $c->add($module => $minimum_version);

adds a module with/without a minimum version as a requirement
or a suggestion, depending on the eval state. You can add a module
with different versions as many times as you wish. The actual
minimum version for the module is calculated inside
(by L<CPAN::Meta::Requirements>).

=head2 register_keyword_parser, remove_keyword_parser, register_method_parser, register_sub_parser

  $c->register_keyword_parser(
    'func_name',
    [$parser_class, 'parser_for_the_func', $used_module],
  );
  $c->remove_keyword_parser('func_name');

  $c->register_method_parser(
    'method_name',
    [$parser_class, 'parser_for_the_method', $used_module],
  );

If you find a module that can export a loader function is actually
C<use>d (such as L<Moose> that can export an C<extends> function
that will load a module internally), you might also register the
loader function as a custom keyword dynamically so that the scanner
can also run a callback for the function to parse its argument
tokens.

You can also remove the keyword when you find the module is C<no>ed
(and when the module supports C<unimport>).

You can also register a method callback on the fly (but you can't
remove it).

If you always want to check some functions/methods when you load a
plugin, just register them using a C<register> method in the plugin.

=head2 requires

returns a CPAN::Meta::Requirements object for requirements.

=head2 suggests

returns a CPAN::Meta::Requirements object for suggestions
(requirements in C<eval>s), or undef when it is not expected to
parse tokens in C<eval>.

=head1 METHODS MOSTLY FOR INTERNAL USE

=head2 new

creates an instance. You usually don't need to call this because
it's automatically created in the scanner.

=head2 has_callbacks, has_callback_for, run_callback_for

  next unless $c->has_callbacks('use');
  next unless $c->has_callbacks_for('use', 'base');
  $c->run_callbacks_for('use', 'base', $tokens);

C<has_callbacks> returns true if a callback for C<use>, C<no>,
C<keyword>, or C<method> is registered. C<has_callbacks_for>
returns true if a callback for the module/keyword/method is
registered. C<run_callbacks_for> is to run the callback.

=head2 has_added

returns true if a module has already been added as a requirement
or a suggestion. Only useful for the ::UniversalVersion plugin.

=head1 AUTHOR

Kenichi Ishigaki, E<lt>ishigaki@cpan.orgE<gt>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2015 by Kenichi Ishigaki.

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

=cut
