# -*- coding: utf-8 -*-
# Copyright (C) 2017  IRISA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# The original code contained here was initially developed by:
#
#     Pierre Vignet.
#     IRISA
#     Dyliss team
#     IRISA Campus de Beaulieu
#     35042 RENNES Cedex, FRANCE
from __future__ import unicode_literals
from __future__ import print_function

# Standard imports
import datetime as dt
from collections import defaultdict
import networkx as nx
import itertools as it
import re
import json
import os
import glob
import json
from logging import DEBUG
# Remove matplotlib dependency
# It is used on demand during the drawing of a graph
try:
    import matplotlib.pyplot as plt
except ImportError:
    pass

# Library imports
from cadbiom.models.guard_transitions.translators.chart_xml \
    import MakeModelFromXmlFile
from cadbiom.models.biosignal.translators.gt_visitors import compile_cond, compile_event
from cadbiom.models.guard_transitions.analyser.ana_visitors import TableVisitor
from cadbiom.models.biosignal.sig_expr import *
from cadbiom.models.guard_transitions.analyser.ana_visitors import SigExpIdCollectVisitor

import cadbiom.commons as cm

LOGGER = cm.logger()

"""
Bx  Ax
% h2 h00
% h3
% h0 h1
% hlast
Bx  Ax
% h2
% h3 h00
% h0 h1
%
% hlast
Bx  Ax
% h2
% h3 h00
% h0 h1
% hlast
%
%
Bx  Ax
% h2 h00
% h3
% h0 h1
% hlast
%
%
%
"""

class Reporter(object):
    """Error reporter.

    .. note:: Link the lexer to the model allows to avoid error in Reporter
        like:  "-> dec -> Undeclared event or state"
        In practice this is time consuming and useless for what we want to do.
        See parse_condition()
    """

    def __init__(self):
        self.error = False
        self.mess = ""
        pass

    def display(self, e):
        self.error = True
        if "Undeclared event or state" not in e:
            LOGGER.debug("\t" + self.mess + " -> " + e)


def load_solutions(file):
    """Open a file with many solution/MACs.

    :param: File name
    :type: <str>
    :return:A tuple of "frontier places" and a list of events in each step.
        ("Bx Ax", [[u'h2', u'h00'], [u'h3'], [u'h0', u'h1'], [u'hlast']])
    :rtype: <tuple <str>, <list>>
    """

    sol_steps = defaultdict(list)
    sol = ""
    with open(file, 'r') as fd:
        for line in fd:
            LOGGER.debug("Load_solutions :: line: " + line)
            # Remove possible \t separator from first line (frontier solution)
            line = line.rstrip('\n').rstrip('\t').replace('\t', ' ')
            # TODO: remove last space ' ' => beware, may be informative...
            # (start transitions have no event names: ori="__start__0")
            line = line.rstrip(' ')
            if line == '' or line[0] == '=':
                # Blank line
                # Skip blank lines and solution separator in some files (=====)
                continue
            elif line[0] != '%':
                if sol == line:
                    # TODO: why this condition ?
                    # => multiple lines with the same solution ?
                    # Same frontier places
                    yield sol, sol_steps[sol]

                    # reinit sol
                    sol_steps[sol] = list()
                    continue
                elif sol == '':
                    # First sol
                    sol = line
                else:
                    # Yield previous sol
                    yield sol, sol_steps[sol]
                    sol = line

            elif line[0] == '%':
                # Remove step with only "% "
                step = line.lstrip('% ')

                if step != '':
                    sol_steps[sol].append(step.split(' '))

        # Yield last sol
        yield sol, sol_steps[sol]


def get_transitions(file, all_places=False):
    """Get all transitions in a file model (bcx format).

    :param arg1: Model in bcx format.
    :param arg2: Ask to return all the places of the model.
        (useful for get_frontier_places() that compute frontier places)
    :type arg1: <str>
    :type arg2: <bool>
    :return: A dictionnary of events as keys, and transitions as values.
        Since many transitions can define an event, values are lists.
        Each transition is a tuple with: origin node, final node, attributes
        like label and condition.
        {u'h00': [('Ax', 'n1', {u'label': u'h00[]'}),]
    :rtype: <dict <list <tuple <str>, <str>, <dict <str>: <str>>>>
        If all_places is True, this func returns also a list of all places in
        the model.
    """

    parser = MakeModelFromXmlFile(file)

