# This file is part of the Coral project.
# Please read the copyright notice in the LICENSE file.


from coral.log import getLogger
logger=getLogger("coral.modeler.diagram.de")

from qt import *

import os
import traceback
import time

import coral
DI = coral.lang.XMIDI20
from coral.modeler.diagram.gestures import *
from coral.modeler.diagram.presentation import *
from coral.modeler.diagram import ui
from coral.modeler.diagram import layout
from coral.modeler.diagram.autogen import RendererGenerator
from coral.modeler.diagram.degen import DiagramElementGenerator
from coral.modeler.dlg.DiagramEditorDlg import DiagramEditorDlg
from coral.modeler import CoralAction, action
from coral.modeler import xmidigraphbuilder
from coral.modeler.coralmodeler import CoralModeler
from coral.modeler.diagram import di_support as DISUPP

#import coral.modeler

from coral.modeler import iconcache
from coral.modeler.window import *

import coral.modeler.xselection

from coral.modeler.diagram import di_support


def suitesDiagram(abstract_element, diagram):
    assert diagram.oclIsKindOf(DI.Diagram)

    deg = DiagramElementGenerator.getExtension(abstract_element.__type__())
    if deg:
        typeinfo = diagram.semanticModel.typeInfo
        accepted = typeinfo in deg.suitableDiagrams(element=abstract_element, xparent=diagram)
        return accepted
    else:
        return False

class CoralPushButton(QToolButton): 
    def __init__(self, parent, action, handler_element = None,  *args): 
        assert action
        self.action = action
        self.handler_element = handler_element
        QToolButton.__init__(self, parent, *args)
        self.setTextLabel(self.action.label)
        self.setIconSet(action.iconset)
        self.connect(self, SIGNAL("clicked()"), self.clicked) 

    def getActionName(self):
        return self.action.name

    def clicked(self):
        if self.handler_element:
            self.handler_element.setActiveBtn(self)

    def executeAction(self, hints=None, parentge=None):
        if hints and parentge:
            self.action(hints, parentge)
        elif hints and not parentge:
            self.action(hints, parentge)
        elif not hints and parentge:
            self.action(hints, parentge)
        else:
            self.action()
            
        self.setOn(0)

