from fontTools.misc.psCharStrings import (
    SimpleT2Decompiler,
    T2WidthExtractor,
    calcSubrBias,
)


def _uniq_sort(l):
    return sorted(set(l))


class StopHintCountEvent(Exception):
    pass


class _DesubroutinizingT2Decompiler(SimpleT2Decompiler):
    stop_hintcount_ops = (
        "op_hintmask",
        "op_cntrmask",
        "op_rmoveto",
        "op_hmoveto",
        "op_vmoveto",
    )

    def __init__(self, localSubrs, globalSubrs, private=None):
        SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)

    def execute(self, charString):
        self.need_hintcount = True  # until proven otherwise
        for op_name in self.stop_hintcount_ops:
            setattr(self, op_name, self.stop_hint_count)

        if hasattr(charString, "_desubroutinized"):
            # If a charstring has already been desubroutinized, we will still
            # need to execute it if we need to count hints in order to
            # compute the byte length for mask arguments, and haven't finished
            # counting hints pairs.
            if self.need_hintcount and self.callingStack:
                try:
                    SimpleT2Decompiler.execute(self, charString)
                except StopHintCountEvent:
                    del self.callingStack[-1]
            return

        charString._patches = []
        SimpleT2Decompiler.execute(self, charString)
        desubroutinized = charString.program[:]
        for idx, expansion in reversed(charString._patches):
            assert idx >= 2
            assert desubroutinized[idx - 1] in [
                "callsubr",
                "callgsubr",
            ], desubroutinized[idx - 1]
            assert type(desubroutinized[idx - 2]) == int
            if expansion[-1] == "return":
                expansion = expansion[:-1]
            desubroutinized[idx - 2 : idx] = expansion
        if not self.private.in_cff2:
            if "endchar" in desubroutinized:
                # Cut off after first endchar
                desubroutinized = desubroutinized[
                    : desubroutinized.index("endchar") + 1
                ]

        charString._desubroutinized = desubroutinized
        del charString._patches

    def op_callsubr(self, index):
        subr = self.localSubrs[self.operandStack[-1] + self.localBias]
        SimpleT2Decompiler.op_callsubr(self, index)
        self.processSubr(index, subr)

    def op_callgsubr(self, index):
        subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
        SimpleT2Decompiler.op_callgsubr(self, index)
        self.processSubr(index, subr)

    def stop_hint_count(self, *args):
        self.need_hintcount = False
        for op_name in self.stop_hintcount_ops:
            setattr(self, op_name, None)
        cs = self.callingStack[-1]
        if hasattr(cs, "_desubroutinized"):
            raise StopHintCountEvent()

    def op_hintmask(self, index):
        SimpleT2Decompiler.op_hintmask(self, index)
        if self.need_hintcount:
            self.stop_hint_count()

    def processSubr(self, index, subr):
        cs = self.callingStack[-1]
        if not hasattr(cs, "_desubroutinized"):
            cs._patches.append((index, subr._desubroutinized))


def desubroutinize(cff):
    for fontName in cff.fontNames:
        font = cff[fontName]
        cs = font.CharStrings
        for c in cs.values():
            c.decompile()
            subrs = getattr(c.private, "Subrs", [])
            decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private)
            decompiler.execute(c)
            c.program = c._desubroutinized
            del c._desubroutinized
        # Delete all the local subrs
        if hasattr(font, "FDArray"):
            for fd in font.FDArray:
                pd = fd.Private
                if hasattr(pd, "Subrs"):
                    del pd.Subrs
                if "Subrs" in pd.rawDict:
                    del pd.rawDict["Subrs"]
        else:
            pd = font.Private
            if hasattr(pd, "Subrs"):
                del pd.Subrs
            if "Subrs" in pd.rawDict:
                del pd.rawDict["Subrs"]
    # as well as the global subrs
    cff.GlobalSubrs.clear()


class _MarkingT2Decompiler(SimpleT2Decompiler):
    def __init__(self, localSubrs, globalSubrs, private):
        SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
        for subrs in [localSubrs, globalSubrs]:
            if subrs and not hasattr(subrs, "_used"):
                subrs._used = set()

    def op_callsubr(self, index):
        self.localSubrs._used.add(self.operandStack[-1] + self.localBias)
        SimpleT2Decompiler.op_callsubr(self, index)

    def op_callgsubr(self, index):
        self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias)
        SimpleT2Decompiler.op_callgsubr(self, index)