#    print(dir(parser))
#    print(type(parser))
    #<type 'instance'>
    #['__doc__', '__init__', '__module__', 'get_model', 'handler', 'model', 'parser'

    g = (trans for transition in parser.handler.top_pile.transitions
         for trans in transition)

    transitions = defaultdict(list)

    for trans in g:

        # Get the names of clocks
        # Some event have many clocks (like _h_2755) for the same
        # ori/ext entities, so we have to extract them and their respective
        # conditions
        if trans.event == '':
            # null event without clock => StartNodes
            # These nodes are used to resolve the problem of
            # Strongly Connected Components (inactivated cycles in the graph)
            # The nodes
            # Avoids having SigConstExpr as event type in parse_event()
            # I create a transition (SCC-__start__?),
            # and a node (__start__?) for this case.
            trans.event = 'SCC-' + trans.ori.name
            events = {trans.event: trans.condition}
        elif re.match('_h[\w]+', trans.event):
            # 1 event (with 1 clock)
            events = {trans.event: trans.condition}
        else:
            # Many events (with many clocks with condition(s))
            events = parse_event(trans.event)

        for event, condition in events.iteritems():
            # LOGGER.debug("NEW trans", event)

            # Handle multiple transitions for 1 event
            transitions[event].append(
                (
                    trans.ori.name, trans.ext.name,
                    {
                        'label': event, #+ '[' + trans.condition + ']',
                        'condition': condition,
                    }
                )
            )

    LOGGER.info("{} transitions loaded".format(len(transitions)))
    # Return a dict instead of defaultdict to avoid later confusions
    #(masked errors) by searching a transition that was not in the model...

    assert len(transitions) != 0, "No transitions found in the model ! " \
        "Please check the names of events (_h_xxx)"

    if all_places:
        # Return all nodes
        return dict(transitions), parser.handler.node_dict.keys()

    return dict(transitions)


def get_frontier_places(transitions, all_places):
    """Return frontier places of a model (deducted from its transitions and
    from all places of the model).

    .. note:: why we use all_places from the model instead of
        (input_places - output_places) to get frontier places ?
        Because some nodes are only in conditions and not in transitions.
        If we don't do that, these nodes are missing when we compute
        valid paths from conditions.

    :param arg1: Model's transitions.
        {u'h00': [('Ax', 'n1', {u'label': u'h00[]'}),]
    :type arg1: <dict>
        keys: names of events
        values: list of transitions as tuples (with in/output, and label).
    :return: Set of frontier places.
    :rtype: <set>
    """

    # Get transitions in events
    g = tuple(trans for event in transitions.values() for trans in event)

    # Get input nodes & output nodes
#    input_places = {trans[0] for trans in g}
    output_places = {trans[1] for trans in g}

    # Get all places that are not in transitions in the "output" place
    return set(all_places) - output_places


def rec(tree, inhibitors_nodes):
    """

    tree = ('H', 'v', (
        ('F', 'v', 'G'),
        '^',
        (
            ('A', 'v', 'B'),
            '^',
            ('C', 'v', ('D', '^', 'E'))
        )
    ))
    """

#    print("TREE", tree, type(tree), dir(tree))

    if isinstance(tree, str):  # terminal node
        path = [tree]
        solutions = [path]
        return solutions
    if isinstance(tree, SigNotExpr):
        LOGGER.debug("NOT OPERAND: {}, {}".\
            format(
                tree.operand,
                type(tree.operand)
            )
        )
        try:
            current_inhibitors = get_places_from_condition(tree.operand.__str__())
            inhibitors_nodes.update(current_inhibitors)
            LOGGER.debug("INHIBITORS found: " + str(current_inhibitors))

            path = [tree.operand.name]
            solutions = [path]
            return solutions
        except AttributeError:
            tree = tree.operand

    if isinstance(tree, SigIdentExpr):
        path = [tree.name]
        solutions = [path]
        return solutions



    lch = tree.left_h
    op  = tree.operator
    rch = tree.right_h
#    print('OZCJSH:', lch, op, rch, sep='\t\t')
    lpaths = rec(lch, inhibitors_nodes)
    rpaths = rec(rch, inhibitors_nodes)
#    print('VFUENK:', lpaths, rpaths)
    if op == 'or':  # or
#        ret = [*lpaths, *rpaths]
        ret = list(it.chain(lpaths, rpaths))
#        print('RET:', ret)
        return ret
    else:  # and
        assert op == 'and'
#        print(list(it.product(lpaths, rpaths)))
#        raw_input('test')

        ret = list(l + r for l, r in it.product(lpaths, rpaths))
#        print('RET:', ret)
        return ret


def get_places_from_condition(condition):
    """Parse condition string and return all places, regardless of operators.

    .. note:: This function is only used to get all nodes in a condition when
        we know they all are inhibitors nodes.

    :param: Condition string.
    :type: <str>
    :return: Set of places.
    :rtype: <set>
    """

    replacement = ['and', 'or', 'not', '(', ')']

    for chr in replacement:
        condition = condition.replace(chr, ' ')

    # Must be exempt of unauthorized chars
    return {elem for elem in condition.split(' ')
            if elem != ''}


