"""
Custom configparser classes
"""

import re
from collections import OrderedDict
from configparser import *  # pylint: disable=no-name-in-module,wildcard-import,unused-wildcard-import

import salt.utils.stringutils


class GitConfigParser(RawConfigParser):
    """
    Custom ConfigParser which reads and writes git config files.

    READ A GIT CONFIG FILE INTO THE PARSER OBJECT

    >>> import salt.utils.configparser
    >>> conf = salt.utils.configparser.GitConfigParser()
    >>> conf.read('/home/user/.git/config')

    MAKE SOME CHANGES

    >>> # Change user.email
    >>> conf.set('user', 'email', 'myaddress@mydomain.tld')
    >>> # Add another refspec to the "origin" remote's "fetch" multivar
    >>> conf.set_multivar('remote "origin"', 'fetch', '+refs/tags/*:refs/tags/*')

    WRITE THE CONFIG TO A FILEHANDLE

    >>> import salt.utils.files
    >>> with salt.utils.files.fopen('/home/user/.git/config', 'w') as fh:
    ...     conf.write(fh)
    >>>
    """

    DEFAULTSECT = "DEFAULT"
    SPACEINDENT = " " * 8

    # pylint: disable=useless-super-delegation
    def __init__(
        self,
        defaults=None,
        dict_type=OrderedDict,
        allow_no_value=True,
    ):
        """
        Changes default value for allow_no_value from False to True
        """
        super().__init__(defaults, dict_type, allow_no_value)

    # pylint: enable=useless-super-delegation

    def _read(self, fp, fpname):
        """
        Makes the following changes from the RawConfigParser:

        1. Strip leading tabs from non-section-header lines.
        2. Treat 8 spaces at the beginning of a line as a tab.
        3. Treat lines beginning with a tab as options.
        4. Drops support for continuation lines.
        5. Multiple values for a given option are stored as a list.
        6. Keys and values are decoded to the system encoding.
        """
        cursect = None  # None, or a dictionary
        optname = None
        lineno = 0
        e = None  # None, or an exception
        while True:
            line = salt.utils.stringutils.to_unicode(fp.readline())
            if not line:
                break
            lineno = lineno + 1
            # comment or blank line?
            if line.strip() == "" or line[0] in "#;":
                continue
            if line.split(None, 1)[0].lower() == "rem" and line[0] in "rR":
                # no leading whitespace
                continue
            # Replace space indentation with a tab. Allows parser to work
            # properly in cases where someone has edited the git config by hand
            # and indented using spaces instead of tabs.
            if line.startswith(self.SPACEINDENT):
                line = "\t" + line[len(self.SPACEINDENT) :]
            # is it a section header?
            mo = self.SECTCRE.match(line)
            if mo:
                sectname = mo.group("header")
                if sectname in self._sections:
                    cursect = self._sections[sectname]
                elif sectname == self.DEFAULTSECT:
                    cursect = self._defaults
                else:
                    cursect = self._dict()
                    self._sections[sectname] = cursect
                # So sections can't start with a continuation line
                optname = None
            # no section header in the file?
            elif cursect is None:
                raise MissingSectionHeaderError(  # pylint: disable=undefined-variable
                    salt.utils.stringutils.to_str(fpname),
                    lineno,
                    salt.utils.stringutils.to_str(line),
                )
            # an option line?
            else:
                mo = self._optcre.match(line.lstrip())
                if mo:
                    optname, vi, optval = mo.group("option", "vi", "value")
                    optname = self.optionxform(optname.rstrip())
                    if optval is None:
                        optval = ""
                    if optval:
                        if vi in ("=", ":") and ";" in optval:
                            # ';' is a comment delimiter only if it follows
                            # a spacing character
                            pos = optval.find(";")
                            if pos != -1 and optval[pos - 1].isspace():
                                optval = optval[:pos]
                        optval = optval.strip()
                        # Empty strings should be considered as blank strings
                        if optval in ('""', "''"):
                            optval = ""
                    self._add_option(cursect, optname, optval)
                else:
                    # a non-fatal parsing error occurred.  set up the
                    # exception but keep going. the exception will be
                    # raised at the end of the file and will contain a
                    # list of all bogus lines
                    if not e:
                        e = ParsingError(fpname)  # pylint: disable=undefined-variable
                    e.append(lineno, repr(line))
        # if any parsing errors occurred, raise an exception
        if e:
            raise e  # pylint: disable=raising-bad-type

    def _string_check(self, value, allow_list=False):
        """
        Based on the string-checking code from the SafeConfigParser's set()
        function, this enforces string values for config options.
        """
        if self._optcre is self.OPTCRE or value:
            is_list = isinstance(value, list)
            if is_list and not allow_list:
                raise TypeError(
                    "option value cannot be a list unless allow_list is True"
                )
            elif not is_list:
                value = [value]
            if not all(isinstance(x, str) for x in value):
                raise TypeError("option values must be strings")

    def get(self, section, option, as_list=False):  # pylint: disable=arguments-differ
        """
        Adds an optional "as_list" argument to ensure a list is returned. This
        is helpful when iterating over an option which may or may not be a
        multivar.
        """
        ret = super().get(section, option)
        if as_list and not isinstance(ret, list):
            ret = [ret]
        return ret

    def set(self, section, option, value=""):
        """
        This is overridden from the RawConfigParser merely to change the
        default value for the 'value' argument.
        """
        self._string_check(value)
        super().set(section, option, value)

    def _add_option(self, sectdict, key, value):
        if isinstance(value, list):
            sectdict[key] = value
        elif isinstance(value, str):
            try:
                sectdict[key].append(value)
            except KeyError:
                # Key not present, set it
                sectdict[key] = value
            except AttributeError:
                # Key is present but the value is not a list. Make it into a list
                # and then append to it.
                sectdict[key] = [sectdict[key]]
                sectdict[key].append(value)
        else:
            raise TypeError(
                "Expected str or list for option value, got %s" % type(value).__name__
            )

    def set_multivar(self, section, option, value=""):
        """
        This function is unique to the GitConfigParser. It will add another
        value for the option if it already exists, converting the option's
        value to a list if applicable.

        If "value" is a list, then any existing values for the specified
        section and option will be replaced with the list being passed.
        """
        self._string_check(value, allow_list=True)
        if not section or section == self.DEFAULTSECT:
            sectdict = self._defaults
        else:
            try:
                sectdict = self._sections[section]
            except KeyError:
                raise NoSectionError(  # pylint: disable=undefined-variable
                    salt.utils.stringutils.to_str(section)
                )
        key = self.optionxform(option)
        self._add_option(sectdict, key, value)

    def remove_option_regexp(self, section, option, expr):
        """
        Remove an option with a value matching the expression. Works on single
        values and multivars.
        """
        if not section or section == self.DEFAULTSECT:
            sectdict = self._defaults
        else:
            try:
                sectdict = self._sections[section]
            except KeyError:
                raise NoSectionError(  # pylint: disable=undefined-variable
                    salt.utils.stringutils.to_str(section)
                )
        option = self.optionxform(option)
        if option not in sectdict:
            return False
        regexp = re.compile(expr)
        if isinstance(sectdict[option], list):
            new_list = [x for x in sectdict[option] if not regexp.search(x)]
            # Revert back to a list if we removed all but one item
            if len(new_list) == 1:
                new_list = new_list[0]
            existed = new_list != sectdict[option]
            if existed:
                del sectdict[option]
                sectdict[option] = new_list
            del new_list
        else:
            existed = bool(regexp.search(sectdict[option]))
            if existed:
                del sectdict[option]
        return existed

    def write(self, fp_):  # pylint: disable=arguments-differ
        """
        Makes the following changes from the RawConfigParser:

        1. Prepends options with a tab character.
        2. Does not write a blank line between sections.
        3. When an option's value is a list, a line for each option is written.
           This allows us to support multivars like a remote's "fetch" option.
        4. Drops support for continuation lines.
        """
        convert = (
            salt.utils.stringutils.to_bytes
            if "b" in fp_.mode
            else salt.utils.stringutils.to_str
        )
        if self._defaults:
            fp_.write(convert("[%s]\n" % self.DEFAULTSECT))
            for key, value in self._defaults.items():
                value = salt.utils.stringutils.to_unicode(value).replace("\n", "\n\t")
                fp_.write(convert(f"{key} = {value}\n"))
        for section in self._sections:
            fp_.write(convert("[%s]\n" % section))
            for key, value in self._sections[section].items():
                if (value is not None) or (self._optcre == self.OPTCRE):
                    if not isinstance(value, list):
                        value = [value]
                    for item in value:
                        fp_.write(convert("\t%s\n" % " = ".join((key, item)).rstrip()))
