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

import traceback
from coral.modeler.editor import Editor
from coral.modeler.window import CoralMDIWindow
from coral import *
from qt import *
from coral.modeler.getFQName import *

from coral import modeler
from coral.modeler import iconcache
from coral import log
from coral import configuration
from coral.modeler import extension
from coral.modeler.modeler import CoralModeler
from coral.modeler import CoralAction, action

import types

logger=log.getLogger("ListEditor")

def metamodelName(element):
    l=element.__type__().__language__()
    return l.getName()+str(l.getVersion())+str(l.getRevision())

#
# Amount of list items created. Used in unittests.
#
DEBUG_N_LIST_ITEMS = 0

class ListItem(QListViewItem):
    def __init__(self, parent, subject):
        global DEBUG_N_LIST_ITEMS
        DEBUG_N_LIST_ITEMS += 1
        QListViewItem.__init__(self, parent)
        self.subject = subject
        self.onElementChanged(0)
        self.setOpen(0)
        
    def setOpen(self, b):
        """This is called when the node is first expanded.
        We use it to populate ourselves."""
        if b:
            self.updateChildren(recurse=1)
        QListViewItem.setOpen(self, b)

    def updateChildren(self,recurse):
        pass

class ElementListItem(ListItem):
    def __init__(self, parent, subject):
        ListItem.__init__(self, parent, subject)

        typename = self.subject.__type__().getName().lower()
        icon=iconcache.getIcon(typename + "lv", metamodelName(self.subject) )
        if icon:
            self.setPixmap(0,icon)

    def updateChildren(self,recurse):
        editor = self.listView()

        children={}
        
        for p in self.subject.__type__().__properties__():
            if not p.isComposition():
                continue
            s = self.subject.__getSlotRO__(p.getName())
            if not s:
                continue
            for i in s:
                if editor.qlvItems.has_key(i):
                    item = editor.qlvItems[i]
                    self.insertItem(item)
                    children[item] = 1
                else:
                    if recurse and isinstance(i,Element):
                        an_item = editor.addElementItem(i,self,recurse-1)
                        if not an_item:
                            continue
                        children[an_item] = 1

        # remove the ListItem children that should not be there
        i = self.firstChild()
        while i:
            next = i.nextSibling()
            if not children.has_key(i):
                i.__kill__()
            i = next

    def onElementChanged(self,recurse=1):
        if not self.subject:
            return

        if not self.subject.__getRootsProject__():
            self.__kill__()
            return

        editor = self.listView()
        if not editor:
            return

        # Explicitly call it so that our view of our children is updated
        # but @recurse can still stop the potentially length recursion
        self.updateChildren(recurse)

        name = getFQName(self.subject, 1)
        if not name:
            name="(noname)"
        else:
            # Must flatten name because QT can't show multi-line strings
            # in ListViews
            name = re.sub("\n", " ", name)

        # Prepend the name of the property owning the element, if exists
        if self.subject.__parentSlot__() and self.subject.__parentSlot__():
            name=str(self.subject.__parentSlot__().__type__().getOppositeProperty().getName())+" "+name
            
        self.setText(0,QString(name+":"+self.subject.__type__().getName()))

        if editor.pickMode and editor.includeCriteria:
            selectable=0
            for c in editor.includeCriteria:
                if c(self.subject):
                    selectable=1
                    break
            self.setSelectable(selectable)
                
    def checkExpandMark(self, outliner):
        """ Return True if we have even one child which we can
        show as a child"""
        if not self.subject:
            return False
        for s in self.subject.__all_slots__():
            p = s.getProperty()
            if not p.isComposition():
                continue
            # primitives not shown
            if p.getTarget().isDatatypeDefinition():
                continue
            if not len(s):
                continue
            for element in s:
                b = outliner.checkType(element)
                if b:
                    return True
        return False
    
            
    def __kill__(self):
        logger.info("__kill__"+unicode(self)+" "+unicode(self.parent()))
        p = self.parent()
        if not p:
            p = self.listView()
        if p:
            p.takeItem(self)

