import copy
import logging
import os
import re

import common as doc


def __doc_obj__(label, obj, id):
    node = f'  {id} [label="{label}&#92;n{obj["Class"]} | '
    return node


def __doc_common_gam__(name, obj, edges, thread):
    inputs = "<table cellborder='0' border='0'>"
    outputs = "<table cellborder='0' border='0'>"
    if "InputSignals" in obj and len(obj["InputSignals"]):
        for i, (l, sig) in enumerate(obj["InputSignals"].items()):
            alias = doc.signal_alias(l, sig)
            mtype = doc.signal_type(sig)
            datasrc = sig["DataSource"]
            edges.append(
                doc.edge(
                    doc.signal(datasrc, alias, mtype),
                    doc.signal(name, l, mtype),
                    thread,
                )
            )
            if i != 0:
                inputs += "<hr/>"
            inputs += f"<tr><td port='in_{doc.format_name(l)}'>{l}</td></tr>"
    else:
        inputs += "<tr><td></td></tr>"
    if "OutputSignals" in obj and len(obj["OutputSignals"]):
        for i, (l, sig) in enumerate(obj["OutputSignals"].items()):
            alias = doc.signal_alias(l, sig)
            mtype = doc.signal_type(sig)
            datasrc = sig["DataSource"]
            edges.append(
                doc.edge(
                    doc.signal(name, l, mtype),
                    doc.signal(datasrc, alias, mtype),
                    thread,
                )
            )
            if i != 0:
                outputs += "<hr/>"
            outputs += f"<tr><td port='out_{doc.format_name(l)}'>{l}</td></tr>"
    else:
        outputs += "<tr><td></td></tr>"
    return (
        "<tr><td>" + inputs + "</table></td><vr/><td>" + outputs + "</table></td></tr>",
        edges,
    )


def __doc_triggered_iogam__(id, obj, edges, thread):
    node = ""

    if len(obj["InputSignals"]) > 0:
        name = id[2:]
        node = __gam__(id, name, "TriggeredIOGAM")

        recl = copy.deepcopy(obj)
        trg = list(obj["InputSignals"].keys())[0]
        sig = obj["InputSignals"][trg]
        alias = doc.signal_alias(trg, sig)
        mtype = doc.signal_type(sig)
        datasrc = sig["DataSource"]
        edges.append(
            doc.edge(
                doc.signal(datasrc, alias, mtype),
                doc.signal(id, trg, mtype),
                thread,
            )
        )
        node += f"<tr><td port='in_{doc.format_name(trg)}' colspan='2'><b>Trigger</b></td></tr><hr/>"
        del recl["InputSignals"][trg]
        sigs, edges = __doc_common_gam__(id, recl, edges, thread)
        node += sigs
        node += "</table>>];"
    return node, edges


__set_keys__ = (
    "Alias",
    "Type",
    "DataSource",
    "Samples",
    "NumberOfElements",
    "NumberOfDimensions",
    "Ranges",
)


