"""Parse tftopl property lists into TFM metrics and recipes."""

import re
import struct
from pathlib import Path


def apply_raw_alphabet_metrics(tfm, raw):
    """Overlay raw WD/IC/kern only for explicitly selected new alphabet
    roles.
    """
    source_chars = {
        int(slot): metrics for slot, metrics in raw["chars"].items()
    }
    if set(tfm.chars) != set(source_chars):
        raise ValueError("PL/raw TFM character coverage differs")
    for slot, metrics in source_chars.items():
        tfm.chars[slot]["wd"] = metrics["wd"]
        tfm.chars[slot]["ic"] = metrics["ic"]
    tfm.kerns = {
        (int(left), int(right)): value for left, right, value in raw["kerns"]
    }


def _tok(s):
    # tokenize parens and atoms
    return re.findall(r"\(|\)|[^\s()]+", s)


def _parse(tokens):
    it = iter(tokens)

    def node():
        items = []
        for t in it:
            if t == "(":
                items.append(node())
            elif t == ")":
                return items
            else:
                items.append(t)
        return items

    top = []
    for t in it:
        if t == "(":
            top.append(node())
    return top


def _charcode(t, v):
    if t == "O":
        return int(v, 8)
    if t == "D":
        return int(v)
    if t == "C":
        return ord(v)
    if t == "H":
        return int(v, 16)
    raise ValueError((t, v))


def _num(items):
    # ['R', '0.5'] or ['D','8'] ...
    t, v = items[0], items[1]
    if t == "R":
        return float(v)
    if t == "D":
        return int(v)
    if t == "O":
        return int(v, 8)
    raise ValueError(items)


FD_NAMES = {
    1: "slant",
    2: "space",
    3: "stretch",
    4: "shrink",
    5: "xheight",
    6: "quad",
    7: "extraspace",
    8: "num1",
    9: "num2",
    10: "num3",
    11: "denom1",
    12: "denom2",
    13: "sup1",
    14: "sup2",
    15: "sup3",
    16: "sub1",
    17: "sub2",
    18: "supdrop",
    19: "subdrop",
    20: "delim1",
    21: "delim2",
    22: "axisheight",
}
FD_NAMES_EX = {
    8: "defaultrulethickness",
    9: "bigopspacing1",
    10: "bigopspacing2",
    11: "bigopspacing3",
    12: "bigopspacing4",
    13: "bigopspacing5",
}


class TFM:
    def __init__(self, plpath):
        with open(plpath) as stream:
            src = stream.read()
        top = _parse(_tok(src))
        self.fontdimen = {}
        self.design_size = None  # TeX points; read from PL DESIGNSIZE
        self.chars = {}  # code -> dict(wd,ht,dp,ic,next,varchar)
        self.kerns = {}  # (left,right) -> amount
        for node in top:
            head = node[0]
            if head == "BOUNDARYCHAR":
                raise ValueError(
                    f"{plpath}: unsupported PL boundary character"
                )
            elif head == "DESIGNSIZE":
                self.design_size = float(_num(node[1:3]))
            elif head == "FONTDIMEN":
                for sub in node[1:]:
                    if not isinstance(sub, list):
                        continue
                    key = sub[0]
                    if key == "PARAMETER":
                        idx = _charcode(sub[1], sub[2])
                        self.fontdimen[idx] = _num(sub[3:5])
                    else:
                        names = {v.upper(): k for k, v in FD_NAMES.items()}
                        names.update(
                            {v.upper(): k for k, v in FD_NAMES_EX.items()}
                        )
                        idx = names.get(key)
                        if idx:
                            self.fontdimen[idx] = _num(sub[1:3])
            elif head == "LIGTABLE":
                if any(
                    isinstance(sub, list) and sub and sub[0] == "SKIP"
                    for sub in node[1:]
                ):
                    raise ValueError(
                        (
                            f"{plpath}: unsupported PL LIGTABLE SKIP; "
                            f"kerning not imported"
                        )
                    )
                labels = []
                for sub in node[1:]:
                    if not isinstance(sub, list):
                        continue
                    if sub[0] == "LABEL":
                        if sub[1] == "BOUNDARYCHAR":
                            raise ValueError(
                                f"{plpath}: unsupported PL boundary label"
                            )
                        labels.append(_charcode(sub[1], sub[2]))
                    elif sub[0] == "KRN":
                        rc = _charcode(sub[1], sub[2])
                        amt = _num(sub[3:5])
                        for left in labels:
                            self.kerns.setdefault((left, rc), amt)
                    elif sub[0] == "STOP":
                        labels = []
                    elif sub[0] != "COMMENT":
                        raise ValueError(
                            f"{plpath}: unsupported PL LIGTABLE "
                            f"command {sub[0]}"
                        )
            elif head == "CHARACTER":
                code = _charcode(node[1], node[2])
                d = {
                    "wd": 0.0,
                    "ht": 0.0,
                    "dp": 0.0,
                    "ic": 0.0,
                    "next": None,
                    "varchar": None,
                }
                for sub in node[3:]:
                    if not isinstance(sub, list):
                        continue
                    k = sub[0]
                    if k == "CHARWD":
                        d["wd"] = _num(sub[1:3])
                    elif k == "CHARHT":
                        d["ht"] = _num(sub[1:3])
                    elif k == "CHARDP":
                        d["dp"] = _num(sub[1:3])
                    elif k == "CHARIC":
                        d["ic"] = _num(sub[1:3])
                    elif k == "NEXTLARGER":
                        d["next"] = _charcode(sub[1], sub[2])
                    elif k == "VARCHAR":
                        vc = {}
                        for p in sub[1:]:
                            if isinstance(p, list) and p[0] in (
                                "TOP",
                                "MID",
                                "BOT",
                                "REP",
                            ):
                                vc[p[0]] = _charcode(p[1], p[2])
                        d["varchar"] = vc
                self.chars[code] = d


