package Mojo::Command;
use Mojo::Base -base;

require Cwd;
require File::Path;
require File::Spec;
require IO::File;

use Carp 'croak';
use Mojo::Server;
use Mojo::Template;
use Mojo::Loader;
use Mojo::Util qw/b64_decode camelize decamelize/;

has hint => <<"EOF";

See '$0 help COMMAND' for more information on a specific command.
EOF
has description => 'No description.';
has message     => <<"EOF";
usage: $0 COMMAND [OPTIONS]

Tip: CGI, FastCGI and PSGI environments can be automatically detected very
     often and work without commands.

These commands are currently available:
EOF
has namespaces => sub { ['Mojo::Command'] };
has quiet      => 0;
has renderer   => sub { Mojo::Template->new };
has usage      => "usage: $0\n";

# Cache
my $CACHE = {};

sub chmod_file {
  my ($self, $path, $mod) = @_;
  chmod $mod, $path or croak qq/Can't chmod path "$path": $!/;
  $mod = sprintf '%lo', $mod;
  print "  [chmod] $path $mod\n" unless $self->quiet;
  return $self;
}

sub chmod_rel_file {
  my ($self, $path, $mod) = @_;
  $self->chmod_file($self->rel_file($path), $mod);
}

sub class_to_file {
  my ($self, $class) = @_;
  $class =~ s/:://g;
  decamelize $class;
  return $class;
}

sub class_to_path {
  my ($self, $class) = @_;
  my $path = join '/', split /::/, $class;
  return "$path.pm";
}

sub create_dir {
  my ($self, $path) = @_;

  # Exists
  if (-d $path) {
    print "  [exist] $path\n" unless $self->quiet;
    return $self;
  }

  # Create
  File::Path::mkpath($path) or croak qq/Can't make directory "$path": $!/;
  print "  [mkdir] $path\n" unless $self->quiet;
  return $self;
}

sub create_rel_dir {
  my ($self, $path) = @_;
  $self->create_dir($self->rel_dir($path));
}

sub detect {
  my ($self, $guess) = @_;

  # PSGI (Plack only for now)
  return 'psgi' if defined $ENV{PLACK_ENV};

  # CGI
  return 'cgi'
    if defined $ENV{PATH_INFO} || defined $ENV{GATEWAY_INTERFACE};

  # No further detection if we have a guess
  return $guess if $guess;

  # FastCGI (detect absence of WINDIR for Windows and USER for UNIX)
  return 'fastcgi' if !defined $ENV{WINDIR} && !defined $ENV{USER};

  # Nothing
  return;
}

sub get_all_data {
  my ($self, $class) = @_;
  $class ||= ref $self;

  # Refresh or use cached data
  my $d = do { no strict 'refs'; \*{"$class\::DATA"} };
  return $CACHE->{$class} unless fileno $d;
  seek $d, 0, 0;
  my $content = join '', <$d>;
  close $d;

  # Ignore everything before __DATA__ (windows will seek to start of file)
  $content =~ s/^.*\n__DATA__\r?\n/\n/s;

  # Ignore everything after __END__
  $content =~ s/\n__END__\r?\n.*$/\n/s;

  # Split
  my @data = split /^@@\s+(.+?)\s*\r?\n/m, $content;
  shift @data;

  # Find data
  my $all = $CACHE->{$class} = {};
  while (@data) {
    my ($name, $content) = splice @data, 0, 2;
    b64_decode $content if $name =~ s/\s*\(\s*base64\s*\)$//;
    $all->{$name} = $content;
  }

  return $all;
}

sub get_data {
  my ($self, $data, $class) = @_;
  my $all = $self->get_all_data($class);
  return $all->{$data};
}

# "You don’t like your job, you don’t strike.
#  You go in every day and do it really half-assed. That’s the American way."
sub help {
  my $self = shift;
  print $self->usage;
  exit;
}

sub rel_dir {
  my ($self, $path) = @_;
  my @parts = split /\//, $path;
  return File::Spec->catdir(Cwd::getcwd(), @parts);
}

sub rel_file {
  my ($self, $path) = @_;
  my @parts = split /\//, $path;
  return File::Spec->catfile(Cwd::getcwd(), @parts);
}