def __doc_iogam__(id, obj, edges, thread):
    node = ""
    if "InputSignals" in obj and "OutputSignals" in obj:
        inputs, outputs = obj["InputSignals"], obj["OutputSignals"]
        output_names = list(outputs.keys())
        input_names = list(inputs.keys())
        input_size = 0
        output_size = 0
        out_i = 0
        in_i = 0
        counter = 0
        while in_i < len(inputs) and out_i < len(outputs):
            irows = []
            orows = []
            bold = False
            while input_size <= output_size and in_i < len(inputs):
                in_label = input_names[in_i]
                input = inputs[in_label]
                input_size += doc.signal_byte_size(input)
                in_i += 1
                alias = doc.signal_alias(in_label, input)
                mtype = doc.signal_type(input)
                if mtype[2]:
                    bold = True
                irow = f"{doc.type_to_string(mtype)}"
                for key, val in input.items():
                    if key not in __set_keys__:
                        bold = True
                        irow += f"<br/>{key}: {val}"
                datasrc = input["DataSource"]
                edges.append(
                    doc.edge(
                        doc.signal(datasrc, alias, mtype),
                        doc.signal(f"{id}_{counter}", f"{len(irows)}", mtype),
                        thread,
                    )
                )
                irows.append(irow)
            while output_size < input_size and out_i < len(outputs):
                out_label = output_names[out_i]
                output = outputs[out_label]
                output_size += doc.signal_byte_size(output)
                out_i += 1
                alias = doc.signal_alias(out_label, output)
                mtype = doc.signal_type(output)
                if mtype[2]:
                    bold = True
                orow = f"{doc.type_to_string(mtype)}"
                for key, val in output.items():
                    if key not in __set_keys__:
                        bold = True
                        orow += f"<br/>{key}: {val}"
                datasrc = output["DataSource"]
                edges.append(
                    doc.edge(
                        doc.signal(f"{id}_{counter}", f"{len(orows)}", mtype),
                        doc.signal(datasrc, alias, mtype),
                        thread,
                    )
                )
                orows.append(orow)
            fill = "#eeeeee"
            fcolor = "#000000"
            total = max(len(orows), len(irows))
            if bold or total > 1:
                fill = "#eeeeee"
                bold = True
            else:
                fill = "#eeeeee"

            inode = f"{id}_{counter} [shape=plaintext,"
            inode += f'tooltip=" IOGAM::{id} ", label=<'
            inode += "<table cellspacing='0' cellborder='0' style='rounded'"
            inode += f" cellpadding='4' border='1' bgcolor='{fill}'>"

            if bold:
                for i in range(total):
                    inode += "<tr>"
                    if i < len(irows):
                        inode += f'<td port="in_{i}"'
                        if len(orows) > len(irows):
                            inode += f' rowspan="{len(orows)}"'
                        inode += ">"
                        inode += f'<font color="{fcolor}">'
                        inode += irows[i]
                        inode += "</font></td><vr/>"
                    if i < len(orows):
                        inode += f'<td port="out_{i}"'
                        if len(orows) < len(irows):
                            inode += f' rowspan="{len(irows)}"'
                        inode += ">"
                        inode += f'<font color="{fcolor}">'
                        inode += orows[i]
                        inode += "</font></td>"
                    inode += "</tr>"
                    if i < total - 1:
                        inode += "<hr/>"

            inode += "</table>>"
            inode += "];\n"
            if not bold:
                e_in, e_out = edges[-2:]
                edges = edges[:-2]
                edges.append((e_in[0], e_out[1], thread))
                inode = ""
            node += inode
            irows = []
            orows = []
            counter += 1
    return node, edges


def __doc_constgam__(id, obj, edges, thread):
    node = f"  {id} [shape=plaintext, label=<<table cellspacing='0' cellborder='0' cellpadding='4' border='1' style='rounded'>"
    node += f"<tr><td colspan='2'><b>{id[2:]}</b><br/>{obj['Class']}</td></tr>"

    if "OutputSignals" in obj:
        for i, (l, sig) in enumerate(obj["OutputSignals"].items()):
            datasrc = sig["DataSource"]
            alias = doc.signal_alias(l, sig)
            mtype = doc.signal_type(sig)
            edges.append(
                doc.edge(
                    doc.signal(id, l, mtype),
                    doc.signal(datasrc, alias, mtype),
                    thread,
                )
            )
            node += "<hr/>"
            node += f'<tr><td>{doc.type_to_string(mtype)}<br/><font point-size="8">{l}</font></td>'
            node += f'<vr/><td port="out_{doc.format_name(l)}">'
            node += f'{sig["Default"]}</td></tr>'
    node += "</table>>];\n"
    return node, edges


