package opts;
use strict;
use warnings;
our $VERSION = '0.01';
use Exporter 'import';
use PadWalker qw/var_name/;
use Getopt::Long;

our @EXPORT = qw/opts/;

my %is_invocant = map{ $_ => undef } qw($self $class);

sub opts {
    {
        package DB;
        # call of caller in DB package sets @DB::args,
        # which requires list context, but does not use return values
        () = caller(1);
    }

    # method call
    if(exists $is_invocant{ var_name(1, \$_[0]) || '' }){
        $_[0] = shift @DB::args;
        shift;
        # XXX: should we provide ways to check the type of invocant?
    }

    my @options;
    my %requireds;
    for(my $i = 0; $i < @_; $i++){

        (my $name = var_name(1, \$_[$i]))
            or  Carp::croak('usage: opts my $var => TYPE, ...');

        $name =~ s/^\$//;

        my $rule = _compile_rule($_[$i+1]);

        if (exists $rule->{default}) {
            $_[$i] = $rule->{default};
        }
        if (exists $rule->{required}) {
            $requireds{$name} = $i;
        }

        push @options, $name . $rule->{type} => \$_[$i];

        $i++ if defined $_[$i+1]; # discard type info
    }
    {
        my $err;
        local $SIG{__WARN__} = sub { $err = shift };
        GetOptions(@options) or Carp::croak($err);

        while ( my ($name, $idx) = each %requireds ) {
            unless (defined($_[$idx])) {
                Carp::croak("missing mandatory parameter named '\$$name'");
            }
        }
    }
}

sub _compile_rule {
    my ($rule) = @_;
    if (!defined $rule) {
        return +{ type => "!" };
    }
    elsif (!ref $rule) { # single, non-ref parameter is a type name
        my $tc = _get_type_constraint($rule) or Carp::croak("cannot find type constraint '$rule'");
        return +{ type => $tc };
    }
    else {
        my %ret;
        if ($rule->{isa}) {
            my $tc = _get_type_constraint($rule->{isa}) or Carp::croak("cannot find type constraint '$rule'");
            $ret{type} = $tc;
        } else {
            $ret{type} = "!";
        }
        for my $key (qw(default required)) {
            if (exists $rule->{$key}) {
                $ret{$key} = $rule->{$key};
            }
        }
        return \%ret;
    }
}

sub _get_type_constraint {
    my $isa = shift;

    return {
        'Bool'     => '!',
        'Str'      => '=s',
        'Int'      => '=i',
        'Num'      => '=f',
        'ArrayRef' => '=s@',
        'HashRef'  => '=s%', 
    }->{$isa};
}

1;
__END__

=head1 NAME

opts - proof of concept

=head1 SYNOPSIS

  # in script.pl
  use opts;
  sub run {
    opts my $p => 'Int';
  }
  run();

  ./script.pl --p=4 # p => 4

  # in script.pl
  sub run {
    opts my $p => { 'Int', required => 1 },
         my $q => 'Int';
  }
  ./script.pl --p=3 --q=4 # p => 3, q => 4
  ./script.pl --p=4       # p => 4, q => undef

  # in script.pl
  sub run {
    opts my $p => {isa => 'Int', default => 3},
  }
  ./script.pl --p=4       # p => 4
  ./script.pl             # p => 3


=head1 DESCRIPTION

opts is DSL for command line option.

=head1 AUTHOR

Kan Fushihara E<lt>kan.fushihara at gmail.comE<gt>

=head1 SEE ALSO

L<http://github.com/tokuhirom/p5-args>, L<Getopt::Long>

=head1 LICENSE

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

=cut