class ProjectListItem(ListItem):
    def updateChildren(self,recurse):
        children={}
        for i in self.subject.contents():
            editor = self.listView()
            if editor.qlvItems.has_key(i):
                item = editor.qlvItems[i]
                self.insertItem(item)
                children[item] = 1
            else:
                if recurse  and isinstance(i,Element):
                    an_item = editor.addElementItem(i,self,recurse-1)
                    if not an_item:
                        continue 
                    children[an_item] = 1

        # remove the ListItem children that should not be there
        i = self.firstChild()
        while i:
            next = i.nextSibling()
            if not children.has_key(i):
                i.__kill__()
            i = next

    def onElementChanged(self,recurse=1):
        if not self.subject:
            return


        if self.subject.isInvalid():
            self.__kill__()
            return
        
        if recurse:
            self.updateChildren(recurse)
       
        icon=iconcache.getIcon("storagelv" )
        if icon:
            self.setPixmap(0,icon)

        name = self.subject.getHrefFilename()
            
        if not name:
            name="(noname)"
        self.setText(0,QString(name+":Project"))

        editor = self.listView()
        self.setSelectable(0)
                                    
    def checkExpandMark(self, outliner):
        return True
    
    def __kill__(self):
        self.listView().removeItem(self)
        
class ElementPick(QDialog):

    def __init__(self,parent=None):
        QDialog.__init__(self,parent)      
        self.layout = QGridLayout(self,1,1,0,6,"Layout")
        
        
    def pick(self, element, theType):
        list=OutlineEditor(self,includeCriteria=[lambda e:e.oclIsKindOf(theType)])
        self.layout.addWidget(list,0,0)
        list.pickMode=1
        if element:
            list.focusOn(element)

        self.show()
        self.exec_loop()
        r=list.getSelectedElement()
        self.removeChild(list)
        list.destroy()
        list=None
        return r
    
