#!/usr/bin/perl
use strict;
use warnings;

use File::Find;
use File::Temp;
use File::Basename;
use File::Spec;
use Digest::SHA;
use Data::Dumper;
use Compress::Zlib;
use Cwd;

my $DEBUG = $ENV{DEBUG} // 0;
my $iscommit = 0;
my $ispipe = 0;
my $istree = 0;

my %objs;

my $cwd = getcwd;
my $root = $ARGV[0] // '.';

if ($root =~ /^commit:/) {
  $root =~ s/^commit:(.*)/$1/;
  $iscommit = 1;
}

if ($root =~ /^tree:/) {
  $root =~ s/^tree:(.*)/$1/;
  $istree = 1;
  die "tree:$root specified whilst $root is a file" if -f $root;
}

if ($root =~ /^blob:/) {
  $root =~ s/^blob:(.*)/$1/;
  die "blob:$root specified whilst $root is a directory" if -d $root;
}

if ($root eq "--") {
  $ispipe = 1;
} else {
  die "$root: doesn't exist" if ! -e $root;
}

my $aroot = $root;
if (!($aroot =~ /^\//)) {
  $aroot = Cwd::abs_path("@{[getcwd]}/$aroot");
}

sub dprint {
  print @_ unless $DEBUG == 0;
}

sub dprintf {
  printf @_ unless $DEBUG == 0;
}


sub findgitdir {
  my $dir = File::Spec->rel2abs(shift // '.');

  while (1) {
    my $git = File::Spec->catdir($dir, '.git');
    return $dir if -d $git;
    my $parent = File::Spec->catdir($dir, File::Spec->updir);
    last if File::Spec->canonpath($parent) eq File::Spec->canonpath($dir);
    $dir = $parent;
  }
  return undef;
}
my $gitdir = findgitdir;

dprint "{{$root [$aroot, git: $gitdir]}}\n";

sub prettyhex {
  my ($resp) = @_;
  $resp =~ s/([^\x20-\x7E])/sprintf("\\x%02X", ord($1))/ge;
  return $resp;
}

sub writeobj {
  my ($obj, $dir, $path, $cnt) = @_;
  File::Path::make_path $dir;
  open my $fh, ">", $path;
  binmode $fh;
  my $out = Compress::Zlib::compress $obj;
  print $fh $out;
  dprint "writing $path\n";
  close $fh;
}

sub hash2path {
  my ($hash, $gitdir) = @_;
  my $dir = ".git/objects/".substr($hash, 0, 2);
  my $path = "$gitdir/$dir/".substr($hash, 2);
  return ($dir, $path);
}

sub hashzlib {
  my ($obj) = @_;
  dprint "\nhashing[]\n";
  my $hash = Digest::SHA::sha1_hex $obj;
  dprint "hash: $hash\n";
  my $out = Compress::Zlib::compress $obj;
  dprint "out_len: ", length $hash, "\n";
  return ($hash, $out);
}

sub readbin {
  my ($file) = @_;
  open my $in, "<", $file;
  binmode $in;
  local $/;
  my $cnt = <$in>;
  close $in;
  return $cnt;
}

sub hashobj {
  my ($file, $gitdir) = @_;
  my $cnt = $ispipe ? do { local $/; <STDIN> } : readbin $file;
  my $type = $iscommit == 0 ? "blob " : "commit ";
  dprint "obj type: $type\n";
  my $obj = $type.length($cnt)."\0".$cnt;
  dprint "obj cont: ", prettyhex $obj, "\n";
  my ($hash, $out) = hashzlib $obj;
  my ($dir, $path) = hash2path $hash, $gitdir;
  return $hash if -e $path;
  writeobj $obj, $dir, $path, $cnt;
  return $hash;
}

sub hashtree {
  my ($gitdir, $filename, @arr) = @_;
  my $d = Data::Dumper->new([\@arr], ["tree"]);
  $d->Useqq(1);
  dprint "\n(", scalar @arr,")\n", $d->Dump;
  my $cnt = join '', @arr;
  my $obj = "tree ".length($cnt)."\0$cnt";
  dprint "hex_out {$filename}: ", unpack("H*", $cnt), "\n";
  dprint "tree_out {$filename}: ", prettyhex $obj, "\n\n";
  my ($hash, $comp) = hashzlib $obj;
  dprint "hashtree: $hash\n";
  my ($dir, $path) = hash2path $hash, $gitdir;
  return $hash if -e $path;
  writeobj $obj, $dir, $path, $cnt;
  return $hash;
}

if ($istree == 0 && (-f $root || $ispipe != 0)) {
  dprint "hashing file $root\n";
  print hashobj($root, $gitdir), "\n";
  exit;
}

if ($istree != 0 && $ispipe != 0) {
  dprint "hashing directory $root\n";
  my @tree = map {
    dprint "line: $_\n";
    my $name = (split "\t", $_)[1];
    my $rest = (split "\t", $_)[0];
    my $mode = (split " ", $rest)[0];
    $mode =~ s/^0+(?=[0-9])//;
    my $hash = pack("H*", (split " ", $rest)[2]);
    "$mode $name\0$hash"
  } split("\n", do { local $/; <STDIN> });
  dprint prettyhex("contents: ".join "", @tree), "\n";
  print hashtree($gitdir, "--", @tree), "\n";
  exit;
}

find({preprocess => sub {
    return sort(grep {
      !($File::Find::dir eq $root && $_ eq '.git')
    } @_);
  }, wanted => sub {
    if (! -f $_ ) {
      dprint "dir: ", $File::Find::name, "\n";
      return;
    }
    my $mode = sprintf "%o", (stat _)[2];
    my $path = $File::Find::name;
    $path =~ s/^\.\///;
    my $abs = "$cwd/$path";
    dprint "{$path, $abs [$aroot]}\n";
    my $hash = hashobj $abs, $gitdir;
    chomp $hash;
    my $name = basename($path);
    $objs{$path} = "$mode blob $hash\t$name";
    dprintf "%s/: %s (%s) %o (%x)\n", $aroot, $path, $hash, $mode;
}}, $root);
dprint "\n\n", Data::Dumper->Dump([\%objs], ["objs"]);

sub walkdirs {
  my ($dir, $callback) = @_;
  opendir(my $dh, $dir);
  my $nentr = () = readdir($dh);
  dprint "nentr: $nentr, path: $dir\n";
  if ($nentr == 2) {
    return;
  }
  rewinddir($dh);
  while (my $entry = readdir($dh)) {
    my $path = "$dir/$entry";
    next if $entry eq '.' || $entry eq '..' || -f $path || $path eq './.git';
    walkdirs($path, $callback);
  }
  my $res = $callback->($dir);
  closedir($dh);
  return $res;
}

sub dir2tree {
  my ($path) = @_;
  my @tree = ();
  opendir(my $dh, $path);
  dprint "$path :\n";
  while (my $entry = readdir($dh)) {
    dprint "entry: $entry\n";
    next if $entry eq '.' || $entry eq '..' || $entry eq '.git';
    $entry =~ s/^\.\///;
    my $key = "$path/$entry";
    $key =~ s/^\.\///;
    dprint "next ($key => ";
    if (exists $objs{$key}) {
      dprint "exists";
    } else {
      dprint "missing";
    }
    dprint ")\n";
    next if ! exists $objs{$key};
    my $val = $objs{$key};
    dprint "$key => $val\n";
    my @tsplit = split "\t", $val;
    my $fname = $tsplit[1];
    my @ssplit = split " ", $tsplit[0];
    my $mode = $ssplit[0];
    my $hash = pack "H*", $ssplit[2];
    my $res = "$mode $fname\0$hash";
    dprintf "($mode, $fname) bin: %s (%s)\n", unpack("H*", $hash), prettyhex $res;
    dprint "npush: ", push(@tree, [$fname, $res]), "\n";
  }
  closedir($dh);
  dprint "hashing $path (ntrees ", scalar @tree,"): ";
  @tree = map {$_->[1]} (sort {
    $a->[0] cmp $b->[0]
  } @tree);
  return hashtree($gitdir, $path, @tree);
}

my $outhash = walkdirs($root, sub {
  my $path = "@_";
  my $abs = Cwd::abs_path("$aroot/$path");
  my $name = basename $abs;
  my $hashdir = dir2tree($path);
  $name = "." if $name eq (basename $aroot);
  $objs{$name} = "40000 tree $hashdir\t$name" if $? == 0;
  dprint "\n----------\n{$abs} $path [$name] ($hashdir) \n";
  return $hashdir if $path eq $root;
});
dprint "\n\n", Data::Dumper->Dump([\%objs], ["objs"]), "\nRESULT:\n";
print "$outhash\n";
