import copy
import importlib.resources
import os
import tempfile
from enum import Enum

import graphviz
from graphviz import Digraph
from pm4py.objects.bpmn.obj import BPMN
from pm4py.objects.bpmn.util import reduction
from pm4py.objects.bpmn.util.sorting import get_sorted_nodes_edges
from pm4py.util import constants

from powl.conversion.variants.to_petri_net import apply as powl_to_pn

from powl.objects.obj import POWL


class Parameters(Enum):
    FORMAT = "format"
    RANKDIR = "rankdir"
    BGCOLOR = "bgcolor"


FREQUENCY_TAG_IMAGES = True
SPLIT_EXCLUSIVE_GATEWAYS = True
min_width = "1.5"  # Set the minimum width in inches
min_height = "0.5"
node_color = "#f2f2f2"
silent_node_color = "black"


class SplitExclusiveGateway(BPMN.ExclusiveGateway):
    pass


class JoinExclusiveGateway(BPMN.ExclusiveGateway):
    pass


def can_connect_without_xor_violation(input_nodes, output_nodes):
    for in_node in input_nodes:
        for out_node in output_nodes:
            if isinstance(in_node, BPMN.ExclusiveGateway) and len(output_nodes) > 1:
                return False
            if isinstance(out_node, BPMN.ExclusiveGateway) and len(input_nodes) > 1:
                return False

    return True


def remove_all_occurrences_from_list(item, items):
    return [other for other in items if other is not item]


def simplify_and_gateways(nodes, edges):
    for n in list(nodes):
        if isinstance(n, BPMN.ParallelGateway):
            input_nodes = [edge[0] for edge in edges if edge[1] == n]
            output_nodes = [edge[1] for edge in edges if edge[0] == n]
            if can_connect_without_xor_violation(input_nodes, output_nodes):
                nodes = remove_all_occurrences_from_list(n, nodes)
                edges = [edge for edge in edges if n not in edge]

                for in_node in input_nodes:
                    for out_node in output_nodes:
                        edges.append((in_node, out_node))
                return simplify_and_gateways(nodes, edges)

    return nodes, edges


def add_node(n, viz):
    n_id = str(id(n))
    if isinstance(n, FrequencyTask):
        font_size = "18"
        if FREQUENCY_TAG_IMAGES:
            if n.skippable:
                if n.selfloop:
                    with importlib.resources.path(
                        "powl.visualization.powl.variants.icons", "skip-loop-tag.svg"
                    ) as gimg:
                        image = str(gimg)
                        viz.node(
                            n_id,
                            label="\n" + n.activity,
                            imagepos="tr",
                            image=image,
                            style="filled",
                            fillcolor=node_color,
                            shape="box",
                            width=min_width,
                            fontsize=font_size,
                        )
                else:
                    with importlib.resources.path(
                        "powl.visualization.powl.variants.icons", "skip-tag.svg"
                    ) as gimg:
                        skip_image = str(gimg)
                        viz.node(
                            n_id,
                            label="\n" + n.activity,
                            imagepos="tr",
                            image=skip_image,
                            style="filled",
                            fillcolor=node_color,
                            shape="box",
                            width=min_width,
                            fontsize=font_size,
                        )
            else:
                if n.selfloop:
                    with importlib.resources.path(
                        "powl.visualization.powl.variants.icons", "loop-tag.svg"
                    ) as gimg:
                        loop_image = str(gimg)
                        viz.node(
                            n_id,
                            label="\n" + n.activity,
                            imagepos="tr",
                            image=loop_image,
                            style="filled",
                            fillcolor=node_color,
                            shape="box",
                            width=min_width,
                            fontsize=font_size,
                        )
                else:
                    viz.node(
                        n_id,
                        label=n.activity,
                        style="filled",
                        fillcolor=node_color,
                        shape="box",
                        width=min_width,
                        fontsize=font_size,
                    )
        else:
            if n.skippable:
                viz.node(
                    n_id,
                    shape="box",
                    label=n.activity,
                    fontsize=font_size,
                    style="dashed",
                    width=min_width,
                )
            else:
                viz.node(
                    n_id,
                    shape="box",
                    label=n.activity,
                    fontsize=font_size,
                    width=min_width,
                )
            if n.selfloop:
                viz.edge(n_id, n_id)

    elif isinstance(n, BPMN.StartEvent):
        with importlib.resources.path(
            "powl.visualization.powl.variants.icons", "play.svg"
        ) as gimg:
            start_image = str(gimg)
            viz.node(
                n_id,
                image=start_image,
                label="",
                shape="none",
                width="0.35",
                height="0.35",
                fixedsize="true",
                style="filled",
                fillcolor=node_color,
            )
    elif isinstance(n, BPMN.EndEvent):
        with importlib.resources.path(
            "powl.visualization.powl.variants.icons", "end.svg"
        ) as gimg:
            end_image = str(gimg)
            viz.node(
                n_id,
                image=end_image,
                label="",
                shape="none",
                width="0.35",
                height="0.35",
                fixedsize="true",
                style="filled",
                fillcolor=node_color,
            )
    elif isinstance(n, BPMN.ParallelGateway):
        viz.node(
            n_id,
            label="",
            shape="square",
            style="filled",
            fillcolor=silent_node_color,
            width="0.3",
            height="0.3",
        )
    elif isinstance(n, SplitExclusiveGateway):
        with importlib.resources.path(
            "powl.visualization.powl.variants.icons", "gate.svg"
        ) as gimg:
            xor_image = str(gimg)
            viz.node(
                n_id,
                label="",
                shape="diamond",
                style="filled",
                fillcolor="lightgreen",
                width="0.4",
                height="0.4",
                fixedsize="true",
                image=xor_image,
            )
    elif isinstance(n, JoinExclusiveGateway):
        with importlib.resources.path(
            "powl.visualization.powl.variants.icons", "gate.svg"
        ) as gimg:
            xor_image = str(gimg)
            viz.node(
                n_id,
                label="",
                shape="diamond",
                style="filled",
                fillcolor="orange",
                width="0.4",
                height="0.4",
                fixedsize="true",
                image=xor_image,
            )
    elif isinstance(n, BPMN.ExclusiveGateway):
        with importlib.resources.path(
            "powl.visualization.powl.variants.icons", "gate.svg"
        ) as gimg:
            xor_image = str(gimg)
        viz.node(
            n_id,
            label="",
            shape="diamond",
            style="filled",
            fillcolor=node_color,
            width="0.4",
            height="0.4",
            fixedsize="true",
            image=xor_image,
        )
    else:
        raise Exception("Unexpected instance of class " + str(type(n)) + "!")


