#!/usr/local/bin/perl
package App::Xslate;
use Any::Moose;
use Any::Moose '::Util::TypeConstraints';
use Cwd ();
use File::Basename ();
use File::Path ();

with any_moose 'X::Getopt';

my $getopt_traits = [any_moose('X::Getopt::Meta::Attribute::Trait')];

has cache_dir => (
    documentation => 'Directory the cache files will be saved in',
    cmd_aliases   => [qw(c)],
    is            => 'ro',
    isa           => 'Str',
    predicate     => 'has_cache_dir',
    traits        => $getopt_traits,
);

has cache => (
    documentation => 'Cache level',
    cmd_aliases   => [qw(a)],
    is            => 'ro',
    isa           => 'Int',
    predicate     => 'has_cache',
    traits        => $getopt_traits,
);

has module => (
    documentation => 'Modules templates will use',
    cmd_aliases   => [qw(M)],
    is            => 'ro',
    isa           => 'ArrayRef[Str]',
    predicate     => 'has_module',
    traits        => $getopt_traits,
);

has input_layer => (
    documentation => 'Input layer for reading',
    cmd_aliases   => [qw(l)],
    is            => 'ro',
    isa           => 'Str',
    predicate     => 'has_input_layer',
    traits        => $getopt_traits,
);

has path => (
    documentation => 'Include paths',
    cmd_aliases   => [qw(I)],
    is            => 'ro',
    isa           => 'ArrayRef[Str]',
    predicate     => 'has_path',
    traits        => $getopt_traits,
);

has syntax => (
    documentation => 'Template syntax (e.g. "TTerse")',
    cmd_aliases   => [qw(s)],
    is            => 'ro',
    isa           => 'Str',
    predicate     => 'has_syntax',
    traits        => $getopt_traits,
);

has escape => (
    documentation => 'Escaping mode ("html" or "none")',
    cmd_aliases   => [qw(p)],
    is            => 'ro',
    isa           => 'Str',
    predicate     => 'has_escape',
    traits        => $getopt_traits,
);

has verbose => (
    documentation => 'Warning level (default to 2)',
    cmd_aliases   => [qw(w)],
    is            => 'ro',
    isa           => 'Str',
    default       => 2,
    predicate     => 'has_verbose',
    traits        => $getopt_traits,
);

# --ignore=pattern
any_moose('X::Getopt::OptionTypeMap')->add_option_type_to_map(
    RegexpRef => '=s'
);
coerce 'RegexpRef' => from 'Str' => via { qr/$_/ };
has ignore => (
    documentation => 'Regular expression the process will ignore',
    cmd_aliases   => [qw(i)],
    is            => 'ro',
    isa           => 'RegexpRef',
    coerce        => 1,
    traits        => $getopt_traits,
);

# --suffix old=new
has suffix => (
    documentation => 'Output suffix mapping (e.g. tx=html)',
    cmd_aliases   => [qw(x)],
    is            => 'ro',
    isa           => 'HashRef',
    traits        => $getopt_traits,
);

has dest => (
    documentation => 'Destination directry',
    cmd_aliases   => [qw(o)],
    is            => 'ro',
    isa           => 'Str',
    default       => Cwd::cwd(),
    traits        => $getopt_traits,
);

has define => (
    documentation => 'Define template variables (e.g. foo=bar)',
    cmd_aliases   => [qw(D)],
    is            => 'ro',
    isa           => 'HashRef',
    predicate     => 'has_define',
    traits        => $getopt_traits,
);

has eval => (
    documentation => 'One line of templates',
    cmd_aliases   => [qw(e)],
    is            => 'ro',
    isa           => 'Str',
    predicate     => 'has_eval',
    traits        => $getopt_traits,
);

has engine => (
    documentation => 'Template engine',
    cmd_aliases   => [qw(E)],
    is            => 'ro',
    isa           => 'Str',
    default       => 'Text::Xslate',
    traits        => $getopt_traits,
);

has debug => (
    documentation => 'Debugging flags',
    cmd_aliases   => ['d'],
    is            => 'ro',
    isa           => 'Str',
    predicate     => 'has_debug',
    traits        => $getopt_traits,
);

has version => (
    documentation => 'Print version information',
    is            => 'ro',
    isa           => 'Bool',
);

