# -*- coding: utf-8 -*-
#-
# :Copyright: (c) 2015-2018, Franz Glasner. All rights reserved.
# :License:   3-clause BSD. See LICENSE.txt for details.
#-
"""Read INI-style configuration files.

"""

from __future__ import division, absolute_import, print_function

import sys
import os
import io
import locale
try:
    from configparser import ConfigParser, NoSectionError, NoOptionError
    # SafeConfigParser is deprecated in Python 3.2+ (together with "readfp()")
    if hasattr(ConfigParser, "read_file"):
        _ConfigParserBase = ConfigParser
    else:
        from configparser import SafeConfigParser
        _ConfigParserBase = SafeConfigParser
except ImportError:
    from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError
    _ConfigParserBase = SafeConfigParser
try:
    from collections import OrderedDict as DictImpl
except ImportError:
    try:
        from ordereddict import OrderedDict as DictImpl
    except ImportError:
        DictImpl = dict

from .compat import PY2, u


__all__ = ["INIConfigParser", "NoSectionError", "NoOptionError",
           "load"]


class INIConfigParser(_ConfigParserBase):

    """A case sensitive config parser that returns all-unicode string
    values.

    """

    def __init__(self, filename, executable=None, encoding=None):
        _ConfigParserBase.__init__(self)
        if executable is None:
            executable = sys.argv[0]
        if PY2:
            if isinstance(filename, str):
                filename = filename.decode(locale.getpreferredencoding())
            if isinstance(executable, str):
                executable = executable.decode(locale.getpreferredencoding())
        self.executable = os.path.normpath(os.path.abspath(executable))
        if encoding is None:
            encoding = locale.getpreferredencoding()
        self.encoding = encoding
        with io.open(filename, mode="rt", encoding=self.encoding) as cf:
            self.read_file(cf, filename)

    def optionxform(self, option):
        return option

    def get_path_list(self, section, option):
        v = self.get(section, option)
        return v.split(os.pathsep)

    def read(self, filenames):
        """Not implemented. Use :meth:`read_file` instead"""
        raise NotImplementedError("use `read_file()' instead")

    def read_file(self, fp, filename):
        """Read from a file-like object `fp`.

        The `fp` argument must be iterable.

        """
        if hasattr(self, "filename"):
            raise RuntimeError("already initialized")
        filename = os.path.normpath(os.path.abspath(filename))
        if PY2:
            if isinstance(filename, str):
                filename = filename.decode(locale.getpreferredencoding())
        self.set(None, u("self"), filename)
        self.set(None, u("here"), os.path.dirname(filename))
        self.set(None, u("root"), os.path.dirname(self.executable))
        if hasattr(_ConfigParserBase, "read_file"):
            _ConfigParserBase.read_file(self, fp, source=filename)
        else:
            _ConfigParserBase.readfp(self, fp, filename=filename)
        self.filename = filename
        self.root = os.path.dirname(self.executable)

    def getx(self, section, option):
        """Extended `get()` with some automatic type conversion support.

        Default: Fetch as string (like :meth:`get`).

        If annotated with ``:bool:`` fetch as bool, if annotated with
        ``:int:`` fetch as int, if annotated with ``:float:`` fetch as
        float.

        """
        v = self.get(section, option)
        if v.startswith(u(":bool:")):
            v = v[6:].lower()
            if v not in self._BOOL_CVT:
                raise ValueError("Not a boolean: %r" % v)
            return self._BOOL_CVT[v]
        elif v.startswith(u(":int:")):
            return int(v[5:], 0)
        elif v.startswith(u(":float:")):
            return float(v[7:])
        else:
            return v

    _BOOL_CVT = {u('1'): True,
                 u('yes'): True,
                 u('true'): True,
                 u('on'): True,
                 u('0'): False,
                 u('no'): False,
                 u('false'): False,
                 u('off'): False}

    def itemsx(self, section, options):
        """Get all the options given in `options` of section `section`.
        Fetch them with :meth:`getx` in the order given.

        Return a list of ``(name, value)`` pairs for each option in
        `options` in the given `section`.

        """
        d = []
        for option in options:
            try:
                val = self.getx(section, option)
            except (NoSectionError, NoOptionError):
                pass
            else:
                d.append((option, val, ))
        return d

    def items_as_dictx(self, section, options):
        """Similar to :meth:`itemsx` but return a (possibly ordered)
        dict instead of a list of key-value pairs.

        """
        return DictImpl(self.itemsx(section, options))


def load(filename, extract=["config"],
         encoding="utf-8"):
    """Load a single INI file and read/interpolate the sections given in
    `extract`.

    Flattens the given sections into the resulting dictionary.

    Then build a tree out of sections which start with any of the `extract`
    content value and a point ``.``.

    The encoding of the file is given in `encoding`.

    """
    conf = DictImpl()
    ini = INIConfigParser(filename, encoding=encoding)
    for sect in extract:
        try:
            cfg = ini.options(sect)
        except NoSectionError:
            pass
        else:
            for option in cfg:
                value = ini.getx(sect, option)
                conf[option] = value
    # try to read "<extract>.xxx" sections as tree
    for treemarker in [ e + '.' for e in extract ]:
        sections = list(ini.sections())
        sections.sort()
        for section in sections:
            cur_cfg = conf
            if section.startswith(treemarker):
                treestr = section[len(treemarker):]
                for treepart in treestr.split('.'):
                    cur_cfg = cur_cfg.setdefault(treepart, DictImpl())
                for option in ini.options(section):
                    value = ini.getx(section, option)
                    cur_cfg[option] = value
    return conf
