#    Gedit snippets plugin
#    Copyright (C) 2005-2006  Jesse van den Kieboom <jesse@icecrew.nl>
#
#    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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

import os
import weakref
import sys
import tempfile
import re

from gi.repository import Gdk, Gtk

import xml.etree.ElementTree as et
from helper import *

class NamespacedId:
        def __init__(self, namespace, id):
                if not id:
                        self.id = None
                else:
                        if namespace:
                                self.id = namespace + '-'
                        else:
                                self.id = 'global-'

                        self.id += id

class SnippetData:
        PROPS = {'tag': '', 'text': '', 'description': 'New snippet',
                        'accelerator': '', 'drop-targets': ''}

        def __init__(self, node, library):
                self.priv_id = node.attrib.get('id')

                self.set_library(library)
                self.valid = False
                self.set_node(node)

        def can_modify(self):
                return (self.library and (isinstance(self.library(), SnippetsUserFile)))

        def set_library(self, library):
                if library:
                        self.library = weakref.ref(library)
                else:
                        self.library = None

                self.id = NamespacedId(self.language(), self.priv_id).id

        def set_node(self, node):
                if self.can_modify():
                        self.node = node
                else:
                        self.node = None

                self.init_snippet_data(node)

        def init_snippet_data(self, node):
                if node == None:
                        return

                self.override = node.attrib.get('override')

                self.properties = {}
                props = SnippetData.PROPS.copy()

                # Store all properties present
                for child in node:
                        if child.tag in props:
                                del props[child.tag]

                                # Normalize accelerator
                                if child.tag == 'accelerator' and child.text != None:
                                        keyval, mod = Gtk.accelerator_parse(child.text)

                                        if Gtk.accelerator_valid(keyval, mod):
                                                child.text = unicode(Gtk.accelerator_name(keyval, mod), 'utf-8')
                                        else:
                                                child.text = ''

                                if self.can_modify():
                                        self.properties[child.tag] = child
                                else:
                                        self.properties[child.tag] = child.text or ''

                # Create all the props that were not found so we stay consistent
                for prop in props:
                        if self.can_modify():
                                child = et.SubElement(node, prop)

                                child.text = props[prop]
                                self.properties[prop] = child
                        else:
                                self.properties[prop] = props[prop]

                self.check_validation()

        def check_validation(self):
                if not self['tag'] and not self['accelerator'] and not self['drop-targets']:
                        return False

                library = Library()
                keyval, mod = Gtk.accelerator_parse(self['accelerator'])

                self.valid = library.valid_tab_trigger(self['tag']) and \
                                (not self['accelerator'] or library.valid_accelerator(keyval, mod))

        def _format_prop(self, prop, value):
                if prop == 'drop-targets' and value != '':
                        return re.split('\\s*[,;]\\s*', value)
                else:
                        return value

        def __getitem__(self, prop):
                if prop in self.properties:
                        if self.can_modify():
                                return self._format_prop(prop, self.properties[prop].text or '')
                        else:
                                return self._format_prop(prop, self.properties[prop] or '')

                return self._format_prop(prop, '')

        def __setitem__(self, prop, value):
                if not prop in self.properties:
                        return

                if isinstance(value, list):
                        value = u','.join(value)

                if not self.can_modify() and self.properties[prop] != value:
                        # ohoh, this is not can_modify, but it needs to be changed...
                        # make sure it is transfered to the changes file and set all the
                        # fields.
                        # This snippet data container will effectively become the container
                        # for the newly created node, but transparently to whoever uses
                        # it
                        self._override()

                if self.can_modify() and self.properties[prop].text != value:
                        if self.library():
                                self.library().tainted = True

                        oldvalue = self.properties[prop].text
                        self.properties[prop].text = value

                        if prop == 'tag' or prop == 'accelerator' or prop == 'drop-targets':
                                container = Library().container(self.language())
                                container.prop_changed(self, prop, oldvalue)

                self.check_validation()

        def language(self):
                if self.library and self.library():
                        return self.library().language
                else:
                        return None

        def is_override(self):
                return self.override and Library().overridden[self.override]

        def to_xml(self):
                return self._create_xml()

        def _create_xml(self, parent=None, update=False, attrib={}):
                # Create a new node
                if parent != None:
                        element = et.SubElement(parent, 'snippet', attrib)
                else:
                        element = et.Element('snippet')

                # Create all the properties
                for p in self.properties:
                        prop = et.SubElement(element, p)
                        prop.text = self[p]

                        if update:
                                self.properties[p] = prop

                return element

        def _override(self):
                # Find the user file
                target = Library().get_user_library(self.language())

                # Create a new node there with override
                element = self._create_xml(target.root, True, {'override': self.id})

                # Create an override snippet data, feed it element so that it stores
                # all the values and then set the node to None so that it only contains
                # the values in .properties
                override = SnippetData(element, self.library())
                override.set_node(None)
                override.id = self.id

                # Set our node to the new element
                self.node = element

                # Set the override to our id
                self.override = self.id
                self.id = None

                # Set the new library
                self.set_library(target)

                # The library is tainted because we added this snippet
                target.tainted = True

                # Add the override
                Library().overridden[self.override] = override

        def revert(self, snippet):
                userlib = self.library()
                self.set_library(snippet.library())

                userlib.remove(self.node)

                self.set_node(None)

                # Copy the properties
                self.properties = snippet.properties

                # Set the id
                self.id = snippet.id

                # Reset the override flag
                self.override = None

