# -*- coding: utf-8 -*-
## Filename    : chart_controler.py
## Author(s)   : Michel Le Borgne
## Created     : 4/3/2010
## Revision    :
## Source      :
##
## Copyright 2012 - 2020 IRISA/IRSET
##
## This library 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 2.1 of the License, or
## any later version.
##
## This library 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.  The software and
## documentation provided hereunder is on an "as is" basis, and IRISA has
## no obligations to provide maintenance, support, updates, enhancements
## or modifications.
## In no event shall IRISA be liable to any party for direct, indirect,
## special, incidental or consequential damages, including lost profits,
## arising out of the use of this software and its documentation, even if
## IRISA have been advised of the possibility of such damage.  See
## the GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this library; if not, write to the Free Software Foundation,
## Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
##
## The original code contained here was initially developed by:
##
##     Michel Le Borgne.
##     IRISA
##     Symbiose team
##     IRISA  Campus de Beaulieu
##     35042 RENNES Cedex, FRANCE
##
##
##     http:
##     mailto:
##
## Contributor(s): Geoffroy Andrieux, Nolwenn Le Meur
##
"""Main GUI controllers + auxiliary class (ChartClipboard)
- :class:`ChartClipboard`: A clipboard to handle the copy of nodes through models
- :class:`ChartControler`: A controler for graphical views
- :class:`NavControler`: A controler for navigation view (overview section in the GUI)
"""
# Standard imports
from __future__ import print_function
import itertools as it
from string import ascii_uppercase
from math import sqrt
import re
# Custom imports
import gtk
from gtk.gdk import (
    Cursor,
    ARROW,
    BOTTOM_LEFT_CORNER,
    BOTTOM_RIGHT_CORNER,
    TOP_LEFT_CORNER,
    TOP_RIGHT_CORNER,
    LINE_ON_OFF_DASH,
)
# Cadbiom imports
from cadbiom_gui.gt_gui.graphics.drawing_style import Arrow
from cadbiom.models.guard_transitions.chart_model import CMacroNode, CNode, CSimpleNode
from cadbiom import commons as cm
LOGGER = cm.logger()
[docs]class ChartClipboard(object):
    """A clipboard used by ChartControler to handle the copy of nodes through models.
    """
    def __init__(self):
        self.clip_node = None
[docs]    def put_node(self, node):
        """register a chart model node in the clipboard"""
        self.clip_node = node 
[docs]    def get_node(self):
        """retrieve the current node in the clipboard"""
        node = self.clip_node
        self.clip_node = None
        return node 
[docs]    def has_element(self):
        """test if the clipboard has a registered node"""
        return self.clip_node is not None 
[docs]    def has_macro(self):
        """test if the clipboard has a registered macro node"""
        return self.clip_node.is_macro() if self.clip_node else False  
[docs]class ChartControler(object):
    """Implement a controler for graphical views
    Used by::
        - the main graph editor widget as this
        - NavControler for the overview widget through inheritance
    Signals::
        - current_change: Inform CharterInfo that the currently selected transition,
        model or node has changed. Used to dispatch related information in the GUI.
        - edit_node: Inform Charter that a MacroNode will be edited.
    TODO: just show metadata on double click on another item
    => add a signal to inform CharterInfo objects
    :param model: Current chart model
    :param mouse_role: Mouse action "select", "resizing", "moving"
    :param current_node: Currently selected node
    :param current_node_center: Tuple of coordinates (x, y) of the current node
    :param current_handle: Id of the node corner currently selected for resizing
        (only for MacroNodes)
    :param current_transition: Currently selected transition
    :param m_vscreen_coord: Mouse virtual screen coordinates (xloc, yloc)
    :param lastx: coordinates of last click in view
    :param lasty: coordinates of last click in view
    :param clipboard: Clipboard object used to copy nodes through models
    :param drawing_style:
    :param signal_dict: Structure to assign observers to each signal of ChartControler
    :param gen_name: Generator of unique names for new nodes
    :param node_copy_count: Generator of ints used in the naming of copied nodes
    :type model: <ChartModel>
    :type mouse_role: <str>
    :type current_node: <CNode>
    :type current_node_center: <tuple <float>,<float>>
    :type current_handle: <int>
    :type current_transition: <CTransition>
    :type m_vscreen_coord: <tuple <float>,<float>>
    :type lastx: <float>
    :type lasty: <float>
    :type clipboard: <ChartClipboard>
    :type drawing_style:
    :type signal_dict: <dict <str>:<list>>
    :type gen_name: <generator>
    :type node_copy_count: <generator>
    """
    cursors = []
    cursors.append(Cursor(ARROW))
    cursors.append(Cursor(TOP_LEFT_CORNER))
    cursors.append(Cursor(TOP_RIGHT_CORNER))
    cursors.append(Cursor(BOTTOM_RIGHT_CORNER))
    cursors.append(Cursor(BOTTOM_LEFT_CORNER))
    LSIGNALS = ["current_change", "edit_node"]
    def __init__(self, model, clipboard):
        self.model = model
        # mouse role
        self.mouse_role = "select"
        # Set default node
        self.current_node = self.model.get_root()
        # Note: current_node_center should be initialized to None
        # only if current_node is also None...
        self.current_node_center = None
        self.current_handle = 0
        self.current_transition = None
        # coordinate of last click in virtual screen 1x1
        self.m_vscreen_coord = None
        # coordinates of last click in view
        self.lastx = 0
        self.lasty = 0
        self.clipboard = clipboard
        self.drawing_style = None
        self.signal_dict = {signal: list() for signal in self.LSIGNALS}
        # Generator for the auto-naming of new nodes
        self.gen_name = self.nodes_names_generator()
        # Generator for the auto-naming of copied nodes
        self.node_copy_count = it.count(1)
