The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#  You may distribute under the terms of either the GNU General Public License
#  or the Artistic License (the same terms as Perl itself)
#
#  (C) Paul Evans, 2016-2017 -- leonerd@leonerd.org.uk

package Devel::MAT::Tool;

use strict;
use warnings;

our $VERSION = '0.31';

use Getopt::Long qw( GetOptionsFromArray );
use List::Util qw( any );

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

   return bless {
      pmat     => $pmat,
      df       => $pmat->dumpfile,
      progress => $args{progress},
   }, $class;
}

sub pmat
{
   my $self = shift;
   return $self->{pmat};
}

sub df
{
   my $self = shift;
   return $self->{df};
}

sub report_progress
{
   my $self = shift;
   $self->{progress}->( @_ ) if $self->{progress};
}

sub get_sv_from_args
{
   my $self = shift;
   my ( $args ) = @_;

   my $sv = Devel::MAT::UI->can( "current_sv" ) && Devel::MAT::UI->current_sv;

   if( @$args ) {
      my $addr = shift @$args;

      # Acccept any root name symbolically
      if( any { $addr eq $_ } Devel::MAT::Dumpfile->ROOTS ) {
         $sv = $self->df->$addr;
      }
      else {
         $addr = do {
            no warnings 'portable';
            hex $addr;
         } if $addr =~ m/^0x/;

         do { no warnings 'numeric'; $addr eq $addr+0 } or
            die "Expected numerical SV address\n";

         $sv = $self->df->sv_at( $addr ) or
            die sprintf "No such SV at address %x\n", $addr;
      }
   }

   $sv or die "Need an SV address\n";

   return $sv;
}

# Some empty defaults
use constant CMD_OPTS => ();
use constant CMD_ARGS_SV => 0;
use constant CMD_ARGS => ();
use constant CMD_SUBS => ();

sub find_subcommand
{
   my $self = shift;
   my ( $subname ) = @_;

   # TODO: sanity check

   return ( ref($self) . "::" . $subname )->new( $self->pmat,
      progress => $self->{progress},
   );
}

sub run_cmd
{
   my $self = shift;
   my ( @args ) = @_;

   # TODO: consider what happens if parent commands have CMD_OPTS
   if( my @subs = $self->CMD_SUBS ) {
      my $subcmd = @args ? shift @args : $subs[0];
      return $self->find_subcommand( $subcmd )->run_cmd( @args );
   }

   my %optspec = $self->CMD_OPTS;

   my %opts;
   my %getopts;

   foreach my $name ( keys %optspec ) {
      my $spec = $optspec{$name};

      $opts{$name} = $spec->{default};

      my $getopt = $name;
      $getopt .= join "|", "", $spec->{alias} if defined $spec->{alias};

      $getopt =~ s/_/-/g;

      $getopt .= "=$spec->{type}" if $spec->{type};

      $getopts{$getopt} = \$opts{$name};
   }

   GetOptionsFromArray( \@args, %getopts ) or return;

   if( $self->CMD_ARGS_SV ) {
      my $sv = $self->get_sv_from_args( \@args );
      unshift @args, $sv;
   }

   # TODO: parsing/checking of ARGS?

   $self->run( %optspec ? \%opts : (), @args );
}

0x55AA;