// @ts-check

// Thanks to https://github.com/mbostock/git-static

var child = require("child_process"),
    mime = require("mime"),
    path = require("path");

var shaRe = /^[0-9a-f]{40}$/,
    emailRe = /^<.*@.*>$/;

function readBlob(repository, revision, file, callback) {
  var git = child.spawn("git", ["cat-file", "blob", revision + ":" + file], {cwd: repository}),
      data = [],
      exit;

  git.stdout.on("data", function(chunk) {
    data.push(chunk);
  });

  git.on("exit", function(code) {
    exit = code;
  });

  git.on("close", function() {
    if (exit > 0) return callback(error(exit));
    callback(null, Buffer.concat(data));
  });

  git.stdin.end();
}

exports.readBlob = readBlob;

exports.getBranches = function(repository, callback) {
  child.exec("git branch -l", {cwd: repository}, function(error, stdout) {
    if (error) return callback(error);
    callback(null, stdout.split(/\n/).slice(0, -1).map(function(s) { return s.slice(2); }));
  });
};

exports.getSha = function(repository, revision, callback) {
  child.exec("git rev-parse '" + revision.replace(/'/g, "'\''") + "'", {cwd: repository}, function(error, stdout) {
    if (error) return callback(error);
    callback(null, stdout.trim());
  });
};

exports.getBranchCommits = function(repository, callback) {
  child.exec("git for-each-ref refs/heads/ --sort=-authordate --format='%(objectname)\t%(refname:short)\t%(authordate:iso8601)\t%(authoremail)'", {cwd: repository}, function(error, stdout) {
    if (error) return callback(error);
    callback(null, stdout.split("\n").map(function(line) {
      var fields = line.split("\t"),
          sha = fields[0],
          ref = fields[1],
          date = new Date(fields[2]),
          author = fields[3];
      if (!shaRe.test(sha) || !date || !emailRe.test(author)) return;
      return {
        sha: sha,
        ref: ref,
        date: date,
        author: author.substring(1, author.length - 1)
      };
    }).filter(function(commit) {
      return commit;
    }));
  });
};

exports.getCommit = function(repository, revision, callback) {
  if (arguments.length < 3) callback = revision, revision = null;
  child.exec(shaRe.test(revision)
      ? "git log -1 --date=iso " + revision + " --format='%H\n%ad'"
      : "git for-each-ref --count 1 --sort=-authordate 'refs/heads/" + (revision ? revision.replace(/'/g, "'\''") : "") + "' --format='%(objectname)\n%(authordate:iso8601)'", {cwd: repository}, function(error, stdout) {
    if (error) return callback(error);
    var lines = stdout.split("\n"),
        sha = lines[0],
        date = new Date(lines[1]);
    if (!shaRe.test(sha) || !date) return void callback(new Error("unable to get commit"));
    callback(null, {
      sha: sha,
      date: date
    });
  });
};

exports.getRelatedCommits = function(repository, branch, sha, callback) {
  if (!shaRe.test(sha)) return callback(new Error("invalid SHA: " + sha));
  child.exec("git log --format='%H' '" + branch.replace(/'/g, "'\''") + "' | grep -C1 " + sha, {cwd: repository}, function(error, stdout) {
    if (error) return callback(error);
    var shas = stdout.split(/\n/),
        i = shas.indexOf(sha);

    callback(null, {
      previous: shas[i + 1],
      next: shas[i - 1]
    });
  });
};

exports.listCommits = function(repository, sha1, sha2, callback) {
  if (!shaRe.test(sha1)) return callback(new Error("invalid SHA: " + sha1));
  if (!shaRe.test(sha2)) return callback(new Error("invalid SHA: " + sha2));
  child.exec("git log --format='%H\t%ad' " + sha1 + ".." + sha2, {cwd: repository}, function(error, stdout) {
    if (error) return callback(error);
    callback(null, stdout.split(/\n/).slice(0, -1).map(function(commit) {
      var fields = commit.split(/\t/);
      return {
        sha: fields[0],
        date: new Date(fields[1])
      };
    }));
  });
};

/** @type {(repository: string, callback: (err: Error, commits?: {sha: string, date: Date, author: string, subject: string}[]) => void) => void} */
exports.listAllCommits = function(repository, callback) {
  child.exec("git log --branches --format='%H\t%ad\t%an\t%s'", {cwd: repository}, function(error, stdout) {
    if (error) return callback(error);
    callback(null, stdout.split(/\n/).slice(0, -1).map(function(commit) {
      var fields = commit.split(/\t/);
      return {
        sha: fields[0],
        date: new Date(fields[1]),
        author: fields[2],
        subject: fields[3]
      };
    }));
  });
};

exports.listTree = function(repository, revision, callback) {
  child.exec("git ls-tree -r " + revision, {cwd: repository}, function(error, stdout) {
    if (error) return callback(error);
    callback(null, stdout.split(/\n/).slice(0, -1).map(function(commit) {
      var fields = commit.split(/\t/);
      return {
        sha: fields[0].split(/\s/)[2],
        name: fields[1]
      };
    }));
  });
};

exports.route = function() {
  var repository = defaultRepository,
      revision = defaultRevision,
      file = defaultFile,
      type = defaultType;

  function route(request, response) {
    var repository_,
        revision_,
        file_;

        // @ts-ignore
    if ((repository_ = repository(request.url)) == null
        || (revision_ = revision(request.url)) == null
        || (file_ = file(request.url)) == null) return serveNotFound();

    readBlob(repository_, revision_, file_, function(error, data) {
      if (error) return error.code === 128 ? serveNotFound() : serveError(error);
      response.writeHead(200, {
        "Content-Type": type(file_),
        "Cache-Control": "public, max-age=300"
      });
      response.end(data);
    });

    function serveError(error) {
      response.writeHead(500, {"Content-Type": "text/plain"});
      response.end(error + "");
    }

    function serveNotFound() {
      response.writeHead(404, {"Content-Type": "text/plain"});
      response.end("File not found.");
    }
  }

  route.repository = function(_) {
    if (!arguments.length) return repository;
    repository = functor(_);
    return route;
  };

  route.sha = // sha is deprecated; use revision instead
  route.revision = function(_) {
    if (!arguments.length) return revision;
    revision = functor(_);
    return route;
  };

  route.file = function(_) {
    if (!arguments.length) return file;
    file = functor(_);
    return route;
  };

  route.type = function(_) {
    if (!arguments.length) return type;
    type = functor(_);
    return route;
  };

  return route;
};

function functor(_) {
  return typeof _ === "function" ? _ : function() { return _; };
}

function defaultRepository() {
  return path.join(__dirname, "repository");
}

function defaultRevision(url) {
  return decodeURIComponent(url.substring(1, url.indexOf("/", 1)));
}

function defaultFile(url) {
  url = url.substring(url.indexOf("/", 1) + 1);
  const pathIdx = url.indexOf('?');
  if(pathIdx !== -1) {
    url = url.slice(0, pathIdx);
  }

  return decodeURIComponent(url);
}

function defaultType(file) {
  var type = mime.getType(file) || "text/plain";
  return text(type) ? type + "; charset=utf-8" : type;
}

function text(type) {
  return /^(text\/)|(application\/(javascript|json)|image\/svg$)/.test(type);
}

function error(code) {
  var e = new Error;
  // @ts-ignore
  e.code = code;
  return e;
}