import re
import copy
import importlib as imp

from comsdk.graph import Graph, Func, State, Selector
from comsdk.edge import Edge


class Params():
    __slots__=(
        'module',
        'entry_func',
        'predicate',
        'selector',
        'function',
        'morphism',
        'parallelism',
        'comment',
        'order',
        'subgraph'
    )
    def __init__(self):
        for slot in self.__slots__:
            setattr(self, slot, None)
    
    def __str__(self):
        stri = ""
        for s in self.__slots__:
            stri += ((s+": {}, ".format(getattr(self, s))) if getattr(self, s) is not None else "")
        return stri

# entities = {}

class GraphFactory():
    __slots__ = (
        'name',
        'states', 
        'graph',
        'issub',
        'tocpp',
        'entities'
    )
    def __init__(self, tocpp=False):
        self.states = {}
        self.entities = {}
        self.tocpp = tocpp
        self.name = None
        self.issub = False

    def add_state(self, statename):
        if statename not in self.states:
            self.states[statename] = State(statename)
            if statename in self.entities:
                self.states[statename].comment = self.entities[statename].comment
    
    def _create_morphism(self, morphname=None):
        comment = ""
        if morphname is None:
            return Func(), Func(), comment
        pred_f, func_f = Func(), Func() 
        morph = self.entities[morphname]
        for m in morph.__slots__:
            if getattr(morph,m) is not None:
                if m!="predicate" and m!="function" and m!="comment":
                    raise Exception("ERROR: Morphisms could not have any params exept comment, predicate and function!\n{}".format(morphname))
                if m=="comment":
                    comment=getattr(morph, m).replace("\0", " ")
                if m=="predicate":
                    if getattr(morph,m) not in self.entities:
                        raise Exception("\tERROR: Predicate {} is not defined!".format(getattr(morph, m)))
                    pred = self.entities[getattr(morph, m)]
                    if self.tocpp:
                        pred_f = Func(pred.module, pred.entry_func, dummy=True, comment=pred.comment)
                    else:
                        pred_f = Func(pred.module, pred.entry_func, comment=pred.comment)
                if m=="function":
                    if getattr(morph,m) not in self.entities:
                       raise Exception("\tERROR: Function: {} is not defined!".format(getattr(morph, m)))
                    fu = self.entities[getattr(morph, m)]
                    if self.tocpp:
                        func_f = Func(fu.module, fu.entry_func, dummy=True, comment=fu.comment)
                    else:
                        func_f = Func(fu.module, fu.entry_func,comment=fu.comment)
        return pred_f, func_f, comment


    def add_connection(self, st1, st2, morphism=None, ordr=0):
        pred, entr, comm = self._create_morphism(morphism)
        self.states[st1].connect_to(self.states[st2], edge=Edge(pred, entr, order=ordr, comment=comm))
        print("{} -> {}".format(st1, st2))

    def build(self, nsub):
        print("BUILDING {}\nStates:".format(self.name))
        for s in self.states:
            print("\t"+ s)
        if self.issub:
            self.graph = Graph(self.states[self.name+str(nsub)+"_"+"__BEGIN__"], self.states[self.name+str(nsub)+"_"+"__END__"])
        else:    
            self.graph = Graph(self.states["__BEGIN__"], self.states["__END__"])
        self.graph.init_graph()
        if self.issub:
            oldkeys = []
            for e in self.entities:
                oldkeys.append(e)
            for old in oldkeys:
                if self.entities[old].selector is not None or self.entities[old].subgraph is not None:
                    self.entities[self.name + str(Parser.subgr_count)+"_"+old] = self.entities[old]
                    del self.entities[old] 
        for s in self.states:
            if s in self.entities and self.entities[s].selector is not None:
                selname = self.entities[s].selector
                if self.tocpp:
                    self.states[s].selector = Selector(len(self.states[s].transfers), self.entities[selname].module, self.entities[selname].entry_func, dummy=True)
                else:
                    self.states[s].selector = Selector(len(self.states[s].transfers), self.entities[selname].module, self.entities[selname].entry_func)
            else:
                self.states[s].selector =  Selector(len(self.states[s].transfers))
            if s in self.entities and self.entities[s].subgraph is not None:
                print("Replacing state {} with subgraph {}".format(s,self.entities[s].subgraph))
                parsr = Parser(subgraph=True, tocpp= self.tocpp)
                subgr = parsr.parse_file(self.entities[s].subgraph)
                self.states[s].replace_with_graph(subgr)
                self.graph = Graph(self.graph.init_state, self.graph.term_state)
        return self.graph


