# -*- coding: utf-8 -*-
# vim:fenc=utf-8

# Copyright (C) 2010-2020 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
#
# X2Go Session Broker is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# X2Go Session Broker 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero 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.
#
# This code was initially written by for the Python X2Go project:
#       2010 Dick Kniep <dick.kniep@lindix.nl>
#

"""\
X2GoConfig - helper class for parsing files in INI file format.

"""
__NAME__ = 'x2goinifiles-pylib'

# modules
import os
import configparser
import io

# Python X2GoBroker modules
import x2gobroker.utils

from x2gobroker.defaults import X2GOBROKER_HOME as _X2GOBROKER_HOME

class X2GoBrokerConfigFile(object):
    """
    Class for processing an INI-file-like configuration file.

    If entries are omitted in such a config file, they are filled with
    default values (as hard-coded in Python X2GoBroker), so the resulting objects
    always contain the same fields.

    The default values are also used to define a data type for each configuration
    option. An on-the-fly type conversion takes place when loading the configuration
    file.

    """
    defaultValues = {
        'DEFAULT': {
            'none': 'empty',
        },
    }
    write_user_config = False
    user_config_file = None

    def __init__(self, config_files=[], defaults={}):
        """\
        :param config_files: a list of configuration file names (e.g. a global filename and a user's home
            directory filename)
        :type config_files: ``list``
        :param defaults: a cascaded Python dicitionary structure with INI file defaults (to override
            Python X2GoBroker's hard-coded defaults in L{defaults}
        :type defaults: ``dict``

        """
        # allow string/unicode objects as config_files, as well
        if type(config_files) == str:
            config_files = [config_files]
        self.config_files = config_files

        if x2gobroker.utils._checkConfigFileDefaults(defaults):
            self.defaultValues = defaults

        # we purposefully do not inherit the RawConfigParser class
        # here as we do not want to run into name conflicts between
        # X2GoBroker config file options and method / property names in
        # RawConfigParser... This is a pre-cautious approach...
        self.iniConfig = configparser.RawConfigParser(self.defaultValues)
        self.iniConfig.optionxform = str

        _create_file = False
        for file_name in self.config_files:
            if file_name.startswith(_X2GOBROKER_HOME):
                if not os.path.exists(file_name):
                    x2gobroker.utils.touch_file(file_name)
                    _create_file = True
                break

        self.load()

        if _create_file:
            self.write_user_config = True
            self.write()

    def __repr__(self):
        result = 'X2GoConfigFile('
        for p in dir(self):
            if '__' in p or not p in self.__dict__: continue
            result += p + '=' + str(self.__dict__[p]) + ','
        result = result.strip(',')
        return result + ')'

    def load(self):
        """\
        R(e-r)ead configuration file(s).

        """
        _found_config_files = self.iniConfig.read(self.config_files)

        for file_name in _found_config_files:
            if file_name.startswith(os.path.normpath(_X2GOBROKER_HOME)):
                # we will use the first file found in the user's home dir for writing modifications
                self.user_config_file = file_name
                break

        self.config_files = _found_config_files
        self._fill_defaults()

    def _storeValue(self, section, key, value):
        """\
        Stores a value for a given section and key.

        This methods affects a :class:`configparser.RawConfigParser`
        object held in RAM. No configuration file is affected by this
        method. To write the configuration to disk use the L{write()}
        method.

        :param section: the INI file section
        :type section: ``str``
        :param key: the INI file key in the given section
        :type key: ``str``
        :param value: the value for the given section and key
        :type value: ``str``, ``list``, ``bool``, ...

        """
        if type(value) is bool:
            self.iniConfig.set(section, key, str(int(value)))
        elif type(value) in (list, tuple):
            self.iniConfig.set(section, key, ", ".join(value))
        elif type(value) is int:
            self.iniConfig.set(section, key, str(value))
        else:
            self.iniConfig.set(section, key, value)

    def _fill_defaults(self):
        """\
        Fills a :class:`configparser.RawConfigParser` object with the
        default config file values as pre-defined in Python X2GoBroker
        or. This RawConfigParser object is held in RAM. No configuration
        file is affected by this method.

        """
        for section, sectiondict in list(self.defaultValues.items()):
            if section != 'DEFAULT' and not self.iniConfig.has_section(section):
                self.iniConfig.add_section(section)
            for key, value in list(sectiondict.items()):
                if self.iniConfig.has_option(section, key): continue
                self._storeValue(section, key, value)

    def update_value(self, section, key, value):
        """\
        Change a value for a given section and key. This method
        does not have any effect on configuration files.

        :param section: the INI file section
        :type section: ``str``
        :param key: the INI file key in the given section
        :type key: ``str``
        :param value: the value for the given section and key
        :type value: ``str``, ``list``, ``bool``, ...

        """
        if not self.iniConfig.has_section(section):
            self.iniConfig.add_section(section)
        self._storeValue(section, key, value)
        self.write_user_config = True

    def write(self):
        """\
        Write the INI file modifications (RawConfigParser object) from RAM to disk.

        For writing the first of the ``config_files`` specified on instance construction
        that is writable will be used.

        """
        if self.user_config_file and self.write_user_config:
            fd = open(self.user_config_file, 'w')
            self.iniConfig.write(fd)
            fd.close()
            self.write_user_config = False

    def get_type(self, section, key):
        """\
        Retrieve a value type for a given section and key. The returned
        value type is based on the default values dictionary.

        :param section: the INI file section
        :type section: ``str``
        :param key: the INI file key in the given section
        :type key: ``str``

        :returns: a Python variable type
        :rtype: class

        """
        if section in list(self.defaultValues.keys()) and key in list(self.defaultValues[section].keys()):
            return type(self.defaultValues[section][key])
        else:
            try:
                return type(self.defaultValues['DEFAULT'][key])
            except KeyError:
                return type('')

    def has_value(self, section, key):
        """\
        Test if a given ``key`` in ``section`` exists (and
        has some sort of a value).

        :param section: the INI file section
        :type section: ``str``
        :param key: the INI file key in the given section
        :type key: ``str``

        :returns: return ``True`` if <key> in <section> exists
        :rtype: ``bool``

        """
        if section in self.iniConfig.sections():
            return ( key in self.iniConfig.options(section) )
        return False

    def get_value(self, section, key, key_type=None):
        """\
        Retrieve a value for a given section and key.

        :param section: the INI file section
        :type section: ``str``
        :param key: the INI file key in the given section
        :type key: ``str``

        :returns: the value for the given section and key
        :rtype: class

        """
        if key_type is None:
            key_type = self.get_type(section, key)

        if self.iniConfig.has_option(section, key) or section == 'DEFAULT':

            if key_type is None:

                return self.iniConfig.get(section, key)

            if key_type is bool:

                return self.iniConfig.getboolean(section, key)

            elif key_type is int:

                try:
                    return self.iniConfig.getint(section, key)
                except ValueError:
                    _val = self.iniConfig.get(section, key)
                    if _val != "not-set": raise
                    else: return _val

            elif key_type is list:

                _val = self.iniConfig.get(section, key)
                _val = _val.strip()
                if _val.startswith('[') and _val.endswith(']'):
                    return eval(_val)
                elif ',' in _val:
                    _val = [ v.strip() for v in _val.split(',') ]
                else:
                    _val = [ _val ]
                return _val

            else:
                _val = self.iniConfig.get(section, key)
                return _val
    get = get_value
    __call__ = get_value

    def get_defaults(self):
        """\
        Get all keys and values from the [DEFAULT] section of the configuration file.

        :returns: the defaults with all keys and values
        :rtype: ``dict``

        """
        _my_defaults = {}
        _ini_defaults = self.iniConfig.defaults()
        for option in list(_ini_defaults.keys()):
            try:
                _my_defaults[option] = self.get('DEFAULT', option, key_type=self.get_type('DEFAULT', option))
            except KeyError:
                continue

        try: del _my_defaults['default']
        except KeyError: pass

        return _my_defaults


    def get_section(self, section):
        """\
        Get all keys and values for a certain section of the config file.

        :param section: the name of the section to get
        :type section: ``str``

        :returns: the section with all keys and values
        :rtype: ``dict``

        """
        _section_config = {}
        for option in self.iniConfig.options(section):
            if option not in self.iniConfig.sections():
                _section_config[option] = self.get(section, option, key_type=self.get_type(section, option))

        return _section_config

    def list_sections(self):
        """\
        Return a list of all present sections in a config file.

        :returns: list of sections in this config file
        :rtype: ``list``

        """
        return [ s for s in self.iniConfig.sections() ]

    @property
    def printable_config_file(self):
        """\
        Returns a printable configuration file as a multi-line string.

        """
        stdout = io.StringIO()
        self.iniConfig.write(stdout)
        _ret_val = stdout.getvalue()
        stdout.close()
        return _ret_val