sub render_data {
  my $self = shift;
  my $data = shift;
  $self->renderer->render($self->get_data($data), @_);
}

sub render_to_file {
  my $self = shift;
  my $data = shift;
  my $path = shift;
  $self->write_file($path, $self->render_data($data, @_));
  return $self;
}

sub render_to_rel_file {
  my $self = shift;
  my $data = shift;
  my $path = shift;
  $self->render_to_file($data, $self->rel_dir($path), @_);
}

sub run {
  my ($self, $name, @args) = @_;

  # Application loader
  return Mojo::Server->new->app if defined $ENV{MOJO_APP_LOADER};

  # Try to detect environment
  $name = $self->detect($name) unless $ENV{MOJO_NO_DETECT};

  # Run command
  if ($name && $name =~ /^\w+$/ && ($name ne 'help' || $args[0])) {

    # Help
    my $help = $name eq 'help' ? 1 : 0;
    $name = shift @args if $help;
    $help = 1           if $ENV{MOJO_HELP};

    # Try all namespaces
    my $module;
    for my $namespace (@{$self->namespaces}) {

      # Generate module
      my $camelized = $name;
      camelize $camelized;
      my $try = "$namespace\::$camelized";

      # Load
      if (my $e = Mojo::Loader->load($try)) {

        # Module missing
        next unless ref $e;

        # Real error
        die $e;
      }

      # Module is a command
      next unless $try->can('new') && $try->can('run');

      # Found
      $module = $try;
      last;
    }

    # Command missing
    die qq/Command "$name" missing, maybe you need to install it?\n/
      unless $module;

    # Run
    my $command = $module->new;
    return $help ? $command->help : $command->run(@args);
  }

  # Test
  return $self if $ENV{HARNESS_ACTIVE};

  # Try all namespaces
  my $commands = [];
  my $seen     = {};
  for my $namespace (@{$self->namespaces}) {

    # Search
    if (my $modules = Mojo::Loader->search($namespace)) {
      for my $module (@$modules) {

        # Load
        if (my $e = Mojo::Loader->load($module)) { die $e }

        # Seen
        my $command = $module;
        $command =~ s/^$namespace\:://;
        push @$commands, [$command => $module]
          unless $seen->{$command};
        $seen->{$command} = 1;
      }
    }
  }

  # Print overview
  print $self->message;

  # Make list
  my $list = [];
  my $len  = 0;
  foreach my $command (@$commands) {

    # Generate name
    my $name = $command->[0];
    decamelize $name;

    # Add to list
    my $l = length $name;
    $len = $l if $l > $len;
    push @$list, [$name, $command->[1]->new->description];
  }

  # Print list
  foreach my $command (@$list) {
    my $name        = $command->[0];
    my $description = $command->[1];
    my $padding     = ' ' x ($len - length $name);
    print "  $name$padding   $description";
  }
  print $self->hint;

  return $self;
}

sub start {
  my $self = shift;

  # Executable
  $ENV{MOJO_EXE} ||= (caller)[1] if $ENV{MOJO_APP};

  # Run
  my @args = @_ ? @_ : @ARGV;
  ref $self ? $self->run(@args) : $self->new->run(@args);
}

sub write_file {
  my ($self, $path, $data) = @_;

  # Directory
  my @parts = File::Spec->splitdir($path);
  pop @parts;
  my $dir = File::Spec->catdir(@parts);
  $self->create_dir($dir);

  # Write unbuffered
  croak qq/Can't open file "$path": $!/
    unless my $file = IO::File->new("> $path");
  $file->syswrite($data);
  print "  [write] $path\n" unless $self->quiet;

  return $self;
}

sub write_rel_file {
  my ($self, $path, $data) = @_;
  $self->write_file($self->rel_file($path), $data);
}

1;
__END__

=head1 NAME

Mojo::Command - Command Base Class

=head1 SYNOPSIS

  # Camel case command name
  package Mojo::Command::Mycommand;

  # Subclass
  use Mojo::Base 'Mojo::Command';

  # Take care of command line options
  use Getopt::Long 'GetOptions';

  # Short description
  has description => <<'EOF';
  My first Mojo command.
  EOF

  # Short usage message
  has usage => <<"EOF";
  usage: $0 mycommand [OPTIONS]

  These options are available:
    --something   Does something.
  EOF

  # <suitable Futurama quote here>
  sub run {
    my $self = shift;

    # Handle options
    local @ARGV = @_ if @_;
    GetOptions('something' => sub { $something = 1 });

    # Magic here! :)
  }