class Parser():
    __slots__ = (
        'fact',
        'issub'
    )
    subgr_count = 0
    def __init__(self, tocpp=False, subgraph=False):
        self.fact = GraphFactory(tocpp=tocpp)
        self.fact.issub = subgraph
        self.issub = subgraph
        if subgraph:
            Parser.subgr_count+=1
    
    def _check_brackets(self, rawfile):
        br = { "[":{"line":0, "count":0}, "(":{"line":0, "count":0}, "{":{"line":0, "count":0}, "\"":{"line":0, "count":0}}
        line = 1
        qu = 0
        for char in rawfile:
            if char == "[":
                br["["]["line"] = line
                br["["]["count"] +=1 
            elif char == "{":
                br["{"]["line"] = line
                br["{"]["count"] +=1 
            elif char == "(":
                br["("]["line"] = line
                br["("]["count"] +=1 
            elif char == "]":
                br["["]["count"] -=1 
            elif char == "}":
                br["{"]["count"] -=1 
            elif char == ")":
                br["("]["count"] -=1 
            elif char =="\"":
                br["\""]["line"] = line
                br["\""]["count"] += 1 if br["\""]["count"]==0 else -1
            elif char == "\n":
                line+=1
        expstr= "Brackets or quotes do not match! Missing closing brackets on lines: "
        fl = False
        for c in br:
            if br[c]["count"] != 0:
                fl= True
                expstr+=str(br[c]["line"])+" "
        if fl:
            raise Exception(expstr)

    def _split_multiple(self,param):
        vals = {}
        first=True
        for s in param.__slots__:
            attr = getattr(param,s)
            if attr is not None and '\0' in attr:
                vals[s] = attr.split('\0')
        l=0
        for sl in vals:
            if l==0:
                l=len(vals[sl])
            elif l!=len(vals[sl]):
                raise Exception("\tERROR: Number of multiple params do not match", l)
        res = [copy.copy(param) for i in range(l)]
        for sl in vals:
            for i, _ in enumerate(res):
                setattr(res[i], sl, vals[sl][i])
        return res

    #Props is line "[proFp=smth, ...]"
    def _param_from_props(self,props):
        parm = Params()
        comment = ""
        if props =="":
            return parm
        props = props.replace("]", '')
        if '\"' in props:
            m = [m for m in re.finditer(r'\".*\"', props)][0]
            comment = props[m.span()[0]+1:m.span()[1]-1]
            props=props[:m.span()[0]]+props[m.span()[1]:]
        if '(' in props:
            mchs = [m for m in re.finditer(r'\((\w+,)*\w+\)', props)]
            for m in mchs:
                props=props[:m.span()[0]]+(props[m.span()[0]:m.span()[1]]).replace(',','\0')+props[m.span()[1]:]
        props = props.replace("(","")
        props = props.replace(")","")
        rs =props.split(r",") #.split(r", ")
        for r in rs:
            r=r.split(r"=", 1)
            if r[0] in parm.__slots__:
                setattr(parm, r[0], r[1])
            else:
                raise Exception("\tERROR:Unknown parameter: "+ r[0])
        if comment != "": 
            setattr(parm, "comment", comment.replace("\0", " "))
        return parm

    def _param_from_entln(self, raw):
        res = re.split(r"\[", raw, 1)
        return res[0], self._param_from_props(res[1])

    def _multiple_morphs(self,props, n):
        p = self._param_from_props(props)
        if p.morphism is None:
            return  [copy.copy(p) for i in range(n)] 
        else:
            return self._split_multiple(p)

    def _topology(self,raw):
        spl = re.split(r"\s*(=>|->|\[|\])\s*", raw)
        spl = list(filter(lambda x: x!="[" and x!="]" and x!="", spl))
        left = spl[0].split(",")
        right = spl[2].split(",")
        if self.issub:
            for i in range(len(left)):
                left[i] = self.fact.name + str(Parser.subgr_count) + "_" + left[i]
            for i in range(len(right)):
                right[i] = self.fact.name + str(Parser.subgr_count) + "_" + right[i]
        if (len(left)>1) and (len(right)>1):
            raise Exception("ERROR: Ambigious multiple connection in line:\n\t{}".format(raw))
        # many to one conection
        elif len(left)>1:
            if len(spl) < 4:
                spl.append("")
            morphs = self._multiple_morphs(spl[3], len(left))
            if len(morphs)!=len(left):
                raise Exception("\tERROR: Count of edges do not match to count of states in many to one connection!\n\t\t{}".format(raw))
            self.fact.add_state(right[0])
            for i, st in enumerate(left):
                self.fact.add_state(st)
                self.fact.add_connection(st, right[0], morphs[i].morphism)
        # one to many connection, here could be selector
        elif len(right)>1:
            if len(spl) < 4:
                spl.append("")
            morphs = self._multiple_morphs(spl[3], len(right))
            self.fact.add_state(left[0])
            if len(morphs)!=len(right):
                raise Exception("\tERROR: Count of edges do not match to count of states in one to many connection!\n\t\t{}".format(raw))
            for i, st in enumerate(right):
                self.fact.add_state(st)
                self.fact.add_connection(left[0], st, morphs[i].morphism, morphs[i].order)
        # one to one connection
        else:
            self.fact.add_state(left[0])
            self.fact.add_state(right[0])
            if len(spl)==4:
                pr =self._param_from_props(spl[3])
                self.fact.add_connection(left[0], right[0], pr.morphism, ordr=pr.order if pr.order is not None else 0)
            elif len(spl)==3:
                self.fact.add_connection(left[0], right[0], None)

    def parse_file(self, filename):
        # @todo В случае, если на вход будет подан файл в отличной от UTF-8 кодировке программа работать не будет
        file = open(filename, encoding='utf-8')# "r")
        dot = file.read()
        self._check_brackets(dot)
        
        comments = [m for m in re.finditer(r'\".*\"', dot)]
        for m in comments:
            dot=dot[:m.span()[0]]+(dot[m.span()[0]:m.span()[1]]).replace(' ','\0')+dot[m.span()[1]:]
        dot = re.sub(r"[ \t\r]", "", dot) #deleting all spaces
        dot = re.sub(r"((digraph)|}|{)", "", dot)
        dot = re.sub(r"\/\/.*", "", dot)
        dot = re.sub(r"^\n$", "", dot)
        dotlines = dot.splitlines()
        dotlines = list(filter(None, dotlines))
        self.fact.name = dotlines[0]
        dotlines = dotlines[1:]
        # ent_re - regular expr for edges, states, functions properties
        ent_re = re.compile(r"^\w+\[.*\]$")
        # top_re - regular expr for topology properties, most time consuming one
        top_re = re.compile(r"^(\w+,?)+(->|=>)(\w+,?)+(\[(\w+=(\(?\w+,?\)?)+,?)+\])?")
        # (r"^\w[\w\s,]*(->|=>)\s*\w[\w\s,=\[\]()]*$")
        for i, ln in enumerate(dotlines):
            if ent_re.match(ln):
                name, parm = self._param_from_entln(ln)
                self.fact.entities[name] = parm
            elif top_re.match(ln):
                self._topology(ln)
        return self.fact.build(Parser.subgr_count)

    checked=[]
    bushes = {}
    selectorends = {}
    def generate_cpp(self, filename=None):
        self.fact.graph.init_state.input_edges_number =0
        states_to_check = [self.fact.graph.init_state]
        while len(states_to_check)!=0:
            for st in states_to_check:
                self.checked.append(st)
                states_to_check.remove(st)
                bush = _Bush(st)
                bush.grow_bush()
                self.bushes[st] = bush
                for outs in bush.outstates:
                    if outs not in states_to_check and outs not in self.checked:
                        states_to_check.append(outs)
        send_token(self.fact.graph.init_state, self.bushes, [])
        preds, morphs, sels, st, body = print_graph(self.fact.graph.init_state, self.fact.entities, self.bushes)
        from mako.template import Template
        if filename is not None:
            f = open(filename, "w")
        else:
            f= open(self.fact.name + ".cpp", "w")
        print(Template(filename="./cpp/template.cpp").render(preds=preds, morphs = morphs, sels = sels, states=st, body=body), file=f)