class _DehintingT2Decompiler(T2WidthExtractor):
    class Hints(object):
        def __init__(self):
            # Whether calling this charstring produces any hint stems
            # Note that if a charstring starts with hintmask, it will
            # have has_hint set to True, because it *might* produce an
            # implicit vstem if called under certain conditions.
            self.has_hint = False
            # Index to start at to drop all hints
            self.last_hint = 0
            # Index up to which we know more hints are possible.
            # Only relevant if status is 0 or 1.
            self.last_checked = 0
            # The status means:
            # 0: after dropping hints, this charstring is empty
            # 1: after dropping hints, there may be more hints
            # 	continuing after this, or there might be
            # 	other things.  Not clear yet.
            # 2: no more hints possible after this charstring
            self.status = 0
            # Has hintmask instructions; not recursive
            self.has_hintmask = False
            # List of indices of calls to empty subroutines to remove.
            self.deletions = []

        pass

    def __init__(
        self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None
    ):
        self._css = css
        T2WidthExtractor.__init__(
            self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX
        )
        self.private = private

    def execute(self, charString):
        old_hints = charString._hints if hasattr(charString, "_hints") else None
        charString._hints = self.Hints()

        T2WidthExtractor.execute(self, charString)

        hints = charString._hints

        if hints.has_hint or hints.has_hintmask:
            self._css.add(charString)

        if hints.status != 2:
            # Check from last_check, make sure we didn't have any operators.
            for i in range(hints.last_checked, len(charString.program) - 1):
                if isinstance(charString.program[i], str):
                    hints.status = 2
                    break
                else:
                    hints.status = 1  # There's *something* here
            hints.last_checked = len(charString.program)

        if old_hints:
            assert hints.__dict__ == old_hints.__dict__

    def op_callsubr(self, index):
        subr = self.localSubrs[self.operandStack[-1] + self.localBias]
        T2WidthExtractor.op_callsubr(self, index)
        self.processSubr(index, subr)

    def op_callgsubr(self, index):
        subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
        T2WidthExtractor.op_callgsubr(self, index)
        self.processSubr(index, subr)

    def op_hstem(self, index):
        T2WidthExtractor.op_hstem(self, index)
        self.processHint(index)

    def op_vstem(self, index):
        T2WidthExtractor.op_vstem(self, index)
        self.processHint(index)

    def op_hstemhm(self, index):
        T2WidthExtractor.op_hstemhm(self, index)
        self.processHint(index)

    def op_vstemhm(self, index):
        T2WidthExtractor.op_vstemhm(self, index)
        self.processHint(index)

    def op_hintmask(self, index):
        rv = T2WidthExtractor.op_hintmask(self, index)
        self.processHintmask(index)
        return rv

    def op_cntrmask(self, index):
        rv = T2WidthExtractor.op_cntrmask(self, index)
        self.processHintmask(index)
        return rv

    def processHintmask(self, index):
        cs = self.callingStack[-1]
        hints = cs._hints
        hints.has_hintmask = True
        if hints.status != 2:
            # Check from last_check, see if we may be an implicit vstem
            for i in range(hints.last_checked, index - 1):
                if isinstance(cs.program[i], str):
                    hints.status = 2
                    break
            else:
                # We are an implicit vstem
                hints.has_hint = True
                hints.last_hint = index + 1
                hints.status = 0
        hints.last_checked = index + 1

    def processHint(self, index):
        cs = self.callingStack[-1]
        hints = cs._hints
        hints.has_hint = True
        hints.last_hint = index
        hints.last_checked = index

    def processSubr(self, index, subr):
        cs = self.callingStack[-1]
        hints = cs._hints
        subr_hints = subr._hints

        # Check from last_check, make sure we didn't have
        # any operators.
        if hints.status != 2:
            for i in range(hints.last_checked, index - 1):
                if isinstance(cs.program[i], str):
                    hints.status = 2
                    break
            hints.last_checked = index

        if hints.status != 2:
            if subr_hints.has_hint:
                hints.has_hint = True

                # Decide where to chop off from
                if subr_hints.status == 0:
                    hints.last_hint = index
                else:
                    hints.last_hint = index - 2  # Leave the subr call in

        elif subr_hints.status == 0:
            hints.deletions.append(index)

        hints.status = max(hints.status, subr_hints.status)


def _cs_subset_subroutines(charstring, subrs, gsubrs):
    p = charstring.program
    for i in range(1, len(p)):
        if p[i] == "callsubr":
            assert isinstance(p[i - 1], int)
            p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias
        elif p[i] == "callgsubr":
            assert isinstance(p[i - 1], int)
            p[i - 1] = (
                gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias
            )


def _cs_drop_hints(charstring):
    hints = charstring._hints

    if hints.deletions:
        p = charstring.program
        for idx in reversed(hints.deletions):
            del p[idx - 2 : idx]

    if hints.has_hint:
        assert not hints.deletions or hints.last_hint <= hints.deletions[0]
        charstring.program = charstring.program[hints.last_hint :]
        if not charstring.program:
            # TODO CFF2 no need for endchar.
            charstring.program.append("endchar")
        if hasattr(charstring, "width"):
            # Insert width back if needed
            if charstring.width != charstring.private.defaultWidthX:
                # For CFF2 charstrings, this should never happen
                assert (
                    charstring.private.defaultWidthX is not None
                ), "CFF2 CharStrings must not have an initial width value"
                charstring.program.insert(
                    0, charstring.width - charstring.private.nominalWidthX
                )

    if hints.has_hintmask:
        i = 0
        p = charstring.program
        while i < len(p):
            if p[i] in ["hintmask", "cntrmask"]:
                assert i + 1 <= len(p)
                del p[i : i + 2]
                continue
            i += 1

    assert len(charstring.program)

    del charstring._hints