class SnippetsTreeBuilder(et.TreeBuilder):
        def __init__(self, start=None, end=None):
                et.TreeBuilder.__init__(self)
                self.set_start(start)
                self.set_end(end)

        def set_start(self, start):
                self._start_cb = start

        def set_end(self, end):
                self._end_cb = end

        def start(self, tag, attrs):
                result = et.TreeBuilder.start(self, tag, attrs)

                if self._start_cb:
                        self._start_cb(result)

                return result

        def end(self, tag):
                result = et.TreeBuilder.end(self, tag)

                if self._end_cb:
                        self._end_cb(result)

                return result

class LanguageContainer:
        def __init__(self, language):
                self.language = language
                self.snippets = []
                self.snippets_by_prop = {'tag': {}, 'accelerator': {}, 'drop-targets': {}}
                self.accel_group = Gtk.AccelGroup()
                self._refs = 0

        def _add_prop(self, snippet, prop, value=0):
                if value == 0:
                        value = snippet[prop]

                if not value or value == '':
                        return

                snippets_debug('Added ', prop ,' ', value, ' to ', str(self.language))

                if prop == 'accelerator':
                        keyval, mod = Gtk.accelerator_parse(value)
                        self.accel_group.connect(keyval, mod, 0, \
                                        Library().accelerator_activated)

                snippets = self.snippets_by_prop[prop]

                if not isinstance(value, list):
                        value = [value]

                for val in value:
                        if val in snippets:
                                snippets[val].append(snippet)
                        else:
                                snippets[val] = [snippet]

        def _remove_prop(self, snippet, prop, value=0):
                if value == 0:
                        value = snippet[prop]

                if not value or value == '':
                        return

                snippets_debug('Removed ', prop, ' ', value, ' from ', str(self.language))

                if prop == 'accelerator':
                        keyval, mod = Gtk.accelerator_parse(value)
                        self.accel_group.disconnect_key(keyval, mod)

                snippets = self.snippets_by_prop[prop]

                if not isinstance(value, list):
                        value = [value]

                for val in value:
                        try:
                                snippets[val].remove(snippet)
                        except:
                                True

        def append(self, snippet):
                tag = snippet['tag']
                accelerator = snippet['accelerator']

                self.snippets.append(snippet)

                self._add_prop(snippet, 'tag')
                self._add_prop(snippet, 'accelerator')
                self._add_prop(snippet, 'drop-targets')

                return snippet

        def remove(self, snippet):
                try:
                        self.snippets.remove(snippet)
                except:
                        True

                self._remove_prop(snippet, 'tag')
                self._remove_prop(snippet, 'accelerator')
                self._remove_prop(snippet, 'drop-targets')

        def prop_changed(self, snippet, prop, oldvalue):
                snippets_debug('PROP CHANGED (', prop, ')', oldvalue)

                self._remove_prop(snippet, prop, oldvalue)
                self._add_prop(snippet, prop)

        def from_prop(self, prop, value):
                snippets = self.snippets_by_prop[prop]

                if prop == 'drop-targets':
                        s = []

                        # FIXME: change this to use
                        # gnomevfs.mime_type_get_equivalence when it comes
                        # available
                        for key, val in snippets.items():
                                if not value.startswith(key):
                                        continue

                                for snippet in snippets[key]:
                                        if not snippet in s:
                                                s.append(snippet)

                        return s
                else:
                        if value in snippets:
                                return snippets[value]
                        else:
                                return []

        def ref(self):
                self._refs += 1

                return True

        def unref(self):
                if self._refs > 0:
                        self._refs -= 1

                return self._refs != 0