def parse_condition(condition, all_nodes, inhibitors_nodes):
    """Return valid paths according the given logical formula and nodes.

    """

    LOGGER.debug("CONDITION: " + condition)
    # Error Reporter
    err = Reporter()
    tvi = TableVisitor(err)
    # Link the lexer to the model allows to avoid error in Reporter
    # like:  "-> dec -> Undeclared event or state"
    # In practice this is time consuming and useless for what we want to do
    #parser = MakeModelFromXmlFile(BIO_MOLDELS_DIR + "Whole NCI-PID database translated into CADBIOM formalism(and).bcx")
    #parser.get_model().accept(tvi)
    symb_tab = tvi.tab_symb
    # Get tree object from condition string
    cond_sexpr = compile_cond(condition, symb_tab, err)
    # Get all possible paths from the condition
    possible_paths = rec(cond_sexpr, inhibitors_nodes)

    # Prune possible paths according to:
    # - Inhibitor nodes that must be removed because they will never
    # be in the graph.
    # - All nodes in transitions (ori -> ext) because we know all transitions
    # in the graph, so we know which entities can be choosen to validate a path.
    # - All frontier places, that are known entities that can be in conditions
    # (not only in ori/ext) of transitions.
    # So: authorized nodes = frontier_places + transition_nodes - inhibitor nodes
    valid_paths = {tuple(path) for path in possible_paths
                   if (set(path) - inhibitors_nodes).issubset(all_nodes)}

    # Debugging only
    if LOGGER.getEffectiveLevel() == DEBUG:
        LOGGER.debug("INHIBIT NODES: " + str(inhibitors_nodes))
        LOGGER.debug("ALL NODES: " + str(all_nodes))
        LOGGER.debug("POSSIBLE PATHS: " + str(possible_paths))
        LOGGER.debug("VALID PATHS: " + str(valid_paths))

        for path in possible_paths:
            pruned_places = set(path) - inhibitors_nodes
            isinsubset = pruned_places.issubset(all_nodes)
            LOGGER.debug(
                "PRUNED PATH: {}, VALID: {}".format(
                    pruned_places,
                    isinsubset
                )
            )

    assert len(valid_paths) > 0, "No valid path for: " + str(condition)
    if len(valid_paths) > 1:
        LOGGER.warning("Multiple valid paths for: {}:\n{}".format(condition,
                                                                  valid_paths))
    return valid_paths


    # condition expressions contains only node ident
    icv = SigExpIdCollectVisitor()
    lst1 = cond_sexpr.accept(icv)
    print(cond_sexpr)
    print(type(cond_sexpr))
    print(dir(cond_sexpr))
    print("LISTE", lst1)
#    <class 'cadbiom.models.biosignal.sig_expr.SigSyncBinExpr'>
#    'accept', 'get_signals', 'get_ultimate_signals', 'is_bot', 'is_clock',
# 'is_const', 'is_const_false', 'is_ident', 'left_h', 'operator', 'right_h', 'test_equal']

    print(cond_sexpr.get_signals())
#    print(cond_sexpr.get_ultimate_signals())
    print("LEFT", cond_sexpr.left_h)
    print("OPERATOR", cond_sexpr.operator)
    print("RIGHT", cond_sexpr.right_h)


#    ret = treeToTab(cond_sexpr)
#    [set([('((formule', True)])]
#    print("treeToTab", ret)
#    print(type(ret))
#    print(dir(ret))


def parse_event(event):
    """Decompile logical formula in event's name.

    :param: Event string.
    :type: <event string>
    :return: A dict of events and their conditions.
    :rtype: <dict>
        keys: event's names; values: logical formula attached (condition)
    """

    def treeToExprDefaultsList(tree):
        if isinstance(tree, SigDefaultExpr):
            return treeToExprDefaultsList(tree.left_h) + \
                treeToExprDefaultsList(tree.right_h)

        else:
            # Here, some tree are from classes SigConstExpr or SigIdentExpr
            # Ex: for the clock "_h_5231":
            #    ... default (_h_5231)" => No condition for this event
            # Other examples:
            # _h_2018 _h_820 _h_4939 _h_5231 _h_3301 _h_4967 _h_2303 _h_3301
            return [tree]

    def filterSigExpression(expr):
        """
        .. note:: No SigConstExpr here => filtered in get_transitions()
            by checking null events (event="") in the model.
        """

        if isinstance(expr, SigWhenExpr):
            # right : SigSyncBinExpr (logical formula), BUT
            # sometimes SigConstExpr (just a True boolean) when clock is empty
            # Ex: "when ()"
            # So, we replace this boolean with an empty condition
            right = '' if isinstance(expr.right_h, SigConstExpr) \
                        else str(expr.right_h)

            return expr.left_h.name, right

        if isinstance(expr, SigIdentExpr):
            return expr.name, ''

        raise AssertionError("You should never have been there ! "
                             "Your expression type is not yet supported...")

#    def filterSigExpression(listOfExpr):
#        return [filterSigExpression(expr) for expr in listOfExpr]

    # Error Reporter
    err = Reporter()
    tvi = TableVisitor(err)
    symb_tab = tvi.tab_symb

    # Get tree object from event string
    event_sexpr = compile_event(event, symb_tab, True, err)[0]

    # Filter when events
    g = (filterSigExpression(expr) \
         for expr in treeToExprDefaultsList(event_sexpr))
    eventToCondStr = \
        {event_name: event_cond for event_name, event_cond in g}


    LOGGER.debug("Clocks from event parsing: " + str(eventToCondStr))

    return eventToCondStr


