# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the MIT License.  See the LICENSE file in the root of this
# repository for complete details.

"""
Global state department.  Don't reload this module or everything breaks.
"""

from __future__ import absolute_import, division, print_function

import platform
import sys
import warnings

from collections import OrderedDict

from ._generic import BoundLogger
from ._loggers import PrintLoggerFactory
from .dev import ConsoleRenderer, _has_colorama
from .processors import StackInfoRenderer, TimeStamper, format_exc_info


_BUILTIN_DEFAULT_PROCESSORS = [
    StackInfoRenderer(),
    format_exc_info,
    TimeStamper(fmt="%Y-%m-%d %H:%M.%S", utc=False),
    ConsoleRenderer(colors=_has_colorama),
]
if (
    sys.version_info[:2] >= (3, 6)
    or platform.python_implementation() == "PyPy"
):
    # Python 3.6+ and PyPy have ordered dicts.
    _BUILTIN_DEFAULT_CONTEXT_CLASS = dict
else:
    _BUILTIN_DEFAULT_CONTEXT_CLASS = OrderedDict
_BUILTIN_DEFAULT_WRAPPER_CLASS = BoundLogger
_BUILTIN_DEFAULT_LOGGER_FACTORY = PrintLoggerFactory()
_BUILTIN_CACHE_LOGGER_ON_FIRST_USE = False


class _Configuration(object):
    """
    Global defaults.
    """

    is_configured = False
    default_processors = _BUILTIN_DEFAULT_PROCESSORS[:]
    default_context_class = _BUILTIN_DEFAULT_CONTEXT_CLASS
    default_wrapper_class = _BUILTIN_DEFAULT_WRAPPER_CLASS
    logger_factory = _BUILTIN_DEFAULT_LOGGER_FACTORY
    cache_logger_on_first_use = _BUILTIN_CACHE_LOGGER_ON_FIRST_USE


_CONFIG = _Configuration()
"""
Global defaults used when arguments to :func:`wrap_logger` are omitted.
"""


def is_configured():
    """
    Return whether ``structlog`` has been configured.

    If ``False``, ``structlog`` is running with builtin defaults.

    :rtype: bool

    .. versionadded: 18.1
    """
    return _CONFIG.is_configured


def get_config():
    """
    Get a dictionary with the current configuration.

    .. note::

       Changes to the returned dictionary do *not* affect ``structlog``.

    :rtype: dict

    .. versionadded: 18.1
    """
    return {
        "processors": _CONFIG.default_processors,
        "context_class": _CONFIG.default_context_class,
        "wrapper_class": _CONFIG.default_wrapper_class,
        "logger_factory": _CONFIG.logger_factory,
        "cache_logger_on_first_use": _CONFIG.cache_logger_on_first_use,
    }


def get_logger(*args, **initial_values):
    """
    Convenience function that returns a logger according to configuration.

    >>> from structlog import get_logger
    >>> log = get_logger(y=23)
    >>> log.msg("hello", x=42)
    y=23 x=42 event='hello'

    :param args: *Optional* positional arguments that are passed unmodified to
        the logger factory.  Therefore it depends on the factory what they
        mean.
    :param initial_values: Values that are used to pre-populate your contexts.

    :rtype: A proxy that creates a correctly configured bound logger when
        necessary.

    See :ref:`configuration` for details.

    If you prefer CamelCase, there's an alias for your reading pleasure:
    :func:`structlog.getLogger`.

    .. versionadded:: 0.4.0
        `args`
    """
    return wrap_logger(None, logger_factory_args=args, **initial_values)


getLogger = get_logger
"""
CamelCase alias for :func:`structlog.get_logger`.

This function is supposed to be in every source file -- we don't want it to
stick out like a sore thumb in frameworks like Twisted or Zope.
"""


def wrap_logger(
    logger,
    processors=None,
    wrapper_class=None,
    context_class=None,
    cache_logger_on_first_use=None,
    logger_factory_args=None,
    **initial_values
):
    """
    Create a new bound logger for an arbitrary *logger*.

    Default values for *processors*, *wrapper_class*, and *context_class* can
    be set using :func:`configure`.

    If you set an attribute here, :func:`configure` calls have *no* effect for
    the *respective* attribute.

    In other words: selective overwriting of the defaults while keeping some
    *is* possible.

    :param initial_values: Values that are used to pre-populate your contexts.
    :param tuple logger_factory_args: Values that are passed unmodified as
        ``*logger_factory_args`` to the logger factory if not `None`.

    :rtype: A proxy that creates a correctly configured bound logger when
        necessary.

    See :func:`configure` for the meaning of the rest of the arguments.

    .. versionadded:: 0.4.0
        `logger_factory_args`
    """
    return BoundLoggerLazyProxy(
        logger,
        wrapper_class=wrapper_class,
        processors=processors,
        context_class=context_class,
        cache_logger_on_first_use=cache_logger_on_first_use,
        initial_values=initial_values,
        logger_factory_args=logger_factory_args,
    )


