package ES::Repo;

use strict;
use warnings;
use v5.10;

use Cwd;
use Encode qw(decode_utf8);
use ES::Util qw(run);
use Path::Class();

use parent qw( ES::BaseRepo );

my %Repos;

#===================================
sub new {
#===================================
    my ( $class, %args ) = @_;

    my $self = $class->SUPER::new(%args);

    my $name = $self->name;
    $self->{tracker} = $args{tracker}
        or die "No <tracker> specified for repo <$name>";
    $self->{keep_hash} = $args{keep_hash};
    $Repos{$self->name} = $self;
    $self;
}

#===================================
sub get_repo {
#===================================
    my $class = shift;
    my $name = shift || '';
    return $Repos{$name} || die "Unknown repo name <$name>";
}

#===================================
sub add_sub_dir {
#===================================
    my ( $self, $branch, $dir ) = @_;
    $self->{sub_dirs}->{$branch} = $dir;
    return ();
}

#===================================
# Returns 'not_changed' if the repo hasn't changed since we last built it,
# 'changed' if it has, and 'new_sub_dir' if this is a sub_dir for a new source.
#===================================
sub has_changed {
#===================================
    my $self = shift;
    my ( $title, $branch, $path ) = @_;

    $path = $self->normalize_path( $path, $branch );
    $branch = $self->normalize_branch( $branch );

    local $ENV{GIT_DIR} = $self->git_dir;
    my $old_info = $self->_last_commit_info(@_);

    my $new;
    if ( $self->{keep_hash} ) {
        # If we're keeping the hash from the last build but there *isn't* a
        # hash that means that the branch wasn't used the last time we built
        # this book. That means we'll skip it entirely when building the book
        # anyway so we should consider the book not to have changed.
        unless ($old_info) {
            # New sub_dirs *might* build, but only if the entire book is built
            # out of new sub_dirs.
            return 'new_sub_dir' if exists $self->{sub_dirs}->{$branch};
            return 'not_changed';
        }

        $new = $self->_last_commit(@_);
    } else {
        # If we aren't keeping the hash from the last build and there *isn't*
        # a hash that means that this is a new repo so we should build it.
        return 'changed' unless $old_info;

        $new = $self->sha_for_branch( $branch ) or die(
                "Remote branch <origin/$branch> doesn't exist in repo "
                . $self->name);
    }
    my $new_info = $new;

    # We check sub_dirs *after* the checks above so we can handle sub_dir for
    # new sources specially.
    return 'changed' if exists $self->{sub_dirs}->{$branch};

    if ($self->{keep_hash}) {
        return $old_info ne $new_info ? 'changed' : 'not_changed';
    }
    return 'not_changed' if $old_info eq $new_info;

    my $changed;
    eval {
        $changed = !!run qw(git diff --shortstat),
                         $self->_last_commit(@_), $new,
                         '--', $path;
        1;
    }
        || do { $changed = 1 };

    return $changed ? 'changed' : 'not_changed';
}

#===================================
sub mark_done {
#===================================
    my $self = shift;
    my ( $title, $branch, $path ) = @_;

    my $new;
    if ( exists $self->{sub_dirs}->{$branch} ) {
        $new = 'local';
    } elsif ( $self->{keep_hash} ) {
        $new = $self->_last_commit($title, $branch, $path);
        return unless $new; # Skipped if nil
    } else {
        $new = $self->sha_for_branch( $branch );
    }

    $self->tracker->set_sha_for_branch( $self->name,
        $self->_tracker_branch(@_), $new );
}

#===================================
sub prepare {
#===================================
    my $self = shift;
    my ( $title, $branch, $path, $dest_root, $prefix ) = @_;
    my $dest = $dest_root->subdir( $prefix );

    if ( exists $self->{sub_dirs}->{$branch} ) {
        $self->_prepare_sub_dir( $title, $branch, $path, $dest );
        return $dest;
    }

    my $resolved_branch = $self->_resolve_branch( @_ );
    unless ( $resolved_branch ) {
        printf(" - %40.40s: Skipping new repo %s for branch %s.\n",
               $title, $self->{name}, $branch);
        return $dest;
    }

    $self->_extract_from_ref( $dest, $branch, $path );
    return $dest;
}

