package Newslash::Plugin::JavaScriptLoader;
use Mojo::Base 'Newslash::Plugin::Preprocessor';

use File::Spec;
use File::Basename qw(basename);
use Encode;
use Mojo::JSON qw(decode_json encode_json);
use List::Util qw(any);
use Mojo::File qw(path);

use constant CACHE_KEY => "JavaScriptLoader";
use constant JS_CONTENT_TYPE => "application/javascript";

sub _generate_content {
    my ($self, $pathname) = @_;
    $pathname = $self->_strip_md5_from_path($pathname);

    my $js_content;
    if ($self->{bundle_config}->{$pathname}) {
        # target is bundle, so pack them
        $js_content = $self->_pack_files($pathname);
    }
    else {
        # target is single file, load it
        $js_content = $self->_load_js($pathname);
    }

    if (!defined $js_content) {
        $self->{app}->log->error("JavaScriptLoader: generate contentfailed: $pathname");
        return;
    }
    return $self->_build_content_object($js_content, $pathname, JS_CONTENT_TYPE);
}

sub _load_js {
    my ($self, $rel_pathname) = @_;

    # check if file exists
    my $abs_path = $self->_get_absolute_path($rel_pathname);
    return if (!-f $abs_path || !-r $abs_path);

    # load file
    open( my $fh, "<:utf8", $abs_path ) or return;
    my $js_body = do { local($/); <$fh> } ;
    close($fh);

    return $js_body;
}

sub _pack_files {
    my ($self, $pathname) = @_;
    $self->{app}->log->debug("JavaScriptLoader: packing to $pathname...");
    my $base_dir = $self->_get_absolute_path($self->{conf}->{source_directory});

    my $config = $self->{bundle_config}->{$pathname};
    if (!$config) {
        $self->{app}->log->error("JavaScriptLoader: no settings to pack $pathname...");
        return;
    }

    my @result;
    my %done;
    for my $target (@{$config->{prior}}) {
        my $path = path($base_dir, $target);
        next if $done{$path->basename};
        $self->{app}->log->debug("JavaScriptLoader: pack $target ($path)");
        push @result, Encode::decode_utf8($path->slurp);
        #my $js;
        #my $fh = FileHandle->new;
        #if ($fh->open($path, "<:utf8")) {
        #    $js = do { local $/; <$fh> };
        #    $fh->close;
        #}
        #else {
        #    $self->{app}->log->error("JavaScriptLoader: cannot open $path");
        #}
        #push @result, $js;
        $done{$target} = 1;
    }

    for my $target (path($base_dir)->list->each) {
        my $basename = $target->basename;
        next if $target->to_abs !~ $config->{target};
        next if ( any { $target->to_abs =~ m/$_/ } $config->{exclude} );
        next if $done{$basename};

        $self->{app}->log->debug("JavaScriptLoader: pack $basename ($target)");
        push @result, Encode::decode_utf8($target->slurp);
        #my $js;
        #my $fh = FileHandle->new;
        #if ($fh->open($target, "<:utf8")) {
        #    $js = do { local $/; <$fh> };
        #    $fh->close;
        #}
        #else {
        #    $self->{app}->log->error("JavaScriptLoader: cannot open $target");
        #}
        #push @result, $js;
        $done{$basename} = 1;
    }

    my $joined = join("\n", @result);
    $self->{app}->log->debug("JavaScriptLoader: packing to $pathname done.");

    my @sources = keys %done;
    $self->{bundled} ||= {};
    for my $src (@sources) {
        $self->{bundled}->{$src} = 1;
    }

    return $joined;
}


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

    # load config file
    if ($self->{conf}->{config_file}) {
        my $abs_path = $self->{conf}->{config_file};
        if ($abs_path !~ m|^/|) {
            $abs_path = path($self->{app}->home, $abs_path)->to_abs;
        }

        # read bundle config
        if (-r $abs_path) {
            my $json_body = Encode::decode_utf8(path($abs_path)->slurp);
            $self->{bundle_config} = decode_json($json_body);
            #$self->load_all_bundles;
        }
        else {
            $self->{app}->log->warn("JavaScriptLoader: cannot read config file: $self->{conf}->{config_file} ($abs_path)");
        }
    }
}

sub load_all_bundles {
    my $self = shift;
    for my $target (keys %{$self->{bundle_config}}) {
        my $content = $self->get_content($target, {reload => 1});
    }
}

sub _is_bundled {
    my ($self, $target) = @_;
    my $pool = $self->{bundled} // {};
    return $pool->{$target};
}

sub register {
    my ($self, $app, $conf) = @_;

    # set default config value
    my $cnf = $app->config->{JavaScriptLoader} ||= {};
    $cnf->{source_directory} //= "public/js";
    $cnf->{mode} //= "development";
    $cnf->{config_file} //= "public/js/bundle-config.json";
    $cnf->{use_bundle} //= 1;
    $cnf->{use_compression} ||= 0;

    $self->{conf} = {%$conf, %$cnf};
    $self->{app} = $app;
    $self->{mode} = $self->{conf}->{mode} || $app->mode;
    $self->{compress} = $cnf->{use_compression};
    $self->{bundled} = {};

    if (!$self->{conf}->{source_directory}) {
        $app->log->warn("JavaScriptLoader: no source_directory given!");
    }
    $self->_load_bundle_config;

    $app->helper('load_js', sub {
                     my ($c, $name) = @_;
                     # check if target is bundled
                     return '' if $self->_is_bundled($name);

                     my $path = $self->get_md5_path($name);
                     if (!$path) {
                         $self->{app}->log->error("JavaScriptLoader: cannot load $name");
                         return '';
                     }
                     return "<script src='$path'></script>";
                 });

    my $r = $app->routes;
    my $base_url = $self->{conf}->{base_url};
    if (substr($base_url, -1, 1) ne "/") {
        $base_url = $base_url . "/";
    }

    $r->any("${base_url}*content_path" => sub {
                my $c = shift;
                my $path = $base_url . $c->stash('content_path');

                if ($path) {
                    my $content = $self->get_content($path);
                    if ($content) {
                        # check Etag
                        if ($c->req->headers->if_none_match
                            && $content->{md5}
                            && $c->req->headers->if_none_match eq "\"$content->{md5}\"") {
                            $c->rendered(304);
                            return;
                        }
                        $c->res->headers->content_type($content->{type});
                        my $output = $content->{content};

                        # check compressable
                        if ($self->{compress}
                            && $content->{type} =~ m{^(text/|application/json|application/javascript)}
                            && ($c->req->headers->accept_encoding // '') =~ /gzip/i
                            && $content->{gz_content}
                           ) {
                            # use gzip compression
                            # Add header
                            $c->res->headers->append(Vary => 'Accept-Encoding');
                            $c->res->headers->content_encoding('gzip');

                            $output = $content->{gz_content};
                        }

                        # add Etag
                        $c->res->headers->content_type($content->{type});
                        if ($content->{md5}) {
                            $c->res->headers->etag("\"$content->{md5}\"");
                        }

                        # send result
                        $c->res->body($output);
                        $c->rendered(200);
                        return;
                    }
                }
                $c->rendered(404);
            });

    $app->helper(javascript_loader => sub { state $javascript_loader = $self; });
}

1;