def __doc_message_gam__(id, obj, edges, thread):
    label = id[2:]
    node = f'  {id} [label="{label}&#92;n{obj["Class"]} | '
    inputs = ""
    if "InputSignals" in obj:
        for i, (l, sig) in enumerate(obj["InputSignals"].items()):
            alias = doc.signal_alias(l, sig)
            mtype = doc.signal_type(sig)
            datasrc = sig["DataSource"]
            edges.append(
                doc.edge(
                    doc.signal(datasrc, alias, mtype),
                    doc.signal(id, l, mtype),
                    thread,
                )
            )
            if i != 0:
                inputs += " | "
            inputs += f"<in_{doc.format_name(l)}> {l}"
    if inputs:
        node += inputs + '", style=filled, fillcolor=lightyellow];'
    else:
        node = ""

    if node:
        add, edges = __doc_events__(id, obj, edges, thread)
        node += add
    return node, edges


def __doc_events__(id, obj, edges, thread):
    nodes = ""
    bgcolor = "lightyellow"
    for event_id, event in obj["+Events"].items():
        if event_id[0] == "+":
            event_id = event_id[1:]
            trigger = event["EventTrigger"]
            message_ids = [id[1:] for id in event if id[0] == "+"]
            for msg_id in message_ids:
                nodes += f"  {id}_{event_id}_{msg_id} [shape=plaintext, label=<"
                nodes += '<table cellspacing="0" cellborder="1" border="0" cellpadding="4"><tr>'
                nodes += f"<td bgcolor='{bgcolor}' align='left'><b>When</b></td><td balign='left'>"
                for i, (sig, val) in enumerate(trigger.items()):
                    if i > 0:
                        nodes += "<br/>"
                    nodes += f"{sig} == {val}"

                nodes += f"</td></tr><tr><td bgcolor='{bgcolor}' align='left'><b>Destination</b></td>"
                msg = event[f"+{msg_id}"]
                fn = msg["Function"]
                nodes += f"<td align='left'>{msg['Destination']}</td></tr>"
                nodes += f"<tr><td bgcolor='{bgcolor}' align='left'><b>Message</b></td><td align='left'>"
                if fn == "SetOutput":
                    params = msg["+Parameters"]
                    nodes += f"<i>set</i> {params['SignalName']} <i>to</i> {params['SignalValue']}"
                else:
                    nodes += f"{fn} "
                nodes += "</td></tr></table>>];\n"
                nodes += f"  {id}:e -> {id}_{event_id}_{msg_id}:w"
                nodes += " [arrowhead=box, constraint=true];\n"
    return nodes, edges


def __sanitize__(expression):
    expression = expression.strip()
    expression = re.sub(" +", " ", expression)
    if expression[0] == "\n":
        expression = expression[1:]
    if expression[-1] == "\n":
        expression = expression[:-1]

    expression = (
        expression.replace("&", "&amp;")
        .replace(">", "&gt;")
        .replace("<", "&lt;")
        .replace("\n", "<br/>")
    )
    return expression


def __gam__(id, label, cls):
    node = f"  {id} [shape=plaintext, label=<<table border='1' cellborder='0' cellpadding='0' style='rounded'>"
    node += f"<tr><td colspan='2'><b>{label}</b><br/>{cls}</td></tr><hr/>"
    return node


def __doc_mathgam__(id, obj, edges, thread):
    label = id[2:]
    import sys

    node = __gam__(id, label, obj["Class"])
    node += (
        "<tr><td colspan='2' align='left' balign='left' bgcolor='#ffeeff'><font face='mono' point-size='9'>"
        + __sanitize__(obj["Expression"])
        + "</font></td></tr><hr/> "
    )

    sigs, edges = __doc_common_gam__(id, obj, edges, thread)
    if sigs:
        node += sigs + "</table>>];"
    else:
        node = ""
    return node, edges


__gams_fns__ = {
    "IOGAM": __doc_iogam__,
    "ConstantGAM": __doc_constgam__,
    "TriggeredIOGAM": __doc_triggered_iogam__,
    "MathExpressionGAM": __doc_mathgam__,
    "MessageGAM": __doc_message_gam__,
}


