/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import {Commit as ParsedCommit, Options, sync as parse} from 'conventional-commits-parser';


/** A parsed commit, containing the information needed to validate the commit. */
export interface Commit {
  /** The full raw text of the commit. */
  fullText: string;
  /** The header line of the commit, will be used in the changelog entries. */
  header: string;
  /** The full body of the commit, not including the footer. */
  body: string;
  /** The footer of the commit, containing issue references and note sections. */
  footer: string;
  /** A list of the references to other issues made throughout the commit message. */
  references: ParsedCommit.Reference[];
  /** The type of the commit message. */
  type: string;
  /** The scope of the commit message. */
  scope: string;
  /** The npm scope of the commit message. */
  npmScope: string;
  /** The subject of the commit message. */
  subject: string;
  /** A list of breaking change notes in the commit message. */
  breakingChanges: ParsedCommit.Note[];
  /** A list of deprecation notes in the commit message. */
  deprecations: ParsedCommit.Note[];
  /** Whether the commit is a fixup commit. */
  isFixup: boolean;
  /** Whether the commit is a squash commit. */
  isSquash: boolean;
  /** Whether the commit is a revert commit. */
  isRevert: boolean;
}

/** A parsed commit which originated from a Git Log entry */
export interface CommitFromGitLog extends Commit {
  author: string;
  hash: string;
  shortHash: string;
}

/**
 * A list of tuples expressing the fields to extract from each commit log entry. The tuple contains
 * two values, the first is the key for the property and the second is the template shortcut for the
 * git log command.
 */
const commitFields = {
  hash: '%H',
  shortHash: '%h',
  author: '%aN',
};
/** The additional fields to be included in commit log entries for parsing. */
export type CommitFields = typeof commitFields;
/** The commit fields described as git log format entries for parsing. */
export const commitFieldsAsFormat = (fields: CommitFields) => {
  return Object.entries(fields).map(([key, value]) => `%n-${key}-%n${value}`).join('');
};
/**
 * The git log format template to create git log entries for parsing.
 *
 * The conventional commits parser expects to parse the standard git log raw body (%B) into its
 * component parts. Additionally it will parse additional fields with keys defined by
 * `-{key name}-` separated by new lines.
 * */
export const gitLogFormatForParsing = `%B${commitFieldsAsFormat(commitFields)}`;
/** Markers used to denote the start of a note section in a commit. */
enum NoteSections {
  BREAKING_CHANGE = 'BREAKING CHANGE',
  DEPRECATED = 'DEPRECATED',
}
/** Regex determining if a commit is a fixup. */
const FIXUP_PREFIX_RE = /^fixup! /i;
/** Regex determining if a commit is a squash. */
const SQUASH_PREFIX_RE = /^squash! /i;
/** Regex determining if a commit is a revert. */
const REVERT_PREFIX_RE = /^revert:? /i;
/**
 * Regex pattern for parsing the header line of a commit.
 *
 * Several groups are being matched to be used in the parsed commit object, being mapped to the
 * `headerCorrespondence` object.
 *
 * The pattern can be broken down into component parts:
 * - `(\w+)` - a capturing group discovering the type of the commit.
 * - `(?:\((?:([^/]+)\/)?([^)]+)\))?` - a pair of capturing groups to capture the scope and,
 * optionally the npmScope of the commit.
 * - `(.*)` - a capturing group discovering the subject of the commit.
 */
const headerPattern = /^(\w+)(?:\((?:([^/]+)\/)?([^)]+)\))?: (.*)$/;
/**
 * The property names used for the values extracted from the header via the `headerPattern` regex.
 */
const headerCorrespondence = ['type', 'npmScope', 'scope', 'subject'];
/**
 * Configuration options for the commit parser.
 *
 * NOTE: An extended type from `Options` must be used because the current
 * @types/conventional-commits-parser version does not include the `notesPattern` field.
 */
const parseOptions: Options&{notesPattern: (keywords: string) => RegExp} = {
  commentChar: '#',
  headerPattern,
  headerCorrespondence,
  noteKeywords: [NoteSections.BREAKING_CHANGE, NoteSections.DEPRECATED],
  notesPattern: (keywords: string) => new RegExp(`(${keywords})(?:: ?)(.*)`),
};

/** Parse a commit message into its composite parts. */
export const parseCommitMessage: (fullText: string) => Commit = parseInternal;

/** Parse a commit message from a git log entry into its composite parts. */
export const parseCommitFromGitLog: (fullText: Buffer) => CommitFromGitLog = parseInternal;

/** Parse a full commit message into its composite parts. */
function parseInternal(fullText: string): Commit;
function parseInternal(fullText: Buffer): CommitFromGitLog;
function parseInternal(fullText: string|Buffer): CommitFromGitLog|Commit {
  // Ensure the fullText symbol is a `string`, even if a Buffer was provided.
  fullText = fullText.toString();
  /** The commit message text with the fixup and squash markers stripped out. */
  const strippedCommitMsg = fullText.replace(FIXUP_PREFIX_RE, '')
                                .replace(SQUASH_PREFIX_RE, '')
                                .replace(REVERT_PREFIX_RE, '');
  /** The initially parsed commit. */
  const commit = parse(strippedCommitMsg, parseOptions);
  /** A list of breaking change notes from the commit. */
  const breakingChanges: ParsedCommit.Note[] = [];
  /** A list of deprecation notes from the commit. */
  const deprecations: ParsedCommit.Note[] = [];

  // Extract the commit message notes by marked types into their respective lists.
  commit.notes.forEach((note: ParsedCommit.Note) => {
    if (note.title === NoteSections.BREAKING_CHANGE) {
      return breakingChanges.push(note);
    }
    if (note.title === NoteSections.DEPRECATED) {
      return deprecations.push(note);
    }
  });

  return {
    fullText,
    breakingChanges,
    deprecations,
    body: commit.body || '',
    footer: commit.footer || '',
    header: commit.header || '',
    references: commit.references,
    scope: commit.scope || '',
    subject: commit.subject || '',
    type: commit.type || '',
    npmScope: commit.npmScope || '',
    isFixup: FIXUP_PREFIX_RE.test(fullText),
    isSquash: SQUASH_PREFIX_RE.test(fullText),
    isRevert: REVERT_PREFIX_RE.test(fullText),
    author: commit.author || undefined,
    hash: commit.hash || undefined,
    shortHash: commit.shortHash || undefined,
  };
}