def print_graph(cur_state, entities, bushes):
    checked = []
    toloadpred = []
    toloadmorph = []
    toloadsel =[]
    tocheck = [cur_state]
    body = ""
    while len(tocheck) !=0:
        cur_state=tocheck[0]
        cur_b = bushes[cur_state]
        cur_b.token+=1
        if cur_b.token < cur_b.state.input_edges_number - cur_b.state.looped_edges_number:
            tocheck.remove(cur_state)
            tocheck.append(cur_state)
            continue
        if cur_state in checked:
            tocheck.remove(cur_state)
            continue
        if len(cur_b.branches)>1 or len(cur_b.incomes)>1:
            body+="{}:\n".format(cur_state.name)
        if len(cur_b.incomes)!=0:
            if cur_b.state.comment!="" and cur_b.state.comment is not None:
                print("STcomm:", cur_b.state.comment)
                body+="//"+cur_b.state.comment+"\n"
            stri = "false "
            for inc in cur_b.incomes:
                stri += "|| SEL_{}[{}] ".format(inc["st"].name, inc["i"])
            body+="if (!({}))".format(stri)
            body+="{\n\tfor (int seli = 0;"+" seli < {};".format(len(cur_state.transfers))+" seli++)\n"+ "\t\tSEL_{}[seli]=false;".format(cur_state.name)+"\n}"
            if cur_state.selector.name != "":
                # print(cur_state.name, cur_state.selector)
                if cur_state.selector not in toloadsel:
                    toloadsel.append(cur_state.selector)
                body+="else {\n"+ "\tSEL_{} = {}(&data);//{}\n".format(cur_state.name, cur_state.selector, cur_state.selector.comment )+"}\n"
            else:
                body+="else {\n\tfor (int seli = 0;"+" seli < {};".format(len(cur_state.transfers))+" seli++)\n"+"\t\tSEL_{}[seli]=true;".format(cur_state.name)+"\n}\n"
        for i, br in enumerate(cur_b.branches):
            body+="if (SEL_{}[{}])".format(cur_state.name, i)+"{\n"
            if br[len(br)-1].output_state not in tocheck:
                tocheck.append(br[len(br)-1].output_state)
            if br[len(br)-1].output_state in checked or br[len(br)-1].output_state is cur_state:
                stri, toloadpred, toloadmorph = cur_b.cpp_branch(i, toloadpred, toloadmorph)
                body+=stri+"\tgoto {};\n".format(br[len(br)-1].output_state.name)+"}\n"
            else:
                stri, toloadpred, toloadmorph = cur_b.cpp_branch(i, toloadpred, toloadmorph) 
                body+=stri+"}\n"
        tocheck.remove(cur_state)
        checked.append(cur_state)
    return _unique(toloadpred), _unique(toloadmorph), _unique(toloadsel), checked, body

