try:
    # Python 3
    import tkinter as tk
except ImportError:
    # Python 2
    import Tkinter as tk

class NodeClass(tk.Canvas):
    def __init__(self, host_canvas, data, node_name):
        tk.Canvas.__init__(self, width=20, height=20, highlightthickness=0)
        self.config(bg='white')

        self._host_canvas = host_canvas
        self._complete = True
        self._marked = False
        self._default_bg = None

        self.bind('<ButtonPress-1>', self._host_event('onNodeButtonPress'))
        self.bind('<ButtonRelease-1>', self._host_event('onNodeButtonRelease'))
        self.bind('<B1-Motion>', self._host_event('onNodeMotion'))

        self.bind('<Button-3>', self._host_event('onItemRightClick'))

        self.bind('<Key>', self._host_event('onNodeKey'))
        self.bind('<Enter>', lambda e: self.focus_set())
        self.bind('<Leave>', lambda e: self.master.focus())

        # Draw myself
        self.render(data, node_name)

    def render(self, data, node_name):
        """Draw on canvas what we want node to look like"""
        self.create_oval(5,5,15,15, fill='red',outline='black')


    def mark(self):
        """Mark the token just so it's easy for the user to pick out"""
        if self._marked:
            self.config(bg=self._default_bg)
        else:
            self._default_bg = self['background']
            self.config(bg='yellow')
        self._marked = not self._marked

    def mark_complete(self):
        """Called by host canvas when all of my edges have been drawn"""
        if not self._complete:
            self._complete = True

    def mark_incomplete(self):
        """Called by host canvas when all of my edges have not been drawn"""
        if self._complete:
            self._complete = False

    @property
    def is_marked(self):
        return self._marked

    @property
    def is_complete(self):
        """Returns True if all edges have been drawn"""
        return self._complete

    def customize_menu(self, menu, item):
        """Ovewrite this method to customize the menu this token displays
        when it is right-clicked"""
        pass


    def _host_event(self, func_name):
        """Wrapper to correct the event's x,y coordinates and pass to host
        canvas.  Argument should be string of name of function from
        _host_canvas to call."""
        func = getattr(self._host_canvas, func_name)
        def _wrapper(event):
            # Modify even to be relative to the host's canvas
            event.x += self.winfo_x()
            event.y += self.winfo_y()
            return func(event)
        return _wrapper

    def __getstate__(self):
        """Because the token object is a live tk object, we must save our
        state variables and reconstruct ourselves instead of letting python
        try to automatically pickle us"""
        ans = {
            '_complete': self._complete,
            '_default_bg': self._default_bg,
            '_marked': self._marked,
        }
        return ans


    def _setstate(self, state):
        """Set state from pickle.  See __getstate__ for details. Not
        __setstate__ because this must be a live tk object to work and python
        could call __setstate__ on an "undead" object."""
        for k,v in state.items():
            setattr(self, k, v)

        # Make sure we display our marked status
        if state['_marked']:
            self._marked = False    # Have to undo what we did in for loop above
            self.mark()

class EdgeClass(object):
    def __init__(self, edge_data):
        """This object mimics a token for the edges.  All of this class's
        returned values are used to configure the actual line drawn
        on the host canvas"""
        self.edge_data = edge_data
        self._marked = False
        self._spline_id = None
        self._host_canvas = None

    def render(self, host_canvas, coords, cfg=None, directed=False):
        """Called whenever canvas is about to draw an edge.
        The host_canvas will be the GraphCanvas object.
        coords is a tuple of the following, use to display the spline which
                    represents the edge.
            - x1,y1 -- Position of the start of the spline
            - xa,ya -- Position of the midpoint of spline
            - x2,y2 -- Position of the end of teh spline
        """
        if cfg is None:
            cfg = self.render_cfg()
        # Amend config options to include options which must be included
        cfg['tags'] = 'edge'
        cfg['smooth'] = True
        if directed:
            # Add arrow
            cfg['arrow'] = tk.LAST
            cfg['arrowshape'] = (30,40,5)
        self._spline_id = host_canvas.create_line(*coords, **cfg)
        self._host_canvas = host_canvas

    def itemconfig(self, cfg=None):
        """Update item config for underlying spline.  If cfg is none,
        auto-regenerate cfg from render_cfg method"""
        if cfg is None:
            cfg = self.render_cfg()
        assert self._host_canvas is not None, "Must draw using render method first"
        self._host_canvas.itemconfig(self._spline_id, cfg)

    def coords(self, coords):
        """Update coordinates for spline."""
        assert self._host_canvas is not None, "Must draw using render method first"
        return self._host_canvas.coords(self._spline_id, coords)

    def delete(self):
        """Remove spline from canvas"""
        self._host_canvas.delete(self._spline_id)

    def render_cfg(self):
        """Creates  config dict used by host canvas's create_line
        method to draw the spline"""
        return {}

    @property
    def id(self):
        """Returns id of spline drawn on host canvas"""
        return self._spline_id

    def mark(self):
        """Return config dictionary when toggling mark status"""
        mark_width = 4.0

        self._marked = not self._marked
        cfg = {}
        if self._marked:
            cfg = {'width': mark_width}
        else:
            cfg = {'width': 1.0}

        self.itemconfig(cfg)

    @property
    def is_marked(self):
        return self._marked

    def customize_menu(self, menu):
        """Ovewrite this method to customize the menu this token displays
        when it is right-clicked"""
        pass

    def __getstate__(self):
        """Because the token object is a live tk object, we must save our
        state variables and reconstruct ourselves instead of letting python
        try to automatically pickle us"""
        ans = {
            '_marked': self._marked,
        }
        return ans

    def _setstate(self, state):
        """Set state from pickle.  See __getstate__ for details. Not
        __setstate__ because this must be a live tk object to work and python
        could call __setstate__ on an "undead" object."""
        for k,v in state.items():
            setattr(self, k, v)

        # Make sure we display our marked status
        if state['_marked']:
            self._marked = False    # Have to undo what we did in for loop above
            self.mark()


