#!/usr/bin/env perl
package MOP4Import::Util::Inspector;
use strict;
use warnings;
use Carp;

use constant DEBUG => $ENV{DEBUG_MOP4IMPORT};

BEGIN {
  print STDERR "Using (file '" . __FILE__ . "')\n" if DEBUG and DEBUG >= 2
}

use MOP4Import::Base::CLI_JSON -as_base
  , [fields =>
     [lib =>
      doc => "library directory list. SCALAR, ARRAY or ':' separated STRING",
      zsh_completer => ": :_directories",
    ]
   ];

use MOP4Import::NamedCodeAttributes ();

use MOP4Import::Util qw/terse_dump fields_hash fields_symbol
                        maybe_fields_hash
                        take_hash_opts_maybe/;
use MOP4Import::Util::FindMethods;

use List::Util ();

sub describe_commands_of {
  (my MY $self, my $classOrObj) = @_;
  my $class = ref $classOrObj || $self->require_module($classOrObj);

  map {$self->format_command_of($class, $_)} $self->list_commands_of($class);
}

sub describe_options_of {
  (my MY $self, my $classOrObj) = @_;
  my $class = ref $classOrObj || $self->require_module($classOrObj);

  my @msg;

  my @options = $self->group_options_of($class);
  my $maxlen = $self->max_option_length($class);

  foreach my $group (@options) {
    my ($pkg, @fields) = @$group;
    push @msg, <<END;

Options from $pkg:
END
    foreach my FieldSpec $fs (@fields) {
      push @msg, $self->format_option($fs, $maxlen);
    }
  }
  @msg;
}

sub list_commands_of {
  my ($self, $pack) = @_;
  $self->require_module($pack);
  FindMethods($pack, sub {s/^cmd_//});
}

sub format_command_of {
  my ($self, $class, $name) = @_;
  "  ".join("        "
            , $name
            , $self->info_command_doc_of($class, $name) // '')."\n";
}

sub list_options_of {
  my ($self, $pack) = @_;
  $self->require_module($pack);
  my $symbol = fields_symbol($pack);
  if (my $array = *{$symbol}{ARRAY}) {
    List::Util::uniq(@$array, $self->list_options_onconfigure_of($pack))
  }
  elsif (my $hash = *{$symbol}{HASH}) {
    sort keys %$hash;
  }
  elsif ($pack->can("meta")) {
    $pack->meta->get_attribute_list
  }
  else {
    ();
  }
}

sub list_options_onconfigure_of {
  my ($self, $pack) = @_;
  $self->require_module($pack);
  my $isa = mro::get_linear_isa($pack);
  my %dup;
  map {
    my $symtab = MOP4Import::Util::symtab($_);
    map {
      if (/^onconfigure_(\w+)$/ and not $dup{$1}++) {
        $1
      } else {
        ()
      }
    } sort keys %$symtab;
  } @$isa;
}

sub group_options_of {
  my ($self, $pack, @opt_names) = @_;
  $self->require_module($pack);
  my $meta = $pack->can("meta") ? $pack->meta : undef;
  @opt_names = $self->list_options_of($pack) unless @opt_names;
  my %package;
  my @unknown;
  foreach my $name (@opt_names) {
    next unless $name =~ /^[a-z]/;
    # FieldSpec もどきをここで作る
    my FieldSpec $spec = $self->get_field_spec_of($pack, $name) or do {
      push @unknown, $name;
      next
    };
    push @{$package{$spec->{package}}}, $spec;
  }

  my $isa = mro::get_linear_isa($pack);

  my @grouped = map {
    $package{$_} ? [$_, @{$package{$_}}] : ();
  } @$isa;

  ((@unknown ? ['', @unknown] : ()), @grouped);
}

sub get_field_spec_of {
  (my MY $self, my ($pack, $name)) = @_;
  $self->require_module($pack);
  if (my $fields = maybe_fields_hash($pack)) {
    my $spec = $fields->{$name};
    return $spec if ref $spec;
    $self->get_field_spec_for_onconfigure($pack, $name)
  } elsif (my $sub = $pack->can("meta")) {
    my $attMeta = $sub->($pack)->get_attribute($name);
    my FieldSpec $spec = +{};
    $spec->{package} = $pack;
    $spec->{name} = $name;
    if (my $doc = $attMeta->{documentation}) {
      $spec->{doc} = $doc;
    }
    elsif ($doc = $attMeta->{isa}) {
      $spec->{doc} = "isa $doc";
    }
    $spec;
  } else {
    undef;
  }
}

sub get_field_spec_for_onconfigure {
  (my MY $self, my ($pack, $name)) = @_;
  my $code = $pack->can("onconfigure_$name")
    or return;

  if (my $atts = MOP4Import::NamedCodeAttributes->m4i_CODE_ATTR_dict($code)) {
    $self->create_field_spec_from_code_attribute($pack, $name, $atts);
  }
  else {
    +{package => $pack, name => $name};
  }
}

sub create_field_spec_from_code_attribute {
  (my MY $self, my ($pack, $name, $atts)) = @_;
  my FieldSpec $spec = +{};
  $spec->{package} = $pack;
  $spec->{name} = $name;
  if ($atts->{Doc}) {
    $spec->{doc} = $atts->{Doc};
  }
  if ($atts->{ZshCompleter}) {
    $spec->{zsh_completer} = $atts->{ZshCompleter};
  }
  $spec;
}

sub max_option_length {
  my ($self, $pack) = @_;
  my $fields = fields_hash($pack);
  my @name = grep {/^[a-z]/} $self->list_options_of($pack);
  List::Util::max(map {length} @name);
}

sub format_option {
  (my MY $self, my FieldSpec $fs, my $maxlen) = @_;
  my $len = ($maxlen // 16);
  sprintf "  --%-${len}s  %s\n", $fs->{name}, $fs->{doc} // "";
}

sub is_getter_of {
  my ($self, $pack, $subName) = @_;
  my $fields = fields_hash($pack);
  exists $fields->{$subName} && $pack->can($subName);
}

sub info_command_doc_of {
  my ($self, $class, $name) = @_;
  $self->info_method_doc_of($class, "cmd_$name");
}

sub info_method_doc_of {
  my ($self, $class, $name, $allow_missing) = @_;

  my $atts = $self->info_code_attributes_of($class, $name, $allow_missing)
    or return undef;

  $atts->{Doc};
}

sub info_code_attribute {
  my ($self, $attName, $code) = @_;
  my $atts = MOP4Import::NamedCodeAttributes->m4i_CODE_ATTR_dict($code)
    or return undef;
  $atts->{$attName};
}

sub info_code_attributes_of {
  my ($self, $class, $name, $allow_missing) = @_;
  unless (defined $class and defined $name) {
    Carp::croak "Usage: \$self->info_code_attributes_of(\$class, \$name, ?\$allow_missing\?)"
  }
  $self->require_module($class);
  if ($class->can('meta')) {
    my $meta = $class->meta->get_method($name) or do {
      return if $allow_missing;
      Carp::croak "No such method: $name";
    };
    $meta->can('attributes')
      or return;
    my $atts = $meta->attributes;
    MOP4Import::NamedCodeAttributes->m4i_parse_attributes(@$atts);
  } else {
    my $sub = $class->can($name) or do {
      return if $allow_missing;
      Carp::croak "No such method: $name";
    };
    MOP4Import::NamedCodeAttributes->m4i_CODE_ATTR_dict($sub);
  }
}

sub info_methods :method {
  (my MY $self, my ($methodPattern, %opts)) = @_;

  my $groupByClass = delete $opts{group};
  my $detail = delete $opts{detail};
  my $all = delete $opts{all};
  my $inc = delete $opts{inc};
  my $pack = do {
    if (my $name = delete $opts{pack}) {
      $self->require_module($name, MOP4Import::Util::lexpand($inc));
    } else {
      ref $self;
    }
  };

  unless (keys %opts == 0) {
    Carp::croak "Unknown options: ".join(", ", sort keys %opts);
  }

  my $re = do {
    if (not defined $methodPattern or $methodPattern eq '') {
      qr{^[a-z]\w+\z}
    }
    elsif (ref $methodPattern eq 'Regexp') {
      $methodPattern
    }
    elsif ($methodPattern =~ m{^/(.*)/\z}) {
      qr{$1}
    } elsif ($methodPattern =~ /\*/) {
      require Text::Glob;
      Text::Glob::glob_to_regex($methodPattern);
    } else {
      qr{^$methodPattern};
    }
  };

  my $isa = mro::get_linear_isa($pack);

  my @all = map {
    my $symtab = MOP4Import::Util::symtab($_);
    my @names = grep {not /\W/ and $_ =~ $re} keys %$symtab;
    my @found = grep {
      my $entry = $symtab->{$_};
      if (ref \$entry eq 'GLOB' and my $code = *{$entry}{CODE}) {
        $all || MOP4Import::Util::has_method_attr($code);
      }
    } @names;
    if (not @found) {
      ()
    } elsif ($detail) {
      +{class => $_, methods => [map {
        my $doc = $self->info_method_doc($_);
        [$_, $doc ? $doc : ()];
      } sort @found]}
    } elsif ($groupByClass) {
      [$_, sort @found]
    } else {
      @found;
    }
  } @$isa;

  if (not $detail and not $groupByClass) {
    my %dup;
    sort grep {not $dup{$_}++}@all;
  } else {
    @all;
  }
}

sub require_module {
  (my MY $self, my ($moduleName, @inc)) = @_;

  my $fn = Module::Runtime::module_notional_filename($moduleName);

  # Use symtab test to avoid require_module() against already loaded modules.
  # In modulino, cmd_help may be called during module loading.
  # At that time, $INC{$moduleFileName} is not yet initialized.
  # This leads to double-require.
  if (MOP4Import::Util::maybe_symtab($moduleName)) {
    return wantarray ? ($moduleName => $INC{$fn}) : $moduleName;
  }

  @inc = map {split /:/} MOP4Import::Util::lexpand($self->{lib})
    if not @inc and ref $self;
  {
    require Module::Runtime;
    local @INC = (@inc, @INC);
    Module::Runtime::require_module($moduleName);
  }
  wantarray ? ($moduleName => $INC{$fn}) : $moduleName;
}

sub list_validator_in_module {
  (my MY $self, my ($typeName, $moduleName, @rest)) = @_;
  my $pack = $self->require_module($moduleName, @rest);
  my $sub = $pack->can($typeName);
  my $realType = $sub ? $sub->() : $typeName;
  MOP4Import::Util::list_validator($realType);
}

sub list_module_path {
  (my MY $self, my @modName) = @_;
  my %modules;
  foreach my $modName (@modName) {
    (undef, my $path) = $self->require_module($modName);
    $modules{$modName} = $path;
  }
  \%modules
}

sub list_module_version {
  (my MY $self, my @modName) = @_;
  my %modules;
  foreach my $modName (@modName) {
    $self->require_module($modName);
    $modules{$modName} = $modName->VERSION;
  }
  \%modules
}

unless (caller) {
  # To avoid redefinition wornings:
  # cli_run -> cli_help -> cli_inspector -> require MOP4Import::Util::Inspector;
  require File::Spec;
  $INC{"MOP4Import/Util/Inspector.pm"} = File::Spec->rel2abs(__FILE__);

  MY->cli_run(\@ARGV);
}

1;
