# Copyright (c) 2000 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

"""XmlEditor module"""

__revision__ = "$Id: XmlEditor.py,v 1.14 2002/07/19 16:16:51 alf Exp $"

#
# NOTE: this file will need rewriting when XMLSchema is available
#

import GDK
from gtk import *
from XmlTree import XmlTree, utf8_to_gtk
from xml.parsers.xmlproc.dtdparser import DTDParser
from xml.parsers.xmlproc.xmldtd import CompleteDTD
from xml.dom.ext import ReleaseNode
from xml.dom.ext.reader import Sax2
from xml.dom.ext import StripXml, Print, PrettyPrint
try:
    from xml.dom import EMPTY_NAMESPACE as NO_NS
except:
    NO_NS = ''

try:
    from Ft.Xml.Xpath import Evaluate
except:
    from xml.xpath import Evaluate

def parse_dtd_file(dtd_file,dtd_obj=None):
    parser = DTDParser()
    dtd = dtd_obj or CompleteDTD(parser)
    parser.set_dtd_consumer(dtd)
    parser.set_dtd_object(dtd)
    parser.parse_resource(dtd_file)
    parser.deref()
    return dtd

def prefix_to_nsuri(node):
    map = {}
    ns_nodes = Evaluate('namespace::node()',node)
    for ns in ns_nodes:
        map[ns.localName]=ns.value
    return map

def get_prefix(qname):
    splitted = qname.split(':',1)
    if len(splitted) == 1:
        return None
    else:
        return splitted[0]
    
def get_uri(node,prefix):
    map = prefix_to_nsuri(node)
    return  map.get(prefix)
    
def getElementsName(child,dtd,list=None):
    """
    A recursive function that permits to extract allowed elements name from
    the complex output tuple of ElementType.get_content_model (something like
     (',', [('caption', '?'), ('|', [('col', '*'), ('colgroup', '*')], ''),
    ('thead', '?'), ('tfoot', '?'), ('|', [('tbody', '+'), ('tr', '+')], '')],
    '') : example of the allowed elements of the HTML tag <table>)
    Inputs the complex tuple to be processed.
    Inputs the dtd object from which the elements have been read
    Inputs the list in which will be stored the elements name
    Returns the list
    """
    templist = list or []
    # processes the case of child == None (occurs when element content
    # is specified to be ANY)
    if (child == None) :
        # the return list is set to all of the elements declared in the
        # DTD
        templist = dtd.get_elements()
    else :
        # if the penultimate element of the complex tuple is a list,
        # then we have to recursively process each element of the list.
        if type(child[-2])==type([]):
            for c in child[-2]:
                templist =  getElementsName(c,dtd,templist)
        # if the penultimate element of the complex tuple is a tuple,
        # then we have to recursively process this last tuple.
        elif type(child[-2])==type(()):
            templist = getElementsName(child[-2],dtd,templist)
        # else the penultimate element of the complex tuple is a string
        # containing an allowed element name. We just have to append it
        # the return list.
        else:
            templist.append(child[-2])
    return templist

def element_to_string(element,pretty=1,encoding='ISO-8859-1') :
    return elements_to_string([element],pretty,encoding)

def elements_to_string(elements,pretty=1,encoding='ISO-8859-1') :
    from StringIO import StringIO
    s = StringIO()
    s.write('<?xml version="1.0" encoding="%s"?>\n'%encoding)
    if pretty:
        for element in elements:
            PrettyPrint(element,s,encoding=encoding)
    else :
        for element in elements :
            Print(element,s,encoding=encoding)
    str = s.getvalue()
    s.close()
    return str



