#!/usr/bin/perl -w

use strict;
use FindBin;
use YAML qw(LoadFile);
use File::Slurp;
use Digest::SHA;
use XML::Writer;
use Cwd;
use File::Temp;
use File::Find;
use File::Which;
use IO::CaptureOutput qw(capture_exec);
use Parallel::ForkManager;

my $htdocsdir = "$FindBin::Bin/htdocs";
my $config = LoadFile("$FindBin::Bin/config.yml");
my %htdocsfiles = ( '.' => 1, '..' => 1, 'no-update.xml' => 1 );
my $releases_dir = "$FindBin::Bin/../../gitian";

sub exit_error {
    print STDERR "Error: ", $_[0], "\n";
    chdir '/';
    exit (exists $_[1] ? $_[1] : 1);
}

sub build_target_by_os {
    $config->{build_targets}{$_[0]} ? $config->{build_targets}{$_[0]} : $_[0];
}

sub get_nbprocs {
    return $ENV{NUM_PROCS} if defined $ENV{NUM_PROCS};
    if (-f '/proc/cpuinfo') {
        return scalar grep { m/^processor\s+:\s/ } read_file '/proc/cpuinfo';
    }
    return 4;
}

sub write_htdocs {
    my ($file, $content) = @_;
    mkdir $htdocsdir unless -d $htdocsdir;
    write_file("$htdocsdir/$file", $content);
    $htdocsfiles{$file} = 1;
}

sub clean_htdocs {
    opendir(my $d, $htdocsdir);
    my @files = grep { ! $htdocsfiles{$_} } readdir $d;
    closedir $d;
    unlink map { "$htdocsdir/$_" } @files;
}

sub get_sha512_hex_of_file {
    my ($file) = @_;
    my $sha = Digest::SHA->new("512");
    $sha->addfile($file);
    return $sha->hexdigest;
}

sub get_version_files {
    my ($config, $version) = @_;
    return if $config->{versions}{$version}{files};
    my $files = {};
    my $vdir = "$releases_dir/$version";
    opendir(my $d, $vdir) or exit_error "Error opening directory $vdir";
    foreach my $file (readdir $d) {
        next unless -f "$vdir/$file";
        if ($file =~ m/^tor-browser-([^-]+)-${version}_(.+)\.mar$/) {
            my ($os, $lang) = ($1, $2);
            $files->{$os}{$lang}{complete} = {
                type => 'complete',
                URL => "$config->{versions}{$version}{download_url}/$file",
                size => -s "$vdir/$file",
                hashFunction => 'SHA512',
                hashValue => get_sha512_hex_of_file("$vdir/$file"),
            };
            next;
        }
        if ($file =~ m/^tor-browser-([^-]+)-(.+)-${version}_(.+)\.incremental\.mar$/) {
            my ($os, $from_version, $lang) = ($1, $2, $3);
            $files->{$os}{$lang}{partial}{$from_version} = {
                type => 'partial',
                URL => "$config->{versions}{$version}{download_url}/$file",
                size => -s "$vdir/$file",
                hashFunction => 'SHA512',
                hashValue => get_sha512_hex_of_file("$vdir/$file"),
            }
        }
    }
    closedir $d;
    $config->{versions}{$version}{files} = $files;
}

sub extract_mar {
    my ($mar_file, $dest_dir) = @_;
    my $old_cwd = getcwd;
    mkdir $dest_dir;
    chdir $dest_dir or exit_error "Cannot enter $dest_dir";
    my $res = system('mar', '-x', $mar_file);
    exit_error "Error extracting $mar_file" if $res;
    my $bunzip_file = sub {
        return unless -f $File::Find::name;
        rename $File::Find::name, "$File::Find::name.bz2";
        system('bunzip2', "$File::Find::name.bz2") == 0
                || exit_error "Error decompressing $File::Find::name";
    };
    find($bunzip_file, $dest_dir);
    my $manifest = -f 'updatev3.manifest' ? 'updatev3.manifest'
                        : 'updatev2.manifest';
    my @lines = read_file($manifest) if -f $manifest;
    foreach my $line (@lines) {
        if ($line =~ m/^addsymlink "(.+)" "(.+)"$/) {
            exit_error "$mar_file: Could not create symlink $1 -> $2"
                unless symlink $2, $1;
        }
    }
    chdir $old_cwd;
}