#===================================
sub _prepare_sub_dir {
#===================================
    my ( $self, $title, $branch, $path, $dest ) = @_;
    my $source_root = $self->{sub_dirs}->{$branch};

    unless ( $self->{keep_hash} ) {
        $self->_extract_from_dir( $source_root, $dest, $path );
        return $dest;
    }

    my $no_uncommitted_changes = eval {
        local $ENV{GIT_WORK_TREE} = $source_root;
        local $ENV{GIT_DIR} = $ENV{GIT_WORK_TREE} . '/.git';
        run qw(git diff-index --quiet HEAD --);
        1;
    };
    unless ( $no_uncommitted_changes ) {
        unless ( $@ =~ /\n---out---\n\n---err---\n\n---------\n/) {
            # If the error message isn't empty then something went wrong checking.
            die "failed to check for outstanding commits: $@";
        }
        printf(" - %40.40s: Not merging the subbed dir for [%s][%s][%s] because it has uncommitted changes.\n",
                $title, $self->{name}, $branch, $path);
        $self->_extract_from_dir( $source_root, $dest, $path );
        return $dest;
    }

    my $resolved_branch = $self->_resolve_branch( $title, $branch, $path );
    unless ( $resolved_branch ) {
        printf(" - %40.40s: Not merging the subbed dir for [%s][%s][%s] because it is new.\n",
                $title, $self->{name}, $branch, $path);
        $self->_extract_from_dir( $source_root, $dest, $path );
        return $dest;
    }

    local $ENV{GIT_DIR} = "$source_root/.git";
    my $subbed_head = run qw(git rev-parse HEAD);
    delete local $ENV{GIT_DIR};
    my $merge_branch = "${resolved_branch}_${subbed_head}_$path";
    $merge_branch =~ s|/|_|g;
    $merge_branch =~ s|\*|splat|g;
    $merge_branch =~ s|:\(glob\)|_glob_|g;
    $merge_branch =~ s|\.$||g; # Fix funny shaped paths

    # Check if we've already merged this path by looking for the merged_branch
    # in the source repo. This is safe because:
    # 1. We prune all branches from the source repo before the build.
    # 2. The merge branch contains the hash of the subbed head.
    my $already_built = eval {
        local $ENV{GIT_DIR} = $self->{git_dir};
        run qw(git rev-parse), $merge_branch;
        1;
    };
    if ( $already_built ) {
        # Logging here would be pretty noisy.
        $self->_extract_from_ref( $dest, $merge_branch, $path );
        return;
    }

    # Merge the HEAD of the subbed dir into the commit that last successfully
    # built the docs.
    printf(" - %40.40s: Merging the subbed dir for [%s][%s][%s] into the last successful build.\n",
            $title, $self->{name}, $branch, $path);
    my $work = Path::Class::dir( '/tmp/mergework' );
    $work->rmtree;
    run qw(git clone --no-checkout), $self->{git_dir}, $work;
    my $original_pwd = Cwd::cwd();
    chdir $work;
    my $merged = eval {
        run qw(git remote add subbed_repo), $source_root;
        run qw(git fetch subbed_repo), $subbed_head;
        run qw(git remote remove subbed_repo);
        unless ($path eq '.') {
            run qw(git config core.sparseCheckout true);
            $self->_write_sparse_config( $work, $path );
        }
        run qw(git checkout -b), $merge_branch, $resolved_branch;
        run qw(git merge -m merge), $subbed_head;
        run qw(git push origin -f), $merge_branch; # Origin here is just our clone.
        1;
    };
    chdir $original_pwd;
    unless ( $merged ) {
        printf(" - %40.40s: Failed to merge the subbed dir for [%s][%s][%s] into the last successful build:\n%s",
                $title, $self->{name}, $branch, $path, $@);
        $dest->rmtree;
        $self->_extract_from_dir( $source_root, $dest, $path );
        return $dest;
    }
    printf(" - %40.40s: Merged the subbed dir for [%s][%s][%s] into the last successful build.\n",
            $title, $self->{name}, $branch, $path);
    $self->_extract_from_ref( $dest, $merge_branch, $path );
}

#===================================
sub _extract_from_dir {
#===================================
    my ( $self, $source_root, $dest_root, $path ) = @_;
    local $ENV{GIT_WORK_TREE} = $source_root;
    local $ENV{GIT_DIR} = $ENV{GIT_WORK_TREE} . '/.git';

    # Copies the $path from the subsitution directory. It is tempting to
    # just symlink the substitution directory into the destination and
    # call it a day but that doesn't work for a whole bunch of non-obvious
    # reasons:
    # 1. We often use relative paths to include asciidoc files from other
    #    repositories and those relative paths don't work at all with symlinks.
    # 2. The paths themselves are actually git pathspecs which can resolve to
    #    more than one file.
    my $files = run qw(git ls-files -zcom --), $path;
    for ( split /\0/, $files ) {
        my $source = $source_root->file( $_ );
        die "Can't find $source" unless -e $source;
        my $dest = $dest_root->file( $_ );
        $dest->parent->mkpath;
        $source->copy_to( $dest ) or die "Error copying from $source: $@";
    }
}

#===================================
sub _extract_from_ref {
#===================================
    my ( $self, $dest, $ref, $path ) = @_;
    local $ENV{GIT_DIR} = $self->{git_dir};

    $dest->mkpath;
    my $tar = $dest->file( '.temp_git_archive.tar' );
    die "File <$tar> already exists" if -e $tar;
    run qw(git archive --format=tar -o), $tar, $ref, $path;

    run qw(tar -x -C), $dest, '-f', $tar;
    $tar->remove;
}

