"""
This defines the Scope object and the scope parser.
Because these components are mutually dependent, it's easiest if they're kept in a
single module.
"""
from __future__ import annotations

import typing as t
import warnings

from globus_sdk._types import ScopeCollectionType


def _iter_scope_collection(obj: ScopeCollectionType) -> t.Iterator[str]:
    if isinstance(obj, str):
        yield obj
    elif isinstance(obj, MutableScope):
        yield str(obj)
    else:
        for item in obj:
            yield str(item)


# TODO: rename MutableScope to Scope
class MutableScope:
    """
    A scope object is a representation of a scope which allows modifications to be
    made. In particular, it supports handling scope dependencies via
    ``add_dependency``.

    `str(MutableScope(...))` produces a valid scope string for use in various methods.

    :param scope_string: The string which will be used as the basis for this Scope
    :type scope_string: str
    :param optional: The scope may be marked as optional. This means that the scope can
        be declined by the user without declining consent for other scopes
    :type optional: bool
    """

    def __init__(
        self,
        scope_string: str,
        *,
        optional: bool = False,
        dependencies: list[MutableScope] | None = None,
    ) -> None:
        if any(c in scope_string for c in "[]* "):
            raise ValueError(
                "MutableScope instances may not contain the special characters '[]* '."
            )
        self.scope_string = scope_string
        self.optional = optional
        self.dependencies: list[MutableScope] = (
            [] if dependencies is None else dependencies
        )

    def serialize(self) -> str:
        base_scope = ("*" if self.optional else "") + self.scope_string
        if not self.dependencies:
            return base_scope
        return (
            base_scope + "[" + " ".join(c.serialize() for c in self.dependencies) + "]"
        )

    def add_dependency(
        self,
        scope: str | MutableScope,
        *,
        optional: bool | None = None,
    ) -> MutableScope:
        """
        Add a scope dependency. The dependent scope relationship will be stored in the
        Scope and will be evident in its string representation.

        :param scope: The scope upon which the current scope depends
        :type scope: str
        :param optional: Mark the dependency an optional one. By default it is not. An
            optional scope dependency can be declined by the user without declining
            consent for the primary scope
        :type optional: bool, optional
        """
        if optional is not None:
            if isinstance(scope, MutableScope):
                raise ValueError(
                    "cannot use optional=... with a MutableScope object as the "
                    "argument to add_dependency"
                )
            warnings.warn(
                "Passing 'optional' to add_dependency is deprecated. "
                "Construct an optional MutableScope object instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            scopeobj = MutableScope(scope, optional=optional)
        else:
            if isinstance(scope, str):
                scopeobj = MutableScope(scope)
            else:
                scopeobj = scope
        self.dependencies.append(scopeobj)
        return self

    def __repr__(self) -> str:
        parts: list[str] = [f"'{self.scope_string}'"]
        if self.optional:
            parts.append("optional=True")
        if self.dependencies:
            parts.append(f"dependencies={self.dependencies!r}")
        return "MutableScope(" + ", ".join(parts) + ")"

    def __str__(self) -> str:
        return self.serialize()

    @staticmethod
    def scopes2str(obj: ScopeCollectionType) -> str:
        """
        Given a scope string, a collection of scope strings, a MutableScope object, a
        collection of MutableScope objects, or a mixed collection of strings and
        Scopes, convert to a string which can be used in a request.

        :param obj: The object or collection to convert to a string
        :type obj: str, MutableScope, iterable of str or MutableScope
        """
        return " ".join(_iter_scope_collection(obj))
