/**
 * @param s A string in camel, pascal, snake, kebab and spaced case format.
 * @returns The string broken into an array of words.
 * @example toWords("convertMy_string") === ["convert", "My", "string"]
 */
export default function toWords(s: string): string[] {
  if (!s) {
    return [];
  }
  const starts: number[] = [0];
  for (let i = 1; i < s.length; i += 1) {
    if (
      // Break if we've switched to uppercase e.g. someThing
      (isLowerCase(s[i - 1]) && isUpperCase(s[i])) ||
      // Break if we're about to switch to lowercase, e.g. HTTPRequest
      (i + 1 < s.length && isUpperCase(s[i]) && isLowerCase(s[i + 1])) ||
      // Break at numbers if we're not:
      // - previously uppercase e.g. HTTP2
      // - just 1 letter e.g. v2
      // - just 2 letters and following character is not a number e.g. mp4
      // - part of a sequence of numbers e.g. 1234
      (isNumber(s[i]) &&
        !isUpperCase(s[i - 1]) &&
        !(i - starts[starts.length - 1] === 1) &&
        !(
          i - starts[starts.length - 1] === 2 &&
          i + 1 < s.length &&
          !isNumber(s[i + 1])
        ) &&
        !isNumber(s[i - 1])) ||
      // Break after separator symbols
      isSeparator(s[i - 1])
    ) {
      // Start new word if this character is upper case and previous one isn't
      // or if this character is a number and previous one isn't
      starts.push(i);
    }
  }

  const words = starts.map((_, index) =>
    s.slice(starts[index], starts[index + 1])
  );

  return words.map((word) => word.replace(/[_ -]/g, "").trim()).filter(Boolean);
}

function isUpperCase(char: string): boolean {
  return !isNumber(char) && !isSeparator(char) && char === char.toUpperCase();
}

function isLowerCase(char: string): boolean {
  return !isNumber(char) && !isSeparator(char) && char === char.toLowerCase();
}

function isNumber(char: string): boolean {
  return "0123456789".includes(char);
}

function isSeparator(char: string): boolean {
  return char === "_" || char === "-" || char === " ";
}
