The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# ============================================================================
package MooseX::App::ParsedArgv;
# ============================================================================

use 5.010;
use utf8;

use Moose;

use Encode qw(decode);

my $SINGLETON;

has 'argv' => (
    is              => 'rw',
    isa             => 'ArrayRef[Str]',
    lazy_build      => 1,
);

has 'options' => (
    is              => 'rw',
    isa             => 'ArrayRef[MooseX::App::ParsedArgv::Option]',
    lazy_build      => 1,
);

has 'extra' => (
    is              => 'rw',
    isa             => 'ArrayRef[Str]',
    lazy_build      => 1,
);


sub BUILD {
    my ($self) = @_;
    $SINGLETON = $self;
}

sub instance {
    my ($class) = @_;
    unless (defined $SINGLETON) {
        return $class->new();
    }
    return $SINGLETON;
}

sub shift_argv {
    my ($self) = @_;
    
    my $argv = $self->argv;
    my $first_argv = shift @{$argv};
    
    my $meta = $self->meta;
    $meta->get_attribute('options')->clear_value($self);
    $meta->get_attribute('extra')->clear_value($self);
    
    return $first_argv;
}

sub _build_options {
    my ($self) = @_;
    my ($options,$extra) = $self->_parse();
    return $options;
}

sub _build_extra {
    my ($self) = @_;
    my ($options,$extra) = $self->_parse();
    return $extra;
}

sub _parse {
    my ($self) = @_;
    
    my @options;
    my %options;
    my $lastkey;
    my $stopprocessing;
    my @extra;
    
    foreach my $element (@{$self->argv}) {
        if ($stopprocessing) {
            push(@extra,$element);
        } else {
            given ($element) {
                # Flags
                when (m/^-([^-][[:alnum:]]*)$/) {
                    undef $lastkey;
                    foreach my $flag (split(//,$1)) {
                        unless (defined $options{$flag}) {
                            $options{$flag} = MooseX::App::ParsedArgv::Option->new( key => $flag );
                            push(@options,$options{$flag});
                        }
                        $lastkey = $options{$flag};
                    }
                }
                # Key-value combined
                when (m/^--([^-=][^=]*)=(.+)$/) {
                    undef $lastkey;
                    my ($key,$value) = ($1,$2);
                    unless (defined $options{$key}) {
                        $options{$key} = MooseX::App::ParsedArgv::Option->new( key => $key );
                        push(@options,$options{$key});
                    }
                    $options{$key}->add_value($value);
                }
                # Key
                when (m/^--([^-].*)/) {
                    my $key = $1;
                    unless (defined $options{$key}) {
                        $options{$key} = MooseX::App::ParsedArgv::Option->new( key => $key );
                        push(@options,$options{$key});
                    }
                    $lastkey = $options{$key};
                }
                # Extra values
                when ('--') {
                    undef $lastkey;
                    $stopprocessing = 1;
                }
                # Value
                default {
                    if (defined $lastkey) {
                        $lastkey->add_value($element);
                        undef $lastkey;
                    } else {
                        push(@extra,$element);
                    }
                }
            } 
        }
    }
    
    my $meta = $self->meta;
    $meta->get_attribute('options')->set_raw_value($self,\@options);
    $meta->get_attribute('extra')->set_raw_value($self,\@extra);
    
    return (\@options,\@extra);
}

sub options_available {
    my ($self) = @_;
    
    my @options;
    foreach my $option (@{$self->options}) {
        next
            if $option->is_consumed;
        push(@options,$option);
    }  
    return @options; 
}

sub _build_argv {
    my @argv = eval {
        require I18N::Langinfo;
        I18N::Langinfo->import(qw(langinfo CODESET));
        my $codeset = langinfo(CODESET());
        # TODO Not sure if this is the right place?
        binmode(STDOUT, ":encoding(UTF-8)")
            if $codeset =~ m/^UTF-?8$/i;
        return map { decode($codeset,$_) } @ARGV;
    };
    
    # TODO handle errors
    return \@argv;
}


{
    package MooseX::App::ParsedArgv::Option;
    
    use Moose;
    
    has 'key' => (
        is              => 'ro',
        isa             => 'Str',
        required        => 1,
    );
    
    has 'value' => (
        is              => 'rw',
        isa             => 'ArrayRef[Str]',
        traits          => ['Array'],
        default         => sub { [] },
        handles => {
            add_value       => 'push',
            has_values      => 'count',
            get_value       => 'get',
        }
    );
    
    has 'consumed' => (
        is              => 'rw',
        isa             => 'Class::MOP::Attribute',
        predicate       => 'is_consumed',
    );
    
    sub consume {
        my ($self,$attribute) = @_;
        Moose->throw_error('Option '.$self->key.' is already consumed')
            if $self->consumed;
        $self->consumed($attribute);  
        
        return $self; 
    }
    
    __PACKAGE__->meta->make_immutable();
}

__PACKAGE__->meta->make_immutable;
1;

__END__

=pod

=head1 NAME

MooseX::App::ParsedArgv - Parses @ARGV

=head1 SYNOPSIS

 use MooseX::App::ParsedArgv;
 my $argv = MooseX::App::ParsedArgv->instance; 
 
 foreach my $option (@{$argv->options}) {
     say "Parsed ".$option->key;
 }

=head1 DESCRIPTION

This is a helper class that holds all options parsed from @ARGV. It is 
implemented as a singleton.

=head1 METHODS

=head2 new

Create a new MooseX::App::ParsedArgv instance 

=head2 instance 

Get the current MooseX::App::ParsedArgv instance. If there is no instance
a new one will be created.

=head2 options_available

Returns an array of all parsed options that have not yet been consumed.

=head2 shift_argv

Shifts the first argument of the local @ARGV

=head2 extra

Returns all extra arguments

=head2 options

Returns all options as MooseX::App::ParsedArgv::Option objects

=cut