sub run {
    my $self = shift;

    my $targets = $self->extra_argv;

    my %args;
    foreach my $field qw(
        cache_dir cache module input_layer path syntax
        escape verbose
            ) {
        my $method = "has_$field";
        $args{ $field } = $self->$field if $self->$method;
    }

    local $ENV{XSLATE} = $self->debug
        if $self->has_debug;

    require Text::Xslate;
    Any::Moose::load_class($self->engine);

    if($self->version) {
        $self->print_version();
        return;
    }

    my $xslate = $self->engine->new(%args);

    if($self->has_eval) {
        my %vars;
        if($self->has_define){
            %vars = %{$self->define};
        }
        $vars{ARGV} = $targets;
        print $xslate->render_string($self->eval, \%vars), "\n";
        return;
    }

    foreach my $target (@$targets) {
        # XXX if you have a directory, just pushed that into the list of
        # path in the xslate object
        if ( -d $target ) {
            local $self->{__process_base} = scalar(File::Spec->splitdir($target));
            local $xslate->{path} = [ $target, @{ $xslate->{path} || [] } ];
            $self->process_tree( $xslate, $target );
        } else {
            my $dirname = File::Basename::dirname($target);
            local $self->{__process_base} = scalar(File::Spec->splitdir($dirname));
            local $xslate->{path} = [ $dirname, @{ $xslate->{path} || [] } ];
            $self->process_file( $xslate, $target );
        }
    }
}

sub process_tree {
    my ($self, $xslate, $dir) = @_;

    opendir( my $fh, $dir ) or die "XXX";

    while (my $e = readdir $fh) {
        next if $e =~ /^\.+$/;
        my $target = File::Spec->catfile( $dir, $e );
        if (-d $target) {
            $self->process_tree( $xslate, $target );
        } else {
            $self->process_file( $xslate, $target );
        }
    }
}

sub process_file {
    my ($self, $xslate, $file) = @_;

    if ( my $ignore = $self->ignore ) {
        if ($file =~ /$ignore/) {
            return;
        }
    }

    my $suffix_map = $self->suffix;
    my $dest = $self->dest;

    my ($suffix) = ($file =~ /\.([^\.]+)$/);

    my $filearg = $file;
    if (my $base = $self->{__process_base}) {
        my @comps = File::Spec->splitdir( File::Basename::dirname($file) );
        splice @comps, 0, $base;
        $filearg = File::Spec->catfile( @comps, File::Basename::basename($file) );
    }
    my $outfile = File::Spec->catfile( $dest, $filearg );
    if ($suffix_map && (my $replace = $suffix_map->{ $suffix })) {
        $outfile =~ s/$suffix$/$replace/;
    }

    my $dir = File::Basename::dirname( $outfile );
    if (! -d $dir) {
        if (! File::Path::mkpath( $dir )) {
            die "Could not create directory $dir: $!";
        }
    }


    my $rendered = $xslate->render( $filearg, $self->define );

    my $fh;
    open( $fh, '>', $outfile )
        or die "Could not open file $outfile for writing: $!";

    print $fh $rendered;

    close $fh or warn "Could not close file $outfile: $!";
}

sub print_version {
    my($self) = @_;
    printf <<'END',
%s (%s) on Text::Xslate/%s, Perl/%vd, Any::Moose/%d (%s/%s).
END
    $0, ref($self),
    Text::Xslate->VERSION,
    $^V,
    Any::Moose->VERSION,
    any_moose(), any_moose()->VERSION,
    ;
}

package main;
use strict;

sub main {
    my $app = App::Xslate->new_with_options();
    $app->run();
}

main() unless caller();

__END__

=head1 NAME

xslate - Process Xslate Templates

=head1 SYNOPSIS

    xslate
        # Text::Xslate options
        [ --cache ]
        [ --cache_dir=/path/to/dir ]
        [ --module=Module ... ]
        [ --input_layer ... ]
        [ --path=/path/to/templates ... ]
        [ --syntax=moniker ]
        [ --escape=mode ]
        [ --verbose=level ]

        # Output options
        [ --dest=/path/to/destination ]
        [ --ignore=pattern ... ]
        [ --suffix old=new ... ]
        [ --define name=value ... ]

        # Target to process
        target

    # testing mode
    xslate -e 'Hello, <: $ARGV[0] :> world!' Xslate

=head1 DESCRIPTION

The xslate script is used to process entire directory trees containing
template files.

=head1 ARGUMENTS

=head2 target

Specifies the target to be processed by Xslate.

If the target is a file, the file is processed, and xslate will exit immediately. If the target is a directory, then the directory is traversed and each file found is processed via xslate.

=head1 AUTHOR

Maki, Daisuke (lestrrat)

=head1 SEE ALSO

L<Text::Xslate>

=cut