def __doc_gam__(label, obj, edges, thread, classfilter=True):
    id = f"fn{label}"
    cls = obj["Class"]
    if classfilter and cls in __gams_fns__:
        return __gams_fns__[cls](id, obj, edges, thread)
    node = __gam__(id, label, cls)
    sigs, edges = __doc_common_gam__(id, obj, edges, thread)
    if sigs:
        node += sigs + "</table>>];"
    else:
        node = ""
    return node, edges


def __process_gamds__(name, cls, signals, edges):
    node = ""
    outputs = {}
    inputs = {}
    done = False
    to_keep = []
    while not done:
        signals = {}
        for edge in edges:
            if edge[0][0] == name:
                sig_name = edge[0][1]
                signals[sig_name] = edge[0][2]
                if sig_name not in outputs:
                    outputs[sig_name] = []
                outputs[sig_name].append(edge)
            elif edge[1][0] == name:
                sig_name = edge[1][1]
                signals[sig_name] = edge[1][2]
                if sig_name not in inputs:
                    inputs[sig_name] = []
                inputs[sig_name].append(edge)
        to_reduce = []
        to_keep = []
        done = True
        for sig_name in signals:
            if sig_name in inputs and sig_name in outputs:
                to_reduce.append(sig_name)
            else:
                to_keep.append(sig_name)

        for sig_name in to_reduce:
            ins = inputs[sig_name]
            outs = outputs[sig_name]
            for edge in ins:
                if edge in edges:
                    done = False
                    edges.remove(edge)
            for edge in outs:
                if edge in edges:
                    done = False
                    edges.remove(edge)
            for in_edge in ins:
                for out_edge in outs:
                    edge = (in_edge[0], out_edge[1], in_edge[-1])
                    edges.append(edge)

    for i, sig_name in enumerate(to_keep):
        if i > 0:
            node += " | "
        sig_type = signals[sig_name]
        node += (
            f'<tr><td port="{doc.format_name(sig_name)}">'
            f"{sig_name} ({doc.type_to_string(sig_type)})"
            "</td></tr>"
        )
    if node:
        label = f"""<table border="0" cellpadding="4" cellborder="1" cellspacing="0" bgcolor="#eeeeee">
            <tr><td><b>{name}</b><br/>{cls}</td></tr>
            {node}
        </table>
        """
        node = f'{name} [shape=plaintext, label=<{label}>, color="#00aaff"];\n'
    return node, edges


def __process_ds__(name, obj, edges):
    cls = obj["Class"]
    sigs = {}
    node = ""
    thread = ""
    multi_thread = False
    for edge in edges:
        if edge[0][0] == name:
            sigs[edge[0][1]] = edge[0][2]
            if not thread:
                thread = edge[-1]
            elif thread != edge[-1]:
                multi_thread = True
        if edge[1][0] == name:
            sigs[edge[1][1]] = edge[1][2]
            if not thread:
                thread = edge[-1]
            elif thread != edge[-1]:
                multi_thread = True
    if sigs:
        if cls == "GAMDataSource" or cls == "RealTimeThreadAsyncBridge":
            node, edges = __process_gamds__(name, cls, sigs, edges)
        else:
            order = []
            lost = []
            if "Signals" in obj:
                for sig in obj["Signals"]:
                    if sig in sigs:
                        order.append(sig)
            else:
                order = list(sigs.keys())
            for sig in sigs:
                if sig not in order:
                    lost.append(sig)
            node = f"{name} [shape=plaintext; label=<"
            node += '<table border="0" cellpadding="4" cellborder="1" cellspacing="0" bgcolor="lightblue">\n'
            node += f"<tr><td><b>{name}</b><br/>{cls}</td></tr>\n"
            for i, sig_name in enumerate(order):
                node += f'<tr><td port="{doc.format_name(sig_name)}">'
                sig_type = sigs[sig_name]
                node += f"{sig_name} ({doc.type_to_string(sig_type)})"
                node += "</td></tr>\n"
            for sig_name in lost:
                logging.warning(f"Lost signal {name}:{sig_name}")
                node += f'<tr><td port="{doc.format_name(sig_name)}">'
                sig_type = sigs[sig_name]
                node += f"{sig_name} ({doc.type_to_string(sig_type)})"
                node += "</td></tr>\n"
            node += '</table>>, color="#00aaff"];\n'
    if not multi_thread and node:
        node = f"subgraph cluster_{thread}" + "{\n" + node + "};\n"
    return node, edges