class DiagramEditor(DiagramEditorDlg):
    
    def __init__(self, _manager,_parent, _diagram=None):
        DiagramEditorDlg.__init__(self,parent=_parent)
        self.viewLayout = QVBoxLayout(self.viewFrame)
        
        self.btnLayout = QHBoxLayout(self.btnFrame)
        self.btnLayout.setAlignment(Qt.AlignLeft)
        
        self.parent = _parent
        self.manager = _manager

        self.diagramview = DiagramView(_manager, self.viewFrame, _diagram, self)
        self.viewLayout.addWidget(self.diagramview)

        self.toolbar = DiagramToolbar(self)
        self.toolbar.populateToolbar()
        self.viewFrame.show()
        self.btnFrame.show()

    def getDiagramView(self):
        return self.diagramview

    def focusOn(self, dgr):
        self.diagramview.focusOn(dgr)

    # This method prints the current diagram.
    # It prints only the parts of a diagram that is populated with items,
    # reducing printed pages to a minimum
    def printCurrentDiagram(self):
        
        # Retrieve the current diagram's canvas
        curCanvas = self.getDiagramView().canvas()

        self.getDiagramView().setSelectionVisible(False)
        # Calculate the bounding rect around items
        brect = self.__findCanvasBoundRect(curCanvas, printresult=0)
        if not brect:
            return
        printer = qt.QPrinter()
        printer.setColorMode(qt.QPrinter.Color)
        if not printer.setup():
            return
        p = qt.QPainter(printer)
        if not p.isActive():
            return
        metrics=qt.QPaintDeviceMetrics(p.device())
        dpiy = metrics.logicalDpiY()
        
        # Margin is 1.5cm
        margin = int(((1.5/2.54)*dpiy))
        printer.setMargins(margin,margin,margin,margin)
        # Create a rectangle the size of the printed area on the page
        body=qt.QRect(0, 0, metrics.width(), metrics.height())
        # Create a view of equal size
        view = qt.QRect(body)
        view.moveBy(brect.left(),brect.top())
        p.translate(-brect.left(),-brect.top())
        # page is used for the pagenumbers
        page = 1
        while True:
            self.__printQrInfo(brect)
            if view.top() > (brect.bottom()):
                break
            # Copy the rectangle so we propagate horizontally with the copy
            view2 = qt.QRect(view)
            # Move one rect down vertically for the next round
            view.moveBy(0, body.height())
        
            while True:
                curCanvas.update()

                # draws the area to the painter
                curCanvas.drawArea(view2,p)
                # Print page number
                pf = p.fontMetrics()
                p.drawText(view2.right() - pf.width(str(page)),
                           view2.bottom() + pf.ascent()+5, str(page))
                # Move on to the next area
                view2.moveBy(body.width(), 0)
                p.translate(-body.width(),0)


                # Don't send newPage() after last page
                # "if we're outside the boundingrect..."
                if not ((view2.left() > brect.right()) and (view.top() >= brect.bottom())):
                    printer.newPage()
                    page += 1
                else:
                    break
                # "if we're outside the boundingrect in x-axis,
                # break the while and start 'a new row'"
                if view2.left()  > (brect.right()):
                   break
            
            p.translate(0,-body.height())
            p.resetXForm()
            p.translate(-view.left(),-view.top())

        p.end()
        self.getDiagramView().setSelectionVisible(True)


    def saveCurrentDiagram( self ):
        from coral.scripts import savediagram

        diagram = self.getDiagramView().diagram

        config = coral.configuration.getCoralConfiguration()

        # show save dialog
        dirname = config.dGet(["GUI","savediagram_directory"])
        if not dirname or not os.path.exists(dirname):
            dirname = "."

        fname = unicode(diagram.name).replace(" ","")

        filters = ";;".join([d for _,_,d in savediagram.getSupportedFormats()])

        selectedfilter = config.dGet(["GUI","savediagram_format"])
        if not selectedfilter:
            selectedfilter = [d for _,_,d in savediagram.getSupportedFormats()][0]
        
        dlg = qt.QFileDialog(dirname,filters)
        dlg.setCaption(u"Save graphics - %s"%unicode(diagram.name))
        dlg.setModal(True)
        dlg.setSelectedFilter(selectedfilter)
        dlg.setMode(qt.QFileDialog.AnyFile)
        dlg.setUrl(QUrlOperator(fname))

        chkbox = qt.QCheckBox("Save selected diagram elements only",dlg)
        chkbox.setEnabled(len(self.diagramview.getCurrentSelection())>0)
        dlg.addWidgets(None,chkbox,None)

        retval = dlg.exec_loop()

        if not retval:
            return
        
        # check file name
        fname = dlg.selectedFile()
        if fname.isNull():
            return
        for _,format,desc in savediagram.getSupportedFormats():
            if unicode(dlg.selectedFilter())==desc:
                break
        name,ext = os.path.splitext(unicode(fname))
        if not ext:
            fname = fname.append("."+format)

        if chkbox.isChecked():
            elms = self.diagramview.getCurrentSelection()
        else:
            elms = [diagram]

        # try to save the graphics
        try:
            savediagram.saveDiagram(elms,unicode(fname),
                                    self.manager.editorSet,
                                    format)
            config.addOption(["GUI","savediagram_directory"],os.path.dirname(unicode(fname)),False)
            config.addOption(["GUI","savediagram_format"],format,False)

        except (IOError,SystemError),e:
            QMessageBox.critical(self,"Error","Unable to save graphics: %s"%unicode(e))



    def __findCanvasBoundRect(self,canvasobj, printresult=0):
        """Calculates the bounding rectangle for all widgets in the canvas. """
        rect = None
        for i in canvasobj.allItems():
            if not rect:
                rect = i.boundingRect()
            else:
                rect = rect.unite(i.boundingRect())
        if printresult:
            logger.info("BoundingRect for canvasitems:")
            self.__printQrInfo(rect)
            logger.debug("")

        return rect
        

    # Debugging function, prints the properties of a QRect
    def __printQrInfo(self,q):
        if isinstance(q,qt.QRect):
            logger.debug("QRECT.left() = " + str(q.left()))
            logger.debug("QRECT.right() = " + str(q.right()))
            logger.debug("QRECT.top() = " + str(q.top()))
            logger.debug("QRECT.bottom() = " + str(q.bottom()))
        else:
            logger.debug("None is not a Qrect")

        

class DiagramToolbar(CoralActionHandler):
    def __init__(self, editor):
        self.editor = editor
        self.view = editor.getDiagramView()
        self.group =None
        CoralActionHandler.__init__(self)

    def selectedButton(self):
        return self.group.selected()
        
    def initActions(self):
        self.diagramActions=[]
        actions = []
        # Diagram-specific actions
        for dtype,entry in CoralModeler.extensionRegistry.getRegisteredExtensionsByType(type="DiagramEditorToolbarEntry"):
            tool = entry()
            name = entry.__name__
            if tool.getIcon():
                iconset = QIconSet(entry.getIcon())
            else:
                iconset = QIconSet(iconcache.getIcon("tool"))

            tool.prepare(self.view.diagram)
            a=CoralAction(self.editor,dtype+ "_" +name, tool, tool.getLabel(),iconset,0)
            
            actions.append(a)
            self.diagramActions.append(a)

        self.commonActions = [
            CoralAction(self.editor,"select",None,"Select",
                        QIconSet(iconcache.getIcon("select")),Qt.Key_Escape),        
            CoralAction(self.editor,
                        "zoomIn",self.editor.getDiagramView().zoomIn,"Zoom In",
                        QIconSet(iconcache.getIcon("zoomIn")),Qt.Key_Plus),

            CoralAction(self.editor,                        
                        "zoomOut",self.editor.getDiagramView().zoomOut,"Zoom Out",
                        QIconSet(iconcache.getIcon("zoomOut")),Qt.Key_Minus),
            CoralAction(self.editor,
                        "print",self.editor.printCurrentDiagram, "Print Diagram...",
                        QIconSet(iconcache.getIcon("printing")),Qt.CTRL + Qt.Key_P),
            CoralAction(self.editor,
                        "print",self.editor.saveCurrentDiagram, "Save Diagram Graphics...",
                        QIconSet(iconcache.getIcon("savegraphics")),Qt.Key_F9)
            ]
        
        return actions + self.commonActions


    def populateToolbar( self ):
        self.group = QButtonGroup(self.editor.btnFrame)
        self.group.setExclusive(True)

        for a in self.commonActions:
            btn =CoralPushButton(self.editor.btnFrame,a, handler_element=self)
            btn.setFixedSize(30,30)
            if a.name == "select":
                self.group.insert(btn)
                btn.setToggleButton(True)
            self.editor.btnLayout.addWidget(btn)

        
        ## Add some space between button groups
        separator = QFrame(self.editor.btnFrame)
        separator.setFixedSize(10,28)
        self.editor.btnLayout.addWidget(separator)
        
        
        # add diagram-specific actions
        if (self.editor.getDiagramView() and self.editor.getDiagramView().diagram
            and self.editor.getDiagramView().diagram.semanticModel):
            diagram = self.editor.getDiagramView().diagram
            dtype = unicode(diagram.semanticModel.typeInfo)
            for a in self.diagramActions:
                if a.name.split("_")[0]==dtype:
                    btn =CoralPushButton(self.editor.btnFrame,a, handler_element=self)
                    btn.setFixedSize(30,30)
                    self.group.insert(btn)
                    btn.setToggleButton(True)
                    self.editor.btnLayout.addWidget(btn)
                    
        self.editor.btnFrame.show()

    def setActiveBtn(self, coralbtn):
        assert isinstance(coralbtn, CoralPushButton)
        if coralbtn.action in self.commonActions:
            ## These actions should be executed immediately
            coralbtn.executeAction()
        

class DiagramView(QCanvasView):
    canvases={}


    def __init__(self,manager,parent, diagram, editor=None):

        self.editor = editor

        self.popupmenupos = None
        
        self.manager=manager
        self.diagram=diagram
        
        self.selection=Selection(self)

        # Define available gestures.
        # Order does matter
        self.gestures=[
             SelectDiagramG(self),
             CreateNewAssociationG(self),
             CreateNewElementG(self),
             ##CreateNewAssociationG(self),
             ResizeSelectionG(self),
             #RelinkG(self),
             MovePointG(self),
             CreateNewWPG(self),
             MoveSelectionG(self),
             SelectBBoxG(self),
             ##Link2ElementsG(self),
             ##AddElementG(self),
             PanView(self),
             ]
        
        self.currentGesture=None
     
        QCanvasView.__init__(self, None, parent, "DiagramView")
        self.setVScrollBarMode(QScrollView.AlwaysOn)
        self.setHScrollBarMode(QScrollView.AlwaysOn)

        self.viewport().setMouseTracking(1)
        self.setAcceptDrops(True)

        ## Register Context-meny extension
        self.contextMenuExtensions=[]

        for dtype,entry in CoralModeler.extensionRegistry.getRegisteredExtensionsByType(type="ContextMenuAction"):
            self.contextMenuExtensions.append(entry())

    def getCurrentSelection(self):
        sel = self.selection.currentSelection()
        return [i.graphElement for i in sel ]
            
    def dragEnterEvent(self, e):
        ## BUG We should do the suitesDiagram test here too
        ## but it would practically involve doing the same as in dropEvent
        e.accept(QTextDrag.canDecode(e))

    def dropEvent(self, e):
        txt=QString()
        if not QTextDrag.decode(e, txt):
            return
        s = unicode(txt).encode("UTF-8")
        coral.getTransactionManager().beginModifyCmd()
        try:
            prj = coral.loadDocument_str(s)
            try:
                prj.setUserProject(False)
                prj.setFilename("<< internal drag-and-drop project >>")
                sel = self.getSelectableItems(e.pos())
                if sel:
                    ge = sel[0].graphElement
                    upam, updi = DISUPP.findUpwardsModelElementAndDI( ge )
                    ui = updi.ui.__getPyValue__()
                    for i in prj.contents()[0].linkedContents:
                        if ui.acceptDrop(i):
                            ui.onDrop(i, upam)
                        else:
                            logger.debug("Not a matching element to drop")
                            QMessageBox.information(self,
                                                    "Drop element",
                                                    "Could not accept the dropped element")
                else:
                    for i in prj.contents()[0].linkedContents:
                        if suitesDiagram(i, self.diagram):
                            xpos = e.pos().x() + self.contentsX()
                            ypos = e.pos().y() + self.contentsY()
                            the_point = DI.Point(x = xpos, y = ypos)
                            xmidigraphbuilder.xmidibuilder_myself_also(i, self.diagram, position=the_point)
                        else:
                            logger.debug("Not a matching element for diagram")
                            QMessageBox.information(self,
                                                    "Create new presentation",
                                                    "Could not accept the element in this diagram")
            finally:
                prj.invalidate()
        finally:
            coral.getTransactionManager().endModifyCmd()
                
    

    def getEditorForView(self):
        return self.editor


    def focusOn(self,diagram):
        """The diagram manager has told us to focus on a certain diagram"""
        
        self.diagram=diagram

        if DiagramView.canvases.get(diagram,None):
            canvas=DiagramView.canvases[diagram]
            self.setCanvas(canvas)
          
        else:
            self.createCompositeWidget(diagram)
                    
        if not self.diagram.zoom:
            self.diagram.zoom=1.0


    def __createWidget( self, ge ):
        # Do not create a widget for if we already have one
        w = ge.widget.__getPyValue__()
        if w:
            return w
        
        if ge.oclIsKindOf(DI.Diagram):
            # Don't trust the given diagram size, always
            # recalculate and make sure contents fit in
            minx, miny, maxx, maxy = di_support.calculateXMIDIBoundingBox(ge.contained)
            if maxx == minx and minx == 0 and maxy == miny and miny == 0:
                maxx = 1000
                maxy = 1000
            if not ge.size:
                ge.size = DI.Dimension()
            else:
                maxx = max(maxx, ge.size.width)
                maxy = max(maxy, ge.size.height)
            ge.size.width = maxx
            ge.size.height = maxy
                
            canvas = QCanvas(ge.size.width, ge.size.height)
            DiagramView.canvases[ge]=canvas
            self.setCanvas(canvas)
            zoom=float(ge.zoom)
            if zoom < 0.1 or zoom > 10:
                zoom=1.0
                ge.zoom=zoom
            self.setZoom(zoom)
            ge.widget = canvas
            ge.isVisible = True
            return canvas
        

        if ge.oclIsKindOf(DI.LeafElement):
            if ge.oclIsKindOf(DI.TextElement):
                ge.widget = TextElementWidget(ge,self.canvas())
            elif ge.oclIsKindOf(DI.Image):
                ge.widget = ImageWidget(ge,self.canvas())
            elif ge.oclIsKindOf(DI.Polyline):
                ge.widget = PolylineWidget(ge,self.canvas())
##            elif ge.oclIsKindOf(DI.Ellipse):
##                ge.widget = EllipseWidget(ge,self.canvas())
            
        else:
            gext = RendererGenerator.getFirstSuitableExtension( ge )
            if not gext and ge.oclIsKindOf(DI.GraphNode):
                gext = DiagramElementExt(DebugRenderer,
                                         layout.DiagramElementLayouter,
                                         ui.DebugUserInterface)
##            else:
##                return None
            if gext:        
                if ge.oclIsKindOf(DI.GraphNode):
                    ge.widget = GraphNodeWidget(ge,gext,self.canvas())
                elif ge.oclIsKindOf(DI.GraphEdge):
                    ge.widget = GraphEdgeWidget(ge,gext,self.canvas())



        return ge.widget.__getPyValue__()

    def __createCompositeWidget( self, ge ):
        widget = self.__createWidget(ge)
        if not ge.oclIsKindOf(DI.LeafElement):
            for e in ge.contained:
                if e.isVisible:
                    self.__createCompositeWidget(e)
        return widget
    
    def createCompositeWidget( self, ge ):
        sz = layout._SizeHinter()
        tm = coral.getTransactionManager()
        tm.beginModifyCmd()
        try:
            widget = self.__createCompositeWidget(ge)
            ## BUG: This is not the correct place
            ## We need an observer that runs when a diagram is
            ## _loaded_ the first time (not modified)
            ## BUG #2: Running this in transactions enables UNDO
            ## of a newly loaded diagram, killing the widgets
            l = ge.layouter.__getPyValue__()
            if widget and l:
                l.layout(sz)
                widget.updateWidget()
        finally:
            tm.endModifyCmd()
                                      
    def tellOthersToFocusOn(self,e,p):
        self.manager.notifyFocusOn(e)

    def getCenterCoords(self):
        return self.inverseWorldMatrix().map(
            self.contentsX()+self.visibleWidth()/2,
            self.contentsY()+self.visibleHeight()/2)


    def setCenterCoords(self, coords):
        self.center(*self.worldMatrix().map(*coords))
    

    def zoomIn(self):
        if float(self.diagram.zoom)<=10.0:
            z=self.diagram.zoom*1.1
        else:
            z=10.0
        self.setZoom(z)


    def zoomOut(self):
        if float(self.diagram.zoom)>=0.1:
            z=self.diagram.zoom/1.1
        else:
            z=0.1
        self.setZoom(z)


    def setZoom(self, z):
        """
        Sets the zoom level of the diagram.
        """
        assert(z>=0.1 and z<=10)
        if not self.diagram:
            return
        
        center = self.getCenterCoords()
        tm = coral.getTransactionManager()
        tm.beginModifyCmd()
        try:
            self.diagram.zoom=float(z)
        finally:
            tm.endModifyCmd()
        m=QWMatrix()
        m.scale(z,z)

        self.setUpdatesEnabled(False)
        self.viewport().setUpdatesEnabled(False)
        self.setWorldMatrix(m)
        self.setCenterCoords(center)
        self.viewport().setUpdatesEnabled(True)
        self.setUpdatesEnabled(True)
        self.updateContents()

    def updateElement(self):
        logger.info("updating editor"+unicode(self.canvas))
        self.calculateCanvasSize()
        self.canvas().update()
        self.updateContents(0,0,self.xSize,self.ySize)


    def setSelectionVisible( self, yes ):
        """Hides the current selection without removing it"""
        for i in self.selection.currentSelection():
            i.setSelectionVisible(yes)
        self.canvas().update()


    def getSelectableItems( self, pointOrRect ):
        """Returns a list of selectable items under a QPoint or
        inside a QRect."""
        items = []
        for i in self.canvas().collisions(pointOrRect):
            if isinstance(i,DiagramElementWidget) and i.selectable():
                items.append(i)
        return items
    

    def snapCoordsToGrid( self, x, y, offset_x=0, offset_y=0 ):
        """snaps the coordinates to an invisible grid. The
        offset coordinates give an offset at which point
        the snapping should _really_ take place. I.e., for
        classes this should be at the middle of the class,
        not in the upper-left corner."""

        if self.diagram:
            #x=x/(float(self.diagram.zoom))
            #y=y/(float(self.diagram.zoom))
            x,y = self.inverseWorldMatrix().map(x,y)

        gridsize=5
        _x = x + offset_x
        _y = y + offset_y
        return (_x - _x%gridsize - offset_x, _y - _y%gridsize - offset_y)

        
    def contentsMouseDoubleClickEvent(self,e):            
        e.sX,e.sY=self.snapCoordsToGrid(e.x(),e.y())
        l=self.canvas().collisions(QPoint(e.sX,e.sY))
             
    def contentsMousePressEvent(self,e):
        e.sX,e.sY=self.snapCoordsToGrid(e.x(),e.y())
        
        if e.button() == Qt.LeftButton or e.button() == Qt.MidButton:
            if self.currentGesture:
                self.currentGesture=self.currentGesture.onMousePress(e)
            else:
                # try to activate a gesture
                for g in self.gestures:
                    gest = g.detectStart(e)
                    if gest:
                        self.currentGesture = gest
                        break

        # PopUp menu (should it be another gesture)?
        if e.button() == Qt.RightButton:
            if self.currentGesture:
                self.currentGesture=self.currentGesture.stop()
            else:
                Select1ElementG(self).detectStart(e,Qt.RightButton)
                self.lCoords=(e.sX,e.sY)
                self.lElements=self.canvas().collisions(QPoint(e.sX,e.sY))
                self.popupmenupos = [e.sX, e.sY]
                self.showPopupMenu(e.globalPos())
                
                
    

    def contentsMouseMoveEvent(self,e):
        e.sX,e.sY=self.snapCoordsToGrid(e.x(),e.y())

        if not self.currentGesture:
            coll = self.canvas().collisions(QPoint(e.sX,e.sY))
            if coll and isinstance(coll[0],HandleWidget):
                self.setCursor(coll[0].cursor())
            else:
                self.setCursor(QCursor(Qt.ArrowCursor))
        
        if self.currentGesture:
            self.currentGesture=self.currentGesture.onMouseMove(e)
                    
    def contentsMouseReleaseEvent(self,e):
        e.sX,e.sY=self.snapCoordsToGrid(e.x(),e.y())

        if self.currentGesture:
            self.currentGesture=self.currentGesture.onMouseRelease(e)


    def keyPressEvent(self,e):
        logger.debug("User pressed " + str(e))

    def contextMenuEvent( self, e ):
        e.accept()
        p = QPopupMenu(self)
        pos = QCursor.pos()

        contextMenu=QPopupMenu(self,"cm")

        presentations=[]
        elements=[]
        selection = {}
        sel = self.selection.currentSelection()
        for w in sel:
            selection[w] = True
        ## Prune contained children
        GraphElementWidget.shallowContainers(selection)
            
        for s in selection.keys():
            pres = s.graphElement
            presentations.append(pres)
            if pres.semanticModel and pres.semanticModel.element:
                elements.append(pres.semanticModel.element)

        for o in self.contextMenuExtensions:
            o.createMenuItem(contextMenu, elements, presentations,
                             diagram = self.diagram,
                             position = self.popupmenupos)

        contextMenu.popup(pos)


##    ## This is what we can't live without apparently
##    ## Needs to be removed
##    ###
##    ## No! It's great, and the speed hit has never been examined.
##    ##    --marcus
##    def copySelectedWidgets(self, isPrimary):
##        import coral.modeler.basicEdit
##        o = coral.Sequence()
##        for s in self.selection.currentSelection():
##            g = s.graphElement
##            o.append(g)
##            e = g.semanticModel.element
##            if e:
##                o.append(e)
##        coral.modeler.basicEdit.copyHelper(o, isPrimary)

        
    def showPopupMenu(self,pos):
        pass
    