"""Read effective TFM kerns as signed fix20 integers.

Execute each character's start/restart and mismatch skip chain. The
first command matching a right character wins, including a zero kern.
Ligatures and boundary programs are deliberately unsupported and
rejected explicitly. This helper does not change the project's PL/raw
width or italic readers.
"""


def read_kerns_fix20(path):
    data = Path(path).read_bytes()

    def invalid(reason):
        raise ValueError(f"{path}: invalid TFM: {reason}")

    if len(data) < 24:
        invalid("truncated size header")
    lf, lh, bc, ec, nw, nh, nd, ni, nl, nk, ne, np = struct.unpack(
        ">12H", data[:24]
    )
    nc = max(0, ec - bc + 1)
    if (
        bc > ec + 1
        or ec > 255
        or lh < 2
        or min(nw, nh, nd, ni) == 0
        or lf != 6 + lh + nc + nw + nh + nd + ni + nl + nk + ne + np
        or len(data) != 4 * lf
    ):
        invalid("inconsistent table sizes")
    char_start = 4 * (6 + lh)
    program_start = char_start + 4 * (nc + nw + nh + nd + ni)
    program = [
        tuple(data[i:i + 4])
        for i in range(program_start, program_start + 4 * nl, 4)
    ]
    kern_start = program_start + 4 * nl
    kerns = struct.unpack(f">{nk}i", data[kern_start:kern_start + 4 * nk])
    if program and (program[0][0] == 255 or program[-1][0] == 255):
        raise ValueError(f"{path}: unsupported TFM boundary program")
    result = {}
    for left in range(bc, ec + 1):
        width, _, it, start = data[
            char_start + 4 * (left - bc):char_start + 4 * (left - bc + 1)
        ]
        if not width or it % 4 != 1:
            continue
        if start >= nl:
            invalid("lig/kern start outside program")
        if program[start][0] > 128:
            start = 256 * program[start][2] + program[start][3]
        seen = set()
        matched = set()
        index = start
        while True:
            if index >= nl or index in seen:
                invalid("out-of-range or cyclic lig/kern program")
            seen.add(index)
            skip, right, op, remainder = program[index]
            if skip <= 128 and right not in matched:
                matched.add(right)
                if op < 128:
                    raise ValueError(
                        f"{path}: unsupported TFM ligature "
                        f"at instruction {index}"
                    )
                ki = 256 * (op - 128) + remainder
                if ki >= nk:
                    invalid("kern index outside table")
                result[left, right] = kerns[ki]
            if skip >= 128:
                break
            index += skip + 1
    return result


def raw_alphabet_metrics(path):
    """Read exact binary TFM fractions into a local JSON-safe snapshot.
    """
    from fontTools.tfmLib import TFM

    tfm = TFM(str(path))
    return {
        "chars": {
            str(slot): {
                "wd": char.get("width", 0),
                "ic": char.get("italic", 0),
            }
            for slot, char in tfm.chars.items()
        },
        "kerns": [
            [left, right, value / (1 << 20)]
            for (left, right), value in read_kerns_fix20(path).items()
        ],
    }


def read_pl(path, pl_path, *, run=None):
    """One tftopl interpretation per role; preserve PL ordering and
    rounding.
    """
    import subprocess

    command = ["tftopl", str(path), str(pl_path)]
    if run is None:
        subprocess.run(command, check=True)
    else:
        run("tftopl " + str(path), command)
    return TFM(pl_path)


def to_data(tfm):
    return dict(
        fontdimen=list(tfm.fontdimen.items()),
        design_size=tfm.design_size,
        chars=list(tfm.chars.items()),
        kerns=[[left, r, v] for (left, r), v in tfm.kerns.items()],
    )


def from_data(data):
    tfm = TFM.__new__(TFM)
    tfm.fontdimen = {int(k): v for k, v in data["fontdimen"]}
    tfm.design_size = data["design_size"]
    tfm.chars = {int(k): v for k, v in data["chars"]}
    tfm.kerns = {(int(left), int(r)): v for left, r, v in data["kerns"]}
    return tfm


def read_raw(path):
    """Keep the existing binary parser for post-native original
    additions.
    """
    from fontTools.tfmLib import TFM as RawTFM

    return RawTFM(str(path))