class XmlEditor(GtkVBox) :
    """XmlEditor"""

    def __init__(self,dtd,node,idref_attributes_readonly=1):
        GtkVBox.__init__(self)
        self.idref_attributes_readonly = idref_attributes_readonly
        self.node_unmodified=node
        self.node=node.cloneNode(1)
        self.dtd=dtd
        self.tree = XmlTree()
        scroll = GtkScrolledWindow()
        scroll.set_policy(POLICY_AUTOMATIC,POLICY_AUTOMATIC)
        scroll.add(self.tree)
        self.tree.set_expand_mode(1)
        self.tree.set_document(self.node)
        self.pack_start(scroll)
        self.editor = None
        self.set_editor('single line')
        self.tree.connect('button-press-event',self.tree_clicked)
        self.tree.connect('tree-select-row',self.item_selected)
        self.tooltips = GtkTooltips()

    def apply_changes(self):
        for (uri,qname) in self.node_unmodified.attributes.keys():
            self.node_unmodified.removeAttributeNS(uri,qname)

        for attr in self.node.attributes.values():
            self.node_unmodified.setAttributeNodeNS(attr.cloneNode(1))

        for child in self.node_unmodified.childNodes[:]:
            self.node_unmodified.removeChild(child)
            ReleaseNode(child)

        for child in self.node.childNodes:
            self.node_unmodified.appendChild(child.cloneNode(1))

    def set_editor(self,style,readonly=0,arg=None):
        if not self.editor:
            self.editor_style = ''
        if self.editor_style != style:
            expand=0
            self.editor_style = style
            if style not in ['single line', 'multi line', 'combo','node editor']:
                print 'Unknown style, using single line'
                style = 'single line'
                
            if self.editor:
                self.remove(self.editor)
                self.editor.destroy()
                
            if style == 'single line':
                self.editor = GtkEntry()
                self.editfield = self.editor

            elif style == 'combo':
                self.editor = GtkCombo()
                self.editor.set_value_in_list(1,0)
                self.editor.set_popdown_strings(arg)
                self.editfield = self.editor.entry
            elif style == 'node editor':
                self.editfield = GtkText()
                self.editor = GtkScrolledWindow()                
                self.editor.set_policy(POLICY_AUTOMATIC,POLICY_AUTOMATIC)
                self.editor.add(self.editfield)
                expand=1
                self.tooltips.set_tip(self.editfield,'Edit the node. Finish with Ctrl-Return. Be careful, you are working on bare metal. Changing the ID of an element for example can have unpredictable results. You have been warned...''' )                
            else:
                self.editfield = GtkText()
                self.editor = GtkScrolledWindow()                
                self.editor.set_policy(POLICY_AUTOMATIC,POLICY_AUTOMATIC)
                self.editor.add(self.editfield)
                
            self.editor.show_all()
            self.pack_start(self.editor,expand)
            if style != 'node editor':
                self.connect_id = self.editfield.connect('changed',self.item_edited)
        else:
            self.editfield.disconnect(self.connect_id)
            self.editfield.delete_text(0,-1)
            self.connect_id = self.editfield.connect('changed',self.item_edited)
            
        self.editfield.set_editable(not(readonly))
            
    def set_editor_value(self,value):
        self.editfield.delete_text(0,-1)
        self.editfield.insert_text(value)

    def item_edited(self,editor):
        treenode = editor.get_data('treenode')
        text = editor.get_chars(0,-1)
        node = self.tree.node_get_row_data(treenode)
        if node.nodeType == node.TEXT_NODE:
            node.data = text
        elif node.nodeType == node.ATTRIBUTE_NODE:
            node.value = text
        else:
            print 'Unknown node type',node.nodeType
        

    def element_hand_edited(self,editor):
        xml = editor.get_chars(0,-1)
        treenode = editor.get_data('treenode')
        xmlnode = editor.get_data('xmlnode')
        try:
            new_node = StripXml(Sax2.FromXml(xml,xmlnode.ownerDocument))
        except Exception,e:
            print e
        else:
            xmlparent = xmlnode.parentNode
            xmlparent.replaceChild(new_node,xmlnode)
                        
    def item_selected(self,tree,treenode,col):
        xmlnode = tree.node_get_row_data(treenode)
        if xmlnode.nodeType == xmlnode.ELEMENT_NODE:
            try:
                elt = self.dtd.get_elem(xmlnode.nodeName)
            except Exception:
                elt = None
            if elt and elt.get_content_model() == None  and \
               xmlnode not in map(lambda node,t=self.tree: t.node_get_row_data(node),
                                  self.tree.base_nodes()) :
                self.set_editor('node editor',0)
                self.editfield.set_data('treenode',treenode)
                self.editfield.set_data('xmlnode',xmlnode)
                self.set_editor_value(element_to_string(xmlnode))
                self.editfield.connect('activate',self.element_hand_edited)
                return
            else:
                self.set_editor('single line',1)
        elif xmlnode.nodeType == xmlnode.TEXT_NODE:
            self.set_editor('multi line',)
        elif xmlnode.nodeType == xmlnode.ATTRIBUTE_NODE:
            try:
                attr = self.dtd.get_elem(xmlnode.ownerElement.nodeName).get_attr(xmlnode.nodeName)
            except Exception:
                self.set_editor('single line',0)
            else:
                if attr and type(attr.get_type()) == type([]):
                    self.set_editor('combo',0,map(utf8_to_gtk,attr.get_type())) # 0 is read-write
                else:
                    self.set_editor('single line',
                                    attr.get_decl() == '#FIXED' or \
                                    attr.get_type() == 'ID' or \
                                    (attr.get_type() == 'IDREF' and self.idref_attributes_readonly)) # false is read-write
        
        self.editfield.set_data('treenode',treenode)
        self.editfield.set_data('xmlnode',xmlnode)
        self.set_editor_value(tree.node_get_text(treenode,2))


    def tree_clicked(self,tree,event):
        if event.button == 3 and tree.get_selection_info(event.x,event.y):
            row,col = tree.get_selection_info(event.x,event.y)
            xmlnode = tree.get_row_data(row)
            treenode = tree.node_nth(row)
            popup = None
            if xmlnode.nodeType == xmlnode.ELEMENT_NODE:
                popup = self.buildElementPopup(xmlnode,treenode)
            elif xmlnode.nodeType == xmlnode.ATTRIBUTE_NODE:
                popup = self.buildAttributePopup(xmlnode,treenode)
            if popup:
                #tree.select_row(row,col)
                popup.show_all()
                popup.popup(None,None,None,3,event.time)

    def add_attribute_activated(self,item,xmlnode,treenode,attribute):
        uri = get_uri(xmlnode,get_prefix(attribute.get_name()))
        if not xmlnode.hasAttributeNS(uri,attribute.get_name()):
            value = attribute.get_default() or ''
            xmlnode.setAttributeNS(uri,attribute.get_name(),value)
        
        
    def delete_child_activated(self,item,xmlnode,treenode):
        xmlnode.parentNode.removeChild(xmlnode)
        ReleaseNode(xmlnode)

    def delete_attribute_activated(self,item,xmlnode,treenode):
        xmlnode.ownerElement.removeAttributeNode(xmlnode)
        ReleaseNode(xmlnode)

        
    def add_child_activated(self,item,xmlnode,treenode,tag,position):
        if tag == '#text':
            new_element = xmlnode.ownerDocument.createTextNode('')
            isleaf=1
        else:
            uri = get_uri(xmlnode,get_prefix(tag))
            new_element = xmlnode.ownerDocument.createElementNS(uri,tag)
            isleaf=0
            
        if position < 0 or position >= len(xmlnode.childNodes):
            # on ajoute à la fin
            xmlnode.appendChild(new_element)
        else:
            xmlnode.insertBefore(new_element,xmlnode.childNodes[position])
                            
    def buildAttributePopup(self,xmlnode,treenode):
        try:
            attr = self.dtd.get_elem(xmlnode.ownerElement.tagName).get_attr(xmlnode.nodeName)
        except Exception:
            attr = None
            
        if attr == None or attr.get_decl() != '#REQUIRED':
            popup = GtkMenu()
            item = GtkMenuItem('delete')
            item.connect('activate',self.delete_attribute_activated,
                         xmlnode,treenode)
            popup.append(item)
            return popup
        

    def buildElementPopup(self,xmlnode,treenode):
        popup = GtkMenu()
        try:
            elt = self.dtd.get_elem(xmlnode.nodeName)
        except Exception:
            # the element is not known by the DTD
            popup.append(GtkMenuItem())
            item = GtkMenuItem('Delete')
            item.connect('activate',self.delete_child_activated,
                         xmlnode,treenode)
            popup.append(item)
            return

        item = GtkMenuItem('Add attribute')
        popup.append(item)
        submenu = GtkMenu()
        item.set_submenu(submenu)
        for attr in elt.get_attr_list():
            subitem = GtkMenuItem(attr)
            submenu.append(subitem)
            subitem.connect('activate',self.add_attribute_activated,
                            xmlnode,treenode,elt.get_attr(attr))

        item = GtkMenuItem('Append Child')
        popup.append(item)
        submenu = self.__make_add_child_submenu(xmlnode,treenode,-1)
        item.set_submenu(submenu)
                
        item = GtkMenuItem('Prepend Child')
        popup.append(item)
        submenu = self.__make_add_child_submenu(xmlnode,treenode,0)
        item.set_submenu(submenu)

        if xmlnode not in map(lambda node,t=self.tree: t.node_get_row_data(node),
                                self.tree.base_nodes()):
            parentnode = xmlnode.parentNode
            index = parentnode.childNodes.index(xmlnode)
            item = GtkMenuItem('Insert sibling before')
            popup.append(item)
            submenu = self.__make_add_child_submenu(parentnode,treenode.parent,index)
            item.set_submenu(submenu)

            item = GtkMenuItem('Insert sibling after')
            popup.append(item)
            submenu = self.__make_add_child_submenu(parentnode,treenode.parent,index+1)
            item.set_submenu(submenu)

            popup.append(GtkMenuItem())
            item = GtkMenuItem('Delete')
            item.connect('activate',self.delete_child_activated,
                         xmlnode,treenode)
            popup.append(item)
        return popup

    def __make_add_child_submenu(self,xmlnode,treenode,position):
        submenu = GtkMenu()
        elt = self.dtd.get_elem(xmlnode.nodeName)
        if elt.get_content_model():
            # Compute current state
            state = elt.get_start_state()
            for child in xmlnode.childNodes[:position]:
                state = elt.next_state(state,child.nodeName)
            authorized = elt.get_valid_elements(state)
        else:
            # ANY content model
            authorized = self.dtd.get_elements()

        possible = getElementsName(elt.get_content_model(),self.dtd)
        for c in authorized:
            if c == '#PCDATA':
                c = '#text'
            subitem = GtkMenuItem(c)
            subitem.connect('activate',self.add_child_activated,
                            xmlnode,treenode,c,position)
            submenu.append(subitem)
        if len(possible)>len(authorized):
            submenu.append(GtkMenuItem())
            for c in possible:
                if c not in authorized:
                    if c == '#PCDATA':
                        c = '#text'
                    subitem = GtkMenuItem(c)
                    subitem.connect('activate',self.add_child_activated,
                                    xmlnode,treenode,c,position)
                    submenu.append(subitem)
        return submenu
        