#===================================
sub _tracker_branch {
#===================================
    my $self   = shift;
    my $title  = shift or die "No <title> specified";
    my $branch = shift or die "No <branch> specified";
    my $path   = shift or die "No <path> specified";
    $path = $self->normalize_path( $path, $branch );
    return "$title/${path}/${branch}";
}

#===================================
sub edit_url {
#===================================
    my ( $self, $branch ) = @_;

    $branch = $self->normalize_branch( $branch );
    return edit_url_for_url_and_branch($self->url, $branch);
}

#===================================
sub edit_url_for_url_and_branch {
#===================================
    my ( $url, $branch ) = @_;
    # If the url is in ssh form, then convert it to https
    $url =~ s/git@([^:]+):/https:\/\/$1\//;
    # Strip trailing .git as it isn't in the edit link
    $url =~ s/\.git$//;
    my $dir = Path::Class::dir( "edit", $branch )->cleanup->as_foreign('Unix');
    return "$url/$dir/";
}

#===================================
sub dump_recent_commits {
#===================================
    my ( $self, $title, $branch, $src_path ) = @_;

    $branch = $self->normalize_branch( $branch );
    $src_path = $self->normalize_path( $src_path, $branch );
    my $description = $self->name . "/$title:$branch:$src_path";
    if ( exists $self->{sub_dirs}->{$branch} ) {
        return "Used " . $self->{sub_dirs}->{$branch} .
                " for $description\n";
    }

    local $ENV{GIT_DIR} = $self->git_dir;
    my $start = $self->_last_commit( $title, $branch, $src_path );
    my $rev_range = $self->{keep_hash} ? $start : "$start...$branch";

    my $commits = eval {
        run( 'git', 'log', $rev_range,
            '--pretty=format:%h -%d %s (%cr) <%an>',
            '-n', 10, '--abbrev-commit', '--date=relative', '--', $src_path );
    } || '';

    unless ( $commits =~ /\S/ ) {
        $commits
            = run( 'git', 'log',
            $branch, '--pretty=format:%h -%d %s (%cr) <%an>',
            '-n', 10, '--abbrev-commit', '--date=relative', '--', $src_path );
    }

    my $header = "Recent commits in $description";
    return
          $header . "\n"
        . ( '-' x length($header) ) . "\n"
        . $commits . "\n\n";
}

#===================================
sub show_file {
#===================================
    my $self = shift;
    my ( $reason, $branch, $file ) = @_;

    if ( exists $self->{sub_dirs}->{$branch} ) {
        my $realpath = $self->{sub_dirs}->{$branch}->file($file);
        return $realpath->slurp( iomode => "<:encoding(UTF-8)" );
    }

    my $resolved_branch = $self->_resolve_branch( @_ );
    die "Can't resolve $branch" unless $resolved_branch;

    local $ENV{GIT_DIR} = $self->git_dir;
    return decode_utf8 run( qw (git show ), $resolved_branch . ':' . $file );
}

#===================================
# Information about the last commit, *not* including flag.
# NOTE: We don't presently need any flags but we probably will in the future.
#===================================
sub _last_commit {
#===================================
    my $self = shift;
    my $sha = $self->_last_commit_info(@_);
    $sha =~ s/\|.+$//;  # Strip flags (|whatever) if it is in the hash
    return $sha;
}

#===================================
# Information about the last commit, including flags.
# NOTE: We don't presently need any flags but we probably will in the future.
#===================================
sub _last_commit_info {
#===================================
    my $self = shift;
    my $tracker_branch = $self->_tracker_branch(@_);
    my $sha = $self->tracker->sha_for_branch($self->name, $tracker_branch);
    return $sha;
}

#===================================
# Converts a branch specification into the branch to actually use in the git
# repo. Returns falsy if we've been instructed to keep the hash used by the
# last build but we have yet to use the branch.
#===================================
sub _resolve_branch {
#===================================
    my $self = shift;
    my ( $title, $branch, $path ) = @_;

    return $branch unless $self->{keep_hash};

    $branch = $self->_last_commit(@_);
    die "--keep_hash can't build on top of --sub_dir" if $branch eq 'local';
    # If the build fails, this message can indicate that there are duplicate entries in the conf.yaml
    return $branch;
}

#===================================
sub add_source {
#===================================
    my ( $self, $sources, $prefix, $path, $exclude, $map_branches, $private, $alternatives ) = @_;

    push @$sources, {
        repo => $self,
        prefix => $prefix,
        path => $path,
        exclude => $exclude,
        map_branches => $map_branches,
        private => $private,
        alternatives => $alternatives,
    };
}


#===================================
sub tracker       { shift->{tracker} }
#===================================

1
