###
### $Release: 0.0100 $
### $Copyright: copyright(c) 2009-2011 kuwata-lab.com all rights reserved. $
### $License: MIT License $
###

use strict;
use warnings;
no warnings 'uninitialized';   # suppress warning 'Use of uninitialized value'


package Kook::Main;
use Data::Dumper;
use File::Basename;

use Kook;
use Kook::Util ('read_file');
use Kook::Cookbook;
use Kook::Kitchen;

sub new {
    my ($class, $argv, $command) = @_;
    if (! $argv || ! @$argv) {
        my @a = @ARGV;    # how to copy array?
        $argv = \@a;
    }
    $command = basename($0) unless $command;
    my $this = {
        argv    => $argv,
        command => $command,
    };
    return bless $this, $class;
}

sub invoke {
    my ($this) = @_;
    die "invoke(): not implemented yet.";
}

sub main {
    my ($this) = @_;
    die "invoke(): not implemented yet.";
}

sub _load_property_file {
    my ($this, $filename) = @_;
    $filename = $Kook::Config::PROPERTIES_FILENAME unless $filename;
    my $props = {};
    if (-f $filename) {
        my $content = read_file($filename);
        my $ret = eval("# line 1 \"$filename\"\n" . $content);
        if ($ret) {
            my %tmp = (%$props, %$ret);
            $props = \%tmp;
        }
        #map { $props->{$_} = $ret->{$_} } keys %$ret if $ret;
        #@$props { keys %$ret } = values %$ret if $ret;
    }
    return $props;
}


package Kook::MainCommand;
our @ISA = ('Kook::Main');
use Data::Dumper;
use Cwd ('realpath');

use Kook::Util ('repr');

my $optdef_strs = [
    "-h:      help",
    #"--help: help",
    "-V:      version",
    "-D[N]:   debug level (default: 1)",
    "-q:      quiet",
    "-f file: kookbook",
    "-F:      forcedly",
    "-n:      not execute (dry run)",
    "-l:      list public recipes",
    "-L:      list all recipes",
    "-R:      search Kookbook in parent directory recursively",
    "--name=value: property name and value",
    "--name:       property name and value(=True)",
];

sub invoke {
    my ($this) = @_;
    ## parse command-line options
    my $optparser = Kook::Util::CommandOptionParser->new($optdef_strs);
    my ($opts, $longopts, $rests) = $optparser->parse2($this->{argv}, $this->{command});
    #print "*** debug: opts=", Dumper($opts);
    #print "*** debug: longopts=", Dumper($longopts);
    #print "*** debug: rests=", Dumper($rests);
    ## handle options
    if ($opts->{h} || $longopts->{help} == 1) {
        print "$this->{command} - build tool like Make, Rake, Ant, or Cook\n";
        print $optparser->help();
        return 0;
    }
    if ($opts->{V}) {
        print $Kook::VERSION, "\n";
        return 0;
    }
    if ($opts->{q}) { $Kook::Config::VERBOSE = 0; }
    if ($opts->{F}) { $Kook::Config::FORCED  = 1; }
    if ($opts->{n}) { $Kook::Config::NOEXEC  = 1; }
    if ($opts->{D}) {
        my $v = $opts->{D};
        $v =~ /^\d+$/  or die "-D$v: integer is required.\n";
        $Kook::Config::DEBUG_LEVEL = 0 + $v;
    }
    ## find cookbook
    my $bookname = $opts->{f} || $Kook::Config::COOKBOOK_FILENAME;
    my $bookpath = $bookname;
    if ($opts->{R}) {
        while (! -e $bookpath) {
            my $parent = "../$bookpath";
            last if realpath($parent) eq realpath($bookpath);
            $bookpath = $parent;
        }
    }
    my $s = $opts->{f} ? '-f ' : '';
    -e $bookpath  or die "$s$bookname: not found.\n";
    -f $bookpath  or die "$s$bookname: not a file.\n";
    ## change directory if cookbook is in parent directory
    if ($bookname ne $bookpath) {
        my $path = substr($bookpath, 0, - length($bookname));
        chdir $path;
    }
    ## property file
    my $props = $this->_load_property_file();
    map { $props->{$_} = $longopts->{$_} } keys %$longopts if %$longopts;
    ## create cookbook
    my $cookbook = Kook::Cookbook->new($bookname, $props);
    ## list recipes
    if ($opts->{l} || $opts->{L}) {
        $this->_list_recipes($cookbook, $opts);
        return 0;
    }
    ## get default product if no argument
    if (! @$rests) {
        my $default_product = $cookbook->default_product();
        unless ($default_product) {
            my $command = $this->{command};
            print STDERR "*** $command: target is not given.\n";
            print STDERR "*** '$command -l' or '$command -L' shows recipes and properties.\n";
            print STDERR "*** (or set '\$kook->{default}' in your kookbook.)\n";
            return 1;
        }
        $rests = [$default_product];
    }
    ## start cooking
    my $kitchen = Kook::Kitchen->new($cookbook);
    $kitchen->start_cooking(@$rests);
    ##
    return 0;
}

