package Config::Model::SimpleUI;

use Carp;
use v5.020;
use strict;
use warnings;
use open      qw(:std :utf8);    # undeclared streams in UTF-8
use Encode qw(decode_utf8);
use Regexp::Common qw/delimited/;

use feature qw/postderef signatures/;
no warnings qw/experimental::postderef experimental::signatures/;

my $syntax = '
cd <elt>, cd <elt:key>, cd - , cd !
   -> jump into node
set elt=value, elt:key=value
   -> set a value
clear elt
   -> clear value or list or hash
delete elt:key
   -> delete a value from a list or hash element
delete elt
   -> like reset, delete a value (set to undef)
display elt elt:key
   -> display a value
ls -> show content of object (args: path and/or filter pattern)
   ls -> show elements of current node
   ls foo* -> shows element matching foo.*
   ls \'foo*\' -> shows elements of node stored in "foo*"
   ls aHash -> shows keys of Hash
   ls \'aHash:"*"\'  -> shows elements of node stored in key "*" of "aHash" hash
ll [-nz] [-v] [ element | pattern ]
   -> show elements of current node and their value
     (options: -nz → hides empty value, -v → verbose)
     (args: element name or filter pattern)
info -> show detailed information on object or current node
  (args: optional path to object)
tree -> show configuration tree from an object or current node
  (args: optional path to object)
help -> show available command
desc[ription] -> show class desc of current node
desc <element>   -> show desc of element from current node
desc <value> -> show effect of value (for enum)
changes -> list unsaved changes
check [elt] -> run check current on current node or elt
fix [ ! | elt ]
  -> fix warnings in current node or of specified element or on all tree (with ! arg)
save -> save current changes
exit -> exit shell
';

my $desc_sub = sub {
    my $self = shift;
    my $obj  = $self->{current_node};
    my $res  = '';

    if (@_) {
        my $item;
        while ( $item = shift ) {
            if ( $obj->get_type() eq 'node' ) {
                my $type = $obj->element_type($item);
                my $elt  = $obj->fetch_element($item);
                my $help = $obj->get_help_as_text($item);
                $res .= "element $item (type $type)";
                $res .= ": " if $help;
                $res .= "\n" if $help =~ /\n/ or length($help) > 40 ;
                $res .= $help  . "\n" if $help;
                if ( $type eq 'leaf' and $elt->value_type eq 'enum' ) {
                    $res .= "  possible values: " . join( ', ', $elt->get_choice ) . "\n";
                }
            }
        }
    }
    else {
        $res = $obj->get_help_as_text();
    }
    return $res;
};

my $ll_sub = sub {
    my $self = shift;
    my @raw_args = @_;

    my @desc_opt = qw/check no/;

    my %opt = map { /^-(\w+)/ ? ($1 => 1) : () } @raw_args;
    push @desc_opt, hide_empty => 1 if $opt{nz} ;
    push @desc_opt, verbose    => 1 if $opt{v} ;

    my @args = grep {! /^-/ } @raw_args;
    push @args, '*' unless @args; # default action is to list all elements

    my $obj = $self->{current_node};
    for (@args) {s/\*/.*/g;} ;
    my $pattern = join ('|',@args);

    return $obj->describe( pattern => qr/^$pattern$/, @desc_opt );
};

my $cd_sub = sub {
    my $self = shift;
    my @cmds = @_;

    # convert usual cd_ism ( .. /foo) to grab syntax ( - ! foo)
    #map { s(^/)  (! );
    #	  s(\.\.)(-)g;
    #	  s(/)   ( )g;
    #      } @cmds ;

    my $new_node = $self->{current_node}->grab("@cmds");
    my $type     = $new_node->get_type;
    my $name     = $new_node->element_name;

    if ( defined $new_node && $type eq 'node' ) {
        $self->{current_node} = $new_node;
    }
    elsif ( defined $new_node && $type eq 'list' ) {
        print "Can't cd in a $type, please add an index (e.g. $name:0)\n";
    }
    elsif ( defined $new_node && $type eq 'hash' ) {
        print "Can't cd in a $type, please add an index (e.g. $name:foo)\n";
    }
    elsif ( defined $new_node && $type eq 'leaf' ) {
        print "Can't cd in a $type\n";
    }
    else {
        print "Cannot find @_\n";
    }

    return "";
};

