# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2018-2019 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.


"""Utilities and data structures used by various config code."""


import typing

import attr
from PyQt5.QtCore import QUrl

from qutebrowser.utils import utils, urlmatch
from qutebrowser.config import configexc

MYPY = False
if MYPY:
    # pylint: disable=unused-import,useless-suppression
    from qutebrowser.config import configdata


class Unset:

    """Sentinel object."""

    __slots__ = ()

    def __repr__(self) -> str:
        return '<UNSET>'


UNSET = Unset()


@attr.s
class ScopedValue:

    """A configuration value which is valid for a UrlPattern.

    Attributes:
        value: The value itself.
        pattern: The UrlPattern for the value, or None for global values.
    """

    value = attr.ib()  # type: typing.Any
    pattern = attr.ib()  # type: typing.Optional[urlmatch.UrlPattern]


class Values:

    """A collection of values for a single setting.

    Currently, this is a list and iterates through all possible ScopedValues to
    find matching ones.

    In the future, it should be possible to optimize this by doing
    pre-selection based on hosts, by making this a dict mapping the
    non-wildcard part of the host to a list of matching ScopedValues.

    That way, when searching for a setting for sub.example.com, we only have to
    check 'sub.example.com', 'example.com', '.com' and '' instead of checking
    all ScopedValues for the given setting.

    Attributes:
        opt: The Option being customized.
    """

    def __init__(self,
                 opt: 'configdata.Option',
                 values: typing.MutableSequence = None) -> None:
        self.opt = opt
        self._values = values or []

    def __repr__(self) -> str:
        return utils.get_repr(self, opt=self.opt, values=self._values,
                              constructor=True)

    def __str__(self) -> str:
        """Get the values as human-readable string."""
        if not self:
            return '{}: <unchanged>'.format(self.opt.name)

        lines = []
        for scoped in self._values:
            str_value = self.opt.typ.to_str(scoped.value)
            if scoped.pattern is None:
                lines.append('{} = {}'.format(self.opt.name, str_value))
            else:
                lines.append('{}: {} = {}'.format(
                    scoped.pattern, self.opt.name, str_value))
        return '\n'.join(lines)

    def __iter__(self) -> typing.Iterator['ScopedValue']:
        """Yield ScopedValue elements.

        This yields in "normal" order, i.e. global and then first-set settings
        first.
        """
        yield from self._values

    def __bool__(self) -> bool:
        """Check whether this value is customized."""
        return bool(self._values)

    def _check_pattern_support(
            self, arg: typing.Optional[urlmatch.UrlPattern]) -> None:
        """Make sure patterns are supported if one was given."""
        if arg is not None and not self.opt.supports_pattern:
            raise configexc.NoPatternError(self.opt.name)

    def add(self, value: typing.Any,
            pattern: urlmatch.UrlPattern = None) -> None:
        """Add a value with the given pattern to the list of values."""
        self._check_pattern_support(pattern)
        self.remove(pattern)
        scoped = ScopedValue(value, pattern)
        self._values.append(scoped)

    def remove(self, pattern: urlmatch.UrlPattern = None) -> bool:
        """Remove the value with the given pattern.

        If a matching pattern was removed, True is returned.
        If no matching pattern was found, False is returned.
        """
        self._check_pattern_support(pattern)
        old_len = len(self._values)
        self._values = [v for v in self._values if v.pattern != pattern]
        return old_len != len(self._values)

    def clear(self) -> None:
        """Clear all customization for this value."""
        self._values = []

    def _get_fallback(self, fallback: typing.Any) -> typing.Any:
        """Get the fallback global/default value."""
        for scoped in self._values:
            if scoped.pattern is None:
                return scoped.value

        if fallback:
            return self.opt.default
        else:
            return UNSET

    def get_for_url(self, url: QUrl = None, *,
                    fallback: bool = True) -> typing.Any:
        """Get a config value, falling back when needed.

        This first tries to find a value matching the URL (if given).
        If there's no match:
          With fallback=True, the global/default setting is returned.
          With fallback=False, UNSET is returned.
        """
        self._check_pattern_support(url)
        if url is not None:
            for scoped in reversed(self._values):
                if scoped.pattern is not None and scoped.pattern.matches(url):
                    return scoped.value

            if not fallback:
                return UNSET

        return self._get_fallback(fallback)

    def get_for_pattern(self,
                        pattern: typing.Optional[urlmatch.UrlPattern], *,
                        fallback: bool = True) -> typing.Any:
        """Get a value only if it's been overridden for the given pattern.

        This is useful when showing values to the user.

        If there's no match:
          With fallback=True, the global/default setting is returned.
          With fallback=False, UNSET is returned.
        """
        self._check_pattern_support(pattern)
        if pattern is not None:
            for scoped in reversed(self._values):
                if scoped.pattern == pattern:
                    return scoped.value

            if not fallback:
                return UNSET

        return self._get_fallback(fallback)