def split_xor_gateways(nodes, edges):
    for node in list(nodes):
        if isinstance(node, BPMN.ExclusiveGateway):
            preset = [e[0] for e in edges if e[1] is node]
            postset = [e[1] for e in edges if e[0] is node]

            if len(preset) == 1 and len(postset) == 1:
                edges = remove_all_occurrences_from_list((preset[0], node), edges)
                edges = remove_all_occurrences_from_list((node, postset[0]), edges)
                edges.append((preset[0], postset[0]))

            elif len(preset) == 1 and len(postset) > 1:
                node.__class__ = SplitExclusiveGateway

            elif len(preset) > 1 and len(postset) == 1:
                node.__class__ = JoinExclusiveGateway

            elif len(preset) > 1 and len(postset) > 1:
                join_node = copy.deepcopy(node)
                join_node.__class__ = JoinExclusiveGateway
                nodes.append(join_node)

                split_node = copy.deepcopy(node)
                split_node.__class__ = SplitExclusiveGateway
                nodes.append(split_node)
                nodes = remove_all_occurrences_from_list(node, nodes)

                for idx, edge in enumerate(edges):
                    if edge[1] is node:
                        edges[idx] = (edge[0], join_node)
                    if edge[0] is node:
                        edges[idx] = (split_node, edge[1])

                edges.append((join_node, split_node))
    return nodes, edges


def apply(powl: POWL) -> graphviz.Digraph:
    pn_2, init_2, final_2 = powl_to_pn(
        powl, parameters={"flatten_frequency_tags": False}
    )
    bpmn_graph = to_bpmn(pn_2, init_2, final_2)

    nodes, edges = get_sorted_nodes_edges(bpmn_graph)

    nodes, edges = simplify_and_gateways(nodes, edges)

    rankdir = "LR"
    bgcolor = constants.DEFAULT_BGCOLOR

    filename = tempfile.NamedTemporaryFile(suffix=".gv")
    viz = Digraph(
        "", filename=filename.name, engine="dot", graph_attr={"bgcolor": bgcolor}
    )
    viz.graph_attr["rankdir"] = rankdir

    gateway_edges = {}

    for e in edges:
        if isinstance(e[0], BPMN.ParallelGateway) or isinstance(
            e[0], BPMN.ParallelGateway
        ):
            if e[0] not in gateway_edges:
                gateway_edges[e[0]] = {"in": [], "out": []}
            gateway_edges[e[0]]["out"].append(e[1])
            continue
        if isinstance(e[1], BPMN.ParallelGateway) or isinstance(
            e[1], BPMN.ParallelGateway
        ):
            if e[1] not in gateway_edges:
                gateway_edges[e[1]] = {"in": [], "out": []}
            gateway_edges[e[1]]["in"].append(e[0])
            continue

    if SPLIT_EXCLUSIVE_GATEWAYS:
        nodes, edges = split_xor_gateways(nodes, edges)

    for node in nodes:
        add_node(node, viz)
    add_concurrent_subgraphs(viz, find_concurrent_groups(nodes, edges))

    for e in edges:
        n_id_1 = str(id(e[0]))
        n_id_2 = str(id(e[1]))

        viz.edge(n_id_1, n_id_2)

    viz.attr(overlap="false")

    viz.format = "svg"

    return viz


