# Source code for fontTools.misc.psLib

    from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes, tostr
    from fontTools.misc import eexec
    from .psOperators import (
        PSOperators,
        ps_StandardEncoding,
        ps_array,
        ps_boolean,
        ps_dict,
        ps_integer,
        ps_literal,
        ps_mark,
        ps_name,
        ps_operator,
        ps_procedure,
        ps_procmark,
        ps_real,
        ps_string,
    )
    import re
    from collections.abc import Callable
    from string import whitespace
    import logging

    log = logging.getLogger(__name__)

    ps_special = b"()<>[]{}%"  # / is one too, but we take care of that one differently

    skipwhiteRE = re.compile(bytesjoin([b"[", whitespace, b"]*"]))
    endofthingPat = bytesjoin([b"[^][(){}<>/%", whitespace, b"]*"])
    endofthingRE = re.compile(endofthingPat)
    commentRE = re.compile(b"%[^\n\r]*")

    # XXX This not entirely correct as it doesn't allow *nested* embedded parens:
    stringPat = rb"""
    	\(
    		(
    			(
    				[^()]*   \   [()]
    			)
    			|
    			(
    				[^()]*  \(   [^()]*  \)
    			)
    		)*
    		[^()]*
    	\)
    """
    stringPat = b"".join(stringPat.split())
    stringRE = re.compile(stringPat)

    hexstringRE = re.compile(bytesjoin([b"<[", whitespace, b"0-9A-Fa-f]*>"]))

    class PSTokenError(Exception):
        pass

    class PSError(Exception):
        pass

    class PSTokenizer(object):
        def __init__(self, buf=b"", encoding="ascii"):
            # Force self.buf to be a byte string
            buf = tobytes(buf)
            self.buf = buf
            self.len = len(buf)
            self.pos = 0
            self.closed = False
            self.encoding = encoding

        def read(self, n=-1):
            """Read at most 'n' bytes from the buffer, or less if the read
            hits EOF before obtaining 'n' bytes.
            If 'n' is negative or omitted, read all data until EOF is reached.
            """
            if self.closed:
                raise ValueError("I/O operation on closed file")
            if n is None or n < 0:
                newpos = self.len
            else:
                newpos = min(self.pos + n, self.len)
            r = self.buf[self.pos : newpos]
            self.pos = newpos
            return r

        def close(self):
            if not self.closed:
                self.closed = True
                del self.buf, self.pos

        def getnexttoken(
            self,
            # localize some stuff, for performance
            len=len,
            ps_special=ps_special,
            stringmatch=stringRE.match,
            hexstringmatch=hexstringRE.match,
            commentmatch=commentRE.match,
            endmatch=endofthingRE.match,
        ):
            self.skipwhite()
            if self.pos >= self.len:
                return None, None
            pos = self.pos
            buf = self.buf
            char = bytechr(byteord(buf[pos]))
            if char in ps_special:
                if char in b"{}[]":
                    tokentype = "do_special"
                    token = char
                elif char == b"%":
                    tokentype = "do_comment"
                    _, nextpos = commentmatch(buf, pos).span()
                    token = buf[pos:nextpos]
                elif char == b"(":
                    tokentype = "do_string"
                    m = stringmatch(buf, pos)
                    if m is None:
                        raise PSTokenError("bad string at character %d" % pos)
                    _, nextpos = m.span()
                    token = buf[pos:nextpos]
                elif char == b"<":
                    tokentype = "do_hexstring"
                    m = hexstringmatch(buf, pos)
                    if m is None:
                        raise PSTokenError("bad hexstring at character %d" % pos)
                    _, nextpos = m.span()
                    token = buf[pos:nextpos]
                else:
                    raise PSTokenError("bad token at character %d" % pos)
            else:
                if char == b"/":
                    tokentype = "do_literal"
                    m = endmatch(buf, pos + 1)
                else:
                    tokentype = ""
                    m = endmatch(buf, pos)
                if m is None:
                    raise PSTokenError("bad token at character %d" % pos)
                _, nextpos = m.span()
                token = buf[pos:nextpos]
            self.pos = pos + len(token)
            token = tostr(token, encoding=self.encoding)
            return tokentype, token

        def skipwhite(self, whitematch=skipwhiteRE.match):
            _, nextpos = whitematch(self.buf, self.pos).span()
            self.pos = nextpos

        def starteexec(self):
            self.pos = self.pos + 1
            self.dirtybuf = self.buf[self.pos :]
            self.buf, R = eexec.decrypt(self.dirtybuf, 55665)
            self.len = len(self.buf)
            self.pos = 4

        def stopeexec(self):
            if not hasattr(self, "dirtybuf"):
                return
            self.buf = self.dirtybuf
            del self.dirtybuf

    class PSInterpreter(PSOperators):
        def __init__(self, encoding="ascii"):
            systemdict = {}
            userdict = {}
            self.encoding = encoding
            self.dictstack = [systemdict, userdict]
            self.stack = []
            self.proclevel = 0
            self.procmark = ps_procmark()
            self.fillsystemdict()

        def fillsystemdict(self):
            systemdict = self.dictstack[0]
            systemdict["["] = systemdict["mark"] = self.mark = ps_mark()
            systemdict["]"] = ps_operator("]", self.do_makearray)
            systemdict["true"] = ps_boolean(1)
            systemdict["false"] = ps_boolean(0)
            systemdict["StandardEncoding"] = ps_array(ps_StandardEncoding)
            systemdict["FontDirectory"] = ps_dict({})
            self.suckoperators(systemdict, self.__class__)

        def suckoperators(self, systemdict, klass):
            for name in dir(klass):
                attr = getattr(self, name)
                if isinstance(attr, Callable) and name[:3] == "ps_":
                    name = name[3:]
                    systemdict[name] = ps_operator(name, attr)
            for baseclass in klass.__bases__:
                self.suckoperators(systemdict, baseclass)

        def interpret(self, data, getattr=getattr):
            tokenizer = self.tokenizer = PSTokenizer(data, self.encoding)
            getnexttoken = tokenizer.getnexttoken
            do_token = self.do_token
            handle_object = self.handle_object
            try:
                while 1:
                    tokentype, token = getnexttoken()
                    if not token:
                        break
                    if tokentype:
                        handler = getattr(self, tokentype)
                        object = handler(token)
                    else:
                        object = do_token(token)
                    if object is not None:
                        handle_object(object)
                tokenizer.close()
                self.tokenizer = None
            except:
                if self.tokenizer is not None:
                    log.debug(
                        "ps error:\n"
                        "- - - - - - -\n"
                        "%s\n"
                        ">>>\n"
                        "%s\n"
                        "- - - - - - -",
                        self.tokenizer.buf[self.tokenizer.pos - 50 : self.tokenizer.pos],
                        self.tokenizer.buf[self.tokenizer.pos : self.tokenizer.pos + 50],
                    )
                raise

        def handle_object(self, object):
            if not (self.proclevel or object.literal or object.type == "proceduretype"):
                if object.type != "operatortype":
                    object = self.resolve_name(object.value)
                if object.literal:
                    self.push(object)
                else:
                    if object.type == "proceduretype":
                        self.call_procedure(object)
                    else:
                        object.function()
            else:
                self.push(object)

        def call_procedure(self, proc):
            handle_object = self.handle_object
            for item in proc.value:
                handle_object(item)

        def resolve_name(self, name):
            dictstack = self.dictstack
            for i in range(len(dictstack) - 1, -1, -1):
                if name in dictstack[i]:
                    return dictstack[i][name]
            raise PSError("name error: " + str(name))

        def do_token(
            self,
            token,
            int=int,
            float=float,
            ps_name=ps_name,
            ps_integer=ps_integer,
            ps_real=ps_real,
        ):
            try:
                num = int(token)
            except (ValueError, OverflowError):
                try:
                    num = float(token)
                except (ValueError, OverflowError):
                    if "#" in token:
                        hashpos = token.find("#")
                        try:
                            base = int(token[:hashpos])
                            num = int(token[hashpos + 1 :], base)
                        except (ValueError, OverflowError):
                            return ps_name(token)
                        else:
                            return ps_integer(num)
                    else:
                        return ps_name(token)
                else:
                    return ps_real(num)
            else:
                return ps_integer(num)

        def do_comment(self, token):
            pass

        def do_literal(self, token):
            return ps_literal(token[1:])

        def do_string(self, token):
            return ps_string(token[1:-1])

        def do_hexstring(self, token):
            hexStr = "".join(token[1:-1].split())
            if len(hexStr) % 2:
                hexStr = hexStr + "0"
            cleanstr = []
            for i in range(0, len(hexStr), 2):
                cleanstr.append(chr(int(hexStr[i : i + 2], 16)))
            cleanstr = "".join(cleanstr)
            return ps_string(cleanstr)

        def do_special(self, token):
            if token == "{":
                self.proclevel = self.proclevel + 1
                return self.procmark
            elif token == "}":
                proc = []
                while 1:
                    topobject = self.pop()
                    if topobject == self.procmark:
                        break
                    proc.append(topobject)
                self.proclevel = self.proclevel - 1
                proc.reverse()
                return ps_procedure(proc)
            elif token == "[":
                return self.mark
            elif token == "]":
                return ps_name("]")
            else:
                raise PSTokenError("huh?")

        def push(self, object):
            self.stack.append(object)

        def pop(self, *types):
            stack = self.stack
            if not stack:
                raise PSError("stack underflow")
            object = stack[-1]
            if types:
                if object.type not in types:
                    raise PSError(
                        "typecheck, expected %s, found %s" % (repr(types), object.type)
                    )
            del stack[-1]
            return object

        def do_makearray(self):
            array = []
            while 1:
                topobject = self.pop()
                if topobject == self.mark:
                    break
                array.append(topobject)
            array.reverse()
            self.push(ps_array(array))

        def close(self):
            """Remove circular references."""
            del self.stack
            del self.dictstack

    def unpack_item(item):
        tp = type(item.value)
        if tp == dict:
            newitem = {}
            for key, value in item.value.items():
                newitem[key] = unpack_item(value)
        elif tp == list:
            newitem = [None] * len(item.value)
            for i in range(len(item.value)):
                newitem[i] = unpack_item(item.value[i])
            if item.type == "proceduretype":
                newitem = tuple(newitem)
        else:
            newitem = item.value
        return newitem

    def suckfont(data, encoding="ascii"):
        m = re.search(rb"/FontName\s+/([^ \t\n\r]+)\s+def", data)
        if m:
            fontName = m.group(1)
            fontName = fontName.decode()
        else:
            fontName = None
        interpreter = PSInterpreter(encoding=encoding)
        interpreter.interpret(
            b"/Helvetica 4 dict dup /Encoding StandardEncoding put definefont pop"
        )
        interpreter.interpret(data)
        fontdir = interpreter.dictstack[0]["FontDirectory"].value
        if fontName in fontdir:
            rawfont = fontdir[fontName]
        else:
            # fall back, in case fontName wasn't found
            fontNames = list(fontdir.keys())
            if len(fontNames) > 1:
                fontNames.remove("Helvetica")
            fontNames.sort()
            rawfont = fontdir[fontNames[0]]
        interpreter.close()
        return unpack_item(rawfont)