The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Cog::App;
use Mo qw'build default';
extends 'Cog::Base';

use Getopt::Long qw(:config pass_through);
use IO::All;
use YAML::XS;
use Cwd 'abs_path';
use File::Basename;
use File::Spec;

has Name => sub {
        my $Name = ref($_[0]);
        $Name =~ s/::App\b//;
        return $Name if $Name =~ /^\w+$/;
        die "Can't determine 'Name' attribute for '${\ref($_[0])}'";
    };

has app_script => basename $0;

has app_root => sub {
        my $root = abs_path(dirname($_[0]->config_file));
        return $root if -d $root;
        die "Can't determine 'app_root' for '${\ref($_[0])}'";
    };

has build_root => sub {
        $_[0]->app_root;
    };

has webapp_root => sub {
        my $root = $_[0]->app_root;
        File::Spec->catdir($root, 'webapp');
    };

has config_file => sub {
        abs_path($_[0]->app_script . '.yaml');
    };

use constant config_class => 'Cog::Config';
use constant maker_class => 'Cog::Maker';
use constant webapp_class => '';
use constant runner_class => 'Cog::Runner';

sub plugins { [] };

has action => ();
has time => time();

# If we use the generic 'bin/cog' script, we need to determine which Cog
# application class we are representing.
sub get_app_class {
    my ($class, @argv) = @_;
    my $app_class;
    @ARGV = @argv;
    Getopt::Long::GetOptions(
        'app=s' => \$app_class,
    );
    $app_class ||= $ENV{COG_APP} || $class;
    unless ($app_class->can('new')) {
        eval "use $app_class; 1"
            or die $@;
    }
    die "$app_class is not a Cog::App application"
        unless $app_class->isa('Cog::App') and
            $app_class ne 'Cog::App';

    return $app_class;
}

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

    my $config_class = $self->config_class;
    eval "require $config_class"
        unless UNIVERSAL::can($config_class, 'new');

    my $config_file = $self->config_file;

    my $hash = $config_class->flatten_namespace(
        -e $config_file ? YAML::XS::LoadFile($config_file) : {}
    );
    my $app_class = $hash->{app_class} ||= ref($self);
    if ($app_class ne ref($self)) {
        eval "require $app_class; 1" or die $@;
        $self = $_[0] = $app_class->new();
    }

    $Cog::Base::initialize->(
        $self,
        $config_class->new(
            %$hash,
            app => $self,
            cli_args => [@ARGV],
        ),
    );
}

sub run {
    my $self = shift;

    $self->parse_command_args;

    my $action = $self->action;
    my $method = "handle_$action";

    my $function = $self->can($method)
        or die "'$action' is an invalid action\n";

    if ($action ne 'init') {
        die "Can't determine 'config_file' for '${\ref($_[0])}'"
          unless $self->config_file;
        $self->_chdir_root();
    }

    $function->($self);

    return 0;
}

sub parse_command_args {
    my $self = shift;
    my $argv = $self->config->cli_args;
    my $script = $self->app_script;
    $script =~ s!.*/!!;
    my $action = '';
    if ($script =~ /^(pre-commit|post-commit)$/) {
        $script =~ s/-/_/;
        $self->action($script);
    }
    elsif (@$argv and $argv->[0] =~ /^[\w\-]+$/) {
        $action = shift @$argv;
        $action =~ s/-/_/g;
    }
    elsif (not @$argv) {
        $action = 'help';
    }
    else {
        die "Invalid cog command. Can't parse these arguments: '@_'";
    }
    $self->action($action);
}

#-----------------------------------------------------------------------------
sub handle_help {
    my $self = shift;
    print $self->usage;
}

sub usage {
    my $self = shift;
    my $Name = $self->Name;
    my $name = $self->app_script;
    $name =~ s!.*/!!;
    return <<"...";
Usage: $name <command>

Commands:
    init   - Make current directory into a $Name app
    update - Update the app with the latest assets
    make   - Prepare the app content for the web
    start  - Start the local app server
    stop   - Stop the server

...
}

sub handle_init {
    my $self = shift;
    my $root = $self->app_root;
    die "Can't init. Cog environment already exists.\n"
        if $self->config->is_init;
    my $share = $self->config->find_share_dir($self);

    my $config_file = $self->config_file;
    if (not -e $config_file) {
        require Template::Toolkit::Simple;
        my $data = +{%$self};
        $data->{app_class} = ref($self);
        my $config = Template::Toolkit::Simple::tt()
            ->path(["$share/template/"])
            ->data($data)
            ->post_chomp
            ->render('config.yaml');
        io($config_file)->print($config);
    }

    $self->_chdir_root;

    my $Name = $self->Name;
    my $name = $self->app_script;
    $name =~ s!.*/!!;

    print <<"...";

$Name was successfully initialized in:

    $root

The next step is to edit:

    $config_file

Then run:

    $name update

...
}

sub handle_update {
    my $self = shift;
    my $root = $self->app_root;

    $self->maker->make_assets();

    my $Name = $self->Name;
    my $name = $self->app_script;
    $name =~ s!.*/!!;

    print <<"...";
$Name was successfully updated in the $root/ subdirectory.

Now run:

    $name make

...
}

sub handle_make {
    my $self = shift;
    $self->maker->make;
    my $Name = $self->Name;
    my $name = $self->app_script;
    $name =~ s!.*/!!;
    print <<"...";

$Name is up to date and ready to use.
To start the web server, run this command:

    $name start

...

}

sub handle_start {
    my $self = shift;
    my $Name = $self->Name;
    print <<"...";
$Name web server is starting up...

...
    my @args = @{$self->config->cli_args};
    unshift @args, ('-p' => $self->config->server_port)
        if $self->config->server_port;
    $self->runner->run(@args);
}

sub handle_stop {
    die 'TODO';
}

sub handle_edit {
    die 'TODO';
}

sub handle_clean {
    my $self = shift;
    $self->maker->make_clean;
    my $Name = $self->Name;
    my $name = $self->app_script;
    $name =~ s!.*/!!;
    print <<"...";

$Name is clean. To rebuild, run this command:

    $name update

...

}

# Put the App in the context of its defined root directory.
sub _chdir_root {
    my $self = shift;
    my $app_root = $self->app_root;
    chdir $app_root
      or die "Can't chdir into $app_root";
}

1;