def _unique(lst):
    for i, el in enumerate(lst):
        for el2 in lst[i+1:]:
            if el2.module == el.module and el2.name == el.name:
                lst.remove(el2)
    return lst

def send_token(cur_state, bushes, checked):
    cur_b = bushes[cur_state]
    if cur_state in checked:
        return
    if len(cur_b.outstates)==0:
        return
    if len(cur_b.incomes) == cur_b.state.input_edges_number - cur_b.state.looped_edges_number:
        checked.append(cur_state)
        for i,br in enumerate(cur_b.branches):
            bushes[br[len(br)-1].output_state].incomes.append({"st":cur_state, "i":i})
            send_token(br[len(br)-1].output_state,bushes, checked)

class _Bush():
    __slots__=(
        'state', 
        'selector',
        'branches',
        'outstates',
        'token',
        'incomes',
        'selectorfin'
    )

    def __init__(self, state):
        self.state = state
        self.selector = state.selector
        self.branches = []
        self.outstates = []
        self.token = 0
        self.incomes = []

    def grow_bush(self):
        for t in self.state.transfers:
            branch = [t]
            self._gen_branch(t.output_state, branch)
        
    def _gen_branch(self, cur_state, branch):
        while len(cur_state.transfers)==1 and cur_state.input_edges_number==1:
            if cur_state._proxy_state is not None:
                cur_state=cur_state._proxy_state
            tr = cur_state.transfers[0]
            branch.append(tr)
            cur_state = tr.output_state
        self.branches.append(branch)
        if cur_state not in self.outstates:
            self.outstates.append(cur_state)
        
    def cpp_branch(self, i, toloadpred, toloadmorph):
        res = ""
        for tr in self.branches[i]:
            edge = tr.edge
            if edge.comment!="":
                res+="\t//{}\n".format(edge.comment)
            if edge.pred_f.name != "":
                if edge.pred_f not in toloadpred:
                    toloadpred.append(edge.pred_f)
                res+="\tcheck_pred({}(&data), \"{}\");".format(edge.pred_f, edge.pred_f)
                res+="//{}\n".format(edge.pred_f.comment) if edge.pred_f.comment != "" else "\n"
            if edge.morph_f.name != "":
                if edge.morph_f not in toloadmorph:
                    toloadmorph.append(edge.morph_f)
                res+="\t{}(&data);".format(edge.morph_f)
                res+="//{}\n".format(edge.morph_f.comment) if edge.morph_f.comment != "" else "\n"
        return res, toloadpred, toloadmorph