class SnippetsSystemFile:
        def __init__(self, path=None):
                self.path = path
                self.loaded = False
                self.language = None
                self.ok = True
                self.need_id = True

        def load_error(self, message):
                sys.stderr.write("An error occurred loading " + self.path + ":\n")
                sys.stderr.write(message + "\nSnippets in this file will not be " \
                                "available, please correct or remove the file.\n")

        def _add_snippet(self, element):
                if not self.need_id or element.attrib.get('id'):
                        self.loading_elements.append(element)

        def set_language(self, element):
                self.language = element.attrib.get('language')

                if self.language:
                        self.language = self.language.lower()

        def _set_root(self, element):
                self.set_language(element)

        def _preprocess_element(self, element):
                if not self.loaded:
                        if not element.tag == "snippets":
                                self.load_error("Root element should be `snippets' instead " \
                                                "of `%s'" % element.tag)
                                return False
                        else:
                                self._set_root(element)
                                self.loaded = True
                elif element.tag != 'snippet' and not self.insnippet:
                        self.load_error("Element should be `snippet' instead of `%s'" \
                                        % element.tag)
                        return False
                else:
                        self.insnippet = True

                return True

        def _process_element(self, element):
                if element.tag == 'snippet':
                        self._add_snippet(element)
                        self.insnippet = False

                return True

        def ensure(self):
                if not self.ok or self.loaded:
                        return

                self.load()

        def parse_xml(self, readsize=16384):
                if not self.path:
                        return

                elements = []

                builder = SnippetsTreeBuilder( \
                                lambda node: elements.append((node, True)), \
                                lambda node: elements.append((node, False)))

                parser = et.XMLTreeBuilder(target=builder)
                self.insnippet = False

                try:
                        f = open(self.path, "r")
                except IOError:
                        self.ok = False
                        return

                while True:
                        try:
                                data = f.read(readsize)
                        except IOError:
                                self.ok = False
                                break

                        if not data:
                                break

                        try:
                                parser.feed(data)
                        except Exception:
                                self.ok = False
                                break

                        for element in elements:
                                yield element

                        del elements[:]

                f.close()

        def load(self):
                if not self.ok:
                        return

                snippets_debug("Loading library (" + str(self.language) + "): " + \
                                self.path)

                self.loaded = False
                self.ok = False
                self.loading_elements = []

                for element in self.parse_xml():
                        if element[1]:
                                if not self._preprocess_element(element[0]):
                                        del self.loading_elements[:]
                                        return
                        else:
                                if not self._process_element(element[0]):
                                        del self.loading_elements[:]
                                        return

                for element in self.loading_elements:
                        snippet = Library().add_snippet(self, element)

                del self.loading_elements[:]
                self.ok = True

        # This function will get the language for a file by just inspecting the
        # root element of the file. This is provided so that a cache can be built
        # for which file contains which language.
        # It returns the name of the language
        def ensure_language(self):
                if not self.loaded:
                        self.ok = False

                        for element in self.parse_xml(256):
                                if element[1]:
                                        if element[0].tag == 'snippets':
                                                self.set_language(element[0])
                                                self.ok = True

                                        break

        def unload(self):
                snippets_debug("Unloading library (" + str(self.language) + "): " + \
                                self.path)
                self.language = None
                self.loaded = False
                self.ok = True

