# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========

import json
import pathlib
import sys
import unicodedata

ROOT = pathlib.Path(__file__).resolve().parent.parent

PY_OUT = ROOT / "python/mirage/utils/generated/width_data.py"
TS_OUT = ROOT / "typescript/packages/core/src/utils/generated/width_data.ts"
FIXTURE = ROOT / "integ/fixtures/wc/width.json"

LICENSE_PY = (ROOT / "scripts/gen_specs.py").read_text().split("\n")[:13]

# Hangul jamo medial vowels and final consonants combine onto the preceding
# syllable, so glibc gives them zero width even though their East Asian Width
# is not W. Verified: `printf 'aᅠb' | wc -L` is 2.
JAMO_ZERO = (0x1160, 0x11FF)

# The format characters glibc gives zero width. Cf is *not* uniformly zero --
# U+00AD, U+0600-U+0605, U+110BD and the other prepended concatenation marks
# measure 1. Every range here was pinned against coreutils 9.7 in
# debian:stable-slim; see python/tests/utils/test_width.py.
ZERO_FORMAT = (
    (0x061C, 0x061C),
    (0x200B, 0x200F),
    (0x2060, 0x2064),
    (0x2066, 0x206F),
    (0xFEFF, 0xFEFF),
)

# GNU splits words on glibc's iswspace, which is Unicode White_Space *minus*
# U+0085. Python's str.isspace() adds U+001C-U+001F and U+0085; JavaScript's
# \s adds U+FEFF. Both were wrong in opposite directions, so the set is
# spelled out once here and generated into both trees.
WHITESPACE = (
    (0x0009, 0x000D),
    (0x0020, 0x0020),
    (0x00A0, 0x00A0),
    (0x1680, 0x1680),
    (0x2000, 0x200A),
    (0x2028, 0x2029),
    (0x202F, 0x202F),
    (0x205F, 0x205F),
    (0x3000, 0x3000),
)


def in_ranges(cp: int, ranges: tuple[tuple[int, int], ...]) -> bool:
    return any(lo <= cp <= hi for lo, hi in ranges)


def is_zero(cp: int) -> bool:
    if unicodedata.category(chr(cp)) in {"Mn", "Me"}:
        return True
    if JAMO_ZERO[0] <= cp <= JAMO_ZERO[1]:
        return True
    return in_ranges(cp, ZERO_FORMAT)


def is_wide(cp: int) -> bool:
    return unicodedata.east_asian_width(chr(cp)) in {"W", "F"}


def ranges_of(predicate) -> list[list[int]]:
    out: list[list[int]] = []
    start: int | None = None
    for cp in range(0x110000):
        if predicate(cp):
            if start is None:
                start = cp
        elif start is not None:
            out.append([start, cp - 1])
            start = None
    if start is not None:
        out.append([start, 0x10FFFF])
    return out


def rows(ranges: list[list[int]], per_row: int, indent: str, fmt: str) -> str:
    lines = []
    for i in range(0, len(ranges), per_row):
        chunk = ranges[i:i + per_row]
        lines.append(indent + " ".join(fmt.format(lo, hi) for lo, hi in chunk))
    return "\n".join(lines)


def render_py(zero: list[list[int]], wide: list[list[int]],
              space: list[list[int]], version: str) -> str:
    head = "\n".join(LICENSE_PY)
    return f"""{head}

# GENERATED by scripts/gen_width_table.py from Unicode {version}. Do not edit
# by hand; run the script and commit both trees' output together.

ZERO_WIDTH: tuple[tuple[int, int], ...] = (
{rows(zero, 4, "    ", "({}, {}),")}
)

WIDE: tuple[tuple[int, int], ...] = (
{rows(wide, 4, "    ", "({}, {}),")}
)

WHITESPACE: tuple[tuple[int, int], ...] = (
{rows(space, 4, "    ", "({}, {}),")}
)
"""


def render_ts(zero: list[list[int]], wide: list[list[int]],
              space: list[list[int]], version: str) -> str:
    head = "\n".join("//" + line[1:] for line in LICENSE_PY)
    return f"""{head}

// GENERATED by scripts/gen_width_table.py from Unicode {version}. Do not edit
// by hand; run the script and commit both trees' output together.

export const ZERO_WIDTH: readonly (readonly [number, number])[] = [
{rows(zero, 4, "  ", "[{}, {}],")}
]

export const WIDE: readonly (readonly [number, number])[] = [
{rows(wide, 4, "  ", "[{}, {}],")}
]

export const WHITESPACE: readonly (readonly [number, number])[] = [
{rows(space, 4, "  ", "[{}, {}],")}
]
"""


def main() -> int:
    version = unicodedata.unidata_version
    zero = ranges_of(is_zero)
    wide = ranges_of(is_wide)
    space = [list(pair) for pair in WHITESPACE]

    PY_OUT.parent.mkdir(parents=True, exist_ok=True)
    (PY_OUT.parent / "__init__.py").write_text("\n".join(LICENSE_PY) + "\n")
    PY_OUT.write_text(render_py(zero, wide, space, version))
    TS_OUT.parent.mkdir(parents=True, exist_ok=True)
    TS_OUT.write_text(render_ts(zero, wide, space, version))
    FIXTURE.parent.mkdir(parents=True, exist_ok=True)
    FIXTURE.write_text(
        json.dumps(
            {
                "unicode_version": version,
                "zero_width": zero,
                "wide": wide,
                "whitespace": space,
            },
            indent=2,
        ) + "\n")
    print(f"unicode {version}: {len(zero)} zero-width, {len(wide)} wide, "
          f"{len(space)} whitespace ranges")
    return 0


if __name__ == "__main__":
    sys.exit(main())
