# Name:         presenter.py
# Purpose:      Presenter part
# Author:       Roman Rolinsky <rolinsky@femagsoft.com>
# Created:      07.06.2007
# RCS-ID:       $Id: presenter.py 64158 2010-04-27 11:59:52Z ROL $

import os,tempfile,shutil
from xml.parsers import expat
import cPickle
from globals import *
import view
from model import Model, MyDocument
from component import Manager
import undo

# Presenter class linking model to view objects
class _Presenter:
    def init(self):
        Model.init()
        self.path = ''
        # Global modified state
        self.setModified(False) # sets applied
        view.frame.Clear()
        view.tree.Clear()
        view.tree.SetPyData(view.tree.root, Model.mainNode)
        view.testWin.Init()
        g.undoMan.Clear()
        # Insert/append mode flags
        self.createSibling = self.insertBefore = False
        # Select main node attributes
        self.setData(view.tree.root)

    def loadXML(self, path):
        Model.loadXML(path)
        view.tree.Flush()
        view.tree.SetPyData(view.tree.root, Model.mainNode)
        self.setData(view.tree.root)
        if g.conf.expandOnOpen:
            view.tree.ExpandAll()

    def saveXML(self, path):
        Model.saveXML(path)

    def open(self, path):
        if not os.path.exists(path):
            wx.LogError('File does not exists: %s' % path)
            raise IOError
        try:
            self.path = os.path.abspath(path)
            TRACE('Loading XML file: %s', self.path)
            self.loadXML(self.path)
            # Change dir
            dir = os.path.dirname(self.path)
            if dir: os.chdir(dir)
            self.setModified(False)
            g.conf.localconf = self.createLocalConf(path)
        except:
            logger.exception('error loading XML file')
            wx.LogError('Error loading XML file: %s' % path)
            raise
            
    def save(self, path):
        # Apply changes if needed
        if not self.applied:
            self.update(self.item)
        try:
            tmpFile,tmpName = tempfile.mkstemp(prefix='xrced-')
            os.close(tmpFile)
            TRACE('Saving temporary file: %s', tmpName)
            self.saveXML(tmpName)
            TRACE('copying to the main file: %s', path)
            shutil.copy(tmpName, path)
            self.path = path
            self.setModified(False)
        except:
            logger.exception('error saving XML file')
            wx.LogError('Error saving XML file: %s' % path)
            raise

    def setModified(self, state=True, setDirty=True):
        '''Set global modified state.'''
        TRACE('setModified %s %s', state, setDirty)
        self.modified = state
        # Set applied flag
        if not state: self.applied = True
        name = os.path.basename(self.path)
        if not name: name = 'UNTITLED'
        # Update GUI
        if state:
            view.frame.SetTitle(progname + ': ' + name + ' *')
            # Update test window
            if view.testWin.IsShown() and setDirty:
                view.testWin.isDirty = True
                if g.conf.autoRefresh:
                    self.refreshTestWin()
        else:
            view.frame.SetTitle(progname + ': ' + name)

    def setApplied(self, state=True):
        '''Set panel state.'''
        TRACE('setApplied %s', state)
        self.applied = state
        if not state and not self.modified: 
            self.setModified(setDirty=False)  # toggle global state

    def createUndoEdit(self, item=None, page=None):
        TRACE('createUndoEdit')
        # Create initial undo object
        if item is None: item = self.item
        if page is None: page = view.panel.nb.GetSelection()
        view.panel.undo = undo.UndoEdit(item, page)

    def registerUndoEdit(self):
        TRACE('registerUndoEdit')
        g.undoMan.RegisterUndo(view.panel.undo)
        view.panel.undo = None

    def panelIsDirty(self):
        '''Check if the panel was changed since last undo.'''
        # Register undo
        if view.panel.undo:
            panel = view.panel.GetActivePanel()
            if view.panel.undo.values != panel.GetValues():
                return True
        return False

    def setData(self, item):
        '''Set data and view for current tree item.'''

        self.item = item
        if item == view.tree.root:
            TRACE('setData: root node')
            self.container = None
            self.comp = Manager.rootComponent
            self.panels = view.panel.SetData(self.container, self.comp, Model.mainNode)
        else:
            node = view.tree.GetPyData(item)
            if node.nodeType != node.COMMENT_NODE:
                TRACE('setData: %s', node.getAttribute('class'))
            self.comp = Manager.getNodeComp(node)
            parentItem = view.tree.GetItemParent(item)
            parentNode = view.tree.GetPyData(parentItem)
            if parentNode == Model.mainNode:
                self.container = Manager.rootComponent
            else:
                parentClass = parentNode.getAttribute('class')
                self.container = Manager.components[parentClass]
            self.panels = view.panel.SetData(self.container, self.comp, node)
        # Create new pending undo
        self.createUndoEdit(self.item)

        if view.testWin.IsShown():
            self.highlight(item)

    def highlight(self, item):
        TRACE('highlight')
        if view.testWin.IsDirty():
            view.testWin.RemoveHighlight()
            return
        try:
            rect = view.testWin.FindObjectRect(item)
            if not rect:
                view.testWin.RemoveHighlight()
                return
            view.testWin.Highlight(rect)
        except:
            logger.exception('highlighting failed')

    def updateCreateState(self, forceSibling, forceInsert):
        if self.container:
            if self.comp.isContainer():
                self.createSibling = forceSibling
            else:
                self.createSibling = True
        else:
            self.createSibling = False
        self.insertBefore = forceInsert        
        TRACE('updateCreateState: %s %s', self.createSibling, self.insertBefore)

    def popupMenu(self, forceSibling, forceInsert, pos):
        '''Show popup menu and set sibling/insert flags.'''
        self.updateCreateState(forceSibling, forceInsert)
        menu = view.XMLTreeMenu(self.container, self.comp, view.tree,
                                self.createSibling, self.insertBefore)
        view.tree.PopupMenu(menu, pos)
        menu.Destroy()        

    def create(self, comp, child=None):
        '''
        Add DOM node as child or sibling depending on flags. Return new item.

        If child is passed replace by existing data.
        '''
        if child is None:
            child = Model.createObjectNode(comp.klass)
            # Set default values
            for k,v in comp.defaults.items():
                comp.addAttribute(child, k, v)
        data = wx.TreeItemData(child)
        item = self.item
        if not self.applied:
            self.update(item)
        if item == view.tree.root:
            self.createSibling = False # can't create sibling of root
        if self.createSibling:
            parentItem = view.tree.GetItemParent(item)
            parentNode = view.tree.GetPyData(parentItem)
        else:
            parentNode = view.tree.GetPyData(item)
        label = comp.getTreeText(child)
        imageId = comp.getTreeImageId(child)
        if self.createSibling:
            node = view.tree.GetPyData(item)
            if self.insertBefore:
                self.container.insertBefore(parentNode, child, node)
                item = view.tree.InsertItemBefore(
                    parentItem, item, label, imageId, data=data)

            else:
                self.container.insertAfter(parentNode, child, node)
                item = view.tree.InsertItem(
                    parentItem, item, label, imageId, data=data)
        else:
            if self.insertBefore and view.tree.ItemHasChildren(item):
                nextNode = view.tree.GetPyData(view.tree.GetFirstChild(item)[0])
                self.comp.insertBefore(parentNode, child, nextNode)
                item = view.tree.PrependItem(item, label, imageId, data=data)
            else:
                self.comp.appendChild(parentNode, child)
                item = view.tree.AppendItem(item, label, imageId, data=data)
        view.tree.SetItemStyle(item, child)
        view.tree.EnsureVisible(item)
        view.tree.UnselectAll()
        if view.testWin.IsShown():
            view.testWin.isDirty = True
        view.tree.SelectItem(item)
        self.setModified()
        return item

    def createRef(self, ref, child=None):
        '''Create object_ref element node.'''
        if child is None:
            child = Model.createRefNode(ref)
        refNode = Model.findResource(ref)
        if refNode: 
            comp = Manager.getNodeComp(refNode)
        else:
            comp = Manager.getNodeComp(child)
        self.create(comp, child)

    def createComment(self):
        '''Create comment node.'''
        node = Model.createCommentNode()
        comp = Manager.getNodeComp(node)
        self.create(comp, node)

    def replace(self, comp, node=None):
        '''Replace DOM node by new or passed node. Return new item.'''
        TRACE('replace')
        if node is None:
            node = Model.createObjectNode(comp.klass)
        if not self.applied:
            self.update(item)
        data = wx.TreeItemData(node)
        item = self.item
        parentItem = view.tree.GetItemParent(item)
        parentNode = view.tree.GetPyData(parentItem)
        oldNode = view.tree.GetPyData(item)
        self.container.replaceChild(parentNode, node, oldNode)
        # Replace tree item: insert new, remove old
        label = comp.getTreeText(node)
        imageId = comp.getTreeImageId(node)
        item = view.tree.InsertItem(parentItem, item, label, imageId, data=data)
        view.tree.Delete(view.tree.GetPrevSibling(item))
        self.item = item
        # Add children
        for n in filter(is_object, node.childNodes):
            view.tree.AddNode(item, comp.getTreeNode(n))
        view.tree.EnsureVisible(item)
        # Update panel
        view.tree.SelectItem(item)
        self.setModified()
        return oldNode

    def subclass(self, item, subclass):
        node = view.tree.GetPyData(item)
        if subclass:
            node.setAttribute('subclass', subclass)
        elif node.hasAttribute('subclass'):
            node.removeAttribute('subclass')
        # Update item label
        view.tree.SetItemImage(item, self.comp.getTreeImageId(node))
        view.tree.SetItemText(item, self.comp.getTreeText(node))        
        # Update panel
        view.tree.SelectItem(item)
        self.setModified()        

    def update(self, item):
        '''Update DOM with new attribute values. Update tree if necessary.'''
        node = view.tree.GetPyData(item)
        isComment = node.nodeType == node.COMMENT_NODE
        if isComment:
            subclass = None
        else:
            subclass = node.getAttribute('subclass')
        # Update (sub)class if needed
        cls = view.panel.textClass.GetValue()
        if not subclass:
            if not isComment and cls != self.comp.klass:
                if node.tagName == 'object_ref' and not cls:
                    if node.hasAttribute('class'):
                        node.removeAttribute('class')
                        TRACE('removed "class" tag')
                else:
                    TRACE('update class: %s', cls)
                    node.setAttribute('class', cls)
        else:
            value = subclass + '(%s)' % self.comp.klass
            if cls != value:
                iLeft = cls.find('(')
                iRight = cls.find(')')
                if iLeft != -1 and iLeft < iRight:
                    subclass = cls[:iLeft]
                    klass = cls[iLeft+1:iRight]
                    TRACE('update class/subclass: %s', cls)
                    node.setAttribute('class', klass)
                    node.setAttribute('subclass', subclass)
                else:
                    TRACE('remove subclass')
                    node.removeAttribute('subclass')
                    node.setAttribute('class', cls)
        if self.comp and self.comp.hasName:
            name = view.panel.textName.GetValue()
            if name:
                node.setAttribute('name', name)
            elif node.hasAttribute('name'): # clean up empty names
                node.removeAttribute('name')
        if item != view.tree.root: 
            for panel in self.panels:
                if not panel.node: continue
                # Replace node contents except object children
                for n in panel.node.childNodes[:]:
                    if not is_object(n):
                        panel.node.removeChild(n)
                        n.unlink()
        for panel in self.panels:
            for a,value in panel.GetValues():
                if value:
                    try:
                        if isinstance(panel, view.AttributePanel) and panel.comp:
                            comp = panel.comp
                        else:
                            comp = self.comp
                        comp.addAttribute(panel.node, a, value)
                    except:
                        logging.exception('addAttribute error: %s %s', a, value)
        if item != view.tree.root:
            view.tree.SetItemImage(item, self.comp.getTreeImageId(node))
            view.tree.SetItemText(item, self.comp.getTreeText(node))
        self.setApplied()
        # Set dirty flag
        if view.testWin.IsShown():
            view.testWin.isDirty = True

    def unselect(self):
        if not self.applied:
            self.update(self.item)
            if view.testWin.IsShown() and view.testWin.item == self.item:
                view.testWin.Destroy()
        view.tree.UnselectAll()
        self.setData(view.tree.root)

    def flushSubtree(self, item=None, node=None):
        # Remember test item index
        TRACE('flushSubtree')
        if view.testWin.item is not None:
            itemIndex = view.tree.ItemFullIndex(view.testWin.item)
        view.tree.FlushSubtree(item, node)
        if view.testWin.item is not None:
            view.testWin.item = view.tree.ItemAtFullIndex(itemIndex)

    def delete(self, item):
        '''Delete selected object(s). Return removed XML node.'''
        TRACE('delete')
        parentItem = view.tree.GetItemParent(item)
        parentNode = view.tree.GetPyData(parentItem)
        node = view.tree.GetPyData(item)
        node = self.container.removeChild(parentNode, node)
        view.tree.Delete(item)
        # If deleting the top-level object, remove view
        if view.testWin.IsShown() and view.testWin.item == item:
            view.testWin.Destroy()
        self.setApplied()
        self.unselect()
        self.setModified()
        return node

    def deleteMany(self, items):
        '''Delete selected object(s).'''
        for item in items:
            if not item.IsOk(): continue # child already deleted
            parentItem = view.tree.GetItemParent(item)
            parentNode = view.tree.GetPyData(parentItem)
            node = view.tree.GetPyData(item)
            node = self.container.removeChild(parentNode, node)
            node.unlink()       # delete completely
            view.tree.Delete(item)
        self.setApplied()
        self.unselect()
        self.setModified()

    def cut(self):
        self.copy()
        return self.delete(view.tree.GetSelection())

    def copy(self):
        # Update values from panel first
        item = view.tree.GetSelection()
        if not self.applied:
            self.update(item)
        node = view.tree.GetPyData(item)
        if self.container.requireImplicit(node):
            implicit = node.parentNode
        else:
            implicit = None
        if wx.TheClipboard.Open():
            if node.nodeType == node.ELEMENT_NODE:
                data = wx.CustomDataObject('XRCED_elem')
                s = node.toxml(encoding=expat.native_encoding)
                # Replace by a pair
                if implicit: s = [s, implicit.toxml(encoding=expat.native_encoding)]
            else:
                # Non-element nodes are normally comments
                data = wx.CustomDataObject('XRCED_node')
                s = node.data
            data.SetData(cPickle.dumps(s))
            wx.TheClipboard.SetData(data)
            wx.TheClipboard.Close()
        else:
            wx.MessageBox("Unable to open the clipboard", "Error")        

    def checkCompatibility(self, comp):
        '''Check parent/child compatibility.'''
        if self.createSibling: container = self.container
        else: container = self.comp
        if not container.canHaveChild(comp):
            wx.LogError('Incompatible parent/child: parent is %s, child is %s!' %
                        (container.klass, comp.klass))
            return False
        return True

    def paste(self):
        success = success_node = False
        if wx.TheClipboard.IsOpened() or wx.TheClipboard.Open():
            try:
                data = wx.CustomDataObject('XRCED_elem')
                if wx.TheClipboard.IsSupported(data.GetFormat()):
                    try:
                        success = wx.TheClipboard.GetData(data)
                    except:
                        # there is a problem if XRCED_node is in clipboard
                        # but previous SetData was for XRCED
                        pass
                if not success:             # try other format
                    data = wx.CustomDataObject('XRCED_node')
                    if wx.TheClipboard.IsSupported(data.GetFormat()):
                        success_node = wx.TheClipboard.GetData(data)
            finally:
                wx.TheClipboard.Close()

        if not success and not success_node:
            wx.MessageBox(
                "There is no data in the clipboard in the required format",
                "Error")
            return

        # XML representation of element or node value string
        data = cPickle.loads(data.GetData()) 
        implicit = None
        if success:
            if type(data) is list:
                node = Model.parseString(data[0])
                implicit = Model.parseString(data[1])
            else:
                node = Model.parseString(data)
        else:
            node = Model.dom.createComment(data)
        comp = Manager.getNodeComp(node)

        # Check compatibility
        if not self.checkCompatibility(comp):
            node.unlink()
            return

        item = view.tree.GetSelection()
        if item and not self.applied:
            self.update(item)
        
        item = self.create(comp, node)
        if implicit:   # copy parameters for implicit node if possible
            parentNode = view.tree.GetPyData(view.tree.GetItemParent(item))
            parentComp = Manager.getNodeComp(parentNode)
            if parentComp.requireImplicit(node) and \
                    parentComp.implicitKlass == implicit.getAttribute('class'):
                parentComp.copyImplicitAttributes(implicit, node.parentNode, parentComp)
            implicit.unlink()

        # Add children
        for n in filter(is_object, node.childNodes):
            view.tree.AddNode(item, comp.getTreeNode(n))
        self.setModified()
        return item

    def moveUp(self):
        parentItem = view.tree.GetItemParent(self.item)
        treeNode = view.tree.GetPyData(self.item)
        node = self.container.getTreeOrImplicitNode(treeNode)
        parent = node.parentNode
        prevNode = node.previousSibling
        while prevNode.nodeType != node.ELEMENT_NODE:
            prevNode = prevNode.previousSibling
        parent.removeChild(node)
        parent.insertBefore(node, prevNode)
        index = view.tree.ItemFullIndex(self.item)
        self.flushSubtree(parentItem, parent)
        index[-1] -= 1
        self.item = view.tree.ItemAtFullIndex(index)
        self.setModified()
        view.tree.SelectItem(self.item)
        
    def moveDown(self):
        parentItem = view.tree.GetItemParent(self.item)
        treeNode = view.tree.GetPyData(self.item)
        node = self.container.getTreeOrImplicitNode(treeNode)
        parent = node.parentNode
        nextNode = node.nextSibling
        while nextNode.nodeType != node.ELEMENT_NODE:
            nextNode = nextNode.nextSibling
        nextNode = nextNode.nextSibling
        while nextNode and nextNode.nodeType != node.ELEMENT_NODE:
            nextNode = nextNode.nextSibling
        parent.removeChild(node)
        parent.insertBefore(node, nextNode)
        index = view.tree.ItemFullIndex(self.item)
        self.flushSubtree(parentItem, parent)
        index[-1] += 1
        self.item = view.tree.ItemAtFullIndex(index)
        self.setModified()
        view.tree.SelectItem(self.item)

    def moveLeft(self):
        parentItem = view.tree.GetItemParent(self.item)
        grandParentItem = view.tree.GetItemParent(parentItem)
        parent = view.tree.GetPyData(parentItem)
        grandParent = view.tree.GetPyData(grandParentItem)
        if grandParent is Model.mainNode:
            grandParentComp = Manager.rootComponent
        else:
            grandParentComp = Manager.getNodeComp(grandParent)
        if not grandParentComp.canHaveChild(self.comp):
            wx.LogError('Incompatible parent/child: parent is %s, child is %s!' %
                        (grandParentComp.klass, self.comp.klass))
            return

        node = view.tree.GetPyData(self.item)
        nextItem = view.tree.GetNextSibling(parentItem)
        self.container.removeChild(parent, node)
        if nextItem:
            nextNode = view.tree.GetPyData(nextItem)
            grandParentComp.insertBefore(grandParent, node, nextNode)
        else:
            grandParentComp.appendChild(grandParent, node)
        index = view.tree.ItemFullIndex(self.item)
        self.flushSubtree(grandParentItem, grandParent)
        index.pop()
        index[-1] += 1
        self.item = view.tree.ItemAtFullIndex(index)
        self.setModified()
        view.tree.SelectItem(self.item)

    def moveRight(self):
        parentItem = view.tree.GetItemParent(self.item)
        parent = view.tree.GetPyData(parentItem)
        newParent = view.tree.GetPyData(view.tree.GetPrevSibling(self.item))
        newParentComp = Manager.getNodeComp(newParent)
        if not newParentComp.canHaveChild(self.comp):
            wx.LogError('Incompatible parent/child: parent is %s, child is %s!' %
                        (newParentComp.klass, self.comp.klass))
            return

        node = view.tree.GetPyData(self.item)
        self.container.removeChild(parent, node)
        newParentComp.appendChild(newParent, node)
        index = view.tree.ItemFullIndex(self.item)
        n = view.tree.GetChildrenCount(view.tree.GetPrevSibling(self.item))
        self.flushSubtree(parentItem, parent)
        index[-1] -= 1
        index.append(n)
        self.item = view.tree.ItemAtFullIndex(index)
        self.setModified()
        view.tree.SelectItem(self.item)

    def createLocalConf(self, path):
        name = os.path.splitext(path)[0]
        name += '.xcfg'
        return wx.FileConfig(localFilename=name)

    def createTestWin(self, item):
        TRACE('createTestWin')
        # Create a window with this resource
        node = view.tree.GetPyData(item)
        # Close old window, remember where it was
        comp = Manager.getNodeComp(node)
        # Create memory XML file
        elem = node.cloneNode(True)
        if not node.hasAttribute('name'):
            name = 'noname'
        else:
            name = node.getAttribute('name')
        elem.setAttribute('name', STD_NAME)
        Model.setTestElem(elem)
        Model.saveTestMemoryFile()
        xmlFlags = 0
        if not g.conf.useSubclassing:
            xmlFlags |= xrc.XRC_NO_SUBCLASSING
        # Use translations if encoding is not specified
        if not Model.dom.encoding:
            xmlFlags |= xrc.XRC_USE_LOCALE
        res = xrc.EmptyXmlResource(xmlFlags)
        xrc.XmlResource.Set(res)        # set as global
        # Init other handlers
        Manager.addXmlHandlers(res)
        Manager.preload(res)
        # Same module list
        res.Load('memory:test.xrc')
        object = None
        testWin = view.testWin
        try:
            try:
                frame, object = comp.makeTestWin(res, name)
                if not object:  # skip the rest
                    raise EOFError
                # Reset previous tree item and locate tool
                if testWin.item:
                    view.tree.SetItemBold(testWin.item, False)
                testWin.SetView(frame, object, item)
                testWin.Show()
                view.tree.SetItemBold(item, True)
            except EOFError:
                pass
            except NotImplementedError:
                wx.LogError('Test window not implemented for %s' % node.getAttribute('class'))
                logger.exception('error creating test view')
            except:
                logger.exception('error creating test view')
                wx.LogError('Error creating test view')
                if get_debug(): raise
        finally:
            # Cleanup
            res.Unload(TEST_FILE)
            xrc.XmlResource.Set(None)
            wx.MemoryFSHandler.RemoveFile(TEST_FILE)

    def closeTestWin(self):
        TRACE('closeTestWin')
        if not view.testWin.object: return
        view.tree.SetItemBold(view.testWin.item, False)
        view.frame.tb.ToggleTool(view.frame.ID_TOOL_LOCATE, False)
        if view.frame.miniFrame:
            view.frame.miniFrame.tb.ToggleTool(view.frame.ID_TOOL_LOCATE, False)
        view.testWin.Destroy()

    def refreshTestWin(self):
        '''Refresh test window after some change.'''
        TRACE('refreshTestWin')
        if not view.testWin.IsDirty(): return
        if not self.applied:
            self.update(self.item)
        # Dumb refresh
        self.createTestWin(view.testWin.item)
        self.highlight(self.item)
        if view.frame.miniFrame and view.frame.miniFrame.IsShown():
            view.frame.miniFrame.Raise()
        else:
            view.frame.Raise()

    def showXML(self):
        '''Show some source.'''
        node = view.tree.GetPyData(self.item)
        dom = MyDocument()
        node = dom.appendChild(node.cloneNode(True))
        Model.indent(dom, node)
        text = node.toxml()#Model.dom.encoding)
        dom.unlink()
        lines = text.split('\n')
        maxLen = max(map(len, lines))
        w = max(40, min(80, maxLen))
        h = max(20, min(40, len(lines)))
        dlg = view.ScrolledMessageDialog(view.frame, text, 'XML Source',
                                         textSize=(w,h), centered=False)
        dlg.Bind(wx.EVT_CLOSE, lambda evt: dlg.Destroy())
        dlg.Bind(wx.EVT_BUTTON, lambda evt: dlg.Destroy(), id=wx.ID_OK)
        dlg.Show()

    def generatePython(self, dataFile, pypath, embed, genGettext):
        try:
            from wx.tools import pywxrc
            rescomp = pywxrc.XmlResourceCompiler()
            rescomp.MakePythonModule([dataFile], pypath, embed, genGettext, 
                                     assignVariables=False)
        except:
            logger.exception('error generating python code')
            wx.LogError('Error generating python code : %s' % pypath)
            raise

# Singleton class
Presenter = g.Presenter = _Presenter()

undo.Presenter = Presenter