def build_graph(solution, steps, transitions):
    """Build a graph for the given solution.

        - Get & make all needed edges
        - Build graph

    :param arg1: Frontier places.
    :param arg2: List of steps (with events in each step).
    :param arg3: A dictionnary of events as keys, and transitions as values
        (see get_transitions()).
    :type arg1: <str>
    :type arg2: <list <list>>
    :type arg3: <dict <list <tuple <str>, <str>, <dict <str>: <str>>>>
    :return:
        - Networkx graph object.
        - Nodes corresponding to transitions with conditions.
        - All nodes in the model
        - Edges between transition node and nodes in condition
        - Normal transitions without condition
    :rtype: <networkx.classes.digraph.DiGraph>, <list>, <list>, <list>, <list>
    """

    def filter_transitions(step_event):
        """ Insert a transittion in a transition event if there is a condition.

        => Insert a node in a edge.
        => Link all nodes involved in the condition with this new node.

        :param: A list of events (transitions) (from a step in a solution).
            [('Ax', 'n1', {u'label': u'h00[]'}),]
        :type: <tuple>
        :return: Fill lists of edges:
            edges_with_cond: link to a transition node for
                transition with condition.
            transition_nodes: add new nodes corresponding to transitions with
                conditions.
            edges_in_cond: Add all edges to nodes linked to a transition
                via the condition (group of nodes involved in the path
                choosen by the solver).
            edges: transition untouched if there is no condition.
        :rtype: None
        """
        assert len(step_event) != 0 # Todo: useful ?

        inhibitors_nodes = set() # Inactivated nodes in paths of conditions
        input_places = {trans[0] for trans in step_event}

        # Color nodes
        # Since we explore all possible paths for each condition,
        # some edges are rewrited multiple times.
        # => included edges between origin <=> transition node
        # These edges must be grey while, edges between a node that is
        # only in a condition and a transition node must be green.
        # => notion of activator vs inhibitor vs normal input/output node
        def color_map(node):
            # print("color for:", node)
            if node in inhibitors_nodes: # Test first (see cond below)
                return 'red'
            if node in input_places: # some /all frontier places are in this set
                return 'grey'
            else:
                return 'green'


        for trans in step_event:
            attributes = trans[2]
            ori = trans[0]
            ext = trans[1]
            event = attributes['label'].split('[')[0]

            # If there is a condition formula associated to this clock
            if attributes['condition'] != '':

                # Add the transition as node
                transition_nodes.append(
                    (
                        event,
                        {
                            'label': attributes['label'],
                            'name': attributes['label'],
                            'color': 'blue',
                            'condition': attributes['condition'],
                        }
                    )
                )

                # Origin => transition node
                edges_with_cond.append(
                    (
                        ori, event,
                        {
                            'label': ori + '-' + event,
                        }
                    )
                )

                # Transition node => ext
                edges_with_cond.append(
                    (
                        event, ext,
                        {
                            'label': event + '-' + ext,
                        }
                    )
                )

                # Add all transitions to nodes linked via the condition
                valid_paths = parse_condition(
                    attributes['condition'],
                    all_nodes,
                    inhibitors_nodes
                )
                for i, path in enumerate(valid_paths):
                    for node in path:
                        edges_in_cond.append(
                            (
                                node, event,
                                {
                                    'label': '{} ({})'.format(
                                        event,
                                        i
                                    ),
#                                    'label': '{} [{}] ({})'.format(
#                                        event,
#                                        ', '.join(path),
#                                        i
#                                    ), #node + '-' + event,
                                    'color': color_map(node),
                                }
                            )
                        )
            else:
                # Normal edges
                edges.append(trans)


    # Get & make all needed edges ##############################################
    LOGGER.debug("BUILD GRAPH FOR SOL: " + str(solution))
    LOGGER.debug("STEPS: " + str(steps))
#
#    print(transitions['_h_2755'])
#    print(transitions['_h_4716'])
#    print(transitions['_h_2206'])
#    print(transitions['_h_3426'])
#    exit()

    frontier_places  = solution.split(' ')
    edges_with_cond  = list() # Edges between ori <-> transition node <-> ext
    edges_in_cond    = list() # Edges between transition node and nodes in condition
    transition_nodes = list() # Nodes inserted because of condition in transition
    edges            = list() # Normal transitions without condition

    # Get all nodes in all transitions (origin & ext)
    try:
        all_transitions = \
            (transitions[step_event] for step_event in it.chain(*steps))
        transitions_ori_ext = \
            (tuple((trans[0], trans[1])) for trans in it.chain(*all_transitions))
    except KeyError:
        print("/!\One event is not in the given model file... Please check it.")
        raise
    all_nodes = set(it.chain(*transitions_ori_ext)) | set(frontier_places)

    # Parse all conditions in transitions;
    # add nodes in conditions and transition nodes
    [filter_transitions(transitions[step_event])
        for step_event in it.chain(*steps)]

#    print("edges without cond", edges)
#    print("edges with cond", edges_with_cond)
#    print("transition nodes added", transition_nodes)
#    raw_input("PAUSE")

    # Make Graph ###############################################################
    G = nx.DiGraph()
    # Add all nodes (some frontier places are in this set)
    G.add_nodes_from(all_nodes, color='grey')
    # Add fontier places
    G.add_nodes_from(frontier_places, color='red')
    # Add all transition nodes
    G.add_nodes_from(transition_nodes, color='blue')

    # Node attribute ?
    # print(G.node['h1'])

    # Add all edges
    G.add_edges_from(edges)
    G.add_edges_from(edges_with_cond)
    G.add_edges_from(edges_in_cond)

    return G, transition_nodes, all_nodes, edges_in_cond, edges