class XmlEditorDialog(GtkDialog):
    """XmlEditorDialog"""
    
    def __init__(self,title,dtd,node):
        GtkDialog.__init__(self)
        self.set_title(title)
        self.editor = XmlEditor(dtd,node)
        self.vbox.pack_start(self.editor)
        
        self.ok_button=GtkButton('OK')
        self.ok_button.set_flags(CAN_DEFAULT|HAS_DEFAULT)
        self.apply_button=GtkButton('Apply')
        self.apply_button.set_flags(CAN_DEFAULT)
        self.cancel_button=GtkButton('Cancel')
        self.cancel_button.set_flags(CAN_DEFAULT)

        self.action_area.pack_start(self.ok_button)
        self.action_area.pack_start(self.apply_button)
        self.action_area.pack_start(self.cancel_button)

        self.ok_button.connect('clicked',self.ok_button_clicked)
        self.apply_button.connect('clicked',self.apply_button_clicked)
        self.cancel_button.connect('clicked',self.cancel_button_clicked)
        
        self.set_usize(350,300)

        self.change_listeners = []

    def add_change_listener(self,listener,*args):
        self.change_listeners.append((listener,args))


    def ok_button_clicked(self,button):
        self.apply_button_clicked(button)
        self.cancel_button_clicked(button)
    
    def apply_button_clicked(self,button):
        self.editor.apply_changes()
        for (l,args) in self.change_listeners:
            apply(l,args)
            
    def cancel_button_clicked(self,button):
        self.destroy()


class XmlEditorWindow(GtkWindow):
    """XmlEditorWindow (inherits from GtkWindow and pass)"""
    pass


# test #########################################################################

if __name__=='__main__':
    from xml.dom.ext.reader import Sax2
    from xml.dom.ext import PrettyPrint
    import sys
    syntax = "XmlEditor.py file dtd"
    if len(sys.argv)!=3 :
        print syntax
    dtd = parse_dtd_file(sys.argv[2])
    domtree=Sax2.FromXmlFile(sys.argv[1]).documentElement
    window=XmlEditorDialog('Edition of '+sys.argv[1],dtd,domtree)
    window.connect('delete_event',lambda x,e:mainquit())
    window.show_all()
    mainloop()
    saveFile = open(sys.argv[1],'w')
    PrettyPrint(domtree.ownerDocument,
                stream = saveFile)
    saveFile.close()
