# -*- coding: utf-8 -*-
# Copyright (c) 2010, Sebastian Wiesner <lunaryorn@googlemail.com>
# All rights reserved.

# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:

# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.


"""
    sphinxcontrib.ansi
    ==================

    This extension parses ANSI color codes in literal blocks.

    .. moduleauthor::  Sebastian Wiesner  <lunaryorn@googlemail.com>
"""

from __future__ import absolute_import, print_function
import logging
import os
from os import path
import re

from typing import Dict, List

from docutils import nodes
from docutils.parsers import rst
from docutils.parsers.rst.directives import flag
from sphinx.util.osutil import copyfile

# DISABLED: from sphinx.util.console import bold


__version__ = '0.7.0'

# -- DIAGNOSTIC SUPPORT:
DIAG = os.environ.get("SPHINXCONTRIB_ANSI_DIAG", "no") == "yes"
console = logging.getLogger("sphinxcontrib.programoutput")
console.setLevel(logging.ERROR)
if DIAG:
    console.setLevel(logging.INFO)


# pylint: disable=too-many-ancestors
class ansi_literal_block(nodes.literal_block):
    """
    Represent a literal block, that contains ANSI color codes.
    """


#: the pattern to find ANSI color codes
COLOR_PATTERN = re.compile('\x1b\\[([^m]+)m')

#: map ANSI color codes to class names
CODE_CLASS_MAP: Dict[int, List[str]] = {
    1: ['bold'],
    2: ['dim'],
    4: ['underscore'],
    30: ['black'],
    31: ['red'],
    32: ['green'],
    33: ['yellow'],
    34: ['blue'],
    35: ['magenta'],
    36: ['cyan'],
    37: ['white'],
    40: ['bg_black'],
    41: ['bg_red'],
    42: ['bg_green'],
    43: ['bg_yellow'],
    44: ['bg_blue'],
    45: ['bg_magenta'],
    46: ['bg_cyan'],
    47: ['bg_white'],
    90: ['black', 'bright'],
    91: ['red', 'bright'],
    92: ['green', 'bright'],
    93: ['yellow', 'bright'],
    94: ['blue', 'bright'],
    95: ['magenta', 'bright'],
    102: ['bg_green', 'bright'],
    96: ['cyan', 'bright'],
    97: ['white', 'bright'],
    100: ['bg_black', 'bright'],
    101: ['bg_red', 'bright'],
    103: ['bg_yellow', 'bright'],
    104: ['bg_blue', 'bright'],
    105: ['bg_magenta', 'bright'],
    106: ['bg_cyan', 'bright'],
    107: ['bg_white', 'bright'],
}


# see https://stackoverflow.com/a/58829514/99834
def string_escape(s, encoding='utf-8'):
    return s.encode('latin1').decode('unicode-escape').encode('latin1').decode(encoding)


# pylint: disable=too-few-public-methods
class ANSIColorParser:
    """
    Traverse a document, look for ansi_literal_block nodes, parse these
    nodes, and replace them with literal blocks, containing proper child
    nodes for ANSI color sequences.
    """

    def __init__(self):
        self.pending_nodes = []
        self.new_nodes = []

    def _finalize_pending_nodes(self):
        """
        Finalize all pending nodes.

        Pending nodes will be append to the new nodes.
        """
        self.new_nodes.extend(self.pending_nodes)
        self.pending_nodes = []

    def _add_text(self, text):
        """
        If ``text`` is not empty, append a new Text node to the most recent
        pending node, if there is any, or to the new nodes, if there are no
        pending nodes.
        """
        if text:
            if self.pending_nodes:
                self.pending_nodes[-1].append(nodes.Text(text))
            else:
                self.new_nodes.append(nodes.Text(text))

    def _colorize_block_contents(self, block):
        raw = block.rawsource
        # create the "super" node, which contains to while block and all it
        # sub nodes, and replace the old block with it
        literal_node = nodes.literal_block()
        literal_node['classes'].append('ansi-block')
        block.replace_self(literal_node)

        # this contains "pending" nodes.  A node representing an ANSI
        # color is "pending", if it has not yet seen a reset
        self.pending_nodes = []
        # these are the nodes, that will finally be added to the
        # literal_node
        self.new_nodes = []
        # this holds the end of the last regex match
        last_end = 0
        # iterate over all color codes
        console.warning("\nANSI_COLORIZE.block:\n%s\nANSI_COLORIZE.block.end\n", raw)

        for match in COLOR_PATTERN.finditer(raw):
            # add any text preceeding this match
            head = raw[last_end : match.start()]
            self._add_text(head)
            # update the match end
            last_end = match.end()
            # get the single format codes
            codes = [int(c) for c in match.group(1).split(';')]
            if codes[-1] == 0:
                # the last code is a reset, so finalize all pending
                # nodes.
                self._finalize_pending_nodes()
            else:
                # create a new color node
                code_node = nodes.inline()
                self.pending_nodes.append(code_node)
                # and set the classes for its colors
                for code in codes:
                    # code_node['classes'].append('ansi-%s' % code)
                    if code in CODE_CLASS_MAP:
                        for entry in CODE_CLASS_MAP[code]:
                            code_node['classes'].append(f'ansi-{entry}')
                    else:
                        console.error("Unknown ANSI code %s found, ignored.", code)

        # add any trailing text
        tail = raw[last_end:]
        self._add_text(tail)
        # move all pending nodes to new_nodes
        self._finalize_pending_nodes()
        # and add the new nodes to the block
        literal_node.extend(self.new_nodes)

    # pylint: disable=no-self-use
    def _strip_color_from_block_content(self, block):
        content = COLOR_PATTERN.sub('', block.rawsource)
        literal_node = nodes.literal_block(content, content)
        block.replace_self(literal_node)

    def __call__(self, app, doctree, docname):
        """
        Extract and parse all ansi escapes in ansi_literal_block nodes.
        """
        handler = self._colorize_block_contents
        if app.builder.name != 'html':
            # strip all color codes in non-html output
            handler = self._strip_color_from_block_content
        for ansi_block in doctree.traverse(ansi_literal_block):
            handler(ansi_block)


def add_stylesheet(app):
    app.add_css_file('ansi.css')


def copy_stylesheet(app, exception):
    if app.builder.name != 'html' or exception:
        return

    verbose = hasattr(app, 'info')
    stylesheet = app.config.html_ansi_stylesheet
    if stylesheet:
        if verbose:
            # DISABLED; app.info(bold('Copying ansi stylesheet... '), nonl=True)
            app.info('Copying ansi stylesheet... ', nonl=True)
        dest = path.join(app.builder.outdir, '_static', 'ansi.css')
        source = path.abspath(path.dirname(__file__))
        copyfile(path.join(source, stylesheet), dest)
        if verbose:
            app.info('done')


class ANSIBlockDirective(rst.Directive):
    """
    This directive interprets its content as literal block with ANSI color
    codes.

    The content is decoded using ``string-escape`` to allow symbolic names
    as \x1b being used instead of the real escape character.
    """

    has_content = True

    option_spec = dict(string_escape=flag)

    def run(self):
        text = '\n'.join(self.content)
        if 'string_escape' in self.options:
            text = string_escape(text)
        return [ansi_literal_block(text, text)]


def setup(app):
    app.require_sphinx('1.0')
    app.add_config_value('html_ansi_stylesheet', None, 'env')
    app.add_directive('ansi-block', ANSIBlockDirective)
    app.connect('builder-inited', add_stylesheet)
    app.connect('build-finished', copy_stylesheet)
    app.connect('doctree-resolved', ANSIColorParser())

    return {
        'version': __version__,
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }
