#!/usr/bin/env npx ts-node

/**
 * Prints a JavaScript regular expression suitable for use in the
 * **overrideScriptDefaults** word breaker decorator.
 *
 * This regular expression matches letters or marks in scripts that
 * conventionally use spaces. This way, the word breaker can make sure to keep
 * contiguous spans of these characters together!
 *
 * If you need to add more Unicode blocks, customize SPACELESS_SCRIPT_BLOCKS.
 */

import {readFileSync} from "fs";
import * as path from "path";

// Where to find UnicodeData.txt and Blocks.txt
const UCD_DIR = path.join("..", "..", "resources", "standards-data", "unicode-character-database");

const SPACELESS_SCRIPT_BLOCKS = new Set([
  "Myanmar",  // a.k.a., Burmese
  "Lao",
  "Thai",
  "Khmer",
  "Katakana",
  "Katakana Phonetic Extensions",
  // Add more scripts here, as necessary!
]);

let blockIter = blocks();
let block = nextBlock();

let eligibleCharacters = [];

// @ts-ignore: TypeScript complains that it can't compile for..of over a
// generator without --downlevelIteration, but it works anyway?
for (let {codePoint, generalCategory} of unicodeData()) {
  if (!block.contains(codePoint)) {
    block = nextBlock();
  }
  console.assert(block.contains(codePoint));

  if (SPACELESS_SCRIPT_BLOCKS.has(block.name) && isLetterOrMark(generalCategory)) {
    eligibleCharacters.push(codePoint);
  }
}

const ranges = groupCodePointsIntoRanges(eligibleCharacters);

const characterClasses = ranges.map(([lower, upper]) => {
  if (lower === upper) {
    return unicodeEscape(lower);
  } else if (lower === upper - 1) {
    return unicodeEscape(lower) + unicodeEscape(upper);
  } else {
    return `${unicodeEscape(lower)}-${unicodeEscape(upper)}`;
  }
}).join("");

console.log(`/**
 * AUTOMATICALLY GENERATED FILE. DO NOT MODIFY
 * See: libexec/create-override-script-regexp.ts for details!
 */
export const HAS_SOUTHEAST_ASIAN_LETTER = /[${characterClasses}]/;`);


////////////////////////////////// Helpers ///////////////////////////////////

function* unicodeData() {
  let unicodeDataFile = readFileSync(path.join(UCD_DIR, "UnicodeData.txt"), "UTF-8");
  for (let line of unicodeDataFile.split("\n")) {
    if (line.trim() == "") {
      continue;
    }

    let parts = line.split(";")
    yield {
      codePoint: parseInt(parts[0], 16),
      generalCategory: parts[2],
    }
  }
}

function* blocks() {
  let blocksFile = readFileSync(path.join(UCD_DIR, "Blocks.txt"), "UTF-8");
  for (let line of blocksFile.split("\n")) {
    if (line.trim() === "") {
      continue;
    }
    if (line.startsWith("#")) {
      continue;
    }

    let [range, name] = line.split("; ");
    let [lower, upper] = range.split("..").map(s => parseInt(s, 16));

    yield {
      name, lower, upper,
      contains(codePoint: number) {
        return lower <= codePoint && codePoint <= upper;
      }
    }
  }
}

function nextBlock() {
  let {value} = blockIter.next();
  if (!value) {
    throw new Error("ran out of blocks");
  }
  return value;
}

function isLetterOrMark(category: string): boolean {
  return category.startsWith("L") || category.startsWith("M");
}

function groupCodePointsIntoRanges(characters: number[]): [number, number][] {
  let ranges = [];

  let previousCharacter = characters[0];
  const candidates = characters.slice(1);
  let currentRange: [number, number] = [previousCharacter, previousCharacter];
  for (let codePoint of candidates) {
    if (codePoint === previousCharacter + 1) {
      currentRange[1] = codePoint;
    } else {
      ranges.push(currentRange);
      currentRange = [codePoint, codePoint];
    }

    previousCharacter = codePoint;
  }

  return ranges;
}

function unicodeEscape(codePoint: number) {
  if (codePoint > 0xFFFF) {
    throw new Error("non-BMP code points not supported");
  }

  let hex = codePoint.toString(16).toUpperCase().padStart(4,'0');
  return `\\u${hex}`;
}