def __process_edges__(edges, datasources):
    # remove duplicated edges
    clean_edges = []
    for edge in edges:
        if edge not in clean_edges:
            clean_edges.append(edge)
    edges = clean_edges
    # generate arrows
    arrows = ""
    for source, target, _ in edges:
        src_obj = source[0]
        src_sig = doc.format_name(source[1])
        trg_obj = target[0]
        trg_sig = doc.format_name(target[1])
        a = f"{src_obj}"
        b = f"{trg_obj}"
        ranked = True
        if src_obj in datasources:
            a += f":{src_sig}"
        else:
            a += f":out_{src_sig}"
        if trg_obj in datasources:
            b += f":{trg_sig}"
        else:
            b += f":in_{trg_sig}"

        in_type = doc.type_to_string(source[2])
        out_type = doc.type_to_string(target[2])
        mtype = in_type if in_type == out_type else f"{in_type} -> {out_type}"
        arrows += f'{a}:e -> {b}:w [tooltip=" {mtype} ", labelfloat=false'
        arrows += f", constraint={ranked}];\n"
    return arrows


def __state_full_graph__(label, state, app):
    graph = f"digraph {label[1:]}" + "{\n"
    graph += "ranksep = 2;\n"
    graph += "nodesep = 0.05;\n"
    graph += 'fontname="monospace";\n'
    graph += "rankdir = LR;\n"
    graph += 'edge [fontname="monospace"];\n'
    graph += 'node [shape=Mrecord, fontname="monospace"];\n'
    graph += f"label=<State: <B>{label[1:]}</B>>;\n"
    graph += "fontsize=40;\n"
    graph += "\n"
    arrows = ""
    edges = []
    datasources = {}
    if "+Threads" in state:
        for tname, obj in state["+Threads"].items():
            if tname[0] == "+":
                graph += f"subgraph cluster_{tname[1:]}" + "{\n"
                if "Functions" in obj:
                    fns = obj["Functions"]
                    for fn in fns:
                        obj = app["+Functions"][f"+{fn}"]
                        node, edges = __doc_gam__(fn, obj, edges, tname[1:])
                        graph += f"{node}\n"
                graph += f"\n  label = <Thread: <B>{tname[1:]}</B> >;\n"
                graph += "  fontsize = 24.0;\n"
                graph += "  penwidth = 3;\n"
                graph += '  color = "#1A1A1A";\n'
                graph += "}\n"
    if "+Data" in app:
        for key, obj in app["+Data"].items():
            if key[0] == "+":
                datasources[key[1:]] = obj["Class"]
                node, edges = __process_ds__(key[1:], obj, edges)
                graph += node
    arrows = __process_edges__(edges, datasources)
    graph += "\n\n" + arrows
    graph += "}\n"
    fname = f"{label[1:]}"

    gpath = os.path.join(doc.path(), f"{fname}_full.dot")
    with open(gpath, "w") as file:
        file.write(graph)
    return gpath


def __simple_gam__(label, obj):
    cls = obj["Class"]
    id = f"fn{doc.format_name(label)}"
    style = "style=rounded"
    if cls == "IOGAM":
        style = 'style="rounded,filled", fillcolor="#eeeeee"'
    elif cls == "MessageGAM":
        style = 'style="rounded,filled", fillcolor=lightyellow'
    if "InputSignals" in obj:
        for _, signal in obj["InputSignals"].items():
            ds = f'ds{doc.format_name(signal["DataSource"])}'
    if "OutputSignals" in obj:
        for _, signal in obj["OutputSignals"].items():
            ds = f'ds{doc.format_name(signal["DataSource"])}'
    return f"{id}[label=<<B>{label}</B><BR/>{cls}>, {style}]\n"