[docs]    def attach(self, signal, obs):
        """Register an observer for the given signal
        :param signal: Name of the signal ("current_change" or "edit_node")
        :param obs: The observer
        :type signal: <str>
        :type obs: <CharterInfo> or <Charter>
        """
        lobs = self.signal_dict[signal]
        if not obs in lobs:
            lobs.append(obs) 
[docs]    def detach(self, signal, obs):
        """Remove an observer for the given signal
        :param signal: Name of the signal ("current_change" or "edit_node")
        :param obs: The observer
        :type signal: <str>
        :type obs: <CharterInfo> or <Charter>
        """
        self.signal_dict[signal].remove(obs) 
[docs]    def notify(self, signal):
        """Emit a signal subsequently to a mouse action
        - current_change: Tell CharterInfo that the current selection has changed
        - edit_node: Tell Charter that the current MacroNode will be edited
        :param signal: name of the signal
        :type signal: <str>
        """
        LOGGER.debug("ChartControler notify signal: %s to %s", signal, self.signal_dict[signal])
        lobs = self.signal_dict[signal]
        if signal == "current_change":
            # Tell CharterInfo that the selection has changed
            for obs in lobs:
                obs.update(self.current_node, self.current_transition)
        elif signal == "edit_node":
            # Tell Charter that the MacroNode will be edited
            for obs in lobs:
                obs.update(self.current_node)
        else:
            raise Exception("ChartControler: Unknown signal") 
[docs]    def set_view(self, view):
        """
        As it says
        """
        self.view = view 
[docs]    def set_mouse_role(self, role):
        """
        As it says
        """
        self.mouse_role = role 
[docs]    def remove_node_or_transition(self, item):
        """Remove the given transition or the given node
        Called by Charter object when Delete key is pressed, or when the Remove
        option is selected on the the context-menu hover an item on the DrawingArea.
        :param item:
        :type item: <CTransition> or <CNode> but not <CTopNode>
        """
        # Remove the item
        item.remove()
        if not isinstance(item, CNode):
            # Transition
            self.current_transition = None
            # TODO: Don't know why, but the deletion of transitions requires
            # a notification to the model (this is not the case for nodes)
            self.model.notify()
        # Reset selection
        self.current_node = None
        self.current_node_center = None
        # Current handle is now the TopNode
        self.current_handle = 0
        # Refresh ChartInfo in order to display actualized data about the model
        self.notify("current_change") 
[docs]    def new_node(self, node_type):
        """Creation of a new node
        Called during the :meth:`on_button_release` callback.
        @param node_type: type of the node (string)
        """
        xnode = self.m_vscreen_coord[0]
        ynode = self.m_vscreen_coord[1]
        if node_type == "simple":
            self.current_node = self.current_node.add_simple_node(
                next(self.gen_name), xnode, ynode
            )
        elif node_type == "macro":
            self.current_node = self.current_node.add_macro_subnode(
                next(self.gen_name), xnode, ynode, 0.25, 0.25
            )
        elif node_type == "start":
            self.current_node = self.current_node.add_start_node(xnode, ynode)
        elif node_type == "trap":
            self.current_node = self.current_node.add_trap_node(xnode, ynode)
        elif node_type == "perm":
            self.current_node = self.current_node.add_perm_node(
                next(self.gen_name), xnode, ynode
            )
        elif node_type == "input":
            self.current_node = self.current_node.add_input_node(
                next(self.gen_name), xnode, ynode
            )
        elif node_type == "env":
            self.current_node = self.current_node.add_env_node(
                next(self.gen_name), xnode, ynode, 0.25, 0.25
            )
        else:  # bug!
            raise TypeError("new_node: UNKNOWN TYPE: %s" % node_type)
        self.current_transition = None
        self.current_handle = 0
        self.m_vscreen_coord = (0.0, 0.0)
        self.notify("current_change")
        self.view.window.set_cursor(ChartControler.cursors[0])
        self.mouse_role = "select" 
[docs]    def nodes_names_generator(self):
        """Return a generator of names for new nodes.
        Names are generated in lexicographic order and we try to avoid names
        that are already in the model (this is important in order to avoid
        overwriting of nodes).
        """
        for size in it.count(start=1):
            for tpl in it.combinations(ascii_uppercase, size):
                name = "".join(tpl)
                if name in self.model.node_dict:
                    continue
                yield name 
[docs]    def new_transition(self, xmo, ymo):
        """Create a new transition
        Called during the :meth:`on_button_release` callback.
        @param xmo, ymo: int mouse screen coordinates
        """
        # are we in a node?
        xloc = xmo / self.view.draw_width
        yloc = ymo / self.view.draw_height
        #        w_coef = 1.0/self.view.draw_width
        #        h_coef = 1.0/self.view.draw_height
        (node, handle, center, trans) = self.model.find_element(
            (xloc, yloc), self.drawing_style
        )
        if not node:  # no extremity (never happens)
            self.view.window.set_cursor(ChartControler.cursors[0])
            self.mouse_role = "select"
            self.model.notify()  # rubout the draft transition
            return
        # are we in same container
        if self.current_node.father != node.father:
            self.view.window.set_cursor(ChartControler.cursors[0])
            self.mouse_role = "select"
            self.model.notify()  # rubout the draft transition
            return
        # we are in extremity node - current node is origin node
        # is origin node correct and extremity node correct
        if self.current_node.is_for_origin() and node.is_for_extremity():
            trans = self.current_node.father.add_transition(self.current_node, node)
            if trans:
                self.current_node.selected = False
                self.current_node = None
                self.m_vscreen_coord = (0.0, 0.0)
                self.current_transition = trans
                trans.selected = True
                self.notify("current_change")
                self.model.notify()
                self.view.window.set_cursor(ChartControler.cursors[0])
                self.mouse_role = "select"
            else:
                self.view.window.set_cursor(ChartControler.cursors[0])
                self.mouse_role = "select"
                self.model.notify()  # rubout the draft transition
        else:
            self.view.window.set_cursor(ChartControler.cursors[0])
            self.mouse_role = "select"
            if self.current_node:
                self.current_node.selected = False
            self.model.notify()  # rubout the draft transition
            return 
[docs]    def on_motion_notify(self, widget, event):
        """Callback moving the cursor in the DrawingArea
        (in the ChartView object)
        :param widget: gtk DrawingArea that emit the event
        :param event: gdk mouse event with attributes (x, y)
        :type widget: <NavView> or <ChartView>
        :type event: <gtk.gdk.Event>
        """
        swi = self.view.draw_width
        she = self.view.draw_height
        # transform mouse coordinates to virtual screen 1.0 x 1.0
        xvirt = event.x / swi
        yvirt = event.y / she
        if self.mouse_role == "select":
            (node, handle, center, trans) = self.model.find_element(
                (xvirt, yvirt), self.drawing_style
            )
            if node == self.model.get_root():
                handle = 0
            # detect a new handle
            if handle != self.current_handle:
                self.current_handle = handle
                self.view.window.set_cursor(ChartControler.cursors[handle])
        elif self.mouse_role == "resizing":
            self.current_node.resize(
                xvirt, yvirt, self.current_handle, swi, she, self.model.get_root()
            )
        elif self.mouse_role == "moving":
            v_dx = xvirt - self.m_vscreen_coord[0]
            v_dy = yvirt - self.m_vscreen_coord[1]
            v_size = self.current_node.accept(self.drawing_style)
            self.current_node.move(v_dx, v_dy, v_size, self.model.get_root())
            self.m_vscreen_coord = (xvirt, yvirt)
        elif self.mouse_role == "new_trans":
            if self.current_node:
                self.draft_transition(event.x, event.y) 
