import os
import matplotlib
from asap.logging import asaplog, asaplog_post_dec

######################################
##    Notation box window           ##
######################################
class NotationWindowCommon:
    """
    A base class to define the functions that the backend-based
    GUI notation class must implement to print/modify/delete notes on a canvas.

    The following methods *must* be implemented in the backend-based
    parent class:
        _get_note : get text in text box
        _get_anchval : get anchor value selected
    """
    def __init__(self,master=None):
        #self.parent = master
        self.canvas = master
        self.event = None
        self.note = None
        self.anchors = ["figure","axes","data"]
        self.seltext = {}
        self.numnote = 0

    @asaplog_post_dec
    def print_text(self):
        """
        Print a note on a canvas specified with the Notation window.
        Called when 'print' button selected on the window.
        """
        anchor = self.anchors[self._get_anchval()]
        notestr = self._get_note().rstrip("\n")
        if len(notestr.strip()) == 0:
            #self._clear_textbox()
            #print "Empty string!"
            return

        myaxes = None
        calcpos = True
        xpos = None
        ypos = None
        if self.seltext:
            # You are modifying a text
            mycanvas = self.canvas
            oldanch = self.seltext['anchor']
            if oldanch != 'figure':
                myaxes = self.seltext['parent']
            calcpos = (anchor != oldanch)
            if not calcpos:
                # printing text in the same coord.
                # you don't have to recalc position
                parent = self.seltext['parent']
                transform = self.seltext['textobj'].get_transform()
                (xpos, ypos) = self.seltext['textobj'].get_position()
            elif anchor == "figure":
                # converting from "axes"/"data" -> "figure"
                (x, y) = self.seltext['textobj']._get_xy_display()
            elif oldanch == "data":
                # converting from "data" -> "axes".
                # need xdata & ydata in the axes
                (x, y) = self.seltext['textobj'].get_position()
            else:
                # converting "figure"/"axes" -> "data"
                # need to calculate xdata & ydata in the axes
                pixpos = self.seltext['textobj']._get_xy_display()
                (w,h) = mycanvas.get_width_height()
                relpos = (pixpos[0]/float(w), pixpos[1]/float(h))
                if not myaxes:
                    myaxes = self._get_axes_from_pos(relpos,mycanvas)
                    if not myaxes:
                        raise RuntimeError, "Axes resolution failed!"
                (x, y) = self._convert_pix2dat(relpos,myaxes)
            self._remove_seltext()
        elif self.event:
            mycanvas = self.event.canvas
            myaxes = self.event.inaxes
            if myaxes and (anchor != "figure"):
                x = self.event.xdata
                y = self.event.ydata
            else:
                x = self.event.x
                y = self.event.y
        else:
            raise RuntimeError, "No valid position to print data"
            return

        # now you know 
        picker = True
        # alignment of the text: ha (horizontal), va (vertical)
        ha = 'left'
        #va = 'center'
        va = 'top'
        if not calcpos:
            # you aready know parent, tansform, xpos and ypos
            pass
        elif anchor == "figure":
            # text instance will be appended to mycanvas.figure.texts
            parent = mycanvas.figure
            transform = parent.transFigure
            (w,h) = mycanvas.get_width_height()
            xpos = x/float(w)
            ypos = y/float(h)            
        elif myaxes:
            ## text instance will be appended to myaxes.texts
            parent = myaxes
            if anchor == "axes":
                transform = myaxes.transAxes
                lims = myaxes.get_xlim()
                xpos = (x-lims[0])/(lims[1]-lims[0])
                lims = myaxes.get_ylim()
                ypos = (y-lims[0])/(lims[1]-lims[0])
            else:
                # anchored on "data"
                transform = myaxes.transData
                xpos = x
                ypos = y
        parent.text(xpos,ypos,notestr,transform=transform,
                    ha=ha,va=va,picker=picker)
        mycanvas.draw()

        self.numnote += 1

        #self._clear_textbox()
        msg = "Added note: '"+notestr+"'"
        msg += " @["+str(xpos)+", "+str(ypos)+"] ("+anchor+"-coord)"
        msg += "\ntotal number of notes are "+str(self.numnote)
        asaplog.push( msg )

    def _get_axes_from_pos(self,pos,canvas):
        """helper function to get axes of a position in a plot (fig-coord)"""
        if len(pos) != 2:
            raise ValueError, "pixel position should have 2 elements"
        for axes in canvas.figure.axes:
            ##check if pos is in the axes
            #if axes.contains_point(pos): ### seems not working
            #    return axes
            try:
                axbox = axes.get_position().get_points()
            except AttributeError: ### WORKAROUND for old matplotlib
                axbox = self._oldpos2new(axes.get_position())
            if (axbox[0][0] <= pos[0] <= axbox[1][0]) and \
               (axbox[0][1] <= pos[1] <= axbox[1][1]):
                return axes
        return None

    ### WORKAROUND for old matplotlib
    def _oldpos2new(self,oldpos=None):
        return [[oldpos[0],oldpos[1]],[oldpos[0]+oldpos[2],oldpos[1]+oldpos[3]]]
        
    def _convert_pix2dat(self,pos,axes):
        """
        helper function to convert a position in figure-coord (0-1) to
        data-coordinate of the axes        
        """
        # convert a relative position from lower-left of the canvas
        # to a data in axes
        if len(pos) != 2:
            raise ValueError, "pixel position should have 2 elements"
        # left-/bottom-pixel, and pixel width & height of the axes
        bbox = axes.get_position()
        try: 
            axpos = bbox.get_points()
        except AttributeError: ### WORKAROUND for old matplotlib
            axpos = self._oldpos2new(bbox)
        # check pos value
        if (pos[0] < axpos[0][0]) or (pos[1] < axpos[0][1]) \
               or (pos[0] > axpos[1][0]) or (pos[1] > axpos[1][1]):
            raise ValueError, "The position is out of the axes"
        xlims = axes.get_xlim()
        ylims = axes.get_ylim()
        wdat = xlims[1] - xlims[0]
        hdat = ylims[1] - ylims[0]
        xdat = xlims[0] + wdat*(pos[0] - axpos[0][0])/(axpos[1][0] - axpos[0][0])
        ydat = ylims[0] + hdat*(pos[1] - axpos[0][1])/(axpos[1][1] - axpos[0][1])
        return (xdat, ydat)

    @asaplog_post_dec
    def _get_selected_text(self,event):
        """helper function to return a dictionary of the nearest note to the event."""
        (w,h) = event.canvas.get_width_height()
        dist2 = w*w+h*h
        selected = {}
        for textobj in self.canvas.figure.texts:
            if textobj.contains(event)[0]:
                d2 = self._get_text_dist2(event,textobj)
                if dist2 >= d2:
                    dist2 = d2
                    selected = {'anchor': 'figure', \
                                'parent': event.canvas.figure, 'textobj': textobj}
                    msg = "Fig loop: a text, '"+textobj.get_text()+"', at "
                    msg += str(textobj.get_position())+" detected"
                    #print msg
        for ax in self.canvas.figure.axes:
            for textobj in ax.texts:
                if textobj.contains(event)[0]:
                    d2 = self._get_text_dist2(event,textobj)
                    if dist2 >= d2:
                        anchor='axes'
                        if ax.transData == textobj.get_transform():
                            anchor = 'data'                    
                        selected = {'anchor': anchor, \
                                    'parent': ax, 'textobj': textobj}
                        msg = "Ax loop: a text, '"+textobj.get_text()+"', at "
                        msg += str(textobj.get_position())+" detected"
                        #print msg 

        if selected:
            msg = "Selected (modify/delete): '"+selected['textobj'].get_text()
            msg += "' @"+str(selected['textobj'].get_position())
            msg += " ("+selected['anchor']+"-coord)"
            asaplog.push(msg)

        return selected

    def _get_text_dist2(self,event,textobj):
        """
        helper function to calculate square of distance between
        a event position and a text object. 
        """
        (x,y) = textobj._get_xy_display()
        return (x-event.x)**2+(y-event.y)**2

    def delete_note(self):
        """
        Remove selected note.
        """
        #print "You selected 'OK'"
        self._remove_seltext()
        self.canvas.draw()

    @asaplog_post_dec
    def _remove_seltext(self):
        """helper function to remove the selected note"""
        if len(self.seltext) < 3:
            raise ValueError, "Don't under stand selected text obj."
            return
        try:
            self.seltext['textobj'].remove()
        except NotImplementedError:
                self.seltext['parent'].texts.pop(self.seltext['parent'].texts.index(self.seltext['textobj']))
        self.numnote -= 1

        textobj = self.seltext['textobj']
        msg = "Deleted note: '"+textobj.get_text()+"'"
        msg += "@"+str(textobj.get_position())\
               +" ("+self.seltext['anchor']+"-coord)"
        msg += "\ntotal number of notes are "+str(self.numnote)
        asaplog.push( msg )

        self.seltext = {}

    @asaplog_post_dec
    def cancel_delete(self):
        """
        Cancel deleting the selected note.
        Called when 'cancel' button selected on confirmation dialog.
        """
        asaplog.push( "Cancel deleting: '"+self.seltext['textobj'].get_text()+"'" )
        self.seltext = {}


#####################################
##    Backend dependent Classes    ##
#####################################
### TkAgg
if matplotlib.get_backend() == 'TkAgg':
    import Tkinter as Tk
    import tkMessageBox

class NotationWindowTkAgg(NotationWindowCommon):
    """
    Backend based class to create widgets to add, modify, or delete
    note on the plot.

    Note:
    Press LEFT-mouse button on the plot to ADD a note on the canvas.
    A notation window will be loaded for specifying note string and
    anchor. The note will be anchored on a position in whether figure-
    (0-1 relative in a figure), panel- (0-1 relative in a plot axes),
    or data-coordinate (data value in a plot axes).
    Press RIGHT-mouse button on a note to MODIFY/DELETE it. A cascade
    menu will be displayed and you can select an operation.
    """
    def __init__(self,master=None):
        self.parent = master._tkcanvas
        NotationWindowCommon.__init__(self,master=master)
        self.anchval = None
        self.textwin = self._create_textwindow(master=None)
        self.menu = self._create_modmenu(master=self.parent)

    ### Notation window widget
    def _create_textwindow(self,master=None):
        """Create notation window widget and iconfy it"""
        twin = Tk.Toplevel(padx=3,pady=3)
        twin.title("Notation")
        twin.resizable(width=True,height=True)
        self.textbox = self._NotationBox(parent=twin)
        self.radio = self._AnchorRadio(parent=twin)
        self.actionbs = self._ActionButtons(parent=twin)
        
        self.textbox.pack(side=Tk.TOP,fill=Tk.BOTH,expand=True)
        self.actionbs.pack(side=Tk.BOTTOM)
        self.radio.pack(side=Tk.BOTTOM)
        #twin.deiconify()
        #twin.minsize(width=twin.winfo_width(),height=twin.winfo_height())
        #twin.lift()
        twin.withdraw()
        return twin

    def _NotationBox(self,parent=None):
        textbox = Tk.Text(master=parent,background='white',
                          height=2,width=20,cursor="xterm",
                          padx=2,pady=2,undo=True,maxundo=10,
                          relief='sunken',borderwidth=3,
                          state=Tk.NORMAL,takefocus=1)
        return textbox

    def _AnchorRadio(self,parent=None):
        radio = Tk.LabelFrame(master=parent,text="anchor",
                            labelanchor="nw",padx=5,pady=3)
        self.anchval = Tk.IntVar(master=radio,value=0)
        self.rFig = self._NewRadioButton(radio,"figure",state=Tk.NORMAL,
                                         variable=self.anchval,value=0,
                                         side=Tk.LEFT)
        self.rAxis = self._NewRadioButton(radio,"panel",state=Tk.DISABLED,
                                          variable=self.anchval,value=1,
                                          side=Tk.LEFT)
        self.rData = self._NewRadioButton(radio,"data",state=Tk.DISABLED,
                                          variable=self.anchval,value=2,
                                          side=Tk.LEFT)
        # set initial selection "figure"
        self.anchval.set(0)
        return radio

    def _NewRadioButton(self,parent,text,state=Tk.NORMAL,variable=None,value=None,side=Tk.LEFT):
        rb = Tk.Radiobutton(master=parent,text=text,state=state,
                          variable=variable,value=value)
        rb.pack(side=side)
        return rb

    def _enable_radio(self):
        """Enable 'panel' and 'data' radio button"""
        self.rAxis.config(state=Tk.NORMAL)
        self.rData.config(state=Tk.NORMAL)
        #self.rFig.config(state=Tk.NORMAL)
        self.rFig.select()

    def _reset_radio(self):
        """Disable 'panel' and 'data' radio button"""
        self.rAxis.config(state=Tk.DISABLED)
        self.rData.config(state=Tk.DISABLED)
        self.rFig.config(state=Tk.NORMAL)
        self.rFig.select()

    def _select_radio(self,selection):
        """Select a specified radio button"""
        if not selection in self.anchors:
            return
        if selection == "data":
            self.rData.select()
        elif selection == "axes":
            self.rAxis.select()
        else:
            self.rFig.select()

    def _get_anchval(self):
        """Returns a integer of a selected radio button"""
        return self.anchval.get()

    def _get_note(self):
        """Returns a note string specified in the text box"""
        return self.textbox.get("1.0",Tk.END)

    def _clear_textbox(self):
        """Clear the text box"""
        self.textbox.delete("1.0",Tk.END)

    def _set_note(self,note=None):
        """Set a note string to the text box"""
        self._clear_textbox()
        if len(note) >0:
            self.textbox.insert("1.0",note)

    def _ActionButtons(self,parent=None):
        actbuts = Tk.Frame(master=parent)
        bCancel = self._NewButton(actbuts,"cancel",self._cancel_text,side=Tk.LEFT)
        bPrint = self._NewButton(actbuts,"print", self._print_text,side=Tk.LEFT)
        return actbuts

    def _NewButton(self, parent, text, command, side=Tk.LEFT):
        if(os.uname()[0] == 'Darwin'):
            b = Tk.Button(master=parent, text=text, command=command)
        else:
            b = Tk.Button(master=parent, text=text, padx=2, pady=2, command=command)
        b.pack(side=side)
        return b

    def _cancel_text(self):
        """
        Cancel adding/modifying a note and close notaion window.
        called when 'cancel' is selected.
        """
        self.close_textwindow()

    def _print_text(self):
        """
        Add/Modify a note. Called when 'print' is selected on the
        notation window.
        """
        self.print_text()
        self.close_textwindow()

    def load_textwindow(self,event):
        """
        Load text window at a event position to add a note on a plot.
        Parameter:
            event:   an even object to specify the position to load
                     text window. 
        """
        self.close_modmenu()
        if event.canvas._tkcanvas != self.parent:
            raise RuntimeError, "Got invalid event!"
        
        self.event = event
        is_ax = (event.inaxes != None)
        (xpix, ypix) = self._disppix2screen(event.x, event.y)
        offset = 5
        self.show_textwindow(xpix+offset,ypix+offset,enableaxes=is_ax)
        
    def show_textwindow(self,xpix,ypix,basetext=None,enableaxes=False):
        """
        Load text window at a position of screen to add a note on a plot.
        Parameters:
            xpix, ypix:   a pixel position from Upper-left corner
                          of the screen.
            basetext:     None (default) or any string.
                          A string to be printed on text box when loaded. 
            enable axes:  False (default) or True.
                          If True, 'panel' & 'data' radio button is enabled. 
        """
        if not self.textwin: return
        self._reset_radio()
        if enableaxes: 
            self._enable_radio()
        self.textwin.deiconify()
        (w,h) = self.textwin.minsize()
        if w*h <= 1:
            self.textwin.minsize(width=self.textwin.winfo_width(),
                                 height=self.textwin.winfo_height())
            (w,h) = self.textwin.minsize()
        self.textwin.geometry("%sx%s+%s+%s"%(w,h,xpix,ypix))
        self.textwin.lift()

    def close_textwindow(self):
        """Close text window."""
        self.seltext = {}
        self._reset_radio()
        self._clear_textbox()
        self.textwin.withdraw()


    ### Modify/Delete menu widget
    def _create_modmenu(self,master=None):
        """Create modify/delete menu widget"""
        if master:
            self.parent = master
        if not self.parent:
            return False
        menu = Tk.Menu(master=self.parent,tearoff=False)
        menu.add_command(label="Modify",command=self._modify_note)
        menu.add_command(label="Delete",command=self._delnote_dialog)
        return menu

    def load_modmenu(self,event):
        """
        Load cascade menu at a event position to modify or delete
        selected text.
        Parameter:
            event:  an even object to specify the position to load
                    text window. 
        """
        self.close_textwindow()
        self.seltext = self._get_selected_text(event)
        if len(self.seltext) == 3:
            tkcanvas = event.canvas._tkcanvas
            xpos = tkcanvas.winfo_rootx() + int(event.x)
            ypos = tkcanvas.winfo_rooty() \
                   + tkcanvas.winfo_height() - int(event.y)
            self.menu.post(xpos,ypos)

    def close_modmenu(self):
        """Close cascade menu."""
        self.seltext = {}
        self.menu.unpost()

    ### load text window for modification 
    def _modify_note(self):
        """helper function to load text window to modify selected note"""
        #print "Modify selected!!"
        textobj = self.seltext['textobj']
        (xtx, ytx) = textobj._get_xy_display()
        is_ax = (self.seltext['anchor'] != 'figure')
        if not is_ax:
            # previous anchor is figure
            pos = textobj.get_position()
            is_ax = (self._get_axes_from_pos(pos,self.canvas) != None)

        (xpix, ypix) = self._disppix2screen(xtx,ytx)
        offset = int(textobj.get_size())*2
        self.show_textwindow(xpix,ypix+offset,basetext=textobj.get_text(),\
                             enableaxes=is_ax)
        self._select_radio(self.seltext['anchor'])
        self._set_note(textobj.get_text())

    ### close all widgets
    def close_widgets(self):
        """Close note window and menu"""
        self.close_textwindow()
        self.close_modmenu()

    ### dialog to confirm deleting note 
    def _delnote_dialog(self):
        """Load dialog to confirm deletion of the text"""
        remind = "Delete text?\n '"+self.seltext['textobj'].get_text()+"'"
        answer = tkMessageBox.askokcancel(parent=self.parent,title="Delete?",
                                          message=remind,
                                          default=tkMessageBox.CANCEL)
        if answer:
            self.delete_note()
        else:
            self.cancel_delete()

    ### helper functions
    def _disppix2screen(self,xpixd,ypixd):
        """
        helper function to calculate a pixel position form Upper-left
        corner of the SCREEN from a pixel position (xpixd, ypixd)
        from Lower-left of the CANVAS (which, e.g., event.x/y returns)

        Returns:
            (x, y):  pixel position from Upper-left corner of the SCREEN.
        """
        xpixs = self.parent.winfo_rootx() + xpixd
        ypixs = self.parent.winfo_rooty() + self.parent.winfo_height() \
               - ypixd
        return (int(xpixs), int(ypixs))
        
