# Copyright 2020 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Tools for configuring Python logging."""

import logging
from pathlib import Path
import sys
from typing import NamedTuple, Optional, Union, Iterator

from pw_cli.color import colors as pw_cli_colors
from pw_cli.env import pigweed_environment

# Log level used for captured output of a subprocess run through pw.
LOGLEVEL_STDOUT = 21

# Log level indicating a irrecoverable failure.
LOGLEVEL_FATAL = 70


class _LogLevel(NamedTuple):
    level: int
    color: str
    ascii: str
    emoji: str


# Shorten all the log levels to 3 characters for column-aligned logs.
# Color the logs using ANSI codes.
# fmt: off
_LOG_LEVELS = (
    _LogLevel(LOGLEVEL_FATAL,   'bold_red',     'FTL', '☠️ '),
    _LogLevel(logging.CRITICAL, 'bold_magenta', 'CRT', '‼️ '),
    _LogLevel(logging.ERROR,    'red',          'ERR', '❌'),
    _LogLevel(logging.WARNING,  'yellow',       'WRN', '⚠️ '),
    _LogLevel(logging.INFO,     'magenta',      'INF', 'ℹ️ '),
    _LogLevel(LOGLEVEL_STDOUT,  'cyan',         'OUT', '💬'),
    _LogLevel(logging.DEBUG,    'blue',         'DBG', '👾'),
)
# fmt: on

_LOG = logging.getLogger(__name__)
_STDERR_HANDLER = logging.StreamHandler()


def c_to_py_log_level(c_level: int) -> int:
    """Converts pw_log C log-level macros to Python logging levels."""
    return c_level * 10


def main() -> None:
    """Shows how logs look at various levels."""

    # Force the log level to make sure all logs are shown.
    _LOG.setLevel(logging.DEBUG)

    # Log one message for every log level.
    _LOG.log(LOGLEVEL_FATAL, 'An irrecoverable error has occurred!')
    _LOG.critical('Something important has happened!')
    _LOG.error('There was an error on our last operation')
    _LOG.warning('Looks like something is amiss; consider investigating')
    _LOG.info('The operation went as expected')
    _LOG.log(LOGLEVEL_STDOUT, 'Standard output of subprocess')
    _LOG.debug('Adding 1 to i')


def _setup_handler(
    handler: logging.Handler,
    formatter: logging.Formatter,
    level: Union[str, int],
    logger: logging.Logger,
) -> None:
    handler.setLevel(level)
    handler.setFormatter(formatter)
    logger.addHandler(handler)


def install(
    level: Union[str, int] = logging.INFO,
    use_color: Optional[bool] = None,
    hide_timestamp: bool = False,
    log_file: Optional[Union[str, Path]] = None,
    logger: Optional[logging.Logger] = None,
    debug_log: Optional[Union[str, Path]] = None,
    time_format: str = '%Y%m%d %H:%M:%S',
    msec_format: str = '%s,%03d',
    include_msec: bool = False,
    message_format: str = '%(levelname)s %(message)s',
) -> None:
    """Configures the system logger for the default pw command log format.

    If you have Python loggers separate from the root logger you can use
    `pw_cli.log.install` to get the Pigweed log formatting there too. For
    example: ::

        import logging

        import pw_cli.log

        pw_cli.log.install(
            level=logging.INFO,
            use_color=True,
            hide_timestamp=False,
            log_file=(Path.home() / 'logs.txt'),
            logger=logging.getLogger(__package__),
        )

    Args:
      level: The logging level to apply. Default: `logging.INFO`.
      use_color: When `True` include ANSI escape sequences to colorize log
          messages.
      hide_timestamp: When `True` omit timestamps from the log formatting.
      log_file: File to send logs into instead of the terminal.
      logger: Python Logger instance to install Pigweed formatting into.
          Defaults to the Python root logger: `logging.getLogger()`.
      debug_log: File to log to from all levels, regardless of chosen log level.
          Logs will go here in addition to the terminal.
      time_format: Default time format string.
      msec_format: Default millisecond format string. This should be a format
          string that accepts a both a string ``%s`` and an integer ``%d``. The
          default Python format for this string is ``%s,%03d``.
      include_msec: Whether or not to include the millisecond part of log
          timestamps.
      message_format: The message format string. By default this includes
          levelname and message. The asctime field is prepended to this unless
          hide_timestamp=True.
    """
    if not logger:
        logger = logging.getLogger()

    colors = pw_cli_colors(use_color)

    env = pigweed_environment()
    if env.PW_SUBPROCESS or hide_timestamp:
        # If the logger is being run in the context of a pw subprocess, the
        # time and date are omitted (since pw_cli.process will provide them).
        timestamp_fmt = ''
    else:
        # This applies a gray background to the time to make the log lines
        # distinct from other input, in a way that's easier to see than plain
        # colored text.
        timestamp_fmt = colors.black_on_white('%(asctime)s') + ' '

    formatter = logging.Formatter(fmt=timestamp_fmt + message_format)

    formatter.default_time_format = time_format
    if include_msec:
        formatter.default_msec_format = msec_format
    else:
        # Python 3.8 and lower does not check if default_msec_format is set.
        # https://github.com/python/cpython/blob/3.8/Lib/logging/__init__.py#L611
        # https://github.com/python/cpython/blob/3.9/Lib/logging/__init__.py#L605
        if sys.version_info >= (
            3,
            9,
        ):
            formatter.default_msec_format = ''

    # Set the log level on the root logger to NOTSET, so that all logs
    # propagated from child loggers are handled.
    logging.getLogger().setLevel(logging.NOTSET)

    # Always set up the stderr handler, even if it isn't used.
    _setup_handler(_STDERR_HANDLER, formatter, level, logger)

    if log_file:
        # Set utf-8 encoding for the log file. Encoding errors may come up on
        # Windows if the default system encoding is set to cp1250.
        _setup_handler(
            logging.FileHandler(log_file, encoding='utf-8'),
            formatter,
            level,
            logger,
        )
        # Since we're using a file, filter logs out of the stderr handler.
        _STDERR_HANDLER.setLevel(logging.CRITICAL + 1)

    if debug_log:
        # Set utf-8 encoding for the log file. Encoding errors may come up on
        # Windows if the default system encoding is set to cp1250.
        _setup_handler(
            logging.FileHandler(debug_log, encoding='utf-8'),
            formatter,
            logging.DEBUG,
            logger,
        )

    if env.PW_EMOJI:
        name_attr = 'emoji'
        colorize = lambda ll: str
    else:
        name_attr = 'ascii'
        colorize = lambda ll: getattr(colors, ll.color)

    for log_level in _LOG_LEVELS:
        name = getattr(log_level, name_attr)
        logging.addLevelName(log_level.level, colorize(log_level)(name))


def all_loggers() -> Iterator[logging.Logger]:
    """Iterates over all loggers known to Python logging."""
    manager = logging.getLogger().manager  # type: ignore[attr-defined]

    for logger_name in manager.loggerDict:  # pylint: disable=no-member
        yield logging.getLogger(logger_name)


def set_all_loggers_minimum_level(level: int) -> None:
    """Increases the log level to the specified value for all known loggers."""
    for logger in all_loggers():
        if logger.isEnabledFor(level - 1):
            logger.setLevel(level)


if __name__ == '__main__':
    install()
    main()
