#!/usr/bin/env node

/**
 * Git COMMIT-MSG hook for validating commit message
 * See https://docs.google.com/document/d/1rk04jEuGfk9kYzfqCuOlPTSJw3hEDZJTBN5E5f1SALo/edit
 *
 * Installation:
 * >> use ghooks, config in package.json
 */

const fs = require("fs");
const util = require("util");
const resolve = require("path").resolve;
const findup = require("findup");

function semverRegex() {
  return /(?<=^v?|\sv?)(?:(?:0|[1-9]\d{0,9}?)\.){2}(?:0|[1-9]\d{0,9})(?:-(?:--+)?(?:0|[1-9]\d*|\d*[a-z]+\d*)){0,100}(?=$| |\+|\.)(?:(?<=-\S+)(?:\.(?:--?|[\da-z-]*[a-z-]\d*|0|[1-9]\d*)){1,100}?)?(?!\.)(?:\+(?:[\da-z]\.?-?){1,100}?(?!\w))?(?!\+)/gi;
}

const config = getConfig();
const MAX_LENGTH = config.maxSubjectLength || 100;
// 忽略commit msg 中 以 ‘WIP、v’开头的字符串或语义化字符串
const IGNORED = new RegExp(
  util.format("(^WIP)|(^v)|(^%s$)", semverRegex().source)
);

function log(...content) {
  return console.log("validate commit msg:", ...content);
}

log("校验 commit message");

// fixup! and squash! are part of Git, commits tagged with them are not intended
// to be merged, cf. https://git-scm.com/docs/git-commit
const PATTERN = /^((fixup! |squash! )?(\w+)(?:\(([^)\s]+)\))?: (.+))(?:\n|$)/;
const MERGE_COMMIT_PATTERN = /^Merge /;
const error = function (title, ...argv) {
  // gitx does not display it
  // http://gitx.lighthouseapp.com/projects/17830/tickets/294-feature-display-hook
  // - error-message-when-hook-fails
  // https://groups.google.com/group/gitx/browse_thread/thread/a03bcab60844b812
  const type = config.warnOnFail ? "warn" : "error";
  console[type](`validate commit msg: commit message 不符合规范`);
  console[type](`validate commit msg: ${title}`, ...argv);
};

const validateMessage = function (raw) {
  console.log(raw);
  // config.types = config.types || resolve(process.cwd(), '.vsc-commitizen.json');
  const types = config.types ? config.types.map((item) => item.label) : null;
  const scopes = config.scopes ? config.scopes.map((item) => item.label) : null;

  const messageWithBody = (raw || "")
    .split("\n")
    .filter((str) => str.indexOf("#") !== 0)
    .join("\n");

  const message = messageWithBody.split("\n").shift();

  if (message === "") {
    log("由于 commit message 为空，所以中止提交。");
    return false;
  }

  let isValid = true;

  if (MERGE_COMMIT_PATTERN.test(message)) {
    log("检测到有 Merge commit。");
    return true;
  }

  if (IGNORED.test(message)) {
    log("忽略 Commit message。");
    return true;
  }

  const match = PATTERN.exec(message);

  if (!match) {
    error(
      `与规范不相符:
             <类型 *>[(<作用域>)]: [<emoji>] <主题 *>
             [空行]
             [详细描述]
             [空行]
             [底部信息]
 
             规范地址：http://confluence.pri.ibanyu.com/pages/viewpage.action?pageId=1737437#Git&Gitlab工作流-4、Git提交信息格式规范
             类型、作用域 查看：./.vsc-commitizen.json`
    );
    isValid = false;
  } else {
    const firstLine = match[1];
    const squashing = !!match[2];
    const type = match[3];
    const scope = match[4];
    const subject = match[5];

    const SUBJECT_PATTERN = new RegExp(config.subjectPattern || ".+");
    const SUBJECT_PATTERN_ERROR_MSG =
      config.subjectPatternErrorMsg || "主题不符合规范!";

    if (firstLine.length > MAX_LENGTH && !squashing) {
      error("主题超过 %d 字符限制！", MAX_LENGTH);
      isValid = false;
    }

    if (scopes !== null && scope !== void 0 && scopes.indexOf(scope) === -1) {
      error('"%s" 是无效的scope！请选择以下范围: %s', scope, scopes.join(", "));
      isValid = false;
    }

    if (types !== null && types.indexOf(type) === -1) {
      error('"%s" 是无效的type！请选择以下类型: %s', type, types.join(", "));
      isValid = false;
    }

    if (!SUBJECT_PATTERN.exec(subject)) {
      error(SUBJECT_PATTERN_ERROR_MSG);
      isValid = false;
    }
  }

  // Some more ideas, do want anything like this ?
  // - Validate the rest of the message (body, footer, BREAKING CHANGE
  // annotations)
  // - allow only specific scopes (eg. fix(docs) should not be allowed ?
  // - auto correct the type to lower case ?
  // - auto correct first letter of the subject to lower case ?
  // - auto add empty line after subject ?
  // - auto remove empty () ?
  // - auto correct typos in type ?
  // - store incorrect messages, so that we can learn

  isValid = isValid || config.warnOnFail;

  if (isValid) {
    // exit early and skip messaging logics
    log("校验完成！👏 👏 👏");
    return true;
  }

  const argInHelp =
    config.helpMessage && config.helpMessage.indexOf("%s") !== -1;

  if (argInHelp) {
    log(config.helpMessage, messageWithBody);
  } else if (message) {
    log(message);
  }

  if (!argInHelp && config.helpMessage) {
    log(config.helpMessage);
  }

  return false;
};

// publish for testing
exports.validateMessage = validateMessage;
exports.getGitFolder = getGitFolder;
exports.config = config;

// hacky start if not run by mocha :-D istanbul ignore next
if (process.argv.join("").indexOf("mocha") === -1) {
  const commitMsgFile = `${getGitFolder()}/COMMIT_EDITMSG`;
  // const incorrectLogFile = commitMsgFile.replace('COMMIT_EDITMSG', 'logs/incorrect-commit-msgs');

  const hasToString = function hasToString(x) {
    return x && typeof x.toString === "function";
  };

  fs.readFile(commitMsgFile, (err, buffer) => {
    const msg = getCommitMessage(buffer);

    if (!validateMessage(msg)) {
      console.log(`提交信息：\n${msg}\n`);
      process.exit(1);
    } else {
      process.exit(0);
    }

    function getCommitMessage(buf) {
      return hasToString(buf) && buf.toString();
    }
  });
}

function getConfig() {
  const pkgFile = findup.sync(process.cwd(), "package.json");
  const pkg = JSON.parse(fs.readFileSync(resolve(pkgFile, "package.json")));
  if (pkg && pkg.config) {
    return pkg.config["validate-commit-msg"];
  }
  const vsc = resolve(process.cwd(), "scripts/.vsc-commitizen.json");
  const config = fs.existsSync(vsc) ? require(vsc) : {};
  return config;
}

function getGitFolder() {
  let gitDirLocation = "./.git";
  if (!fs.existsSync(gitDirLocation)) {
    throw new Error(`Cannot find file ${gitDirLocation}`);
  }

  if (!fs.lstatSync(gitDirLocation).isDirectory()) {
    const unparsedText = `${fs.readFileSync(gitDirLocation)}`;
    gitDirLocation = unparsedText.substring("gitdir: ".length).trim();
  }

  if (!fs.existsSync(gitDirLocation)) {
    throw new Error(`Cannot find file ${gitDirLocation}`);
  }

  return gitDirLocation;
}
