function lowerToPascalCase(str: string) {
  return str.replace(/(?:^|\s)\w/g, (match) => match.toUpperCase()).replaceAll(
    " ",
    "",
  );
}
function lowerToTrainCase(str: string) {
  return str.replace(/(?:^|\s)\w/g, (match) => match.toUpperCase()).replaceAll(
    " ",
    "-",
  );
}

/**
 * @param str
 * @returns camelCased string (single letter words are lower cased, e.g. SSN -> ssn)
 */
function lowerToCamelCase(str: string): string {
  const words = str.trim().split(/\s+/);
  const result: string[] = [];
  let i = 0;

  while (i < words.length) {
    if (words[i].length === 1) {
      // We’ve hit the start of a chain of single-letter words
      let j = i;
      while (j < words.length && words[j].length === 1) {
        j++;
      }
      const chainIsAtStart = i === 0;
      // Process that entire chain
      for (let k = i; k < j; k++) {
        result[k] = chainIsAtStart
          ? words[k].toLowerCase()
          : words[k].toUpperCase();
      }
      i = j;
    } else {
      // Normal multi-letter word
      if (i === 0) {
        // first word: all lower
        result[i] = words[i].toLowerCase();
      } else {
        // subsequent words: capitalize first letter
        result[i] = words[i][0].toUpperCase() +
          words[i].slice(1).toLowerCase();
      }
      i++;
    }
  }

  return result.join("");
}

function lowerToSnakeCase(str: string) {
  return str.replace(" ", "_");
}

function lowerToKebabCase(str: string) {
  return str.replace(" ", "-");
}

function lowerToMacroCase(str: string) {
  return str.replace(/\w\S*/g, (match) => match.toUpperCase()).replaceAll(
    " ",
    "_",
  );
}

function lowerToTitleCase(str: string) {
  return str.replace(/(?:^|\s)\w/g, (match) => match.toUpperCase());
}

type CaseType =
  | "pascal"
  | "camel"
  | "snake"
  | "kebab"
  | "macro"
  | "upper"
  | "lower"
  | "train"
  | "title"
  | "";

function parseCase(str: string) {
  const isCaseMap = new Map<CaseType, (str: string) => boolean>([
    ["pascal", (str: string) => {
      return /^[A-Z][a-zA-Z]*$/.test(str);
    }],
    ["camel", (str: string) => {
      return /^[a-z][a-zA-Z]*$/.test(str);
    }],
    ["snake", (str: string) => {
      return /^[a-z][a-z0-9_]*$/.test(str);
    }],
    ["kebab", (str: string) => {
      return /^[a-z][a-z0-9-]*$/.test(str);
    }],
    ["macro", (str: string) => {
      return /^[A-Z]*$/.test(str);
    }],
    ["upper", (str: string) => {
      return /^[A-Z]*$/.test(str);
    }],
    ["lower", (str: string) => {
      return /^[a-z]*$/.test(str);
    }],
    ["train", (str: string) => {
      return /([A-Z][a-z]*(?:-|$))+/.test(str);
    }],
  ]);
  for (const [key, value] of isCaseMap) {
    if (value(str)) return key;
  }
  return "";
}

function coerceCaseToLower(str: string, caseType: CaseType) {
  switch (caseType) {
    case "pascal":
    case "camel":
      return str.replace(/[A-Z]/g, (match) => " " + match.toLowerCase().trim());
    case "macro":
    case "snake":
    case "upper":
      return str.replaceAll("_", " ").toLowerCase();
    case "train":
    case "kebab":
      return str.replaceAll("-", " ").toLowerCase();
    default:
      return str.toLowerCase();
  }
}

export function toCase(str: string, toCase: CaseType) {
  const caseType = parseCase(str) || "";
  if (caseType === toCase) return str;
  const lowerStr = coerceCaseToLower(str, caseType);
  switch (toCase) {
    case "pascal":
      return lowerToPascalCase(lowerStr);
    case "camel":
      return lowerToCamelCase(lowerStr);
    case "snake":
      return lowerToSnakeCase(lowerStr);
    case "kebab":
      return lowerToKebabCase(lowerStr);
    case "macro":
      return lowerToMacroCase(lowerStr);
    case "upper":
      return lowerStr.toUpperCase();
    case "lower":
      return lowerStr.toLowerCase();
    case "train":
      return lowerToTrainCase(lowerStr);
    case "title":
      return lowerToTitleCase(lowerStr);
    default:
      return str;
  }
}

if (import.meta.main) {
  console.log(toCase("SSN", "camel"));
}
