# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.
#
# Authors:  Philippe Normand <philippe@fluendo.com>
#

"""
Config management module
"""

# FIXME: without this line configobj.OPTION_DEFAULTS is None in some tests..
# EEK!
from elisa.extern import configobj
from elisa.extern.configobj import ConfigObj, Section, ConfigObjError

from elisa.core import log

import os
import textwrap

class ConfigError(Exception):

    def __init__(self, msg):
        Exception.__init__(self)
        self.msg = msg

    def __str__(self):
        return self.msg

class SectionNotFound(Exception):
    pass

class Config(log.Loggable):
    """ Configuration system

    Each configuration is stored in a text file. The configuration is
    structured in sections. Each section stores a set of options. Example::

      [section_name]
      some_list = ['some','list']
      some_string = 'foobar'
      some_int = 1

    """

    def __init__(self, config_file=None, default_config=""):
        """ Load a config stored in the given file for the given application.

        @param config_file: the config filename to read
        @type config_file:  string or None. None implies to use
                            CONFIG_DIR/CONFIG_FILE
        @raise ConfigError: when the config file contains format error
        """
        log.Loggable.__init__(self)
        self.first_load = False

        config_dir = None

        if config_file is not None:
                
            if not os.path.exists(config_file) or not open(config_file).read():
                config_file = self._create_config(config_file, default_config)

            config_dir = os.path.dirname(config_file)
            if not config_dir:
                config_dir = os.getcwd()
                
        self._filename = config_file
        self._config_dir = config_dir

        self.info("Using %r config file", config_file)
        try:
            self._config = ConfigObj(config_file, unrepr=True)
        except ConfigObjError, ex:
            errors = [ error.message for error in ex.errors ]
            errors = ';'.join(errors)
            raise ConfigError("Config format error in %s: %s" % (config_file,
                                                                 errors))

    def get_config_dir(self):
        """ Config directory name accessor

        @returns: the current config directory absolute path
        @rtype:   string
        """
        return self._config_dir

    def get_filename(self):
        """ Config filename accessor

        @returns: the config filename from which the config has been read
        @rtype:   string
        """
        return self._filename

    def set_filename(self, filename):
        """ Config filename setter

        Updates _config_dir and _filename private attributes

        @param filename: full path to the config file
        @type filename:  string
        """
        self._config.filename = filename
        self._config_dir = os.path.dirname(filename)
        self._filename = filename

    def _create_config(self, config_file, default_config):
        self.debug("Creating config %r", config_file)

        dirname = os.path.dirname(config_file)
        if dirname and not os.path.exists(dirname):
            try:
                os.makedirs(dirname)
            except OSError, error:
                self.warning(error)
                raise ConfigError("Could not create %r : %r" % (dirname,
                                                                error))

        try:
            f = open(config_file,'w')
        except IOError, error:
            self.warning(error)
            raise ConfigError("Could not create %r : %s" % (config_file, error))
        
        f.write(default_config)
        f.close()
        self.first_load = True
        return config_file
        
    def get_option(self, key, section='general', default=None):
        """ Fetch the option value stored in the given section, at the
        given key. Return a default value if the key is not found.

        @param key:     the option key to look for
        @type key:      string
        @param section: the section name to search in
        @type section:  string
        @param default: the default value to use if the option is not found
        @type default:  object
        @returns:       value of given option in given section
        @rtype:         object
        """
        section = self.get_section(section)
        if section is None:
            return default
        
        return section.get(key, default)

    def set_option(self, key, value, section='general'):
        """ Store an option value under key id at the given section.

        @param key:     the option key to look for
        @type key:      string
        @param value:   the value to store under given key
        @type value:    object
        @param section: the section name to search in
        @type section:  string

        @raise SectionNotFound: when the given section does not exist
        """
        section_obj = self.get_section(section)
        if section_obj is None:
            raise SectionNotFound(section)

        section_obj[key] = value

    def del_option(self, key, section='general'):
        """ Remove the option identified by key under the specified
        section.

        @param key:     the option key to look for
        @type key:      string
        @param section: the section name to search in
        @type section:  string

        @raise SectionNotFound: when the given section does not exist
        """
        section_obj = self.get_section(section)
        if section_obj is None:
            raise SectionNotFound(section)

        del section_obj[key]

    def write(self, filename=None):
        """
        save the config in a text file (handled by ConfigObj)

        """
        outfile = filename
        try:
            my_filename = self.get_filename()
            if not outfile:
                if my_filename:
                    outfile = open(my_filename, 'w')
            else:
                outfile = open(outfile, 'w')
        except IOError, error:
            self.warning(error)
        else:
            if outfile:
                self.info('Saving config to file %r' % outfile)
                self._config.write(outfile=outfile)

    def rename_section(self, old_name, new_name):
        """ Rename a section of the config

        Options and comments stored in the section are kept intact.
        The config is update in-place. No result is returned by this
        method.

        @param old_name: the section to rename
        @type old_name:  string
        @param new_name: the new section name
        @type new_name:  string
        """
        section = self.get_section(old_name)
        if section:
            try:
                self._config.rename(old_name, new_name)
            except KeyError:
                pass

    def get_section(self, section_name, default=None):
        """ Fetch a section from the config

        @param section_name: the section name to look for
        @type section_name:  string
        @param default:      the default value to use if the section is
                             not found
        @type default:       object
        @returns:            the ConfigObj section identified by section_name
        @rtype:              L{elisa.extern.configobj.ConfigObj} or empty dict
        """
        return self._config.get(section_name, default)

    def set_section(self, section_name, section={}, doc={}):
        """
        Store section_data in a new section identified by section_name
        in the config

        @param section_name: the section name to update
        @type section_name:  string
        @param section:      the section data
        @type section:       dict
        @param doc:          documentation of section's options
        @type doc:           dict
        """
        real_section = Section(self._config,
                              self._config.depth+1,
                              self._config.main,
                              indict=section,
                              name=section_name)

        for key in section.iterkeys():
            doc_value = doc.get(key, '')
            real_section.comments[key] = \
                    ['# %s' % line for line in textwrap.wrap(doc_value, 77)]

        self._config[section_name] = real_section
        return real_section

    def del_section(self, section_name):
        """ Remove the section identified by section_name

        @param section_name: the section name to delete
        @type section_name:  string
        """
        if self._config.has_key(section_name):
            del self._config[section_name]

    def delete_file(self):
        """
        Delete the configuration file on disk.
        """
        try:
            os.remove(self._filename)
        except OSError:
            pass
        else:
            self.info("Deleted configuration file '%s'." % self._filename)

    def as_dict(self):
        """ Helper method to convert the Config instance to a dictionary

        @returns: a mapping of the config's options by section name
        @rtype: dict
        """
        r = {}
        for name, section in self._config.iteritems():
            r.update({name:section.dict()})
        return r