def remove_hints(cff, *, removeUnusedSubrs: bool = True):
    for fontname in cff.keys():
        font = cff[fontname]
        cs = font.CharStrings
        # This can be tricky, but doesn't have to. What we do is:
        #
        # - Run all used glyph charstrings and recurse into subroutines,
        # - For each charstring (including subroutines), if it has any
        #   of the hint stem operators, we mark it as such.
        #   Upon returning, for each charstring we note all the
        #   subroutine calls it makes that (recursively) contain a stem,
        # - Dropping hinting then consists of the following two ops:
        #   * Drop the piece of the program in each charstring before the
        #     last call to a stem op or a stem-calling subroutine,
        #   * Drop all hintmask operations.
        # - It's trickier... A hintmask right after hints and a few numbers
        #    will act as an implicit vstemhm. As such, we track whether
        #    we have seen any non-hint operators so far and do the right
        #    thing, recursively... Good luck understanding that :(
        css = set()
        for c in cs.values():
            c.decompile()
            subrs = getattr(c.private, "Subrs", [])
            decompiler = _DehintingT2Decompiler(
                css,
                subrs,
                c.globalSubrs,
                c.private.nominalWidthX,
                c.private.defaultWidthX,
                c.private,
            )
            decompiler.execute(c)
            c.width = decompiler.width
        for charstring in css:
            _cs_drop_hints(charstring)
        del css

        # Drop font-wide hinting values
        all_privs = []
        if hasattr(font, "FDArray"):
            all_privs.extend(fd.Private for fd in font.FDArray)
        else:
            all_privs.append(font.Private)
        for priv in all_privs:
            for k in [
                "BlueValues",
                "OtherBlues",
                "FamilyBlues",
                "FamilyOtherBlues",
                "BlueScale",
                "BlueShift",
                "BlueFuzz",
                "StemSnapH",
                "StemSnapV",
                "StdHW",
                "StdVW",
                "ForceBold",
                "LanguageGroup",
                "ExpansionFactor",
            ]:
                if hasattr(priv, k):
                    setattr(priv, k, None)
    if removeUnusedSubrs:
        remove_unused_subroutines(cff)


def _pd_delete_empty_subrs(private_dict):
    if hasattr(private_dict, "Subrs") and not private_dict.Subrs:
        if "Subrs" in private_dict.rawDict:
            del private_dict.rawDict["Subrs"]
        del private_dict.Subrs


def remove_unused_subroutines(cff):
    for fontname in cff.keys():
        font = cff[fontname]
        cs = font.CharStrings
        # Renumber subroutines to remove unused ones

        # Mark all used subroutines
        for c in cs.values():
            subrs = getattr(c.private, "Subrs", [])
            decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private)
            decompiler.execute(c)

        all_subrs = [font.GlobalSubrs]
        if hasattr(font, "FDArray"):
            all_subrs.extend(
                fd.Private.Subrs
                for fd in font.FDArray
                if hasattr(fd.Private, "Subrs") and fd.Private.Subrs
            )
        elif hasattr(font.Private, "Subrs") and font.Private.Subrs:
            all_subrs.append(font.Private.Subrs)

        subrs = set(subrs)  # Remove duplicates

        # Prepare
        for subrs in all_subrs:
            if not hasattr(subrs, "_used"):
                subrs._used = set()
            subrs._used = _uniq_sort(subrs._used)
            subrs._old_bias = calcSubrBias(subrs)
            subrs._new_bias = calcSubrBias(subrs._used)

        # Renumber glyph charstrings
        for c in cs.values():
            subrs = getattr(c.private, "Subrs", None)
            _cs_subset_subroutines(c, subrs, font.GlobalSubrs)

        # Renumber subroutines themselves
        for subrs in all_subrs:
            if subrs == font.GlobalSubrs:
                if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"):
                    local_subrs = font.Private.Subrs
                elif (
                    hasattr(font, "FDArray")
                    and len(font.FDArray) == 1
                    and hasattr(font.FDArray[0].Private, "Subrs")
                ):
                    # Technically we shouldn't do this. But I've run into fonts that do it.
                    local_subrs = font.FDArray[0].Private.Subrs
                else:
                    local_subrs = None
            else:
                local_subrs = subrs

            subrs.items = [subrs.items[i] for i in subrs._used]
            if hasattr(subrs, "file"):
                del subrs.file
            if hasattr(subrs, "offsets"):
                del subrs.offsets

            for subr in subrs.items:
                _cs_subset_subroutines(subr, local_subrs, font.GlobalSubrs)

        # Delete local SubrsIndex if empty
        if hasattr(font, "FDArray"):
            for fd in font.FDArray:
                _pd_delete_empty_subrs(fd.Private)
        else:
            _pd_delete_empty_subrs(font.Private)

        # Cleanup
        for subrs in all_subrs:
            del subrs._used, subrs._old_bias, subrs._new_bias
