module.exports = (input) => {
  if (!input) return [];
  if (typeof input !== "string" || input.match(/^\s+$/)) return [];

  const lines = input.split("\n");
  if (lines.length === 0) return [];

  const files = [];
  let currentFile = null;
  let currentChunk = null;
  let deletedLineCounter = 0;
  let addedLineCounter = 0;
  let currentFileChanges = null;

  const normal = (line) => {
    currentChunk?.changes.push({
      type: "normal",
      normal: true,
      ln1: deletedLineCounter++,
      ln2: addedLineCounter++,
      content: line,
    });
    currentFileChanges.oldLines--;
    currentFileChanges.newLines--;
  };

  const start = (line) => {
    const [fromFileName, toFileName] = parseFiles(line) ?? [];

    currentFile = {
      chunks: [],
      deletions: 0,
      additions: 0,
      from: fromFileName,
      to: toFileName,
    };

    files.push(currentFile);
  };

  const restart = () => {
    if (!currentFile || currentFile.chunks.length) start();
  };

  const newFile = () => {
    restart();
    currentFile.new = true;
    currentFile.from = "/dev/null";
  };

  const deletedFile = () => {
    restart();
    currentFile.deleted = true;
    currentFile.to = "/dev/null";
  };

  const index = (line) => {
    restart();
    currentFile.index = line.split(" ").slice(1);
  };

  const fromFile = (line) => {
    restart();
    currentFile.from = parseOldOrNewFile(line);
  };

  const toFile = (line) => {
    restart();
    currentFile.to = parseOldOrNewFile(line);
  };

  const toNumOfLines = (number) => +(number || 1);

  const chunk = (line, match) => {
    if (!currentFile) return;

    const [oldStart, oldNumLines, newStart, newNumLines] = match.slice(1);

    deletedLineCounter = +oldStart;
    addedLineCounter = +newStart;
    currentChunk = {
      content: line,
      changes: [],
      oldStart: +oldStart,
      oldLines: toNumOfLines(oldNumLines),
      newStart: +newStart,
      newLines: toNumOfLines(newNumLines),
    };
    currentFileChanges = {
      oldLines: toNumOfLines(oldNumLines),
      newLines: toNumOfLines(newNumLines),
    };
    currentFile.chunks.push(currentChunk);
  };

  const del = (line) => {
    if (!currentChunk) return;

    currentChunk.changes.push({
      type: "del",
      del: true,
      ln: deletedLineCounter++,
      content: line,
    });
    currentFile.deletions++;
    currentFileChanges.oldLines--;
  };

  const add = (line) => {
    if (!currentChunk) return;

    currentChunk.changes.push({
      type: "add",
      add: true,
      ln: addedLineCounter++,
      content: line,
    });
    currentFile.additions++;
    currentFileChanges.newLines--;
  };

  const eof = (line) => {
    if (!currentChunk) return;

    const [mostRecentChange] = currentChunk.changes.slice(-1);

    currentChunk.changes.push({
      type: mostRecentChange.type,
      [mostRecentChange.type]: true,
      ln1: mostRecentChange.ln1,
      ln2: mostRecentChange.ln2,
      ln: mostRecentChange.ln,
      content: line,
    });
  };

  const schemaHeaders = [
    [/^diff\s/, start],
    [/^new file mode \d+$/, newFile],
    [/^deleted file mode \d+$/, deletedFile],
    [/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index],
    [/^---\s/, fromFile],
    [/^\+\+\+\s/, toFile],
    [/^@@\s+-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/, chunk],
    [/^\\ No newline at end of file$/, eof],
  ];

  const schemaContent = [
    [/^-/, del],
    [/^\+/, add],
    [/^\s+/, normal],
  ];

  const parseContentLine = (line) => {
    for (const [pattern, handler] of schemaContent) {
      const match = line.match(pattern);
      if (match) {
        handler(line, match);
        break;
      }
    }
    if (
      currentFileChanges.oldLines === 0 &&
      currentFileChanges.newLines === 0
    ) {
      currentFileChanges = null;
    }
  };

  const parseHeaderLine = (line) => {
    for (const [pattern, handler] of schemaHeaders) {
      const match = line.match(pattern);
      if (match) {
        handler(line, match);
        break;
      }
    }
  };

  const parseLine = (line) => {
    if (currentFileChanges) {
      parseContentLine(line);
    } else {
      parseHeaderLine(line);
    }
    return;
  };

  for (const line of lines) parseLine(line);

  return files;
};

const fileNameDiffRegex = /a\/.*(?=["']? ["']?b\/)|b\/.*$/g;
const gitFileHeaderRegex = /^(a|b)\//;
const parseFiles = (line) => {
  let fileNames = line?.match(fileNameDiffRegex);
  return fileNames?.map((fileName) =>
    fileName.replace(gitFileHeaderRegex, "").replace(/("|')$/, "")
  );
};

const qoutedFileNameRegex = /^\\?['"]|\\?['"]$/g;
const parseOldOrNewFile = (line) => {
  let fileName = leftTrimChars(line, "-+").trim();
  fileName = removeTimeStamp(fileName);
  return fileName
    .replace(qoutedFileNameRegex, "")
    .replace(gitFileHeaderRegex, "");
};

const leftTrimChars = (string, trimmingChars) => {
  string = makeString(string);
  if (!trimmingChars && String.prototype.trimLeft) return string.trimLeft();

  let trimmingString = formTrimmingString(trimmingChars);

  return string.replace(new RegExp(`^${trimmingString}+`), "");
};

const timeStampRegex =
  /\t.*|\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(.\d+)?\s(\+|-)\d\d\d\d/;
const removeTimeStamp = (string) => {
  const timeStamp = timeStampRegex.exec(string);
  if (timeStamp) {
    string = string.substring(0, timeStamp.index).trim();
  }
  return string;
};

const formTrimmingString = (trimmingChars) => {
  if (trimmingChars === null || trimmingChars === undefined) return "\\s";
  else if (trimmingChars instanceof RegExp) return trimmingChars.source;
  return `[${makeString(trimmingChars).replace(
    /([.*+?^=!:${}()|[\]/\\])/g,
    "\\$1"
  )}]`;
};

const makeString = (itemToConvert) => (itemToConvert ?? "") + "";