#!/usr/bin/env perl

use strict;
use warnings;
use feature 'say';

BEGIN {
    use File::Basename qw(dirname);
    use File::Spec;
    my $d = dirname(File::Spec->rel2abs($0));
    require "$d/../setenv.pl";
}

use CSS::Sass;
use File::ChangeNotify;
use File::Find::Rule;
use Getopt::Long;
use MIME::Base64;
use MIME::Types;
use Path::Tiny;
use Pod::Usage;
use Try::Tiny;

# Store ARGV in case we need to restart later.
my @ARGVorig = @ARGV;

my %styles = (
    nested => SASS_STYLE_NESTED,
    expanded => SASS_STYLE_EXPANDED,
    compact => SASS_STYLE_COMPACT,
    compressed => SASS_STYLE_COMPRESSED,
);
my $style = 'compressed';
GetOptions(
    'style|t=s' => sub { die "Unknown style\n" unless $_[1] =~ /nested|expanded|compact|compressed/; $style = $_[1]; },
    'verbose' => \my $verbose,
    'watch' => \my $watch,
    'script=s'=> \my $watch_script,
    'script-param=s'=> \my $watch_script_param,
    'help|?' => \my $help,
) or pod2usage(2);
pod2usage(1) if $help;

my $mime_types = MIME::Types->new;

my $sass = CSS::Sass->new(
    output_style => $styles{$style},
    dont_die => 1,
    sass_functions => {
        'inline-image($url)' => sub {
            my ($url) = @_;
            die '$url should be a string' unless $url->isa("CSS::Sass::Value::String");
            # URL is given with reference to the file, which we don't have here. Assume.
            my $data = encode_base64(path("web/cobrands/fixmystreet/$url")->slurp_raw, "");
            my $type = $mime_types->mimeTypeOf($url);
            return "url('data:$type;base64,$data')";
        }
    },
);

# Get directories from the command line, defaulting to 'web' if none.
# We need absolute paths so that the include files lookup works.
my @dirs = map { m{/} ? $_ : "web/cobrands/$_" } @ARGV;
@dirs = 'web/cobrands' unless @dirs;
@dirs = map { path($_)->absolute->stringify } @dirs;

# Initial compilation, to also discover all the included files.
my %includes;
my %include_to_main;
foreach my $file (File::Find::Rule->file->extras({ follow => 1 })->name(qr/^[^_].*\.scss$/)->in(@dirs)) {
    my @includes = compile($file, $verbose);
    $includes{$file} = \@includes;
    map { push @{$include_to_main{$_}}, $file } @includes ? @includes : $file;
}

# If we're not watching, we're done!
exit unless $watch;

my $watcher = File::ChangeNotify->instantiate_watcher(
    directories => [ @dirs, keys %include_to_main ],
    filter => qr/\.scss$/,
);

say "\033[34mWatching for changes\033[0m";

while ( my @events = $watcher->wait_for_events() ) {
    for my $file (map { $_->path } @events) {
        verbose($file, "%s was updated");
        for my $inc (@{$include_to_main{$file}}) {
            my @includes = compile($inc, 1);
            # From CSS::Sass::Watchdog test, we see includes are sorted
            if (@includes && @{$includes{$inc}} && "@{$includes{$inc}}" ne "@includes") {
                say "\033[34mRestarting to update includes\033[0m";
                exec( $^X, $0, @ARGVorig ) or die "Can't re-exec myself($^X,$0): $!\n";
            }
        }
    }
    system($watch_script, $watch_script_param) if $watch_script;
}

# Given a SCSS file, compile it and generate a .map file,
# show an error if any, and return the list of includes.
sub compile {
    my ($file, $verbose) = @_;
    (my $output_file = $file) =~ s/scss$/css/;
    my $source_map_file = "$output_file.map";

    $sass->options->{source_map_file} = $source_map_file;
    $sass->options->{include_paths} = [dirname($file), "web/vendor/"];
    my ($css, $stats) = $sass->compile_file($file);
    unless ($css) {
        warn "\033[31m" . $sass->last_error . "\033[0m";;
        return;
    }

    my $written = write_if_different($output_file, $css);
    if ($written) {
        verbose($file, "    \033[32mupdated\033[0m %s");
    } elsif ($verbose) {
        verbose($file, "  \033[33munchanged\033[0m %s");
    }
    write_if_different($source_map_file, $stats->{source_map_string});
    return @{$stats->{included_files} || []};
}

# Write a file, only if it has changed.
sub write_if_different {
    my ($fn, $data) = @_;
    $fn = path($fn);
    my $current = try {
        $fn->slurp_utf8;
    } catch {
        return if $_->{err} eq 'No such file or directory';
        die $_;
    };
    if (!$current || $current ne $data) {
        $fn->spew_utf8($data);
        return 1;
    }
    return 0;
}

sub verbose {
    my ($file, $format) = @_;
    # Strip most of file path, keep dir/file
    (my $pr = $file) =~ s{.*/(.*/.*)\.scss}{$1};
    say sprintf $format, $pr;
}

__END__

=head1 NAME

make_css - Generate CSS files from SCSS files, watch for changes.

=head1 SYNOPSIS

make_css [options] [dirs ...]

 Options:
   --verbose        display more information
   --watch          wait for file updates and compile automatically
   --script         script to run after recompilation in watch mode
   --script-param   param to pass to --script when executing
   --style, -t      CSS output style (one of nested, expanded, compact, compressed)
   --help           this help message

If no directories are specified, any .scss files under web/cobrands that do not
begin with a "_" will be processed. "web/cobrands/" may be omitted from a given
directory.

=cut
