"""The package provides top-level helpers which use a lazily initialised
default parser. These are convenience functions, for more control it
is perfectly acceptable to instantiate and call parsers directly.

The default parser does use a cache keyed on the user-agent string,
but its exact behaviour is unspecified, if you require a consistent
behaviour or specific algorithm, set up your own parser (global or
not).

For convenience, direct aliases are also provided for:

- :mod:`core types <.types>`
- :mod:`caching utilities <.caching>`
- :mod:`ua_parser.basic.Parser` as :class:`BasicParser`

This way importing anything but the top-level package should not be
necessary unless you want to *implement* a parser.
"""

from __future__ import annotations

__all__ = [
    "OS",
    "BasicResolver",
    "Cache",
    "CachingResolver",
    "DefaultedResult",
    "Device",
    "Domain",
    "Matchers",
    "PartialResult",
    "Resolver",
    "Result",
    "UserAgent",
    "load_builtins",
    "load_lazy_builtins",
    "parse",
    "parse_device",
    "parse_os",
    "parse_user_agent",
]

import importlib.util
import threading
from typing import Callable, Optional, cast

from .basic import Resolver as BasicResolver
from .caching import CachingResolver, S3Fifo as Cache
from .core import (
    DefaultedResult,
    Device,
    Domain,
    Matchers,
    OS,
    PartialResult,
    Resolver,
    Result,
    UserAgent,
)
from .loaders import load_builtins, load_lazy_builtins
from .utils import IS_GRAAL

_ResolverCtor = Callable[[Matchers], Resolver]
Re2Resolver: Optional[_ResolverCtor] = None
if importlib.util.find_spec("re2"):
    from .re2 import Resolver as Re2Resolver
RegexResolver: Optional[_ResolverCtor] = None
if importlib.util.find_spec("ua_parser_rs"):
    from .regex import Resolver as RegexResolver
BestAvailableResolver: _ResolverCtor = next(
    filter(None, (RegexResolver, Re2Resolver, BasicResolver))
)


VERSION = (1, 0, 2)


class Parser:
    """Wrapper object, provides convenience methods around an
    underlying :class:`Resolver`.

    """

    @classmethod
    def from_matchers(cls, m: Matchers, /) -> Parser:
        """from_matchers(Matchers) -> Parser

        Instantiates a parser from the provided
        :class:`~ua_parser.core.Matchers` using the default resolver
        stack.

        """
        return cls(CachingResolver(BestAvailableResolver(m), Cache(2000)))

    def __init__(self, resolver: Resolver) -> None:
        self.resolver = resolver

    def __call__(self, ua: str, domains: Domain, /) -> PartialResult:
        """Parses the ``ua`` string, returning a parse result with *at least*
        the requested :class:`domains <Domain>` resolved (whether to success or
        failure).
        """
        return self.resolver(ua, domains)

    def parse(self: Resolver, ua: str) -> Result:
        """Convenience method for parsing all domains."""
        return self(ua, Domain.ALL).complete()

    def parse_user_agent(self: Resolver, ua: str) -> Optional[UserAgent]:
        """Convenience method for parsing the :class:`UserAgent` domain."""
        return self(ua, Domain.USER_AGENT).user_agent

    def parse_os(self: Resolver, ua: str) -> Optional[OS]:
        """Convenience method for parsing the :class:`OS` domain."""
        return self(ua, Domain.OS).os

    def parse_device(self: Resolver, ua: str) -> Optional[Device]:
        """Convenience method for parsing the :class:`Device` domain."""
        return self(ua, Domain.DEVICE).device


parser: Parser
"""Global :class:`Parser`, lazy-initialised on first access, used by
the global helper functions.

Can be *set* to configure a customised global parser.

Accessing the parser explicitely can be used eagerly force its
initialisation, rather than pay for it at first call.
"""

_lazy_globals_lock = threading.Lock()


def __getattr__(name: str) -> Parser:
    global parser
    with _lazy_globals_lock:
        if name == "parser":
            # if two threads access `ua_parser.parser` before it's
            # initialised, the second one will wait until the first
            # one's finished by which time the parser global should be
            # set and can be returned with no extra work
            if p := globals().get("parser"):
                return cast(Parser, p)

            if RegexResolver or Re2Resolver or IS_GRAAL:
                matchers = load_lazy_builtins()
            else:
                matchers = load_builtins()
            parser = Parser.from_matchers(matchers)
            return parser

    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def parse(ua: str) -> Result:
    """Parses the :class:`.UserAgent`, :class:`.OS`, and :class:`.Device`
    information using the :data:`global parser <parser>`.

    Equivalent to calling each of :func:`parse_user_agent`,
    :func:`parse_os`, and :func:`parse_device` but *may* be more
    efficient than calling them separately depending on the underlying
    parser.

    Even in the best case, prefer the domain-specific helpers if
    you're not going to use *all* of them.
    """
    # import required to trigger __getattr__ and initialise the
    # parser, a `global` access fails to and we get a NameError
    from . import parser

    return parser(ua, Domain.ALL).complete()


def parse_user_agent(ua: str) -> Optional[UserAgent]:
    """Parses the :class:`browser <.UserAgent>` information using the
    :data:`global parser <parser>`.
    """
    from . import parser

    return parser(ua, Domain.USER_AGENT).user_agent


def parse_os(ua: str) -> Optional[OS]:
    """Parses the :class:`.OS` information using the :data:`global parser
    <parser>`.
    """
    from . import parser

    return parser(ua, Domain.OS).os


def parse_device(ua: str) -> Optional[Device]:
    """Parses the :class:`.Device` information using the :data:`global
    parser <parser>`.
    """
    from . import parser

    return parser(ua, Domain.DEVICE).device