def draw_graph(output_dir, solution, solution_index, G,
               transition_nodes, all_nodes,
               edges_in_cond, edges,
               matplotlib_export=False):
    """Draw graph with colors and export it to graphml format.

    .. note:: Legend:
        - red: frontier places (in solution variable),
        - white: middle edges,
        - blue: transition edges

    :param arg1: Output directory for graphml files.
    :param arg2: Solution string (mostly a set of frontier places).
    :param arg3: Index of the solution in the Cadbiom result file
        (used to distinguish exported filenames).
    :param arg4: Networkx graph object.
    :param arg5: Nodes corresponding to transitions with conditions.
        List of tuples: event, node
    :param arg6: All nodes in the model
    :param arg7: Edges between transition node and nodes in condition
    :param arg8: Normal transitions without condition
    :param arg9: If True, an image is exported by matplotlib (optional).
    :type arg1: <str>
    :type arg2: <str>
    :type arg3: <int> or <str>
    :type arg4: <networkx.classes.digraph.DiGraph>
    :type arg5: <list>
    :type arg6: <list>
    :type arg7: <list>
    :type arg7: <list>
    :type arg8: <boolean>
    """

    creation_date = dt.datetime.now().strftime("%H-%M-%S")
    # Save & show
    if not matplotlib_export:
        # Save the graph without matplotlib requirement
        # PS: inhibitors will still have not the attribute 'color' = 'white'
        nx.write_graphml(
            G,
            "{}{}_{}_{}.graphml".format(
                output_dir, creation_date, solution_index, solution[:75]
            )
        )
        return

    # Drawing ##################################################################
    # draw_circular(G, **kwargs) On a circle.
    # draw_random(G, **kwargs)   Uniformly at random in the unit square.
    # draw_spectral(G, **kwargs) Eigenvectors of the graph Laplacian.
    # draw_spring(G, **kwargs)   Fruchterman-Reingold force-directed algorithm.
    # draw_shell(G, **kwargs)    Concentric circles.
    # draw_graphviz(G[, prog])   Draw networkx graph with graphviz layout.

    # Get a list of transition nodes (without dictionnary of attributes)
    transition_nodes_names = [node[0] for node in transition_nodes]

    pos = nx.circular_layout(G)

    # Legend of conditions in transition nodes
    f = plt.figure(1)
    ax = f.add_subplot(1,1,1)
    text = '\n'.join(transition_nodes_names)
    ax.text(0, 0, text, style='italic', fontsize=10,
            bbox={'facecolor': 'white', 'alpha': 0.5, 'pad': 10})

    # Draw nodes:
    # - red: frontier places (in solution variable),
    # - white: middle edges,
    # - blue: transition edges
    frontier_places  = set(solution.split(' '))
    def color_map(node):
        # print("color for:", node)
        if node in frontier_places: # Test first (see cond below)
            return 'red'
        if node in unziped_transition_nodes:
            return 'blue'
        if node in all_nodes: # some /all frontier places are in this set
            return 'grey'
        else:
            return 'white'

    # Color nodes
    colors = [color_map(node) for node in G.nodes_iter()]
    nx.draw(G, pos=pos, with_labels=True,
            node_color=colors, node_size=1000, alpha=0.5,
            ax=ax)

    # Draw edges involved in transitions with conditions
    edges_colors = [edge[2]['color'] for edge in edges_in_cond]
    nx.draw_networkx_edges(G, pos, edgelist=edges_in_cond,
                           edge_color=edges_colors, width=2, alpha=0.5)

    # Draw labels for normal transitions (move pos to the end of the arrow)
    # ex: [('Ax', 'n1', {u'condition': u'', u'label': u'h00[]'}),]
    edges_labels = {(edge[0], edge[1]): edge[2]['label'] for edge in edges}
    nx.draw_networkx_edge_labels(G, pos, edges_labels, label_pos=0.3)

    # Save & show
    plt.legend()
    plt.savefig(
        output_dir + creation_date + '_' + solution[:75] + ".svg",
        format="svg")
    plt.show()


def process_solutions(output_dir, sol_steps, transitions):
    """Build a graph per solution"""

    for sol_index, (sol, steps) in enumerate(sol_steps):

        draw_graph(output_dir, sol, sol_index,
                   *build_graph(sol, steps, transitions)
        )


def test_main():
    """Test"""

    # chart_model.py
    # chart_xml.py
    parser = MakeModelFromXmlFile(BIO_MOLDELS_DIR + "mini_test_publi.bcx")
    print(type(parser.parser))
    print(dir(parser))
    print("HANDLER")
    print(dir(parser.handler))
    print(dir(parser.parser))
    print(dir(parser.get_model()))
    print("ICI")
#    print(parser.get_model().get_simple_node())
    print(parser.handler.node_dict)
    print(parser.handler.top_pile)
    print(parser.handler.pile_dict)
    print(parser.handler.transition.event)
    print(type(parser.handler.top_pile))

    transitions = dict()
    for transition in parser.handler.top_pile.transitions:
        # print(type(transition)) => list
        for trans in transition:
            # 'action', 'activated', 'clean', 'clock', 'condition', 'event',
            # 'ext', 'ext_coord', 'fact_ids', 'get_influencing_places',
            # 'get_key', 'is_me', 'macro_node', 'name', 'note', 'ori',
            # 'ori_coord', 'remove', 'search_mark', 'selected', 'set_action',
            # 'set_condition', 'set_event', 'set_name', 'set_note'

