__author__ = 'Peter Hofmann'
__version__ = '0.1.2'

import os
import sys
from collections import Iterable
from io import StringIO
if sys.version_info < (3,):
    from ConfigParser import SafeConfigParser as ConfigParser
else:
    from configparser import ConfigParser
from scripts.loggingwrapper import DefaultLogging


class ConfigParserWrapper(DefaultLogging):
    """
    @type _config: ConfigParser
    """

    _boolean_states = {
        'yes': True, 'true': True, 'on': True,
        'no': False, 'false': False, 'off': False,
        'y': True, 't': True, 'n': False, 'f': False}

    def __init__(self, logfile=None, verbose=True):
        """
            Wrapper for the SafeConfigParser class for easy use.

            @attention: config_file argument may be file path or stream.

            @param logfile: file handler or file path to a log file
            @type logfile: file | FileIO | StringIO | None
            @param verbose: No stdout or stderr messages. Warnings and errors will be only logged to a file, if one is given
            @type verbose: bool

            @return: None
            @rtype: None
        """
        super(ConfigParserWrapper, self).__init__(
            label="ConfigParserWrapper", logfile=logfile, verbose=verbose)
        self._config = ConfigParser()
        self._config_file_path = None

    def read(self, config_file):
        """
            Read a configuration file in ini format

            @attention: config_file argument may be file path or stream.

            @param config_file: file handler or file path to a config file
            @type config_file: file | FileIO | StringIO

            @rtype: None
        """
        assert isinstance(config_file, str) or self.is_stream(config_file), "Invalid config file path: {}".format(config_file)
        if isinstance(config_file, str) and not os.path.isfile(config_file):
            self._logger.error("Config file does not exist: '{}'".format(config_file))
            raise Exception("File does not exist")

        if isinstance(config_file, str):
            self._config.read(config_file)
            self._config_file_path = config_file
        elif self.is_stream(config_file):
            if sys.version_info < (3,):
                self._config.readfp(config_file)
            else:
                self._config.read_file(config_file)
            self._config_file_path = config_file.name
        else:
            self._logger.error("Invalid config file argument '{}'".format(config_file))
            raise Exception("Unknown argument")

    def write(self, file_path):
        """
        Write config file

        @param file_path: Output file path
        @type file_path: str

        @rtype: None
        """
        with open(file_path, "w") as write_handler:
            self._config.write(write_handler)

    def set_value(self, option, value, section=None):
        """

        @param section:
        @type section: str
        @param value:
        @type value: any

        @rtype: None
        """
        if not self._config.has_section(section):
            self._config.add_section(section)
        self._config.set(section, option, value)

    def validate_sections(self, list_sections):
        """
            Validate a list of section names for availability.

            @param list_sections: list of section names
            @type list_sections: list of str

            @return: None if all valid, otherwise list of invalid sections
            @rtype: None | list[str]
        """
        assert isinstance(list_sections, Iterable), "Invalid, not a list: '{}'".format(list_sections)
        invalid_sections = []
        for section in list_sections:
            if not self._config.has_section(section):
                invalid_sections.append(section)
        if len(invalid_sections) > 0:
            return invalid_sections
        return None

    def log_invalid_sections(self, list_sections):
        """
            print out a list of invalid section names to log.

            @param list_sections: list of section names
            @type list_sections: list[str]

            @return: None
            @rtype: None
        """
        assert isinstance(list_sections, Iterable), "Invalid, not a list: '{}'".format(list_sections)
        for section in list_sections:
            self._logger.warning("Invalid section '{}'".format(section))

    def get_value(self, option, section=None, is_digit=False, is_boolean=False, is_path=False, silent=False):
        """
            get a value of an option in a specific section of the config file.

            @attention: Set obligatory to False if a section or option that does not exist is no error.

            @param option: name of option in a section
            @type option: str
            @param section: name of section
            @type section: str
            @param is_digit: value is a number and will be returned as such
            @type is_digit: bool
            @param is_boolean: value is bool and will be returned as True or False
            @type is_boolean: bool
            @param is_path: value is a path and will be returned as absolute path
            @type is_path: bool
            @param silent: Error is given if error not available unless True
            @type silent: bool


            @return: None if not available or ''. Else: depends on given arguments
            @rtype: None | str | int | float | bool
        """
        assert section is None or isinstance(section, str), "Invalid section: '{}'".format(section)
        assert isinstance(option, str), "Invalid option: '{}'".format(option)
        assert isinstance(is_digit, bool), "Invalid argument, 'is_digit' must be boolean, but got: '{}'".format(type(is_digit))
        assert isinstance(is_boolean, bool), "Invalid argument, 'is_boolean' must be boolean, but got: '{}'".format(type(is_boolean))
        assert isinstance(silent, bool), "Invalid argument, 'silent' must be boolean, but got: '{}'".format(type(silent))
        assert isinstance(is_path, bool), "Invalid argument, 'is_path' must be boolean, but got: '{}'".format(type(is_path))
        if section is None:
            section = self._get_section_of_option(option)
        if not self._config.has_section(section):
            if not silent:
                if section is None:
                    self._logger.error("Missing option '{}'".format(option))
                else:
                    self._logger.error("Missing section '{}'".format(section))
            return None
        if not self._config.has_option(section, option):
            if not silent:
                self._logger.error("Missing option '{}' in section '{}'".format(option, section))
            return None

        value = self._config.get(section, option)
        if value == '':
            if not silent:
                self._logger.warning("Empty value in '{}': '{}'".format(section, option))
            return None

        if is_digit:
            return self._string_to_digit(value)

        if is_boolean:
            return self._is_true(value)

        if is_path:
            return self._get_full_path(value)
        return value

    def _get_section_of_option(self, option):
        """
            get the section of a unique option

            @param option: name of option in a section
            @type option: str

            @return: Section name. None if not available
            @rtype: None | str
        """
        assert isinstance(option, str), "Invalid argument, 'option' must be string, but got: '{}'".format(type(option))
        for section in self._config.sections():
            if self._config.has_option(section, option):
                return section
        return None

    def search_sections_of(self, option):
        """
            get the section of a unique option

            @param option: name of option in a section
            @type option: str

            @return: Section name. None if not available
            @rtype: set[str]
        """
        assert isinstance(option, str), "Invalid argument, 'option' must be string, but got: '{}'".format(type(option))
        result = set()
        for section in self._config.sections():
            if self._config.has_option(section, option):
                result.add(section)
        return result

    def _string_to_digit(self, value):
        """
            parse string to an int or float.

            @param value: some string to be converted
            @type value: str

            @return: None if invalid, otherwise int or float
            @rtype: None | int | float
        """
        assert isinstance(value, str), "Invalid argument, 'value' must be string, but got: '{}'".format(type(value))
        try:
            if '.' in value:
                return float(value)
            return int(value)
        except ValueError:
            self._logger.error("Invalid digit value '{}'".format(value))
            return None

    def _is_true(self, value):
        """
            parse string to True or False.

            @param value: some string to be converted
            @type value: str

            @return: None if invalid, otherwise True or False
            @rtype: None | bool
        """
        assert isinstance(value, str), "Invalid argument, 'value' must be string, but got: '{}'".format(type(value))

        if value.lower() not in ConfigParserWrapper._boolean_states:
            self._logger.error("Invalid bool value '{}'".format(value))
            return None
        return ConfigParserWrapper._boolean_states[value.lower()]

    @staticmethod
    def _get_full_path(value):
        """
            convert string to absolute normpath.

            @param value: some string to be converted
            @type value: str

            @return: absolute normpath
            @rtype: str
        """
        assert isinstance(value, str), "Invalid argument, 'value' must be string, but got: '{}'".format(type(value))

        parent_directory, filename = os.path.split(value)

        if not parent_directory and not os.path.isfile(value):
            for path in os.environ["PATH"].split(os.pathsep):
                path = path.strip('"')
                exe_file = os.path.join(path, filename)
                if os.path.isfile(exe_file):
                    value = exe_file
                    break

        value = os.path.expanduser(value)
        value = os.path.normpath(value)
        value = os.path.abspath(value)
        return value