my %run_dispatch = (
    help => sub { return $syntax; },
    set  => sub ($self, @args) {
        if (@args) {
            $self->{current_node}->load(join('',@args));
        }
        else {
            say "No command given.";
        }
        return "";
    },
    display => sub ($self, @args) {
        unless (@args) {
            say "Nothing to display";
            return;
        }
        return $self->{current_node}->grab_value(@args);
    },
    info => sub ($self, @args) {
        my $cnode = $self->{current_node};
        my $target = @args ? $cnode->grab(steps => [@args]) : $cnode;
        return join("\n", $target->get_info );
    },
    ls => sub ($self, @args) {
        my $target = $self->{current_node};
        my $pattern = '*';
        foreach my $item (@args) {
            # added weird comment to avoid problem with cpan-mode syntax highlight
            if ($item =~ /\*/ and not $item =~ /^["']/) { #") {
                $pattern = $item;
                last;
            }
            $target = $target->grab(steps => $item);
        }
        $pattern =~ s/\*/.*/g;

        my $i    = $self->{current_node}->instance;
        my @res  = $target->can('children') ? grep {/^$pattern$/} $target->children : ();
        return join( ' ', @res );
    },
    tree => sub ($self, @args) {
        my $i    = $self->{current_node}->instance;
        my $cnode = $self->{current_node};
        my $target = @args ? $cnode->grab(steps => [@args]) : $cnode;
        my @res  = $target->dump_tree( mode => 'user' );
        return join( ' ', @res );
    },
    delete => sub ($self, $del = '') {
        if ($del) {
            my ( $elt_name, $key ) = split /\s*:\s*/, $del;
            my $elt = $self->{current_node}->fetch_element($elt_name);
            if ( length($key) ) {
                $elt->delete($key);
            }
            else {
                $elt->store(undef);
            }
        }
        else {
            say "delete what ?";
        }
        return '';
    },
    clear => sub ($self, $elt_name) {
        if ($elt_name) {
            $self->{current_node}->fetch_element($elt_name)->clear();
        }
        else {
            say "Expected element name for clear command. I.e. one of ",
                join(' ',$self->{current_node}->get_element_name);
        }
        return '';
    },
    check => sub ($self, $elt_name) {
        if ($elt_name) {
            $self->{current_node}->fetch_element($elt_name)->check();
        }
        else {
            $self->{current_node}->check;
        }
        return '';
    },
    fix => sub ($self, $elt_name) {
        if ($elt_name eq '!') {
            $self->{root}->instance->apply_fixes;
        }
        elsif ($elt_name) {
            $self->{current_node}->fetch_element($elt_name)->apply_fixes;
        }
        else {
            $self->{current_node}->apply_fixes;
        }
        return '';
     },
    save => sub ($self) {
        $self->{root}->instance->write_back();
        return "done";
    },
    changes => sub ($self) {
        return $self->{root}->instance->list_changes;
    },
    ll          => $ll_sub,
    cd          => $cd_sub,
    description => $desc_sub,
    desc        => $desc_sub,
);

$run_dispatch{reset} = $run_dispatch{clear};
$run_dispatch{dump}  = $run_dispatch{tree};

sub simple_ui_commands {
    my @cmds = sort keys %run_dispatch;
    return @cmds;
}

sub new ($type, %args) {
    my $self = {};

    foreach my $p (qw/root title prompt/) {
        $self->{$p} = delete $args{$p}
            or croak "SimpleUI->new: Missing $p parameter";
    }

    $self->{current_node} = $self->{root};

    return bless $self, $type;
}

sub run_loop ($self) {

    my $user_cmd;
    print $self->prompt;
    ## no critic (InputOutput::ProhibitExplicitStdin)
    while ( defined( $user_cmd = <STDIN> ) ) {
        chomp $user_cmd;
        last if $user_cmd eq 'exit' or $user_cmd eq 'quit';
        my $res = $self->run($user_cmd);
        print $res, "\n" if defined $res;
        print $self->prompt;
    }
    print "\n";

    my $instance = $self->{root}->instance;
    if ( $instance->c_count ) {
        if ($instance->has_changes) {
            $instance->say_changes;
            print "write back data before exit ? (Y/n)";
            ## no critic (InputOutput::ProhibitExplicitStdin)
            $user_cmd = <STDIN>;
            $instance->write_back unless $user_cmd =~ /n/i;
            print "\n";
        }
    }
    return;
}

sub prompt ($self) {
    my $ret  = $self->{prompt} . ':';
    my $loc  = $self->{current_node}->location_short;
    $ret .= " $loc " if $loc;
    return $ret . '$ ';
}

sub run ($self, $user_cmd) {

    return '' unless $user_cmd =~ /\w/;

    my $re = $RE{delimited}{-delim=>q{'"}};
    my ( $action, @args ) = ( $user_cmd =~ /((?:[^\s"']|$re)+)/g );

    if ( defined $run_dispatch{$action} ) {
        my $res;
        my $ok = eval {
            $res = $run_dispatch{$action}->( $self, @args );
            1;
        };
        say $@->message unless $ok;
        return $res;
    }
    else {
        return "Unexpected command '$action'";
    }
}

sub list_cd_path ($self) {
    my $c_node = $self->{current_node};

    my @result;
    foreach my $elt_name ( $c_node->get_element_name ) {
        my $t = $c_node->element_type($elt_name);

        if ( $t eq 'list' or $t eq 'hash' ) {
            push @result,
                map { "$elt_name:$_" } $c_node->fetch_element($elt_name)->fetch_all_indexes;
        }
        else {
            push @result, $elt_name;
        }
    }

    return \@result;
}
1;

#ABSTRACT: Simple interface for Config::Model

=head1 SYNOPSIS

 use Config::Model;
 use Config::Model::SimpleUI ;

 # define configuration tree object
 my $model = Config::Model->new;
  $model->create_config_class(
    name    => "Foo",
    element => [
        [qw/foo bar/] => {
            type       => 'leaf',
            value_type => 'string'
        },
    ]
 );
 $model ->create_config_class (
    name => "MyClass",

    element => [

        [qw/foo bar/] => {
            type       => 'leaf',
            value_type => 'string'
        },
        hash_of_nodes => {
            type       => 'hash',     # hash id
            index_type => 'string',
            cargo      => {
                type              => 'node',
                config_class_name => 'Foo'
            },
        },
    ],
 ) ;

 my $inst = $model->instance(root_class_name => 'MyClass' );

 my $root = $inst->config_root ;

 # put data
 my $steps = 'foo=FOO hash_of_nodes:fr foo=bonjour -
   hash_of_nodes:en foo=hello ';
 $root->load( steps => $steps );

 my $ui = Config::Model::SimpleUI->new(
    root => $root ,
    title => 'My class ui',
    prompt => 'class ui',
 );

 # engage in user interaction
 $ui -> run_loop ;

 print $root->dump_tree ;

Once the synopsis above has been saved in C<my_test.pl>, you can do:

 $ perl my_test.pl
 class ui:$ ls
 foo  bar  hash_of_nodes
 class ui:$ ll hash_of_nodes
 name         value        type         comment
 hash_of_nodes <Foo>        node hash    keys: "en" "fr"

 class ui:$ cd hash_of_nodes:en

 class ui: hash_of_nodes:en $ ll
 name         value        type         comment
 foo          hello        string
 bar          [undef]      string

 class ui: hash_of_nodes:en $ set bar=bonjour

 class ui: hash_of_nodes:en $  ll
 name         value        type         comment
 foo          hello        string
 bar          bonjour      string

 class ui: hash_of_nodes:en $ ^D

At the end, the test script dumps the configuration tree. The modified
C<bar> value can be found in there:

 foo=FOO
 hash_of_nodes:en
   foo=hello
   bar=bonjour -
 hash_of_nodes:fr
   foo=bonjour - -

=head1 DESCRIPTION

This module provides a pure ASCII user interface using STDIN and
STDOUT.

=head1 USER COMMAND SYNTAX

=over

=item cd ...

Jump into node or value element. You can use C<< cd <element> >>,
C<< cd <elt:key> >> or C<cd -> to go up one node or C<cd !>
to go to configuration root.

=item set elt=value

Set a leaf value.

=item set elt:key=value

Set a leaf value locate in a hash or list element.

=item clear elt

Clear leaf value (set to C<undef>) or removed all elements of hash or list.

=item delete elt

Delete leaf value.

=item delete elt:key

Delete a list or hash element

=item display node_name elt:key

Display a value

=item ls [path] [ pattern ]

Show elements of current node or of a node pointed by path. Elements
can be filtered with a shell pattern. See inline help for more details.

=item ll [-nz] [-v] [ pattern ... ]

Describe elements of current node. Can be used with shell patterns or element names.
Skip empty element with C<-nz> option. Display more information with C<-v> option

=item tree [path]

Show configuration tree from current node or of a node pointed by path.

=item info [path]

Show debug information on current node or on the element pointed by
path. The debug information may show model parametersm default or computed
values.

=item help

Show available commands.

=item desc[ription]

Show class description of current node.

=item desc(elt)

Show description of element from current node.

=item desc(value)

Show effect of value (for enum)

=item changes

Show unsaved changes

=item check

Without parameter, show warnings starting from current node. With an
element name as parameter, do the same on the element.

=item fix

Try to fix warning starting from current node. With an element name as parameter,
do the same on the element. With "C<!>" as parameter, try to fix warnings starting
from root node by calling L<apply_fixes|Config::Model::Instance/apply_fixes> there.

=item exit

Exit shell

=back

=head1 CONSTRUCTOR

=head2 parameters

=over

=item root

Root node of the configuration tree

=item title

UI title

=item prompt

UI prompt. The prompt will be completed with the location of the
current node.

=back

=head1 Methods

=head2 run_loop

Engage in user interaction until user enters '^D' (CTRL-D).

=head1 AUTHOR

Dominique Dumont, (ddumont at cpan dot org)

=head1 SEE ALSO

L<Config::Model>,
L<Config::Model::Instance>,
L<Config::Model::Node>,

=cut