class SnippetsUserFile(SnippetsSystemFile):
        def __init__(self, path=None):
                SnippetsSystemFile.__init__(self, path)
                self.tainted = False
                self.need_id = False

        def _set_root(self, element):
                SnippetsSystemFile._set_root(self, element)
                self.root = element

        def add_prop(self, node, tag, data):
                if data[tag]:
                        prop = et.SubElement(node, tag)
                        prop.text = data[tag]

                        return prop
                else:
                        return None

        def new_snippet(self, properties=None):
                if (not self.ok) or self.root == None:
                        return None

                element = et.SubElement(self.root, 'snippet')

                if properties:
                        for prop in properties:
                                sub = et.SubElement(element, prop)
                                sub.text = properties[prop]

                self.tainted = True

                return Library().add_snippet(self, element)

        def set_language(self, element):
                SnippetsSystemFile.set_language(self, element)

                filename = os.path.basename(self.path).lower()

                if not self.language and filename == "global.xml":
                        self.modifier = True
                elif self.language and filename == self.language + ".xml":
                        self.modifier = True
                else:
                        self.modifier = False

        def create_root(self, language):
                if self.loaded:
                        snippets_debug('Not creating root, already loaded')
                        return

                if language:
                        root = et.Element('snippets', {'language': language})
                        self.path = os.path.join(Library().userdir, language.lower() + '.xml')
                else:
                        root = et.Element('snippets')
                        self.path = os.path.join(Library().userdir, 'global.xml')

                self._set_root(root)
                self.loaded = True
                self.ok = True
                self.tainted = True
                self.save()

        def remove(self, element):
                try:
                        self.root.remove(element)
                        self.tainted = True
                except:
                        return

                try:
                        first = self.root[0]
                except:
                        # No more elements, this library is useless now
                        Library().remove_library(self)

        def save(self):
                if not self.ok or self.root == None or not self.tainted:
                        return

                path = os.path.dirname(self.path)

                try:
                        if not os.path.isdir(path):
                                os.makedirs(path, 0755)
                except OSError:
                        # TODO: this is bad...
                        sys.stderr.write("Error in making dirs\n")

                try:
                        write_xml(self.root, self.path, ('text', 'accelerator'))
                        self.tainted = False
                except IOError:
                        # Couldn't save, what to do
                        sys.stderr.write("Could not save user snippets file to " + \
                                        self.path + "\n")

        def unload(self):
                SnippetsSystemFile.unload(self)
                self.root = None

class Singleton(object):
        _instance = None

        def __new__(cls, *args, **kwargs):
                if not cls._instance:
                        cls._instance = super(Singleton, cls).__new__(
                                         cls, *args, **kwargs)
                        cls._instance.__init_once__()

                return cls._instance