# {'name': '', 'clock': None, 'selected': False, 'activated': False,
# 'search_mark': False, 'note': '', 'ext': <cadbiom.models.guard_transitions.chart_model.CSimpleNode object at 0x7f391c7406d0>,
# 'ext_coord': 0.0, 'ori': <cadbiom.models.guard_transitions.chart_model.CSimpleNode object at 0x7f391c740650>,
# 'action': u'', 'macro_node': <cadbiom.models.guard_transitions.chart_model.CTopNode object at 0x7f391c7db490>,
# 'ori_coord': 0.0, 'event': u'h5', 'condition': u'', 'fact_ids': []}
            # print(dir(trans))
            print("NEW trans", trans.event)
#            print(trans.__dict__)
#            print(trans.name, trans.clock, trans.selected, trans.activated,
#                  trans.search_mark, trans.note, trans.ext, trans.ext_coord,
#                  trans.ori, trans.action, trans.macro_node, trans.ori_coord,
#                  trans.event, trans.condition, trans.fact_ids
#                  )

            transitions[trans.event] = trans.condition

#print("ORI", trans.ori.__dict__)
#{'name': 'n4', 'yloc': 0.906099768906, 'selected': False,
#'father': <cadbiom.models.guard_transitions.chart_model.CTopNode object at 0x7f09eed91490>,
#'xloc': 0.292715433748, 'search_mark': False, 'was_activated': False,
#'incoming_trans': [<cadbiom.models.guard_transitions.chart_model.CTransition object at 0x7f09eecf67d0>],
#'model': <cadbiom.models.guard_transitions.chart_model.ChartModel object at 0x7f09ef2cf3d0>,
#'outgoing_trans': [<cadbiom.models.guard_transitions.chart_model.CTransition object at 0x7f09eecf6850>],
#'activated': False, 'hloc': 1.0}
            print("ORI", trans.ori.name)
            try:
                print("ori INCO", [(tr.event, tr.condition) for tr in trans.ori.incoming_trans])
            except: pass
            try:
                print("ori OUTGO", [(tr.event, tr.condition) for tr in trans.ori.outgoing_trans])
            except: pass
            print("EXT", trans.ext.name)
            try:
                print("ext INCO", [(tr.event, tr.condition) for tr in trans.ext.incoming_trans])
            except: pass
            try:
                print("ext OUTGO", [(tr.event, tr.condition) for tr in trans.ext.outgoing_trans])
            except: pass
    print(transitions)



def sol_digging(decompiled_filename, sol_steps, transitions, conditions=True):
    """Convert all events for all solutions in a complete MAC file
    and write them in a separate file in the json format.

    This is a function to quickly search all transition attributes involved
    in a solution.

    :param arg1: Path of the json file.
    :param arg2: List of steps involved in a solution. See load_solutions().
    :param arg3: A dictionnary of events as keys, and transitions as values.
        Since many transitions can define an event, values are lists.
        Each transition is a tuple with: origin node, final node, attributes
        like label and condition.
        {u'h00': [('Ax', 'n1', {u'label': u'h00[]'}),]
        See get_transitions().
    :param arg4: (Optional) Integrate in the final file,
        the conditions for each transition.
    :type arg1: <str>
    :type arg2: <list>
    :type arg3: <dict <list <tuple <str>, <str>, <dict <str>: <str>>>>
    :type arg4: <bool>
    """

    def get_transition_def(step_event):
        """Dump each transition in the given event to a list of dictionnaries.

        .. note:: ori="JUN_nucl_gene" ext="JUN_nucl" event="_h_391"
        :return: A list of dictionaries
            (1 dict for 1 transition in the given event)
        :rtype: <list <dict>>
        """

        # Many transitions per event (ex: complex dissociation)
        decomp_transitions = list()
        for trans in step_event:

            decomp_transition = {
                #"event": trans[2]['label'].split('[')[0],
                "ori": trans[0],
                "ext": trans[1],
            }
            # If condition boolean is set (by default),
            # we add the event's transition to the json data.
            if conditions:
                decomp_transition["condition"] = trans[2]['condition']

            decomp_transitions.append(decomp_transition)

        return decomp_transitions


    # sol_steps structure:
    # ("Bx Ax", [[u'h2', u'h00'], [u'h3'], [u'h0', u'h1'], [u'hlast']])
    decomp_solutions = list()
    for sol, steps in sol_steps:
        # Decompile steps in each solution
        decomp_steps = list()
        for step in steps:
            # Decompile events in each step
            decomp_events = list()
            for event in step:
                # Decompile transitions in each event
                decomp_event = dict()
                # Get transitions for the given event
                # Structure of transitions:
                # {u'h00': [('Ax', 'n1', {u'label': u'h00[]'}),]
                step_event = transitions[event]
                if len(step_event) == 0:
                    decomp_event['event'] = "ERROR, no transition"
                else:
                    # Get list of transitions
                    decomp_event['event'] = event
                    decomp_event['transitions'] = get_transition_def(step_event)
                # Add event and its transitions
                decomp_events.append(decomp_event)
            # Add step and its events
            decomp_steps.append(decomp_events)
        # Add solution and its steps
        solution = {
            "solution": sol,
            "steps": decomp_steps,
        }
        decomp_solutions.append(solution)

    # Debugging only
    if LOGGER.getEffectiveLevel() == DEBUG:
        LOGGER.debug(json.dumps(decomp_solutions, sort_keys=True, indent=4))
    # Write file
    with open(decompiled_filename, 'w') as f_d:
        json.dump(decomp_solutions, f_d, sort_keys=True, indent=4)