sub _list_recipes {
    my ($this, $cookbook, $opts)  = @_;
    my $show_all = $opts->{L};
    my $format   = $Kook::Config::RECIPE_LIST_FORMAT;  # "  %-20s: %s\n";
    my $format2  = $Kook::Config::RECIPE_OPTS_FORMAT;  # "    %-20s  %s\n";
    ## find default recipe
    my $default_recipe;
    my $default = $cookbook->default_product();
    if ($default) {
        $default_recipe = $cookbook->find_recipe($default);
        if (! $default_recipe) {
            print STDOUT "*** \$kook->{default} = '$default': recipe not found.\n";
        }
    }
    ## properties
    print "Properties:\n";
    for (@{$cookbook->{property_tuples}}) {
        my ($name, $value, $desc) = @$_;
        next if ! $show_all && $desc == -1;
        printf($format, $name, repr($value));
    }
    print "\n";
    ## task and file recipes
    my @task_recipes = ( @{$cookbook->{specific_task_recipes}}, @{$cookbook->{generic_task_recipes}} );
    my @file_recipes = ( @{$cookbook->{specific_file_recipes}}, @{$cookbook->{generic_file_recipes}} );
    my $kind = $default_recipe ? $default_recipe->{kind} : undef;
    my $s = " (default=$default)";
    my $title;
    $title = "Task recipes" . ($kind eq 'task' ? $s : '') . ':';
    $this->__list_recipes($title, \@task_recipes, $format, $format2, $show_all);
    print "\n";
    $title = "File recipes" . ($kind eq 'file' ? $s : '') . ':';
    $this->__list_recipes($title, \@file_recipes, $format, $format2, $show_all);
    print "\n";
    ## tips
    if (! $opts->{q}) {
        my $tip = $this->get_tip($default);
        print "(Tips: $tip)\n";
    }
    return 0;
}

sub __list_recipes {
    my ($this, $title, $recipes, $format, $format2, $show_all) = @_;
    print $title, "\n";
    for my $recipe (@$recipes) {
        next unless $show_all || $recipe->{desc};
        printf($format, $recipe->{product}, $recipe->{desc});
        next unless $Kook::Config::VERBOSE;
        next unless $recipe->{spices} && @{$recipe->{spices}};
        my $optparser = Kook::Util::CommandOptionParser->new($recipe->{spices});
        for (@{$optparser->{helps}}) {
            my ($opt, $desc) = @$_;
            printf($format2, $opt, $desc) if $desc;
        }
    }
}

our $TIPS = [
    "you can set '\$kook->{default}' in your kookbook.",
    "you can override properties with '--propname=propvalue'.",
    "it is able to separate properties into 'Properties.pl' file.",
    "try 'kk' command which is shortcat for 'plkook' command.",
    "ingreds=>['\$(1).c', if_exists('\$(1).h')] is a friend of C programmer.",
    #"'c%\"gcc $(ingred)\"' is more natural than '\"gcc %s\" % c.ingreds[0]'.",
];

sub get_tip {
    my ($this, $default_product) = @_;
    my $len = @$TIPS;
    my $index = int(rand() * $len);
    if ($default_product) {
        ## don't display tip about default product
        $index ||= int(rand() * $len);
        $index ||= 1;
    }
    else {
        ## show tip about default product frequently
        $index = 0 if rand() < 0.5;
    }
    return $TIPS->[$index];
}

sub main {
    my ($this) = @_;
    my $status;
    eval {
        $status = $this->invoke();
    };
    if ($@) {
        print STDERR $@;
        $status = 1;
    }
    return $status;
}


package Kook::MainApplication;
our @ISA = ('Kook::Main');
use strict;
use Data::Dumper;
use File::Basename;

use Kook::Misc ('_debug', '_trace');
use Kook::Util ('repr', 'first');

