# Abstract Syntax Tree produced by parser

import re

from . import fracttypes


class Node:
    def __init__(self, type, pos, children=None, leaf=None, datatype=None):
        self.type = type
        if children:
            self.children = children
        else:
            self.children = []
        self.leaf = leaf
        self.datatype = datatype
        self.pos = pos

    def __str__(self):
        return f"[{self.type} : {self.leaf}]"

    def pretty(self, depth=0, pos=False):
        str = " " * depth + f"[{self.type} : {self.leaf}"
        if pos:
            str += f"<{self.pos}>"
        if self.datatype is not None:
            str += f"({fracttypes.strOfType(self.datatype)})"
        if self.children:
            str += "\n"
            for child in self.children:
                assert(isinstance(child, Node))
                str += child.pretty(depth + 1, pos) + "\n"
            str += " " * depth + "]"
        else:
            str += "]"
        return str

    def _ordLeaf(self):
        if self.leaf is None:
            return 0
        return self.leaf

    def __iter__(self):
        return NodeIter(self)

    def childByName(self, name):
        'find a child with leaf == name'
        for child in self.children:
            if child.leaf == name:
                return child
        return None

    def DeepCmp(self, other):
        if not self:
            return -1
        if not other:
            return 1

        if self.type < other.type:
            return -1
        if self.type > other.type:
            return 1

        if self._ordLeaf() < other._ordLeaf():
            return -1
        if self._ordLeaf() > other._ordLeaf():
            return 1

        if not self.children and not other.children:
            return 0

        for (child, otherchild) in zip(self.children, other.children):
            eql = child.DeepCmp(otherchild)
            if eql:
                return eql
        return eql


class NodeIter:
    def __init__(self, node):
        self.nodestack = [(node, -1)]

    def __iter__(self):
        return self

    def getNode(self, node, child):
        if child == -1:
            return node
        else:
            return node.children[child]

    def __next__(self):
        # print map(lambda (n,x) :"%s %s" % (n,x), self.nodestack)
        if self.nodestack == []:
            raise StopIteration

        (node, child) = self.nodestack.pop()
        ret = self.getNode(node, child)
        child += 1
        while len(node.children) <= child:
            if not self.nodestack:
                return ret
            (node, child) = self.nodestack.pop()

        self.nodestack.append((node, child + 1))
        self.nodestack.append((node.children[child], -1))

        return ret


def CheckTree(tree, nullOK=0):
    if nullOK and tree is None:
        return 1
    if not isinstance(tree, Node):
        raise Exception(f"bad node type {tree}")
    if tree.children:
        if not isinstance(tree.children, list):
            raise Exception(f"children not a list: {tree.children} instead")
        for child in tree.children:
            CheckTree(child, 0)
    return 1

# shorthand named ctors for specific node types


def Formlist(list, pos):
    return Node("formlist", pos, list, "")


def Set(id, s, pos):
    return Node("set", pos, [id, s], None)


def SetType(id, t, pos):
    type = fracttypes.typeOfStr(t)
    return Node("set", pos, [id, Empty(pos)], None, type)


def Number(n, pos):
    if re.search('[.eE]', n):
        t = fracttypes.Float
        n = float(n)
    else:
        t = fracttypes.Int
        n = int(n)

    return Node("const", pos, None, n, t)


def Const(n, pos):
    if isinstance(n, str):
        n = n.lower()
    return Node("const", pos, None, n == "true" or n == "yes", fracttypes.Bool)


def Binop(op, left, right, pos):
    return Node("binop", pos, [left, right], op)


def ID(id, pos):
    return Node("id", pos, None, id)


def Mag(exp, pos):
    return Node("unop", pos, [exp], "cmag")


def Negate(exp, pos):
    return Node("unop", pos, [exp], "t__neg")


def Not(exp, pos):
    return Node("unop", pos, [exp], "t__not")


def String(s, list, pos):
    return Node("string", pos, list, s, fracttypes.String)


def Funcall(id, arglist, pos):
    return Node("funcall", pos, arglist, id)


def Assign(id, exp, pos):
    return Node("assign", pos, [id, exp], None)


def Decl(type, id, pos, exp=None):
    if exp is None:
        l = None
    else:
        l = [exp]
    return Node("decl", pos, l, id, fracttypes.typeOfStr(type))


def DeclArray(type, id, indexes, pos):
    return Node("declarray", pos, indexes, id, fracttypes.typeOfStr(type))


def ArrayLookup(id, indexes, pos):
    return Node("arraylookup", pos, indexes, id)


def Stmlist(id, list, pos):
    return Node("stmlist", pos, list, id.lower())


def Setlist(id, list, pos):
    return Node("setlist", pos, list, id.lower())


def Empty(pos):
    return Node("empty", pos, None, "")


def Formula(id, stmlist, pos):
    # rather gruesome: we re-lex the formula ID to extract the symmetry spec
    # if any. Then we smuggle it into the top-level node
    m = re.match(r".*?(\s*\(\s*(\w+)\s*\))", id)
    if m:
        symmetry = m.group(2)
        id = id[:m.start(1)]
    else:
        symmetry = None

    n = Node("formula", pos, stmlist, id)
    n.symmetry = symmetry

    return n


def Param(id, settinglist, type, pos):
    return Node("param", pos, settinglist, id, fracttypes.typeOfStr(type))


def Func(id, settinglist, type, pos):
    return Node("func", pos, settinglist, id, fracttypes.typeOfStr(type))


def Heading(settinglist, pos):
    return Node("heading", pos, settinglist)


def Repeat(body, test, pos):
    return Node("repeat", pos, [test, Stmlist("", body, pos)], "")


def While(test, body, pos):
    return Node("while", pos, [test, Stmlist("", body, pos)], "")


def If(test, left, right, pos):
    return Node("if", pos,
                [test, Stmlist("", left, pos), Stmlist("", right, pos)], "")


def Error2(str, pos):
    if str == "$":
        return Node(
            "error", pos, None,
            f"{pos}: Error: unexpected preprocessor directive")
    return Node("error", pos, None,
                f"{pos}: Syntax error: unexpected '{str}' ")


def Error(type, value, pos):
    if type == "NEWLINE":
        return Node("error", pos, None,
                    f"{pos}: Syntax error: unexpected newline")

    return Node("error", pos, None,
                f"{pos}: Syntax error: unexpected {type.lower()} '{value}'")


def PreprocessorError(value, pos):
    return Node("error", pos, None, value)
