import type { Token as JSToken, JSXToken } from "js-tokens";
import jsTokens from "js-tokens";
import * as charCodes from "charcodes";

import {
  isStrictReservedWord,
  isKeyword,
} from "@babel/helper-validator-identifier";

import { defs, type InternalTokenType } from "./defs.ts";

/**
 * Names that are always allowed as identifiers, but also appear as keywords
 * within certain syntactic productions.
 *
 * https://tc39.es/ecma262/#sec-keywords-and-reserved-words
 *
 * `target` has been omitted since it is very likely going to be a false
 * positive.
 */
const sometimesKeywords = new Set(["as", "async", "from", "get", "of", "set"]);

type Token = {
  type: InternalTokenType | "uncolored";
  value: string;
};

/**
 * RegExp to test for newlines in terminal.
 */
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;

/**
 * RegExp to test for the three types of brackets.
 */
const BRACKET = /^[()[\]{}]$/;

/**
 * Get the type of token, specifying punctuator type.
 */
const getTokenType = function (
  token: JSToken | JSXToken,
): InternalTokenType | "uncolored" {
  if (token.type === "IdentifierName") {
    const tokenValue = token.value;
    if (
      isKeyword(tokenValue) ||
      isStrictReservedWord(tokenValue, true) ||
      sometimesKeywords.has(tokenValue)
    ) {
      return "keyword";
    }

    const firstChar = tokenValue.charCodeAt(0);
    if (firstChar < 128) {
      // ASCII characters
      if (
        firstChar >= charCodes.uppercaseA &&
        firstChar <= charCodes.uppercaseZ
      ) {
        return "capitalized";
      }
    } else {
      const firstChar = String.fromCodePoint(tokenValue.codePointAt(0)!);
      if (firstChar !== firstChar.toLowerCase()) {
        return "capitalized";
      }
    }
  }

  if (token.type === "Punctuator" && BRACKET.test(token.value)) {
    return "uncolored";
  }

  if (token.type === "Invalid" && token.value === "@") {
    return "punctuator";
  }

  switch (token.type) {
    case "NumericLiteral":
      return "number";

    case "StringLiteral":
    case "JSXString":
    case "NoSubstitutionTemplate":
      return "string";

    case "RegularExpressionLiteral":
      return "regex";

    case "Punctuator":
    case "JSXPunctuator":
      return "punctuator";

    case "MultiLineComment":
    case "SingleLineComment":
      return "comment";

    case "Invalid":
    case "JSXInvalid":
      return "invalid";

    case "JSXIdentifier":
      return "jsxIdentifier";

    default:
      return "uncolored";
  }
};

/**
 * Turn a string of JS into an array of objects.
 */
function* tokenize(text: string): Generator<Token> {
  for (const token of jsTokens(text, { jsx: true })) {
    switch (token.type) {
      case "TemplateHead":
        yield { type: "string", value: token.value.slice(0, -2) };
        yield { type: "punctuator", value: "${" };
        break;

      case "TemplateMiddle":
        yield { type: "punctuator", value: "}" };
        yield { type: "string", value: token.value.slice(1, -2) };
        yield { type: "punctuator", value: "${" };
        break;

      case "TemplateTail":
        yield { type: "punctuator", value: "}" };
        yield { type: "string", value: token.value.slice(1) };
        break;

      default:
        yield {
          type: getTokenType(token),
          value: token.value,
        };
    }
  }
}

export function highlight(text: string) {
  if (text === "") return "";

  let highlighted = "";

  for (const { type, value } of tokenize(text)) {
    if (type in defs) {
      highlighted += value
        .split(NEWLINE)
        .map(str => defs[type as InternalTokenType](str))
        .join("\n");
    } else {
      highlighted += value;
    }
  }

  return highlighted;
}