def __simple_ds__(label, obj, edges):
    cls = obj["Class"]
    thread = ""
    multi_thread = False
    sources = []
    targets = []
    id = f"ds{doc.format_name(label)}"
    for edge in edges:
        if edge[0][0] == label:
            if not thread:
                thread = edge[-1]
            elif thread != edge[-1]:
                multi_thread = True
        if edge[1][0] == label:
            if not thread:
                thread = edge[-1]
            elif thread != edge[-1]:
                multi_thread = True

    fillcolor = "lightblue"
    bordercolor = "#00aaff"
    style = "penwidth=2"
    if cls == "GAMDataSource":
        fillcolor = "#eeeeee"
    node = f"{id}[label=<<B>{label}</B><BR/>{cls}>, "
    node += f'{style}, fillcolor="{fillcolor}", color="{bordercolor}"]\n'
    if thread and not multi_thread:
        node = f"subgraph cluster_{thread}" + "{\n  " + node + "}"
    return node


def __simple_id__(name, datasources):
    if name in datasources:
        return f"ds{doc.format_name(name)}"
    return name


def __simple_edges__(edges, datasources):
    arrows = ""
    done = []

    for source, target, _ in edges:
        src_obj = __simple_id__(source[0], datasources)
        trg_obj = __simple_id__(target[0], datasources)
        if (src_obj, trg_obj) not in done:
            arrows += f"{src_obj}:e -> {trg_obj}:w\n"
            done.append((src_obj, trg_obj))
    return arrows


def __state_simple_graph__(label, state, app):
    graph = f"digraph {label[1:]}" + "{\n"
    graph += "rankdir = LR;\n"
    graph += (
        'node [shape=rect, style="filled", fillcolor=white, fontname="monospace"];\n'
    )
    graph += 'edge [fontname="monospace"];\n'
    graph += f"label=<State: <B>{label[1:]}</B>>;\n"
    graph += 'fontname="monospace";\n'
    graph += "fontsize=40;\n"
    graph += "\n"
    arrows = ""
    edges = []
    datasources = {}
    if "+Threads" in state:
        for tname, obj in state["+Threads"].items():
            if tname[0] == "+":
                graph += f"subgraph cluster_{tname[1:]}" + "{\n"
                if "Functions" in obj:
                    fns = obj["Functions"]
                    for fn in fns:
                        obj = app["+Functions"][f"+{fn}"]
                        _, edges = __doc_gam__(
                            fn, obj, edges, tname[1:], classfilter=False
                        )
                        graph += f"  {__simple_gam__(fn, obj)}"
                graph += f"\n  label = <Thread: <B>{tname[1:]}</B> >;\n"
                graph += "  fontsize = 24.0;\n"
                graph += "  penwidth = 3;\n"
                graph += '  color = "#1A1A1A";\n'
                graph += "}\n"
    if "+Data" in app:
        for key, obj in app["+Data"].items():
            if key[0] == "+":
                datasources[key[1:]] = obj["Class"]
                node, edges = __process_ds__(key[1:], obj, edges)
                if node:
                    graph += __simple_ds__(key[1:], obj, edges)
    graph += __simple_edges__(edges, datasources)
    # arrows = __process_edges__(edges, datasources)
    # graph += "\n" + ext + "\n" + arrows
    graph += "}\n"
    fname = f"{label[1:]}"

    gpath = os.path.join(doc.path(), f"{fname}_simple.dot")
    with open(gpath, "w") as file:
        file.write(graph)
    return gpath


def doc_state(label, state, app):
    fname = f"{label[1:]}"
    if doc.build():
        logging.info(f"Building svg grpah {fname}")
        src_path = __state_full_graph__(label, state, app)
        os.system(f"dot -Tsvg -o{os.path.join(doc.path(), fname)}_full.svg {src_path}")
        src_path = __state_simple_graph__(label, state, app)
        os.system(f"dot -Tsvg -o{os.path.join(doc.path(), fname)}.svg {src_path}")
    return f"![{fname} state]({fname}.svg)" + "{width=100% height=90%}\n"