[docs]    def draft_transition(self, xmo, ymo):
        """
        draw a transition arrow in dotted line
        @param xmo,ymo: mouse coordinates
        """
        view = self.view
        self.model.notify()
        xr1 = int(self.current_node_center[0] * self.view.draw_width)
        yr1 = int(self.current_node_center[1] * self.view.draw_height)
        # graphic context
        pixmap = view.pixmap
        grc = view.window.new_gc()
        grc.set_line_attributes(1, LINE_ON_OFF_DASH, 0, 0)
        # line
        pixmap.draw_line(grc, xr1, yr1, int(xmo), int(ymo))
        # arrow
        unx = xmo - xr1
        uny = ymo - yr1
        norm = sqrt(unx ** 2 + uny ** 2)
        if norm != 0:
            unx = unx / norm
            uny = uny / norm
            arr = Arrow()
            arr.draw(view, (unx, uny), (xmo, ymo))
        grc = view.window.new_gc()
        view.window.draw_drawable(
            grc, view.pixmap, 0, 0, 0, 0, view.draw_width, view.draw_height
        ) 
    ## API for communication with charter ######################################
[docs]    def set_action_select(self, widget):
        """
        As it says
        """
        self.mouse_role = "select" 
[docs]    def set_action_new_simple_node(self, widget):
        """
        As it says
        """
        self.mouse_role = "simple" 
[docs]    def set_action_new_macro_node(self, widget):
        """
        As it says
        """
        self.mouse_role = "macro" 
[docs]    def set_action_new_start_node(self, widget):
        """
        As it says
        """
        self.mouse_role = "start" 
[docs]    def set_action_new_trap_node(self, widget):
        """
        As it says
        """
        self.mouse_role = "trap" 
[docs]    def set_action_new_transition(self, widget):
        """
        As it says
        """
        if self.current_node:
            self.current_node.selected = False
        self.current_node = None
        self.current_handle = 0
        self.m_vscreen_coord = None
        if self.current_transition:
            self.current_transition.selected = False
        self.current_transition = None
        self.mouse_role = "new_trans" 
[docs]    def set_action_new_perm_node(self, widget):
        """
        As it says
        """
        self.mouse_role = "perm"  
[docs]class NavControler(object):
    """Controler for navigation view (overview section in the GUI)
    Implement a controler for navigation views
    """
    def __init__(self):
        self.lastx = 0  # coordinates of last click in view
        self.lasty = 0
        self.in_ret = False
        self.obs = []
[docs]    def set_view(self, view):
        """
        As it says
        """
        # attribute view is assigned when a view is created
        self.view = view 
[docs]    def attach(self, obs):
        """
        observer management
        """
        if not obs in self.obs:
            self.obs.append(obs) 
[docs]    def detach(self, obs):
        """
        observer management
        """
        self.obs.remove(obs) 
[docs]    def notify(self, depx, depy):
        """
        observer management
        """
        for obs in self.obs:
            obs.update(depx, depy) 
[docs]    def on_motion_notify(self, widget, event):
        """
        callback
        """
        if self.in_ret:
            depx = event.x - self.lastx
            depy = event.y - self.lasty
            alloc = widget.get_allocation()
            # limits
            vv1 = self.view.x_ret + depx + self.view.w_ret
            vv2 = self.view.x_ret + depx
            if (vv1 > alloc.width) or (vv2 < 0):
                depx = 0
            vv1 = self.view.y_ret + depy + self.view.h_ret
            vv2 = self.view.y_ret + depy
            if (vv1 > alloc.height) or (vv2 < 0):
                depy = 0
            self.lastx = self.lastx + depx
            self.lasty = self.lasty + depy
            # reticule move
            if (not depx == 0) or (not depy == 0):
                depx = float(depx) / alloc.width
                depy = float(depy) / alloc.height
                self.notify(depx, depy)