class Library(Singleton):
        def __init_once__(self):
                self._accelerator_activated_cb = []
                self.loaded = False
                self.check_buffer = Gtk.TextBuffer()

        def set_dirs(self, userdir, systemdirs):
                self.userdir = userdir
                self.systemdirs = systemdirs

                self.libraries = {}
                self.containers = {}
                self.overridden = {}
                self.loaded_ids = []

                self.loaded = False

        def add_accelerator_callback(self, cb):
                self._accelerator_activated_cb.append(cb)

        def remove_accelerator_callback(self, cb):
                self._accelerator_activated_cb.remove(cb)

        def accelerator_activated(self, group, obj, keyval, mod):
                ret = False

                for cb in self._accelerator_activated_cb:
                        ret = cb(group, obj, keyval, mod)

                        if ret:
                                break

                return ret

        def add_snippet(self, library, element):
                container = self.container(library.language)
                overrided = self.overrided(library, element)

                if overrided:
                        overrided.set_library(library)
                        snippets_debug('Snippet is overriden: ' + overrided['description'])
                        return None

                snippet = SnippetData(element, library)

                if snippet.id in self.loaded_ids:
                        snippets_debug('Not added snippet ' + str(library.language) + \
                                        '::' + snippet['description'] + ' (duplicate)')
                        return None

                snippet = container.append(snippet)
                snippets_debug('Added snippet ' + str(library.language) + '::' + \
                                snippet['description'])

                if snippet and snippet.override:
                        self.add_override(snippet)

                if snippet.id:
                        self.loaded_ids.append(snippet.id)

                return snippet

        def container(self, language):
                language = self.normalize_language(language)

                if not language in self.containers:
                        self.containers[language] = LanguageContainer(language)

                return self.containers[language]

        def get_user_library(self, language):
                target = None

                if language in self.libraries:
                        for library in self.libraries[language]:
                                if isinstance(library, SnippetsUserFile) and library.modifier:
                                        target = library
                                elif not isinstance(library, SnippetsUserFile):
                                        break

                if not target:
                        # Create a new user file then
                        snippets_debug('Creating a new user file for language ' + \
                                        str(language))
                        target = SnippetsUserFile()
                        target.create_root(language)
                        self.add_library(target)

                return target

        def new_snippet(self, language, properties=None):
                language = self.normalize_language(language)
                library = self.get_user_library(language)

                return library.new_snippet(properties)

        def revert_snippet(self, snippet):
                # This will revert the snippet to the one it overrides
                if not snippet.can_modify() or not snippet.override in self.overridden:
                        # It can't be reverted, shouldn't happen, but oh..
                        return

                # The snippet in self.overriden only contains the property contents and
                # the library it belongs to
                revertto = self.overridden[snippet.override]
                del self.overridden[snippet.override]

                if revertto:
                        snippet.revert(revertto)

                        if revertto.id:
                                self.loaded_ids.append(revertto.id)

        def remove_snippet(self, snippet):
                if not snippet.can_modify() or snippet.is_override():
                        return

                # Remove from the library
                userlib = snippet.library()
                userlib.remove(snippet.node)

                # Remove from the container
                container = self.containers[userlib.language]
                container.remove(snippet)

        def overrided(self, library, element):
                id = NamespacedId(library.language, element.attrib.get('id')).id

                if id in self.overridden:
                        snippet = SnippetData(element, None)
                        snippet.set_node(None)

                        self.overridden[id] = snippet
                        return snippet
                else:
                        return None

        def add_override(self, snippet):
                snippets_debug('Add override:', snippet.override)
                if not snippet.override in self.overridden:
                        self.overridden[snippet.override] = None

        def add_library(self, library):
                library.ensure_language()

                if not library.ok:
                        snippets_debug('Library in wrong format, ignoring')
                        return False

                snippets_debug('Adding library (' + str(library.language) + '): ' + \
                                library.path)

                if library.language in self.libraries:
                        # Make sure all the user files are before the system files
                        if isinstance(library, SnippetsUserFile):
                                self.libraries[library.language].insert(0, library)
                        else:
                                self.libraries[library.language].append(library)
                else:
                        self.libraries[library.language] = [library]

                return True

        def remove_library(self, library):
                if not library.ok:
                        return

                if library.path and os.path.isfile(library.path):
                        os.unlink(library.path)

                try:
                        self.libraries[library.language].remove(library)
                except KeyError:
                        True

                container = self.containers[library.language]

                for snippet in list(container.snippets):
                        if snippet.library() == library:
                                container.remove(snippet)

        def add_user_library(self, path):
                library = SnippetsUserFile(path)
                return self.add_library(library)

        def add_system_library(self, path):
                library = SnippetsSystemFile(path)
                return self.add_library(library)

        def find_libraries(self, path, searched, addcb):
                snippets_debug("Finding in: " + path)

                if not os.path.isdir(path):
                        return searched

                files = os.listdir(path)
                searched.append(path)

                for f in files:
                        f = os.path.realpath(os.path.join(path, f))

                        # Determine what language this file provides snippets for
                        if os.path.isfile(f):
                                addcb(f)

                return searched

        def normalize_language(self, language):
                if language:
                        return language.lower()

                return language

        def remove_container(self, language):
                for snippet in self.containers[language].snippets:
                        if snippet.id in self.loaded_ids:
                                self.loaded_ids.remove(snippet.id)

                        if snippet.override in self.overridden:
                                del self.overridden[snippet.override]

                del self.containers[language]

        def get_accel_group(self, language):
                language = self.normalize_language(language)
                container = self.container(language)

                self.ensure(language)
                return container.accel_group

        def save(self, language):
                language = self.normalize_language(language)

                if language in self.libraries:
                        for library in self.libraries[language]:
                                if isinstance(library, SnippetsUserFile):
                                        library.save()
                                else:
                                        break

        def ref(self, language):
                language = self.normalize_language(language)

                snippets_debug('Ref:', language)
                self.container(language).ref()

        def unref(self, language):
                language = self.normalize_language(language)

                snippets_debug('Unref:', language)

                if language in self.containers:
                        if not self.containers[language].unref() and \
                                        language in self.libraries:

                                for library in self.libraries[language]:
                                        library.unload()

                                self.remove_container(language)

        def ensure(self, language):
                self.ensure_files()
                language = self.normalize_language(language)

                # Ensure language as well as the global snippets (None)
                for lang in (None, language):
                        if lang in self.libraries:
                                # Ensure the container exists
                                self.container(lang)

                                for library in self.libraries[lang]:
                                        library.ensure()

        def ensure_files(self):
                if self.loaded:
                        return

                searched = []
                searched = self.find_libraries(self.userdir, searched, \
                                self.add_user_library)

                for d in self.systemdirs:
                        searched = self.find_libraries(d, searched, \
                                        self.add_system_library)

                self.loaded = True

        def valid_accelerator(self, keyval, mod):
                mod &= Gtk.accelerator_get_default_mod_mask()

                return (mod and (Gdk.keyval_to_unicode(keyval) or \
                                keyval in range(Gdk.KEY_F1, Gdk.KEY_F12 + 1)))

        def valid_tab_trigger(self, trigger):
                if not trigger:
                        return True

                if trigger.isdigit():
                        return False

                self.check_buffer.set_text(trigger)

                start, end = self.check_buffer.get_bounds()
                text = unicode(self.check_buffer.get_text(start, end, False), 'utf-8')

                s = start.copy()
                e = end.copy()

                end.backward_word_start()
                start.forward_word_end()

                return (s.equal(end) and e.equal(start)) or (len(text) == 1 and not (text.isalnum() or text.isspace()))

        # Snippet getters
        # ===============
        def _from_prop(self, prop, value, language=None):
                self.ensure_files()

                result = []
                language = self.normalize_language(language)

                if not language in self.containers:
                        return []

                self.ensure(language)
                result = self.containers[language].from_prop(prop, value)

                if len(result) == 0 and language and None in self.containers:
                        result = self.containers[None].from_prop(prop, value)

                return result

        # Get snippets for a given language
        def get_snippets(self, language=None):
                self.ensure_files()
                language = self.normalize_language(language)

                if not language in self.libraries:
                        return []

                snippets = []
                self.ensure(language)

                return list(self.containers[language].snippets)

        # Get snippets for a given accelerator
        def from_accelerator(self, accelerator, language=None):
                return self._from_prop('accelerator', accelerator, language)

        # Get snippets for a given tag
        def from_tag(self, tag, language=None):
                return self._from_prop('tag', tag, language)

        # Get snippets for a given drop target
        def from_drop_target(self, drop_target, language=None):
                return self._from_prop('drop-targets', drop_target, language)

# ex:ts=8:et:
