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


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 l in labels:
                            self.kerns.setdefault((l, rc), amt)
                    elif sub[0] == 'STOP':
                        labels = []
                    elif sub[0] != 'COMMENT':
                        raise ValueError(
                            f'{plpath}: unsupported PL LIGTABLE 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