=head1 DESCRIPTION

L<Mojo::Command> is an abstract base class for L<Mojo> commands.

See L<Mojolicious::Commands> for a list of commands that are available by
default.

=head1 ATTRIBUTES

L<Mojo::Command> implements the following attributes.

=head2 C<description>

  my $description = $command->description;
  $command        = $command->description('Foo!');

Short description of command, used for the command list.

=head2 C<hint>

  my $hint  = $commands->hint;
  $commands = $commands->hint('Foo!');

Short hint shown after listing available commands.

=head2 C<message>

  my $message = $commands->message;
  $commands   = $commands->message('Hello World!');

Short usage message shown before listing available commands.

=head2 C<namespaces>

  my $namespaces = $commands->namespaces;
  $commands      = $commands->namespaces(['Mojolicious::Commands']);

Namespaces to search for available commands, defaults to L<Mojo::Command>.

=head2 C<quiet>

  my $quiet = $command->quiet;
  $command  = $command->quiet(1);

Limited command output.

=head2 C<usage>

  my $usage = $command->usage;
  $command  = $command->usage('Foo!');

Usage information for command, used for the help screen.

=head1 METHODS

L<Mojo::Command> inherits all methods from L<Mojo::Base> and implements the
following new ones.

=head2 C<chmod_file>

  $command = $command->chmod_file('/foo/bar.txt', 0644);

Portably change mode of a file.

=head2 C<chmod_rel_file>

  $command = $command->chmod_rel_file('foo/bar.txt', 0644);

Portably change mode of a relative file.

=head2 C<class_to_file>

  my $file = $command->class_to_file('Foo::Bar');

Convert a class name to a file.

  FooBar -> foo_bar

=head2 C<class_to_path>

  my $path = $command->class_to_path('Foo::Bar');

Convert class name to path.

  Foo::Bar -> Foo/Bar.pm

=head2 C<create_dir>

  $command = $command->create_dir('/foo/bar/baz');

Portably create a directory.

=head2 C<create_rel_dir>

  $command = $command->create_rel_dir('foo/bar/baz');

Portably create a relative directory.

=head2 C<detect>

  my $env = $commands->detect;
  my $env = $commands->detect($guess);

Try to detect environment.

=head2 C<get_all_data>

  my $all = $command->get_all_data;
  my $all = $command->get_all_data('Some::Class');

Extract all embedded files from the C<DATA> section of a class.

=head2 C<get_data>

  my $data = $command->get_data('foo_bar');
  my $data = $command->get_data('foo_bar', 'Some::Class');

Extract embedded file from the C<DATA> section of a class.

=head2 C<help>

  $command->help;

Print usage information for command.

=head2 C<rel_dir>

  my $path = $command->rel_dir('foo/bar');

Portably generate an absolute path from a relative UNIX style path.

=head2 C<rel_file>

  my $path = $command->rel_file('foo/bar.txt');

Portably generate an absolute path from a relative UNIX style path.

=head2 C<render_data>

  my $data = $command->render_data('foo_bar', @arguments);

Render a template from the C<DATA> section of the command class.

=head2 C<render_to_file>

  $command = $command->render_to_file('foo_bar', '/foo/bar.txt');

Render a template from the C<DATA> section of the command class to a file.

=head2 C<render_to_rel_file>

  $command = $command->render_to_rel_file('foo_bar', 'foo/bar.txt');

Portably render a template from the C<DATA> section of the command class to a
relative file.

=head2 C<run>

  $commands->run;
  $commands->run(@ARGV);

Load and run commands.

=head2 C<start>

  Mojo::Command->start;
  Mojo::Command->start(@ARGV);

Start the command line interface.

=head2 C<write_file>

  $command = $command->write_file('/foo/bar.txt', 'Hello World!');

Portably write text to a file.

=head2 C<write_rel_file>

  $command = $command->write_rel_file('foo/bar.txt', 'Hello World!');

Portably write text to a relative file.

=head1 SEE ALSO

L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>.

=cut