var events = require("events");
var fp = require("lodash/fp");
var NodeGit = require("../");
var Commit = NodeGit.Commit;
var LookupWrapper = NodeGit.Utils.lookupWrapper;

var _amend = Commit.prototype.amend;
var _parent = Commit.prototype.parent;

/**
 * Retrieves the commit pointed to by the oid
 * @async
 * @param {Repository} repo The repo that the commit lives in
 * @param {String|Oid|Commit} id The commit to lookup
 * @return {Commit}
 */
Commit.lookup = LookupWrapper(Commit);

/**
 * Amend a commit
 * @async
 * @param {String} update_ref
 * @param {Signature} author
 * @param {Signature} committer
 * @param {String} message_encoding
 * @param {String} message
 * @param {Tree|Oid} tree
 * @param {Oid} callback
 */
Commit.prototype.amend = function (
  updateRef, author, committer, message_encoding, message, tree, callback) {
    var repo = this.repo;
    var _this = this;
    var treePromise;

    if (tree instanceof NodeGit.Oid){
      treePromise = repo.getTree(tree);
    } else {
      treePromise = Promise.resolve(tree);
    }

    return treePromise
      .then(function(treeObject){
        return _amend.call(_this,
          updateRef,
          author,
          committer,
          message_encoding,
          message,
          treeObject
        );
      });
};

/**
 * Amend a commit with the given signature
 * @async
 * @param {String} updateRef
 * @param {Signature} author
 * @param {Signature} committer
 * @param {String} messageEncoding
 * @param {String} message
 * @param {Tree|Oid} tree
 * @param {Function} onSignature Callback to be called with string to be signed
 * @return {Oid}
*/
Commit.prototype.amendWithSignature = function(
  updateRef,
  author,
  committer,
  messageEncoding,
  message,
  tree,
  onSignature
) {
    let repo = this.repo;
    let parentOids = this.parents();
    let _this = this;
    let promises = [];

    if (tree instanceof NodeGit.Oid) {
      promises.push(repo.getTree(tree));
    } else {
      promises.push(Promise.resolve(tree));
    }

    parentOids.forEach(function (parentOid) {
      promises.push(repo.getCommit(parentOid));
    });

    let treeObject;
    let parents;
    let commitContent;
    let commit;
    let skippedSigning;
    let resolvedAuthor;
    let resolvedCommitter;
    let resolvedMessageEncoding;
    let resolvedMessage;
    let resolvedTree;

    let createCommitPromise = Promise.all(promises)
      .then(function(results) {
        treeObject = fp.head(results);
        parents = fp.tail(results);
        return _this.getTree();
      })
      .then(function(commitTreeResult) {
        let commitTree = commitTreeResult;

        let truthyArgs = fp.omitBy(
          fp.isNil,
          {
            author,
            committer,
            messageEncoding,
            message,
            tree: treeObject
          }
        );

        let commitFields = {
          author: _this.author(),
          committer: _this.committer(),
          messageEncoding: _this.messageEncoding(),
          message: _this.message(),
          tree: commitTree
        };

        ({
          author: resolvedAuthor,
          committer: resolvedCommitter,
          messageEncoding: resolvedMessageEncoding,
          message: resolvedMessage,
          tree: resolvedTree
        } = fp.assign(
          commitFields,
          truthyArgs
        ));

        return Commit.createBuffer(
          repo,
          resolvedAuthor,
          resolvedCommitter,
          resolvedMessageEncoding,
          resolvedMessage,
          resolvedTree,
          parents.length,
          parents
        );
      })
      .then(function(commitContentResult) {
        commitContent = commitContentResult;
        if (!commitContent.endsWith("\n")) {
          commitContent += "\n";
        }
        return onSignature(commitContent);
      })
      .then(function({ code, field, signedData }) {
        switch (code) {
          case NodeGit.Error.CODE.OK:
            return Commit.createWithSignature(
              repo,
              commitContent,
              signedData,
              field
            );
          case NodeGit.Error.CODE.PASSTHROUGH:
            skippedSigning = true;
            return Commit.create(
              repo,
              updateRef,
              resolvedAuthor,
              resolvedCommitter,
              resolvedMessageEncoding,
              resolvedMessage,
              resolvedTree,
              parents.length,
              parents
            );
          default: {
            const error = new Error(
              `Commit.amendWithSignature threw with error code ${code}`
            );
            error.errno = code;
            throw error;
          }
        }
      });

    if (!updateRef) {
      return createCommitPromise;
    }

    return createCommitPromise
      .then(function(commitOid) {
        if (skippedSigning) {
          return commitOid;
        }

        return repo.getCommit(commitOid)
          .then(function(commitResult) {
            commit = commitResult;
            return repo.getReference(updateRef);
          }).then(function(ref) {
            return ref.setTarget(
              commitOid,
              `commit (amend): ${commit.summary()}`
            );
          }).then(function() {
            return commitOid;
          });
      });
};

/**
 * Retrieve the commit time as a Date object.
 * @return {Date}
 */
Commit.prototype.date = function() {
  return new Date(this.timeMs());
};

/**
 * Generate an array of diff trees showing changes between this commit
 * and its parent(s).
 *
 * @async
 * @param {Function} callback
 * @return {Array<Diff>} an array of diffs
 */