def sol_digging_main(output_dir, model_file, solution_path, conditions=True):
    """Entry point for sol_digging

    .. note:: This functions tests if the solution_path is a directory
        or just a file.
    """

    # Get transitions from the model
    model_transitions = get_transitions(model_file)

    if os.path.isfile(solution_path):
        # The given path is a solution file
        # Add _decomp to the solution filename
        filename, file_extension = os.path.splitext(solution_path)
        sol_digging(
            output_dir + filename + '_decomp' + file_extension,
            load_solutions(solution_path),
            model_transitions,
            conditions=conditions,
        )

    elif os.path.isdir(solution_path):
        # The given path is a directory
        solution_path = solution_path if solution_path[-1] == '/' \
            else solution_path + '/'

        # Decompilation of all files in the directory
        for file_number, solution_file in \
            enumerate(glob.glob(solution_path + '*cam_complete.txt'), 1):

            # Add _decomp to the solution filename
            filename, file_extension = os.path.splitext(solution_file)
            sol_digging(
                output_dir + filename + '_decomp' + file_extension,
                load_solutions(solution_file),
                model_transitions,
                conditions=conditions,
            )

        LOGGER.info("Files processed: " + str(file_number))


def parse_trajectories_main(output_dir, model_file, solution_file):
    """Entry point for parse_trajectories"""

    process_solutions(
        output_dir,
        load_solutions(solution_file),
        get_transitions(model_file)
    )


def graph_isomorph_test(model_file_1, model_file_2, output_dir='graphs/',
                        make_graphs=False, make_json=False):
    """Entry point for model consistency checking.

    This functions checks if the 2 given models have the same topology,
    nodes & edges attributes/roles.

    .. note:: cf graphmatcher
        https://networkx.github.io/documentation/development/reference/generated/networkx.algorithms.isomorphism.categorical_edge_match.html

    :param arg1: File for the model 1.
    :param arg2: File for the model 2.
    :param arg3: Output path.
    :param arg4: If True, make a graphml file in output path.
    :param arg5: If True, make a json dump of results in output path.
    :type arg1: <str>
    :type arg2: <str>
    :type arg3: <str>
    :type arg4: <boolean>
    :type arg5: <boolean>
    :return: Dictionary with the results of tests.
        keys: 'topology', 'nodes', 'edges'; values: booleans
    :rtype: <dict <str>: <boolean>>
    """

    import networkx.algorithms.isomorphism as iso

    # Load transitions in the models
    # Transitions structure format:
    # {u'h00': [('Ax', 'n1', {u'label': u'h00[]'}),]
    transitions_1, all_places_1 = get_transitions(model_file_1, all_places=True)
    transitions_2, all_places_2 = get_transitions(model_file_2, all_places=True)

    # Get all frontier places in the models
    # (places that are never in output position in all transitions)
    # EDIT: why we use all_places from the model instead of
    # (input_places - output_places) to get frontier places ?
    # Because some nodes are only in conditions and not in transitions.
    # If we don't do that, these nodes are missing when we compute
    # valid paths from conditions.
    front_places_1 = " ".join(get_frontier_places(transitions_1, all_places_1))
    front_places_2 = " ".join(get_frontier_places(transitions_2, all_places_2))
    LOGGER.debug("Frontier places 1: " + str(front_places_1))
    LOGGER.debug("Frontier places 2: " + str(front_places_2))

    # Build graphs & get networkx object
    # We give all events in the model as a list of steps
    # So we simulate a cadbiom solution (with all events in the model).
    res_1 = build_graph(front_places_1, [transitions_1.keys()], transitions_1)
    G1 = res_1[0]
    res_2 = build_graph(front_places_2, [transitions_2.keys()], transitions_2)
    G2 = res_2[0]

    # Checking
    nm = iso.categorical_node_match('color', 'grey')
    em = iso.categorical_edge_match('color', '')

    check_state = \
    {
        'topology': nx.is_isomorphic(G1, G2),
        'nodes': nx.is_isomorphic(G1, G2, node_match=nm),
        'edges': nx.is_isomorphic(G1, G2, edge_match=em),
    }

    LOGGER.info("Topology checking: " + str(check_state['topology']))
    LOGGER.info("Nodes checking: " + str(check_state['nodes']))
    LOGGER.info("Edges checking: " + str(check_state['edges']))

    # Draw graph
    if make_graphs:
        draw_graph(output_dir, front_places_1, "first", *res_1)
        draw_graph(output_dir, front_places_2, "second", *res_2)

    # Export to json file
    if make_json:
        with open(output_dir + "comp_results.json", 'w') as fd:
            fd.write(json.dumps(check_state, sort_keys=True, indent=4) + '\n')

    return check_state


