"use strict";

const VOID_ELEMENTS = new Set([
  "area",
  "base",
  "br",
  "col",
  "embed",
  "hr",
  "img",
  "input",
  "link",
  "meta",
  "param",
  "source",
  "track",
  "wbr",
]);

function findOpeningTagEnd(value) {
  let quote = null;
  let escaped = false;

  for (let index = 0; index < value.length; index += 1) {
    const character = value[index];

    if (quote !== null) {
      if (escaped) {
        escaped = false;
        continue;
      }

      if (character === "\\") {
        escaped = true;
        continue;
      }

      if (character === quote) {
        quote = null;
      }

      continue;
    }

    if (character === '"' || character === "'") {
      quote = character;
      continue;
    }

    if (character === ">") {
      return index;
    }
  }

  return -1;
}

function parseAttributes(value) {
  const attributes = [];

  const pattern = /[^\s"'=<>`]+(?:\s*=\s*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s"'=<>`]+))?/g;

  let match;

  while ((match = pattern.exec(value)) !== null) {
    attributes.push(match[0].trim());
  }

  return attributes;
}

function parseOpeningTag(value) {
  const endIndex = findOpeningTagEnd(value);

  if (endIndex === -1) {
    return null;
  }

  const openingPart = value.slice(0, endIndex + 1);

  const remainder = value.slice(endIndex + 1).trim();

  const match = /^<([A-Za-z][\w$:.-]*)([\s\S]*?)(\/?)>$/.exec(openingPart);

  if (!match) {
    return null;
  }

  const tagName = match[1];

  const attributes = parseAttributes(match[2].trim());

  const selfClosing = match[3] === "/";

  const inlineClosing = remainder === `</${tagName}>`;

  return {
    tagName,
    attributes,
    selfClosing,
    inlineClosing,
    remainder,
  };
}

function formatOpeningTag(value, unit, depth, printWidth = 100) {
  const parsed = parseOpeningTag(value);

  if (!parsed) {
    return {
      lines: [`${unit.repeat(depth)}${value.trim()}`],
      opensElement: false,
    };
  }

  const baseIndent = unit.repeat(depth);

  const attributeIndent = unit.repeat(depth + 1);

  const normalizedSingleLine = value.replace(/\s+/g, " ").trim();

  const shouldBreak =
    parsed.attributes.length > 1 ||
    normalizedSingleLine.length > printWidth ||
    value.includes("\n");

  const opensElement =
    !parsed.selfClosing &&
    !parsed.inlineClosing &&
    !VOID_ELEMENTS.has(parsed.tagName.toLowerCase());

  if (!shouldBreak) {
    return {
      lines: [`${baseIndent}${normalizedSingleLine}`],
      opensElement,
    };
  }

  const lines = [
    `${baseIndent}<${parsed.tagName}`,
    ...parsed.attributes.map((attribute) => `${attributeIndent}${attribute}`),
  ];

  if (parsed.inlineClosing) {
    lines.push(`${baseIndent}></${parsed.tagName}>`);
  } else if (parsed.selfClosing) {
    lines.push(`${baseIndent}/>`);
  } else {
    lines.push(`${baseIndent}>`);

    if (parsed.remainder) {
      lines.push(`${baseIndent}${parsed.remainder}`);
    }
  }

  return {
    lines,
    opensElement,
  };
}

function isMultilineOpeningTagStart(value) {
  if (!value.startsWith("<")) {
    return false;
  }

  if (
    value.startsWith("</") ||
    value.startsWith("<!--") ||
    value.startsWith("<!") ||
    value.startsWith("<?")
  ) {
    return false;
  }

  return findOpeningTagEnd(value) === -1;
}

function isClosingTag(value) {
  return /^<\/[A-Za-z][\w$:.-]*\s*>/.test(value);
}

function isInlineElement(value) {
  return /^<([A-Za-z][\w$:.-]*)\b[^>]*>[\s\S]*<\/\1\s*>$/.test(value);
}

function countLeadingClosingBraces(value) {
  let index = 0;
  let count = 0;

  while (index < value.length) {
    while (index < value.length && /\s/.test(value[index])) {
      index += 1;
    }

    if (value[index] !== "}") {
      break;
    }

    count += 1;
    index += 1;
  }

  return count;
}

/**
 * Count braces outside strings and HTML comments.
 *
 * This supports WRN blocks, function bodies, lifecycle hooks,
 * watcher bodies and multiline JavaScript object literals.
 */
function countStructuralBraces(value) {
  let openings = 0;
  let closings = 0;

  let quote = null;
  let escaped = false;
  let htmlComment = false;

  for (let index = 0; index < value.length; index += 1) {
    if (!quote && !htmlComment && value.startsWith("<!--", index)) {
      htmlComment = true;
      index += 3;
      continue;
    }

    if (htmlComment && value.startsWith("-->", index)) {
      htmlComment = false;
      index += 2;
      continue;
    }

    if (htmlComment) {
      continue;
    }

    const character = value[index];

    if (quote !== null) {
      if (escaped) {
        escaped = false;
        continue;
      }

      if (character === "\\") {
        escaped = true;
        continue;
      }

      if (character === quote) {
        quote = null;
      }

      continue;
    }

    if (character === '"' || character === "'" || character === "`") {
      quote = character;
      continue;
    }

    if (character === "{") {
      openings += 1;
    } else if (character === "}") {
      closings += 1;
    }
  }

  return {
    openings,
    closings,
  };
}

function collectOpeningTag(inputLines, startIndex) {
  const collected = [inputLines[startIndex].trim()];

  let index = startIndex;

  while (index + 1 < inputLines.length) {
    const joined = collected.join(" ");

    if (findOpeningTagEnd(joined) !== -1) {
      break;
    }

    index += 1;
    collected.push(inputLines[index].trim());
  }

  return {
    value: collected.join(" "),
    endIndex: index,
  };
}

function formatWrn(source, options = {}) {
  const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize || 4);

  const printWidth = options.printWidth || 100;

  const inputLines = source.replace(/\r\n/g, "\n").split("\n");

  const output = [];

  let codeDepth = 0;
  let htmlDepth = 0;
  let index = 0;
  let previousWasBlank = false;

  while (index < inputLines.length) {
    const originalLine = inputLines[index];

    let value = originalLine.trim();

    if (value === "") {
      if (!previousWasBlank && output.length > 0) {
        output.push("");
      }

      previousWasBlank = true;
      index += 1;
      continue;
    }

    previousWasBlank = false;

    if (isMultilineOpeningTagStart(value)) {
      const collected = collectOpeningTag(inputLines, index);

      value = collected.value;
      index = collected.endIndex;
    }

    const leadingClosingBraces = countLeadingClosingBraces(value);

    const lineCodeDepth = Math.max(0, codeDepth - leadingClosingBraces);

    let lineHtmlDepth = htmlDepth;

    if (isClosingTag(value)) {
      lineHtmlDepth = Math.max(0, htmlDepth - 1);
    }

    const depth = lineCodeDepth + lineHtmlDepth;

    if (
      value.startsWith("<") &&
      !value.startsWith("</") &&
      !value.startsWith("<!--") &&
      !value.startsWith("<!") &&
      !value.startsWith("<?") &&
      !isInlineElement(value)
    ) {
      const formattedTag = formatOpeningTag(value, unit, depth, printWidth);

      output.push(...formattedTag.lines);

      if (formattedTag.opensElement) {
        htmlDepth += 1;
      }
    } else {
      output.push(`${unit.repeat(depth)}${value}`);
    }

    if (isClosingTag(value)) {
      htmlDepth = lineHtmlDepth;
    }

    const braces = countStructuralBraces(value);

    codeDepth = Math.max(
      0,
      lineCodeDepth + braces.openings - Math.max(0, braces.closings - leadingClosingBraces),
    );

    index += 1;
  }

  while (output.length > 0 && output[output.length - 1] === "") {
    output.pop();
  }

  return `${output.join("\n")}\n`;
}

module.exports = {
  countStructuralBraces,
  formatOpeningTag,
  formatWrn,
  parseAttributes,
  parseOpeningTag,
};