Commit.prototype.getDiff = function(callback) {
  return this.getDiffWithOptions(null, callback);
};

/**
 * Generate an array of diff trees showing changes between this commit
 * and its parent(s).
 *
 * @async
 * @param {Object} options
 * @param {Function} callback
 * @return {Array<Diff>} an array of diffs
 */
Commit.prototype.getDiffWithOptions = function(options, callback) {
  var commit = this;

  return commit.getTree().then(function(thisTree) {
    return commit.getParents().then(function(parents) {
      var diffs;
      if (parents.length) {
        diffs = parents.map(function(parent) {
          return parent.getTree().then(function(parentTree) {
            return thisTree.diffWithOptions(parentTree, options);
          });
        });
      } else {
        diffs = [thisTree.diffWithOptions(null, options)];
      }

      return Promise.all(diffs);
    });
  }).then(function(diffs) {
    if (typeof callback === "function") {
      callback(null, diffs);
    }

    return diffs;
  }, callback);
};

/**
 * Retrieve the entry represented by path for this commit.
 * Path must be relative to repository root.
 *
 * @async
 * @param {String} path
 * @return {TreeEntry}
 */
Commit.prototype.getEntry = function(path, callback) {
  return this.getTree().then(function(tree) {
    return tree.getEntry(path).then(function(entry) {
      if (typeof callback === "function") {
        callback(null, entry);
      }

      return entry;
    });
  }, callback);
};

/**
 * Retrieve the commit's parents as commit objects.
 *
 * @async
 * @param {number} limit Optional amount of parents to return.
 * @param {Function} callback
 * @return {Array<Commit>} array of commits
 */
Commit.prototype.getParents = function(limit, callback) {
  var parents = [];

  // Shift arguments.
  if (typeof limit === "function") {
    callback = limit;
  }

  // If no limit was set, default to the maximum parents.
  limit = typeof limit === "number" ? limit : this.parentcount();
  limit = Math.min(limit, this.parentcount());

  for (var i = 0; i < limit; i++) {
    var oid = this.parentId(i);
    var parent = this.repo.getCommit(oid);

    parents.push(parent);
  }

  // Wait for all parents to complete, before returning.
  return Promise.all(parents).then(function(parents) {
    if (typeof callback === "function") {
      callback(null, parents);
    }

    return parents;
  }, callback);
};

/**
 * @typedef extractedSignature
 * @type {Object}
 * @property {String} signature the signature of the commit
 * @property {String} signedData the extracted signed data
 */

/**
 * Retrieve the signature and signed data for a commit.
 * @param  {String} field Optional field to get from the signature,
 *                        defaults to gpgsig
 * @return {extractedSignature}
 */
Commit.prototype.getSignature = function(field) {
  return Commit.extractSignature(this.repo, this.id(), field);
};

/**
 * Get the tree associated with this commit.
 *
 * @async
 * @return {Tree}
 */
Commit.prototype.getTree = function(callback) {
  return this.repo.getTree(this.treeId(), callback);
};

/**
 * Walk the history from this commit backwards.
 *
 * An EventEmitter is returned that will emit a "commit" event for each
 * commit in the history, and one "end" event when the walk is completed.
 * Don't forget to call `start()` on the returned event.
 *
 * @fires EventEmitter#commit Commit
 * @fires EventEmitter#end Array<Commit>
 * @fires EventEmitter#error Error
 *
 * @return {EventEmitter}
 * @start start()
 */
Commit.prototype.history = function() {
  var event = new events.EventEmitter();
  var oid = this.id();
  var revwalk = this.repo.createRevWalk();

  revwalk.sorting.apply(revwalk, arguments);

  var commits = [];

  event.start = function() {
    revwalk.walk(oid, function commitRevWalk(error, commit) {
      if (error) {
        if (error.errno === NodeGit.Error.CODE.ITEROVER) {
          event.emit("end", commits);
          return;
        } else {
          return event.emit("error", error);
        }
      }

      event.emit("commit", commit);
      commits.push(commit);
    });
  };

  return event;
};

/**
 * Get the specified parent of the commit.
 * 
 * @param {number} the position of the parent, starting from 0
 * @async
 * @return {Commit} the parent commit at the specified position
 */
Commit.prototype.parent = function (id) {
  var repository = this.repo;
  return _parent.call(this, id).then(function(parent) {
    parent.repo = repository;
    return parent;
  });
};

/**
 * Retrieve the commit's parent shas.
 *
 * @return {Array<Oid>} array of oids
 */
Commit.prototype.parents = function() {
  var result = [];

  for (var i = 0; i < this.parentcount(); i++) {
    result.push(this.parentId(i));
  }

  return result;
};

/**
 * Retrieve the SHA.
 * @return {String}
 */
Commit.prototype.sha = function() {
  return this.id().toString();
};

/**
 * Retrieve the commit time as a unix timestamp.
 * @return {Number}
 */
Commit.prototype.timeMs = function() {
  return this.time() * 1000;
};

/**
 * The sha of this commit
 * @return {String}
 */
Commit.prototype.toString = function() {
  return this.sha();
};