sub mar_filename {
    my ($version, $os, $lang) = @_;
    "$releases_dir/$version/tor-browser-$os-${version}_$lang.mar";
}

sub create_incremental_mar {
    my ($config, $pm, $from_version, $new_version, $os, $lang) = @_;
    my $mar_file = "tor-browser-$os-${from_version}-${new_version}_$lang.incremental.mar";
    print "Starting $mar_file\n";
    my $mar_file_path = "$releases_dir/$new_version/$mar_file";
    my $finished_file = sub {
        exit_error "Error creating $mar_file" unless $_[1] == 0;
        print "Finished $mar_file\n";
        $config->{versions}{$new_version}{files}{$os}{$lang}{partial}{$from_version} = {
            type => 'partial',
            URL => "$config->{versions}{$new_version}{download_url}/$mar_file",
            size => -s $mar_file_path,
            hashFunction => 'SHA512',
            hashValue => get_sha512_hex_of_file($mar_file_path),
        };
    };
    return if $pm->start($finished_file);
    my $tmpdir = File::Temp->newdir();
    extract_mar(mar_filename($from_version, $os, $lang), "$tmpdir/A");
    extract_mar(mar_filename($new_version, $os, $lang), "$tmpdir/B");
    my ($out, $err, $success) = capture_exec('make_incremental_update.sh',
                                   $mar_file_path, "$tmpdir/A", "$tmpdir/B");
    if (!$success) {
        unlink $mar_file_path if -f $mar_file_path;
        exit_error "making incremental mar:\n" . $err;
    }
    $pm->finish;
}

sub create_missing_incremental_mars {
    my ($config, $version) = @_;
    my $pm = Parallel::ForkManager->new(get_nbprocs);
    $pm->run_on_finish(sub { $_[2]->(@_) });
    my $v = $config->{versions}{$version};
    foreach my $from_version (@{$v->{incremental_from}}) {
        $config->{versions}{$from_version} //= { download_url => '' };
        get_version_files($config, $from_version);
        my $from_v = $config->{versions}{$from_version};
        foreach my $os (keys %{$v->{files}}) {
            foreach my $lang (keys %{$v->{files}{$os}}) {
                next if defined $v->{files}{$os}{$lang}{partial}{$from_version};
                next unless defined $from_v->{files}{$os}{$lang}{complete};
                create_incremental_mar($config, $pm, $from_version, $version, $os, $lang);
            }
        }
    }
    $pm->wait_all_children;
}

sub get_config {
    my ($config, $version, $os, $name) = @_;
    return $config->{versions}{$version}{$os}{$name}
        // $config->{versions}{$version}{$name}
        // $config->{$name};
}

sub get_response {
    my ($config, $version, $os, @patches) = @_;
    my $res;
    my $writer = XML::Writer->new(OUTPUT => \$res, ENCODING => 'UTF-8');
    $writer->xmlDecl;
    $writer->startTag('updates');
    if (get_config($config, $version, $os, 'unsupported')) {
        $writer->startTag('update',
            unsupported => 'true',
            detailsURL => get_config($config, $version, $os, 'detailsURL'),
        );
        goto CLOSETAGS;
    }
    my $minversion = get_config($config, $version, $os, 'minSupportedOSVersion');
    $writer->startTag('update',
        type => 'minor',
        displayVersion => $version,
        appVersion => $version,
        platformVersion => get_config($config, $version, $os, 'platformVersion'),
        buildID => '20000101000000',
        detailsURL => get_config($config, $version, $os, 'detailsURL'),
        defined $minversion ? ( minSupportedOSVersion => $minversion ) : (),
    );
    foreach my $patch (@patches) {
        my @sorted_patch = map { $_ => $patch->{$_} } sort keys %$patch;
        $writer->startTag('patch', @sorted_patch);
        $writer->endTag('patch');
    }
    CLOSETAGS:
    $writer->endTag('update');
    $writer->endTag('updates');
    $writer->end;
    return $res;
}