my $optdef_strs2 = [
    "-h:      help",
    #"--help: help",
    #"-V:      version",
    "-D[N]:   debug level (default: 1)",
    #"-q:      quiet",
    #"-f file: kookbook",
    "-F:      forcedly",
    #"-l:      list public recipes",
    #"-L:      list all recipes",
    #"-n:      not invoke (dry run)",
    "-X file:",
    "--name=value: property name and value",
    "--name:       property name and value(=1)",
];

sub invoke {
    my ($this) = @_;
    my $quiet = $Kook::Config::VERBOSE;
    $Kook::Config::VERBOSE = 0;
    eval {
        $this->_invoke();
    };
    $Kook::Config::VERBOSE = $quiet;
    die $@ if $@;
}

sub _invoke {
    my ($this) = @_;
    ## parse command-line options
    my $optparser = Kook::Util::CommandOptionParser->new($optdef_strs2);
    my ($opts, $longopts, $rests) = $optparser->parse2($this->{argv}, $this->{command});
    _trace("opts="     . repr($opts));
    _trace("longopts=" . repr($longopts));
    _trace("rests="    . repr($rests));
    ## handle options
    my $bookname = $opts->{X}  or die "-X: script filename required.\n";
    $this->{command} = basename($bookname);
    ## property file
    my $props = $this->_load_property_file();
    if (%$longopts) {
        map { $props->{$_} = $longopts->{$_} } keys %$longopts;
    }
    ## help
    if ($opts->{h} || $longopts->{help} == 1) {
        my $target = $rests->[0];
        $this->_show_help($bookname, $props, $target, $optparser);
        return 0;
    }
    ## other options
    #if ($opts->{V}) {
    #    print $Kook::RELEASE, "\n";
    #    return 0;
    #}
    #if ($opts->{q}) { $Kook::Config::VERBOSE = 0; }
    if ($opts->{F}) { $Kook::Config::FORCED = 1; }
    if ($opts->{D}) {
        $opts->{D} =~ /^\d+$/  or die "-D$opts->{D}: integer is required.\n";
        $Kook::Config::DEBUG_LEVEL = 0 + $opts->{D};
    }
    ## create cookbook
    my $cookbook = Kook::Cookbook->new($bookname, $props);
    if (! @$rests) {
        my $default_product = $cookbook->default_product()  or
            die "sub-command is required (try '-h' to show all sub-commands).\n";
        $rests = [$default_product];
    }
    ## check whether recipe exists or not
    my $target = $rests->[0];
    my $recipes = $cookbook->{specific_task_recipes};
    my $recipe = first { $_->{product} eq $target } @$recipes  or
        die "$target: sub-command not found.\n";
    ## start cooking
    my $kitchen = Kook::Kitchen->new($cookbook);
    $kitchen->start_cooking(@$rests);
    ##
    return 0;
}

sub _show_help {
    my ($this, $bookname, $props, $target, $optparser) = @_;
    my $cookbook = Kook::Cookbook->new($bookname, $props);
    $target ? $this->_show_help_for($cookbook, $target)
            : $this->_show_help_all($cookbook, $optparser);
}

sub _show_help_for {
    my ($this, $cookbook, $target) = @_;
    my $recipes = $cookbook->{specific_task_recipes};
    my $recipe = first { $_->{product} eq $target } @$recipes  or
        die "$target: sub command not found.\n";
    print "$this->{command} $target - $recipe->{desc}\n";
    if ($recipe->{spices} && @{$recipe->{spices}}) {
        my $optparser = Kook::Util::CommandOptionParser->new($recipe->{spices});
        for (@{$optparser->{helps}}) {
            my ($opt, $desc) = @$_;
            printf $Kook::Config::SUBCOMMANDS_FORMAT, $opt, $desc if $desc;
        }
    }
}

sub _show_help_all {
    my ($this, $cookbook, $optparser) = @_;
    my $recipes = $cookbook->{specific_task_recipes};
    my $desc = $cookbook->{desc};
    print "$this->{command} - $desc\n";
    if (0) {
        print "\n";
        print "global-options:\n";
        print $optparser->help();
    }
    print "\n";
    print "sub-commands:\n";
    for my $recipe (@$recipes) {
        printf $Kook::Config::OPTION_HELP_FORMAT, $recipe->{product}, $recipe->{desc} if $recipe->{desc};
    }
    print "\n";
    print "(Type '$this->{command} -h subcommand' to show options of sub-commands.)\n";
}

sub main {
    my ($this) = @_;
    return $this->invoke();
}


1;