class TkPassthroughNodeClass(NodeClass):
    def __init__(self, *args, **kwargs):
        self._default_label_color = 'black'
        self._default_outline_color = 'black'

        NodeStyle.__init__(self, *args, **kwargs)


    def render(self, data, node_name):
        """Draw on canvas what we want node to look like.  If data contains
        keys that can configure a tk.Canvas oval, it will do so.  If data
        contains keys that start with "label_" and can configure a text
        object, it will configure the text."""

        # Take a first cut at creating the marker and label
        self.label = self.create_text(0, 0, text=node_name)
        self.marker = self.create_oval(0, 0, 10, 10,
                                       fill='red',outline='black')

        # Modify marker using options from data
        cfg = self.itemconfig(self.marker)
        for k,v in cfg.copy().items():
            cfg[k] = data.get(k, cfg[k][-1])
        self.itemconfig(self.marker, **cfg)
        self._default_outline_color = data.get('outline',self._default_outline_color)

        # Modify the text label using options from data
        cfg = self.itemconfig(self.label)
        for k,v in cfg.copy().items():
            cfg[k] = data.get('label_'+k, cfg[k][-1])
        self.itemconfig(self.label, **cfg)
        self._default_label_color = data.get('label_fill',self._default_label_color)

        # Figure out how big we really need to be
        bbox = self.bbox(self.label)
        bbox = [abs(x) for x in bbox]
        br = ( max((bbox[0] + bbox[2]),20), max((bbox[1]+bbox[3]),20) )

        self.config(width=br[0], height=br[1]+7)

        # Place label and marker
        mid = ( int(br[0]/2.0), int(br[1]/2.0)+7 )
        self.coords(self.label, mid)
        self.coords(self.marker, mid[0]-5,0, mid[0]+5,10)


    def mark_complete(self):
        """Called by host canvas when all of my edges have been drawn"""
        self._complete = True
        self.itemconfig(self.marker, outline=self._default_outline_color)
        self.itemconfig(self.label, fill=self._default_label_color)

    def mark_incomplete(self):
        """Called by host canvas when all of my edges have not been drawn"""
        self._complete = False
        self.itemconfig(self.marker, outline='')
        self.itemconfig(self.label, fill='grey')


class TkPassthroughEdgeClass(EdgeClass):
    _tk_line_options = [
        'stipple', 'activefill', 'joinstyle', 'dash',
        'disabledwidth', 'dashoffset', 'activewidth', 'fill', 'splinesteps',
        'offset', 'disabledfill', 'disableddash', 'width', 'state',
        'disabledstipple', 'activedash', 'tags', 'activestipple',
        'capstyle', 'arrowshape', 'smooth', 'arrow'
    ]
    _marked_width = 4.0

    def render_cfg(self):
        """Called whenever canvas is about to draw an edge.
        Must return dictionary of config options for create_line"""
        cfg = {}
        for k in self._tk_line_options:
            v = self.edge_data.get(k, None)
            if v:
                cfg[k] = v
        self._native_width = cfg.get('width', 1.0)
        return cfg

    def mark(self):
        """Return config dictionary when toggling marked status"""
        self._marked = not self._marked

        cfg = {}
        if self._marked:
            cfg = {'width': self._marked_width}
        else:
            cfg = {'width': self._native_width}

        self.itemconfig(cfg)