sub write_responses {
    my ($config) = @_;
    foreach my $version (values %{$config->{channels}}) {
        get_version_files($config, $version);
        create_missing_incremental_mars($config, $version);
        my $files = $config->{versions}{$version}{files};
        my $migrate_archs = $config->{versions}{$version}{migrate_archs} // {};
        foreach my $old_os (keys %$migrate_archs) {
            my $new_os = $migrate_archs->{$old_os};
            foreach my $lang (keys %{$files->{$new_os}}) {
                $files->{$old_os}{$lang}{complete} =
                        $files->{$new_os}{$lang}{complete};
            }
        }
        foreach my $os (keys %$files) {
            foreach my $lang (keys %{$files->{$os}}) {
                my $resp = get_response($config, $version, $os,
                                $files->{$os}{$lang}{complete});
                write_htdocs("$version-$os-$lang.xml", $resp);
                foreach my $from_version (keys %{$files->{$os}{$lang}{partial}}) {
                    $resp = get_response($config, $version, $os,
                                $files->{$os}{$lang}{complete},
                                $files->{$os}{$lang}{partial}{$from_version});
                    write_htdocs("$from_version-$version-$os-$lang.xml", $resp);
                }
            }
        }
    }
}

sub write_htaccess {
    my ($config) = @_;
    my $htaccess = "RewriteEngine On\n";
    my $flags = "[last]";
    foreach my $channel (sort keys %{$config->{channels}}) {
        my $version = $config->{channels}{$channel};
        my $files = $config->{versions}{$version}{files};
        $htaccess .= "RewriteRule "
                  .  "^$channel/[^\/]+/$version/ "
                  .  "no-update.xml $flags\n";
        foreach my $os (sort keys %$files) {
            my $bt = build_target_by_os($os);
            foreach my $lang (sort keys %{$files->{$os}}) {
                foreach my $from_version (sort keys %{$files->{$os}{$lang}{partial}}) {
                    $htaccess .= "RewriteRule ^$channel/$bt/$from_version/$lang "
                              .  "$from_version-$version-$os-$lang.xml $flags\n";
                }
                $htaccess .= "RewriteRule ^$channel/$bt/[^\/]+/$lang "
                          .  "$version-$os-$lang.xml $flags\n";
            }
            $htaccess .= "RewriteRule ^$channel/$bt "
                      .  "$version-$os-en-US.xml $flags\n";
        }
    }
    write_htdocs('.htaccess', $htaccess);
}

sub check_deps {
    foreach my $bin (qw(bunzip2 mar mbsdiff make_incremental_update.sh)) {
        exit_error "Cannot find $bin in PATH" unless which($bin);
    }
}

sub osname {
    my ($osname) = capture_exec('uname', '-s');
    my ($arch) = capture_exec('uname', '-m');
    chomp($osname, $arch);
    if ($osname eq 'Linux' && $arch eq 'x86_64') {
        return 'linux64';
    }
    if ($osname eq 'Linux' && $arch =~ m/^i.86$/) {
        return 'linux32';
    }
    exit_error 'Unknown OS';
}

my $martools_tmpdir;
sub extract_martools {
    my $osname = osname;
    my $marzip = "$FindBin::Bin/../../../gitian-builder/inputs/mar-tools-$osname.zip";
    $martools_tmpdir = File::Temp->newdir();
    my $old_cwd = getcwd;
    chdir $martools_tmpdir;
    my (undef, undef, $success) = capture_exec('unzip', $marzip);
    chdir $old_cwd;
    exit_error "Error extracting $marzip" unless $success;
    $ENV{PATH} .= ":$martools_tmpdir/mar-tools";
}

extract_martools;
check_deps;
write_responses($config);
write_htaccess($config);
clean_htdocs;
