# vPresent is an immersive presentation creation and display application.
# Copyright (C) 2007-2008 by Infiscape Corporation
#
# 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 Street, Fifth Floor, Boston, MA 02110-1301, USA.

# Maestro is Copyright (C) 2006-2007 by Infiscape
#
# Original Author: Aron Bierbaum
#
# 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 Street, Fifth Floor, Boston, MA 02110-1301, USA.

import os.path
import re
import types
import xml.dom.minidom

try:
   import xml.etree.ElementTree as ET
except:
   import elementtree.ElementTree as ET

from PyQt4 import QtGui


class Setting:
   '''
   Adapter for SettingsDatabase entries. A setting has three components:

      1. A value (a string which may be None), accessed through the value
         property.
      2. Zero or more atributes (string values), accessed using the []
         operator (see the special methods __setitem__() and __getitem__()).
      3. Zero or more children (instances of Setting), accessed using the
         getChildren() and addChild() methods.

   User code can access the 'value' property or any of the interface
   methods. Accessing the 'element' member directly is discouraged.
   '''
   def __init__(self, element = None, name = None, value = None,
                attributes = None):
      if element is None:
         if name is None:
            raise ValueError, 'Cannot create a Setting object without a name'

         element = ET.Element(name)
         element.text = value

         if attributes is not None:
            for k, v in attributes.iteritems():
               element.attrib[k] = str(v)

      self.element = element

   def __getattr__(self, name):
      if name == 'name':
         return self.__dict__['element'].tag
      elif name == 'value':
         return self.__dict__['element'].text
      elif name == 'children':
         return [Setting(e) for e in self.__dict__['element'].getchildren()]
      elif not self.__dict__.has_key(name):
         raise AttributeError, "Setting instance has no attribute '%s'" % name
      else:
         return self.__dict__[name]

   def __setattr__(self, name, value):
      if name == 'value':
         self.__dict__['element'].text = str(value)
      elif name == 'children':
         elt = self.__dict__['element']
         elt.clear()
         new_children = [v.element for v in value]
         elt[:] = new_children
      else:
         self.__dict__[name] = value

   def __getitem__(self, key):
      return self.element.attrib[key]

   def __setitem__(self, key, value):
      self.element.attrib[key] = str(value)

   def get(self, key, default = None):
      try:
         return self.__getitem__(key)
      except KeyError:
         return default

   def getName(self):
      return self.element.tag

   def getChild(self, pattern):
      '''
      getChild(str) -> Setting object or None

      Returns the first child of this object that matches the given pattern.
      If no matching child is found, then None is returned.
      '''
      child = None
      e     = self.element.find(pattern)

      if e is not None:
         child = Setting(e)

      return child

   def getChildAtIndex(self, index):
      return Setting(self.element[index])

   def getChildren(self, pattern = None):
      if pattern is None:
         children = self.element.getchildren()
      else:
         children = self.element.findall(pattern)

      return [Setting(c) for c in children]

   def addChild(self, child):
      self.insertChild(len(self.element.getchildren()), child)

   def insertChild(self, index, child):
      if not isinstance(child, Setting):
         raise ValueError, 'Invalid type for child'

      self.element.insert(index, child.element)

   def removeChild(self, indexOrChild):
      '''
      Removes the identified child from this object. The type of the
      indexOrChild parameter may be an integer or another Setting object.
      '''
      if type(indexOrChild) == types.IntType:
         del self.element[indexOrChild]
      else:
         if not isinstance(indexOrChild, Setting):
            raise ValueError, 'Invalid type for indexOrChild'

         for (i, e) in enumerate(self.element):
            if e is indexOrChild.element:
               del self.element[i]
               return

         # XXX: Is this really the correct exception type to raise?
         raise IndexError, "No such child"

class SettingsDatabase:
   '''
   Provides an interface to a database of application settings by mimicking
   the behavior/interface of a Python dictionary. The database createas a
   mapping of strings (keys) to objects of type Setting (values).

   Behind the scenes, the database is an XML document as represented by
   ElementTree.Element objects. This class and the Setting class set out to
   mask this detail so that calling code does not need to use the ElementTree
   interface directly.
   '''

   sDatabaseVersion = '1'

   def __init__(self, file):
      self.file = file
      self.root = None

   def load(self):
      '''
      Attempts to load the settings database in the file named when this
      object was created. If loading and parsing the database fails, an
      exception will be raised.
      '''
      self.root = ET.parse(self.file)

      # Ensure that the database is versioned. If it is not, then we assume
      # that it is using the most recent version.
      root_elt = self.root.getroot()
      if root_elt is not None and not root_elt.attrib.has_key('version'):
         root_elt.attrib['version'] = SettingsDatabase.sDatabaseVersion

   def create(self, rootToken):
      '''
      Creates an empty settings database using the given token (a string) as
      the root. If this instance already held a loaded settings database, it
      is replaced with the empty database. Saving this new database to disk
      without changing the file name would cause all previous settings to be
      lost.
      '''
      root_elt = ET.Element(rootToken)
      root_elt.attrib['version'] = SettingsDatabase.sDatabaseVersion
      self.root = ET.ElementTree(root_elt)

   def createFile(file, rootToken):
      '''
      Creates a new, empty settings database using the given token (a string)
      as the root and writes the new database to the named file. If file
      names an extant file, that file is overwritten. If the new, empty
      database cannot be written to the named file, an exception will be
      raised.
      '''
      SettingsDatabase.writeTree(file, ET.Element(rootToken))

   createFile = staticmethod(createFile)

   sLeadSpacesRe  = re.compile('>\s+')
   sTrailSpacesRe = re.compile('\s+<')

   def writeTree(fileName, root, indent = '   ', newline = '\n'):
      '''
      Stores the given XML tree (an ElementTree.Element object) to the named
      file (a string). The XML is normalized and formatted before being
      written to the file. If the XML cannot be written to the named file, an
      exception (usually IOError) is raised.
      '''
      cfg_text = ET.tostring(root)
      cfg_text = SettingsDatabase.sLeadSpacesRe.sub('>', cfg_text)
      cfg_text = SettingsDatabase.sTrailSpacesRe.sub('<', cfg_text)
      dom = xml.dom.minidom.parseString(cfg_text)
      dom.normalize()
      output_file = file(fileName, 'w')
      output_file.write(dom.toprettyxml(indent = indent, newl = newline))
      output_file.close()

   writeTree = staticmethod(writeTree)

   def save(self, newFileName = None):
      '''
      Saves this settings database to a file--either the file named when this
      object was created or the named file. If the save operation fails, an
      exception (usually IOError) is raised. If this database is currently
      empty (i.e., self.root is None), an exception of type Exception is
      raised.
      '''
      if self.root is None:
         raise Exception, 'Empty settings database'

      if newFileName is None:
         newFileName = self.file

      SettingsDatabase.writeTree(newFileName, self.root.getroot())

   def __getitem__(self, key):
      if self.root is None:
         raise Exception, 'Empty settings database'

      element = self.root.find(key)
      if element is None:
         raise KeyError, "%s is not a valid settings database entry" % key

      return Setting(element)

   def __setitem__(self, key, value):
      '''
      Replaces the value associated with the given key (a string) with the
      given new value (an object that can be converted to a string). If the
      key does not exist in this settings database, then it is added
      automatically. The value is stored in its string form, so it is critical
      that the given value supports conversion to a string object. Moreover,
      it is important that the value can be reconstituted from a string if it
      is retrieved again later.
      '''
      if not self.has_key(key):
         path     = key.split('/')
         cur_node = self.root.getroot()
         for i in xrange(len(path)):
            parent   = cur_node
            cur_node = parent.find(path[i])
            if cur_node is None:
               for j in xrange(i, len(path)):
                  new_elt = ET.Element(path[j])
                  parent.append(new_elt)
                  parent = new_elt
               break

      # If we have been given a Setting object, then we replace the current
      # Element object identified by key with the Element object in value.
      if isinstance(value, Setting):
         parent = self.__getParent(key)
         assert parent is not None

         children = parent.getchildren()
         new_elt = value.element

         for i in range(len(children)):
            if children[i].tag == new_elt.tag:
               children[i] = new_elt
               break
      # Otherwise, we get the Element object identified by key and set its
      # text attribute to the string form of value.
      else:
         elt = self.root.find(key)
         assert elt is not None
         elt.text = str(value)

   def __getParent(self, key):
      path = key.split('/')
      path.pop()

      if len(path) == 0:
         return self.root.getroot()
      else:
         path = '/'.join(path)
         return self.root.getroot().find(path)

   def __iter__(self):
      if self.rot is not None:
         return self.root.getiterator()
      else:
         return None

   def __delitem__(self, key):
      if self.root is not None:
         # The removal operation can only remove a child from a node. Since
         # the Element interface has no way to get the parent Element, we
         # cannot use __getitem__() to get the element to remove. We have to
         # find it and its parent ourselves.
         path     = item.split('/')
         cur_node = self.root.getroot()

         for p in path:
            parent   = cur_node
            cur_node = parent.find(p)
            if cur_node is None:
               raise KeyError, "%s is not a child of the root" % item

         parent.remove(cur_node)

   def has_key(self, key):
      if self.root is not None:
         element = self.root.find(key)
         return element is not None
      else:
         return False

   def get(self, key):
      '''
      get(key) -> Setting object or None

      Retrieves the Setting object associated with the given key (a string).
      If there is no such key/value mapping in this database, then None is
      returned.
      '''
      if self.root is not None:
         element = self.root.find(key)
         if element is None:
            return None
         else:
            return Setting(element)
      else:
         return None

   def keys(self):
      '''
      keys() -> list

      Returns the keys of this settings database as a flattended list.
      '''
      if self.root is not None:
         return self.root.getiterator()
      else:
         return []

   def findall(self, pattern):
      '''
      findall(pattern) -> list

      Returns a list of Setting objects for all settings in this database
      matching the given pattern (a string). The pattern to be used must
      conform to the ElementTree path format.
      '''
      if self.root is not None:
         return [Setting(e) for e in self.root.findall(pattern)]
      else:
         return []

   def add(self, name, value = None, **attributes):
      if self.root is None:
         raise Exception, 'Empty settings database'

      path = name.split('/')
      append_type = path.pop()
      item = '/'.join(path)
      parent_elt = self.root.find(item)

      if parent_elt is None:
         parent = self.add(item)
      else:
         parent = Setting(parent_elt)

      child = Setting(name = append_type, value = value,
                      attributes = attributes)
      parent.addChild(child)

      return child