def configure(
    processors=None,
    wrapper_class=None,
    context_class=None,
    logger_factory=None,
    cache_logger_on_first_use=None,
):
    """
    Configures the **global** defaults.

    They are used if :func:`wrap_logger` has been called without arguments.

    Can be called several times, keeping an argument at `None` leaves is
    unchanged from the current setting.

    After calling for the first time, :func:`is_configured` starts returning
    ``True``.

    Use :func:`reset_defaults` to undo your changes.

    :param list processors: List of processors.
    :param type wrapper_class: Class to use for wrapping loggers instead of
        :class:`structlog.BoundLogger`.  See :doc:`standard-library`,
        :doc:`twisted`, and :doc:`custom-wrappers`.
    :param type context_class: Class to be used for internal context keeping.
    :param callable logger_factory: Factory to be called to create a new
        logger that shall be wrapped.
    :param bool cache_logger_on_first_use: `wrap_logger` doesn't return an
        actual wrapped logger but a proxy that assembles one when it's first
        used.  If this option is set to `True`, this assembled logger is
        cached.  See :doc:`performance`.

    .. versionadded:: 0.3.0
        `cache_logger_on_first_use`
    """
    _CONFIG.is_configured = True
    if processors is not None:
        _CONFIG.default_processors = processors
    if wrapper_class is not None:
        _CONFIG.default_wrapper_class = wrapper_class
    if context_class is not None:
        _CONFIG.default_context_class = context_class
    if logger_factory is not None:
        _CONFIG.logger_factory = logger_factory
    if cache_logger_on_first_use is not None:
        _CONFIG.cache_logger_on_first_use = cache_logger_on_first_use


def configure_once(*args, **kw):
    """
    Configures iff structlog isn't configured yet.

    It does *not* matter whether is was configured using :func:`configure`
    or :func:`configure_once` before.

    Raises a :class:`RuntimeWarning` if repeated configuration is attempted.
    """
    if not _CONFIG.is_configured:
        configure(*args, **kw)
    else:
        warnings.warn("Repeated configuration attempted.", RuntimeWarning)


def reset_defaults():
    """
    Resets global default values to builtin defaults.

    :func:`is_configured` starts returning ``False`` afterwards.
    """
    _CONFIG.is_configured = False
    _CONFIG.default_processors = _BUILTIN_DEFAULT_PROCESSORS[:]
    _CONFIG.default_wrapper_class = _BUILTIN_DEFAULT_WRAPPER_CLASS
    _CONFIG.default_context_class = _BUILTIN_DEFAULT_CONTEXT_CLASS
    _CONFIG.logger_factory = _BUILTIN_DEFAULT_LOGGER_FACTORY
    _CONFIG.cache_logger_on_first_use = _BUILTIN_CACHE_LOGGER_ON_FIRST_USE


class BoundLoggerLazyProxy(object):
    """
    Instantiates a BoundLogger on first usage.

    Takes both configuration and instantiation parameters into account.

    The only points where a BoundLogger changes state are bind(), unbind(), and
    new() and that return the actual BoundLogger.

    If and only if configuration says so, that actual BoundLogger is cached on
    first usage.

    .. versionchanged:: 0.4.0
        Added support for `logger_factory_args`.
    """

    def __init__(
        self,
        logger,
        wrapper_class=None,
        processors=None,
        context_class=None,
        cache_logger_on_first_use=None,
        initial_values=None,
        logger_factory_args=None,
    ):
        self._logger = logger
        self._wrapper_class = wrapper_class
        self._processors = processors
        self._context_class = context_class
        self._cache_logger_on_first_use = cache_logger_on_first_use
        self._initial_values = initial_values or {}
        self._logger_factory_args = logger_factory_args or ()

    def __repr__(self):
        return (
            "<BoundLoggerLazyProxy(logger={0._logger!r}, wrapper_class="
            "{0._wrapper_class!r}, processors={0._processors!r}, "
            "context_class={0._context_class!r}, "
            "initial_values={0._initial_values!r}, "
            "logger_factory_args={0._logger_factory_args!r})>".format(self)
        )

    def bind(self, **new_values):
        """
        Assemble a new BoundLogger from arguments and configuration.
        """
        if self._context_class:
            ctx = self._context_class(self._initial_values)
        else:
            ctx = _CONFIG.default_context_class(self._initial_values)
        cls = self._wrapper_class or _CONFIG.default_wrapper_class
        _logger = self._logger
        if not _logger:
            _logger = _CONFIG.logger_factory(*self._logger_factory_args)

        if self._processors is None:
            procs = _CONFIG.default_processors
        else:
            procs = self._processors
        logger = cls(_logger, processors=procs, context=ctx)

        def finalized_bind(**new_values):
            """
            Use cached assembled logger to bind potentially new values.
            """
            if new_values:
                return logger.bind(**new_values)
            else:
                return logger

        if self._cache_logger_on_first_use is True or (
            self._cache_logger_on_first_use is None
            and _CONFIG.cache_logger_on_first_use is True
        ):
            self.bind = finalized_bind
        return finalized_bind(**new_values)

    def unbind(self, *keys):
        """
        Same as bind, except unbind *keys* first.

        In our case that could be only initial values.
        """
        return self.bind().unbind(*keys)

    def new(self, **new_values):
        """
        Clear context, then bind.
        """
        if self._context_class:
            self._context_class().clear()
        else:
            _CONFIG.default_context_class().clear()
        bl = self.bind(**new_values)
        return bl

    def __getattr__(self, name):
        """
        If a logging method if called on a lazy proxy, we have to create an
        ephemeral BoundLogger first.
        """
        bl = self.bind()
        return getattr(bl, name)