def graph_infos(model_file, output_dir='graphs/',
                make_graph=True, make_json=True, advanced_measures=False):
    """Entry point for model stats.

    :param arg1: File for the model.
    :param arg2: Output path.
    :param arg3: If True, make a graphml file in output path.
    :param arg4: If True, make a json dump of results in output path.
    :type arg1: <str>
    :type arg2: <str>
    :type arg3: <boolean>
    :type arg4: <boolean>
    :return: Dictionary with the results of measures on the given graph.
        keys: measure's name; values: measure's value
    :rtype: <dict>
    """

    # Load transitions in the model
    # Transitions structure format:
    # {u'h00': [('Ax', 'n1', {u'label': u'h00[]'}),]
    transitions_1, all_places_1 = get_transitions(model_file, all_places=True)

    # Get all frontier places in the models
    # (places that are never in output position in all transitions)
    # EDIT: why we use all_places from the model instead of
    # (input_places - output_places) to get frontier places ?
    # Because some nodes are only in conditions and not in transitions.
    # If we don't do that, these nodes are missing when we compute
    # valid paths from conditions.
    front_places = get_frontier_places(transitions_1, all_places_1)
    front_places_str = " ".join(front_places)
    LOGGER.debug("Frontier places 1: " + front_places_str)

    # Build graphs & get networkx object
    # We give all events in the model as a list of steps
    # So we simulate a cadbiom solution (with all events in the model).
    res_1 = build_graph(front_places_str, [transitions_1.keys()], transitions_1)
    G = res_1[0]

    infos = {
        'model': model_file,
        'nodes': len(G.nodes()),
        'edges': len(G.edges()),
        'transitions:': len(transitions_1),
        'places': len(all_places_1),
        'frontier places': len(front_places),
    }

    if advanced_measures:
        infos.update({
            'degree': nx.degree_centrality(G),
            'in_degree': nx.in_degree_centrality(G),
            'out_degree': nx.out_degree_centrality(G),
            'betweenness': nx.betweenness_centrality(G),
            'closeness': nx.closeness_centrality(G),
        })

    # Draw graph
    if make_graph:
        draw_graph(output_dir, front_places_str, 'stat', *res_1)

    # Export to json file
    if make_json:
        with open(output_dir + "infos_results.json", 'w') as fd:
            fd.write(json.dumps(infos, sort_keys=True, indent=4) + '\n')

    LOGGER.info(str(infos))
    return infos


if __name__ == "__main__":

    LOG_DIR = "./logs/"
    BIO_MOLDELS_DIR = "./bio_models/"

    graph_isomorph_test(BIO_MOLDELS_DIR + "mini_test_publi.bcx",
                        BIO_MOLDELS_DIR + "mini_test_publi_mod.bcx")
    exit()

    process_solutions(load_solutions(LOG_DIR + "../run/Whole NCI-PID database translated into CADBIOM formalism(and)_SRP9_cam_complete.txt"),
                get_transitions(BIO_MOLDELS_DIR + "Whole NCI-PID database translated into CADBIOM formalism(and).bcx"))
    exit()


    #sort_solutions("/media/DATA/Projets/dyliss_tgf/cadbiom/run/Whole NCI-PID database translated into CADBIOM formalism(and)_SRP9_cam_complete.txt")
    #sort_solutions("/media/DATA/Projets/dyliss_tgf/cadbiom/run/Whole NCI-PID database translated into CADBIOM formalism(and)_SRP9_cam.txt")
    #sort_solutions("/media/DATA/Projets/dyliss_tgf/cadbiom/run/pid_and_clock_no_perm_p21corrected_start_SRP9_complete.txt")
    #exit()

#    cond = "((((not(ATF2_JUND_macroH2A_nucl))or(Fra1_JUND_active_nucl))or(Fra1_JUN_active_nucl))or(TCF4_betacatenin_active_nucl))or(JUN_FOS_active_nucl)"
#    ret = parse_condition(cond)
#    print(ret)
#    exit()

#    sol_steps = load_solutions(LOG_DIR + "../run/pid_and_clock_SRP9_cam_complete_o.txt")
#    g = [sol_step for sol_step in sol_steps]
#    print(g)
#    exit()

    sol_digging(load_solutions(LOG_DIR + "../run/pid_and_clock_no_perm_p21corrected_start_SRP9_complete.txt"),
             get_transitions(BIO_MOLDELS_DIR + "Whole NCI-PID database translated into CADBIOM formalism(and).bcx"))
    exit()

    sol_digging(load_solutions(LOG_DIR + "sols_new_solver.txt"),
             get_transitions(BIO_MOLDELS_DIR + "mini_test_publi.bcx"))
    exit()
    process_solutions(load_solutions(LOG_DIR + "sols_new_solver.txt"),
                      get_transitions(BIO_MOLDELS_DIR + "mini_test_publi.bcx"))
    exit()

#    build_graph(load_solutions(LOG_DIR + "../run/pid_and_clock_SRP9_cam_complete_o.txt"),
#                get_transitions(BIO_MOLDELS_DIR + "pid_and_clock.bcx"))
    process_solutions(load_solutions(LOG_DIR + "../run/pid_and_clock_SRP9_cam_complete.txt"),
                get_transitions(BIO_MOLDELS_DIR + "pid_and_clock.bcx"))