class FrequencyTask(BPMN.Task):
    def __init__(
        self, name, properties, id="", in_arcs=None, out_arcs=None, process=None
    ):
        super().__init__(
            id=id, name=name, in_arcs=in_arcs, out_arcs=out_arcs, process=process
        )
        self.activity = properties["activity"]
        self.skippable = properties["skippable"]
        self.selfloop = properties["selfloop"]
        script_dir = os.path.dirname(os.path.realpath(__file__))
        self.image = os.path.join(script_dir, "xor2.png")


def to_bpmn(net, im, fm):
    """
    Converts an accepting Petri net into a BPMN diagram

    Parameters
    --------------
    net
        Petri net
    im
        initial marking
    fm
        final marking

    Returns
    --------------
    bpmn_graph
        BPMN diagram
    """

    bpmn_graph = BPMN()

    entering_dictio = {}
    exiting_dictio = {}

    for place in net.places:
        node = BPMN.ExclusiveGateway()
        bpmn_graph.add_node(node)
        entering_dictio[place] = node
        exiting_dictio[place] = node

    for trans in net.transitions:
        if trans.label is None:
            if len(trans.in_arcs) > 1:
                node = BPMN.ParallelGateway(
                    gateway_direction=BPMN.Gateway.Direction.CONVERGING
                )
            elif len(trans.out_arcs) > 1:
                node = BPMN.ParallelGateway(
                    gateway_direction=BPMN.Gateway.Direction.DIVERGING
                )
            else:
                node = BPMN.ExclusiveGateway(
                    gateway_direction=BPMN.Gateway.Direction.UNSPECIFIED
                )
            bpmn_graph.add_node(node)
            entering_dictio[trans] = node
            exiting_dictio[trans] = node
        else:
            if len(trans.in_arcs) > 1:
                entering_node = BPMN.ParallelGateway(
                    gateway_direction=BPMN.Gateway.Direction.CONVERGING
                )
            else:
                entering_node = BPMN.ExclusiveGateway(
                    gateway_direction=BPMN.Gateway.Direction.UNSPECIFIED
                )

            if len(trans.out_arcs) > 1:
                exiting_node = BPMN.ParallelGateway(
                    gateway_direction=BPMN.Gateway.Direction.DIVERGING
                )
            else:
                exiting_node = BPMN.ExclusiveGateway(
                    gateway_direction=BPMN.Gateway.Direction.UNSPECIFIED
                )

            task = FrequencyTask(name=trans.label, properties=trans.properties)
            bpmn_graph.add_node(task)

            bpmn_graph.add_flow(BPMN.Flow(entering_node, task))
            bpmn_graph.add_flow(BPMN.Flow(task, exiting_node))

            entering_dictio[trans] = entering_node
            exiting_dictio[trans] = exiting_node

    for arc in net.arcs:
        bpmn_graph.add_flow(
            BPMN.Flow(exiting_dictio[arc.source], entering_dictio[arc.target])
        )

    start_node = BPMN.StartEvent(name="start", isInterrupting=True)
    end_node = BPMN.NormalEndEvent(name="end")
    bpmn_graph.add_node(start_node)
    bpmn_graph.add_node(end_node)
    for place in im:
        bpmn_graph.add_flow(BPMN.Flow(start_node, entering_dictio[place]))
    for place in fm:
        bpmn_graph.add_flow(BPMN.Flow(exiting_dictio[place], end_node))

    bpmn_graph = reduction.apply(bpmn_graph)

    for node in bpmn_graph.get_nodes():
        node.set_process(bpmn_graph.get_process_id())

    for edge in bpmn_graph.get_flows():
        edge.set_process(bpmn_graph.get_process_id())

    return bpmn_graph


def find_concurrent_groups(nodes, edges):
    predecessors = {n: [] for n in nodes}
    successors = {n: [] for n in nodes}
    for src, dst in edges:
        successors[src].append(dst)
        predecessors[dst].append(src)

    groups = {}
    for node, preds in predecessors.items():
        if len(preds) == 1:
            pred_id = str(id(preds[0]))
            if pred_id not in groups:
                groups[pred_id] = []
            groups[pred_id].append(node)

    return [group for group in groups.values() if len(group) > 1]


def add_concurrent_subgraphs(graph, concurrent_elements):
    for group in concurrent_elements:
        with graph.subgraph() as s:
            s.attr(rank="same")
            for n in group:
                s.node(str(id(n)))