class Settings:
   '''
   Manages system-wide and user-specific settings for the vPresent GUI. In
   general, user-specific preferences take precedence over system-wide
   settings, although exceptions for this are allowed by the get(), set(),
   and add() methods.
   '''
   def __init__(self, siteFile, userFile, rootName):
      # Create two SettingsDatabase objects for the system-wide settings and
      # the user-specific settings. If either of these files does not exist,
      # they are not created automatically.
      self.siteSettings = SettingsDatabase(siteFile)
      self.userSettings = SettingsDatabase(userFile)

      # Ensure that some system-wide and user-specific database structure
      # exists. If loading one or both of the settings files fails, at least
      # we will have an in-memory database to use. The handler of this object
      # may choose to save the files later. If they are not saved, then any
      # changes to the databases made during their lifetime will be lost.
      # (This will be true regardless of whether the loading below succeeds.)
      self.siteSettings.create(rootName)
      self.userSettings.create(rootName)

      # Attempt to load the system-wide settings file if it exists.
      if os.path.exists(siteFile):
         try:
            self.siteSettings.load()
         except IOError, ex:
            QtGui.QMessageBox.warning(
               None, 'Warning',
               "Failed to read system-wide settings file %s: %s" % \
                  (siteFile, ex.strerror)
            )

      # Attempt to load the user-specific settings file if it exists.
      if os.path.exists(userFile):
         try:
            self.userSettings.load()
         except IOError, ex:
            QtGui.QMessageBox.warnig(
               None, 'Warning',
               "Failed to read user-specific settings file %s: %s" % \
                  (userFile, ex.strerror)
            )

   def save(self, userFile = None, siteFile = None):
      '''
      Attempts to save the user-specific and system-wide settings databases
      to permanent storage. If saving of the user-specific settings fails and
      an exception is raised, then no attempt will be made to save the
      system-wide settings. To save the databases individually, use the
      user() and site() methods to retrieve the database references and invoke
      their respective save() methods directly.
      '''
      self.userSettings.save(userFile)

      try:
         self.siteSettings.save(siteFile)
      # Silently ignore failures to write to the site-wide file.
      except IOError:
         pass

   def __getitem__(self, key):
      '''
      __getitem__(key) -> Setting

      Returns the value (a Setting object) associated with the given key (a
      string). The database query is performed on the user-specific settings
      database first. If that fails, then we fall back on the system-wide
      settings database. If that fails, an exception of type KeyError is
      raised.
      '''
      try:
         return self.userSettings[key]
      except KeyError:
         return self.siteSettings[key]

   def __setitem__(self, key, value):
      '''
      Changes the value (a Setting object) associated with the given key (a
      string) in the user-specific settings database. To change the value in
      the system-wide settings database instead, use set().
      '''
      self.userSettings[key] = value

   def __iter__(self):
      return self.userSettings.__iter__() + self.siteSettings.__iter__()

   def __delitem__(self, key):
      del self.userSettings[key]

   def has_key(self, key):
      return self.userSettings.has_key(key) or self.siteSettings.has_key(key)

   def get(self, key, site = False):
      '''
      get(key, site = False) -> Setting object or None

      Retrieves a Setting object for the given key (a string). If key is for
      an unknown setting, then None is returned. If site is True, then only
      the system-wide settings database will be queried for the named key.
      Otherwise, the user-specific settings database is queried first for key.
      If it does not contain key, then the system-wide settings database is
      queried.

      The presence of the site parameter allows the caller to indicate that
      the user-speicifc setting (if one exists) should be ignored.
      '''
      if site:
         value = self.siteSettings.get(key)
      else:
         if self.userSettings.has_key(key):
            value = self.userSettings.get(key)
         else:
            value = self.siteSettings.get(key)

      return value

   def getValue(self, key, default = None, site = False):
      setting = self.get(key, site)

      if setting is None:
         value = default
      else:
         value = setting.value

      return value

   def set(self, key, value, site = False):
      '''
      set(key, value, site = False)

      Changes the value of the identified key (a string). If site is True,
      then the value (a string or an object that has a valid and meaningful
      string representation) is changed in the system-wide database.
      Otherwise, the value in the user-specific database is changed. If key
      does not exist in the database to be modified, then it is added
      automatically.

      The presence of the site parameter allows the caller to indicate that
      the user-speicifc setting (if one exists) should be ignored.
      '''
      if site:
         self.siteSettings[key] = value
      else:
         self.userSettings[key] = value

   def keys(self):
      '''
      keys() -> (list, list)

      Returns the keys of the user-specific and system-wide settings database
      as a pair of flatted lists.
      '''
      return (self.userSettings.keys(), self.siteSettings.keys())

   def findall(self, pattern):
      '''
      findall(pattern) -> list

      Returns a list of all Setting objects matching the given pattern. The
      returned list is ordered such that the Setting objects from the
      user-specific database come first followed by those from the system-wide
      settings database.
      '''
      # XXX: Should this be retunred as a tuple, or should the lists be
      # merged somehow? Basically, should we do anything to handle the case
      # when a setting matches both databases? Or is that just what is
      # expected from this sort of operation?
      return self.userSettings.findall(pattern) + \
             self.siteSettings.findall(pattern)

   def add(self, key, value = None, site = False, **attributes):
      '''
      Adds the named setting key (a string) with its value (a string) and
      attributes (a dictionary) to the settings database. If site is True,
      then the new Setting object will be added to the system-wide settings
      database. Otherwise, it will be added to the user-specific settings
      database.
      '''
      if site:
         return self.siteSettings.add(key, value, **attributes)
      else:
         return self.userSettings.add(key, value, **attributes)

   def user(self):
      '''
      user() -> SettingsDatabase

      Returns the reference to the user-specific SettingsDatabase object.
      '''
      return self.userSettings

   def site(self):
      '''
      site() -> SettingsDatabase

      Returns the reference to the system-wide SettingsDatabase object.
      '''
      return self.siteSettings