class OutlineEditor(Editor,CoralMDIWindow,QListView):

    def __init__(self,parent,excludeCriteria=None,includeCriteria=None):
        Editor.__init__(self)
        CoralMDIWindow.__init__(self,parent)
        QListView.__init__(self,parent)

        self.setCaption("Model Outline")
        self.pickMode=0
        self.showInternalProjects=False
        
        if includeCriteria:
            self.includeCriteria=includeCriteria
        else:
            self.includeCriteria=[]
           
        if excludeCriteria:
            self.excludeCriteria=excludeCriteria
        else:
            self.excludeCriteria=[]
        
        self.setRootIsDecorated(1)
        self.setTreeStepSize(12)
        self.addColumn("Element")
        #self.addColumn("Type")

        self.qlvItems={}
        self.connect(self,SIGNAL("clicked(QListViewItem *)"),
                     self.clicked)
     
  
        self.connect(self,SIGNAL("rightButtonPressed(QListViewItem *,const QPoint &, int)"),
                     self.rightClicked)

        self.result=None

        self.contextMenuExtensions=[]


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

        getTransactionManager().addObserver(self.onModelChanged)

        for s in coral.getAllProjects():
            self.addProject(s)

    def startDrag(self):
        
        item = self.currentItem()
        if item:
            modelitem = item.subject
            if modelitem and not isinstance(modelitem, coral.Project):
                if modelitem.oclIsKindOf(AnyAbstractElement):
                    dobj = coral.lang.CoralEx10.InternalQtDragAndDrop.DragAndDrop()
                    dobj.linkedContents = [modelitem]
                    o = coral.Set()
                    o.add(dobj)
                    xmi_str = coral.saveDocument_str(o, "2.0")
                    d =  QTextDrag(QString(xmi_str), self);
                    logger.debug("\nDrag & Drop started. Output:")
                    d.dragCopy()

    def contentsMouseMoveEvent(self,e):
        if e.state() & Qt.LeftButton or e.state() & Qt.RightButton :
            ## Initiate drag procedure
            self.startDrag()


    def setIncludeCriteria( self, includeCriteria=None ):
        if includeCriteria:
            self.includeCriteria=includeCriteria
        else:
            self.includeCriteria=[]
           

    def setExcludeCriteria( self, excludeCriteria=None ):
        if excludeCriteria:
            self.excludeCriteria=excludeCriteria
        else:
            self.excludeCriteria=[]
        
    
    def close(self,x=0):
        getTransactionManager().removeObserver(self.onModelChanged)
        return QListView.close(self,x)

    def onModelChanged(self,x):
        logger.debug("Begin OutlineEditor.onModelChanged")

        updated={}
        tm=getTransactionManager()
        tr=tm.getCurrentTransaction()

        # ElementItems added this round.
        # Used so we don't add a whole chain of new items,
        # only the first one
        recently_added = {}

        for c in tr:
            if isinstance(c,ElementChangeSlotCommand):
                element=c.getSlot().getElement()

                if element in self.qlvItems:
                    if element not in updated:
                        item = self.qlvItems[element]
                        item.onElementChanged(0)
                        self.__setExpandMark(item)
                        updated[element]=1
                         
                else:
                    # Only the first new child is added
                    # This is ok.
                    parent = element.__parent__()
                    if parent in self.qlvItems and self.qlvItems[parent].isOpen():
                        if parent in recently_added:
                            continue
                        recently_added[parent] = True
                        if not parent.__getRootsProject__():
                            continue
                        self.addElementItem(element, self.qlvItems[parent])
                        updated[element] = 1
            elif isinstance(c, ProjectNameChangeCommand):
                project=c.getProject()
                item = self.qlvItems.get(project, None)
                if item:
                    item.onElementChanged(0)
                updated[project]=1
            elif isinstance(c, coral.NewProjectCommand):
                element = c.getNewProject()
                self.addProject(element)
            elif isinstance(c, coral.ProjectChangeCommand):
                element = c.getAtom()
                oldproject = c.getOldProject()
                # HACK! We don't have a ProjectInvalitedCommand
                if oldproject and oldproject.isInvalid():
                    item = self.qlvItems.get(oldproject,None)
                    if item:
                        self.removeItem(item)
                if element:
                    if oldproject: 
                        item = self.qlvItems.get(oldproject,None)
                        if item:
                            item.onElementChanged()
                    updated[oldproject] = 1
                    the_project = element.__getProject__()
                    if the_project:
                        item = self.qlvItems.get(the_project,None)
                        if item:
                            item.onElementChanged()
                    updated[the_project] = 1

        logger.debug("End OutlineEditor.onModelChanged")
                    
    def focusOn(self, element):
        if not self.checkType(element):
            return None
        logger.debug("OutlineEditor.focusOn" + str(self) + " " + str(element))
        self.focus=element

        self.clearSelection()

        if not self.qlvItems.has_key(element):
            # This handles the case where the user activates an element
            # whose parents are not in the outline editor.
            previous_parent = self
            for p in coral.Element__getParentsAndMyself__(element):
                if not p.__parent__():
                    storage=p.__getProject__()
                    if not storage:
                        continue # ??
                    previous_parent = self.addProject(storage,force=1)            
                else:
                    if self.checkType(p.__parent__()):
                        if not self.qlvItems.has_key(p.__parent__()):
                            previous_parent=self.addElement(p.__parent__())
                        else:
                            previous_parent=self.qlvItems[p.__parent__()]
                if not self.qlvItems.has_key(p):
                    self.addElementItem(p, previous_parent, 0)


        p = self.qlvItems[element]

        self.setSelected(p,1)

        self.ensureItemVisible(p)

        
    def addElement(self,element):
        return self.addElementItem(element,self,recurse=1)

    def addProject(self,storage,force=0):
        if storage.isInvalid():
            return
        if self.qlvItems.has_key(storage):
            return self.qlvItems[storage]

        if storage.isUserProject() or self.showInternalProjects or force:
            item = ProjectListItem(self, storage)
            self.__setExpandMark(item)
            self.qlvItems[storage] = item
            assert self.qlvItems.has_key(storage)
            return item
        return None
    
    def checkType(self,element):
        """ returns true if the type element is compatible with this view
        If the type of element is in excludeTypes it will not be shown.       
        If includeTypes is defined, then an element will be show if its type is in includeTypes"""

        if isinstance(element,coral.Project):
            return 1
        
        if not self.includeCriteria and not self.excludeCriteria:
            return 1
            
        for c in self.excludeCriteria:
            if c(element):
                return 0
       
        if not self.includeCriteria or self.pickMode:
            return 1
        else:        
            for c in self.includeCriteria:
                if c(element):
                    return 1
                #if element.oclIsKindOf(t):
                #    return 1                
            return 0
        
   
    
    def __setExpandMark(self, item):
        b = item.checkExpandMark(self)
        if b or ( not b and item.isExpandable() ):
            item.setExpandable(b)
            
    def addElementItem(self,element,parent,recurse=0):
        """Add a new ListItem for an element to parent, if it passes
        the matching criteria."""
        if not self.checkType(element):
            return None
        if self.qlvItems.has_key(element):
            return self.qlvItems[element]
        item = ElementListItem(parent, element)
        self.__setExpandMark(item)
        self.qlvItems[element] = item
        return item

    def __removeItem(self, item):
        i = item.firstChild()
        while i:
            next = i.nextSibling()
            self.__removeItem(i)
            i = next
        del self.qlvItems[item.subject]
        # Qt oddity, can't call parent() on top-most elements
        p = item.parent()
        if not p:
            p = item.listView()
        p.takeItem(item)
        
    def removeItem(self, item):
        """Removes an item from a ListView. The given parameter
        cannot be used after this. Especially note how this means
        you have to save item.nextSibling() before calling this method."""
        # Hm, could we use a QListViewItemIterator?
        self.__removeItem(item)
        
    def rightClicked(self, item, pos, c):
        # Need to be able to show/hide even when in pickmode!
        ###if self.pickMode:
        ###    return
        
        contextMenu=QPopupMenu(self,"cm")

        if item:
            for o in self.contextMenuExtensions:
                o.createMenuItem(contextMenu,[item.subject],[])
        if not item or isinstance(item.subject, coral.Project):
            contextMenu.insertSeparator()
            if self.showInternalProjects:
                verb="Hide"
            else:
                verb="Show"
                
            contextMenu.insertItem(verb+" System Projects",self.showHideInternalProjects)
        contextMenu.popup(QPoint(pos.x(),pos.y()),0)

    def showHideInternalProjects(self,a):
        self.showInternalProjects=not self.showInternalProjects
        self.clear()
        self.qlvItems={}
        for s in coral.getAllProjects():
            self.addProject(s)

    def clicked(self,item):
        if not item:
            return

        item.updateChildren(recurse=1)
        if self.pickMode and item.isSelectable():
            self.done(1)
        else:
            if not isinstance(item.subject,Project):
                self.notifyFocusOn(item.subject)
        
    def keyPressEvent(self,e):
        QListView.keyPressEvent(self,e)
        logger.debug(unicode(e.key()))
        if e.key()==Qt.Key_Return:
            item=self.selectedItem()
            if item and item.isSelectable():
                if self.pickMode:
                    self.done(1)
        
##     def contentsMouseDoubleClickEvent(self,e):
##         pass
    
##     def contentsMouseMoveEvent(self,e):
##         pass

    def apply(self):
        if self.firstChild():
            self.firstChild().updateChildren(recurse=1)

    def done(self,x):
        if self.selectedItem():
            self.result=self.selectedItem().subject
        else:
            self.result=None
        return self.parent().done(x)
        
    def getSelectedElement(self):
        return self.result

    
    
if __name__=='__main__':
    a=QApplication([])
    model=loadModel("coral/modeler/outline/unittests/model.xmi")
    r=ElementPick().pick(model, coral.lang.UML14.Class)
    logger.debug("Selected "+unicode(r)+" "+unicode(r.name))
