# DExTer : Debugging Experience Tester
# ~~~~~~   ~         ~~         ~   ~~
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Provides formatted/colored console output on both Windows and Linux.

Do not use this module directly, but instead use via the appropriate platform-
specific module.
"""

import abc
import re
import sys
import threading
import unittest

from io import StringIO

from dex.utils.Exceptions import Error


class _NullLock(object):
    def __enter__(self):
        return None

    def __exit__(self, *params):
        pass


_lock = threading.Lock()
_null_lock = _NullLock()


class PreserveAutoColors(object):
    def __init__(self, pretty_output):
        self.pretty_output = pretty_output
        self.orig_values = {}
        self.properties = ["auto_reds", "auto_yellows", "auto_greens", "auto_blues"]

    def __enter__(self):
        for p in self.properties:
            self.orig_values[p] = getattr(self.pretty_output, p)[:]
        return self

    def __exit__(self, *args):
        for p in self.properties:
            setattr(self.pretty_output, p, self.orig_values[p])


class Stream(object):
    def __init__(self, py_, os_=None):
        self.py = py_
        self.os = os_
        self.orig_color = None
        self.color_enabled = self.py.isatty()


class PrettyOutputBase(object, metaclass=abc.ABCMeta):
    stdout = Stream(sys.stdout)
    stderr = Stream(sys.stderr)

    def __init__(self):
        self.auto_reds = []
        self.auto_yellows = []
        self.auto_greens = []
        self.auto_blues = []
        self._stack = []

    def __enter__(self):
        return self

    def __exit__(self, *args):
        pass

    def _set_valid_stream(self, stream):
        if stream is None:
            return self.__class__.stdout
        return stream

    def _write(self, text, stream):
        text = str(text)

        # Users can embed color control tags in their output
        # (e.g. <r>hello</> <y>world</> would write the word 'hello' in red and
        # 'world' in yellow).
        # This function parses these tags using a very simple recursive
        # descent.
        colors = {
            "r": self.red,
            "y": self.yellow,
            "g": self.green,
            "b": self.blue,
            "d": self.default,
            "a": self.auto,
        }

        # Find all tags (whether open or close)
        tags = [t for t in re.finditer("<([{}/])>".format("".join(colors)), text)]

        if not tags:
            # No tags.  Just write the text to the current stream and return.
            # 'unmangling' any tags that have been mangled so that they won't
            # render as colors (for example in error output from this
            # function).
            stream = self._set_valid_stream(stream)
            stream.py.write(text.replace(r"\>", ">"))
            return

        open_tags = [i for i in tags if i.group(1) != "/"]
        close_tags = [i for i in tags if i.group(1) == "/"]

        if len(open_tags) != len(close_tags) or any(
            o.start() >= c.start() for (o, c) in zip(open_tags, close_tags)
        ):
            raise Error(
                'open/close tag mismatch in "{}"'.format(text.rstrip()).replace(
                    ">", r"\>"
                )
            )

        open_tag = open_tags.pop(0)

        # We know that the tags balance correctly, so figure out where the
        # corresponding close tag is to the current open tag.
        tag_nesting = 1
        close_tag = None
        for tag in tags[1:]:
            if tag.group(1) == "/":
                tag_nesting -= 1
            else:
                tag_nesting += 1
            if tag_nesting == 0:
                close_tag = tag
                break
        else:
            assert False, text

        # Use the method on the top of the stack for text prior to the open
        # tag.
        before = text[: open_tag.start()]
        if before:
            self._stack[-1](before, lock=_null_lock, stream=stream)

        # Use the specified color for the tag itself.
        color = open_tag.group(1)
        within = text[open_tag.end() : close_tag.start()]
        if within:
            colors[color](within, lock=_null_lock, stream=stream)

        # Use the method on the top of the stack for text after the close tag.
        after = text[close_tag.end() :]
        if after:
            self._stack[-1](after, lock=_null_lock, stream=stream)

    def flush(self, stream):
        stream = self._set_valid_stream(stream)
        stream.py.flush()

    def auto(self, text, stream=None, lock=_lock):
        text = str(text)
        stream = self._set_valid_stream(stream)
        lines = text.splitlines(True)

        with lock:
            for line in lines:
                # This is just being cute for the sake of cuteness, but why
                # not?
                line = line.replace("DExTer", "<r>D<y>E<g>x<b>T</></>e</>r</>")

                # Apply the appropriate color method if the expression matches
                # any of
                # the patterns we have set up.
                for fn, regexs in (
                    (self.red, self.auto_reds),
                    (self.yellow, self.auto_yellows),
                    (self.green, self.auto_greens),
                    (self.blue, self.auto_blues),
                ):
                    if any(re.search(regex, line) for regex in regexs):
                        fn(line, stream=stream, lock=_null_lock)
                        break
                else:
                    self.default(line, stream=stream, lock=_null_lock)

    def _call_color_impl(self, fn, impl, text, *args, **kwargs):
        try:
            self._stack.append(fn)
            return impl(text, *args, **kwargs)
        finally:
            fn = self._stack.pop()

    @abc.abstractmethod
    def red_impl(self, text, stream=None, **kwargs):
        pass

    def red(self, *args, **kwargs):
        return self._call_color_impl(self.red, self.red_impl, *args, **kwargs)

    @abc.abstractmethod
    def yellow_impl(self, text, stream=None, **kwargs):
        pass

    def yellow(self, *args, **kwargs):
        return self._call_color_impl(self.yellow, self.yellow_impl, *args, **kwargs)

    @abc.abstractmethod
    def green_impl(self, text, stream=None, **kwargs):
        pass

    def green(self, *args, **kwargs):
        return self._call_color_impl(self.green, self.green_impl, *args, **kwargs)

    @abc.abstractmethod
    def blue_impl(self, text, stream=None, **kwargs):
        pass

    def blue(self, *args, **kwargs):
        return self._call_color_impl(self.blue, self.blue_impl, *args, **kwargs)

    @abc.abstractmethod
    def default_impl(self, text, stream=None, **kwargs):
        pass

    def default(self, *args, **kwargs):
        return self._call_color_impl(self.default, self.default_impl, *args, **kwargs)

    def colortest(self):
        from itertools import combinations, permutations

        fns = (
            (self.red, "rrr"),
            (self.yellow, "yyy"),
            (self.green, "ggg"),
            (self.blue, "bbb"),
            (self.default, "ddd"),
        )

        for l in range(1, len(fns) + 1):
            for comb in combinations(fns, l):
                for perm in permutations(comb):
                    for stream in (None, self.__class__.stderr):
                        perm[0][0]("stdout " if stream is None else "stderr ", stream)
                        for fn, string in perm:
                            fn(string, stream)
                        self.default("\n", stream)

        tests = [
            (self.auto, "default1<r>red2</>default3"),
            (self.red, "red1<r>red2</>red3"),
            (self.blue, "blue1<r>red2</>blue3"),
            (self.red, "red1<y>yellow2</>red3"),
            (self.auto, "default1<y>yellow2<r>red3</></>"),
            (self.auto, "default1<g>green2<r>red3</></>"),
            (self.auto, "default1<g>green2<r>red3</>green4</>default5"),
            (self.auto, "default1<g>green2</>default3<g>green4</>default5"),
            (self.auto, "<r>red1<g>green2</>red3<g>green4</>red5</>"),
            (self.auto, "<r>red1<y><g>green2</>yellow3</>green4</>default5"),
            (self.auto, "<r><y><g><b><d>default1</></><r></></></>red2</>"),
            (self.auto, "<r>red1</>default2<r>red3</><g>green4</>default5"),
            (self.blue, "<r>red1</>blue2<r><r>red3</><g><g>green</></></>"),
            (self.blue, "<r>r<r>r<y>y<r><r><r><r>r</></></></></></></>b"),
        ]

        for fn, text in tests:
            for stream in (None, self.__class__.stderr):
                stream_name = "stdout" if stream is None else "stderr"
                fn("{} {}\n".format(stream_name, text), stream)


class TestPrettyOutput(unittest.TestCase):
    class MockPrettyOutput(PrettyOutputBase):
        def red_impl(self, text, stream=None, **kwargs):
            self._write("[R]{}[/R]".format(text), stream)

        def yellow_impl(self, text, stream=None, **kwargs):
            self._write("[Y]{}[/Y]".format(text), stream)

        def green_impl(self, text, stream=None, **kwargs):
            self._write("[G]{}[/G]".format(text), stream)

        def blue_impl(self, text, stream=None, **kwargs):
            self._write("[B]{}[/B]".format(text), stream)

        def default_impl(self, text, stream=None, **kwargs):
            self._write("[D]{}[/D]".format(text), stream)

    def test_red(self):
        with TestPrettyOutput.MockPrettyOutput() as o:
            stream = Stream(StringIO())
            o.red("hello", stream)
            self.assertEqual(stream.py.getvalue(), "[R]hello[/R]")

    def test_yellow(self):
        with TestPrettyOutput.MockPrettyOutput() as o:
            stream = Stream(StringIO())
            o.yellow("hello", stream)
            self.assertEqual(stream.py.getvalue(), "[Y]hello[/Y]")

    def test_green(self):
        with TestPrettyOutput.MockPrettyOutput() as o:
            stream = Stream(StringIO())
            o.green("hello", stream)
            self.assertEqual(stream.py.getvalue(), "[G]hello[/G]")

    def test_blue(self):
        with TestPrettyOutput.MockPrettyOutput() as o:
            stream = Stream(StringIO())
            o.blue("hello", stream)
            self.assertEqual(stream.py.getvalue(), "[B]hello[/B]")

    def test_default(self):
        with TestPrettyOutput.MockPrettyOutput() as o:
            stream = Stream(StringIO())
            o.default("hello", stream)
            self.assertEqual(stream.py.getvalue(), "[D]hello[/D]")

    def test_auto(self):
        with TestPrettyOutput.MockPrettyOutput() as o:
            stream = Stream(StringIO())
            o.auto_reds.append("foo")
            o.auto("bar\n", stream)
            o.auto("foo\n", stream)
            o.auto("baz\n", stream)
            self.assertEqual(
                stream.py.getvalue(), "[D]bar\n[/D][R]foo\n[/R][D]baz\n[/D]"
            )

            stream = Stream(StringIO())
            o.auto("bar\nfoo\nbaz\n", stream)
            self.assertEqual(
                stream.py.getvalue(), "[D]bar\n[/D][R]foo\n[/R][D]baz\n[/D]"
            )

            stream = Stream(StringIO())
            o.auto("barfoobaz\nbardoobaz\n", stream)
            self.assertEqual(
                stream.py.getvalue(), "[R]barfoobaz\n[/R][D]bardoobaz\n[/D]"
            )

            o.auto_greens.append("doo")
            stream = Stream(StringIO())
            o.auto("barfoobaz\nbardoobaz\n", stream)
            self.assertEqual(
                stream.py.getvalue(), "[R]barfoobaz\n[/R][G]bardoobaz\n[/G]"
            )

    def test_PreserveAutoColors(self):
        with TestPrettyOutput.MockPrettyOutput() as o:
            o.auto_reds.append("foo")
            with PreserveAutoColors(o):
                o.auto_greens.append("bar")
                stream = Stream(StringIO())
                o.auto("foo\nbar\nbaz\n", stream)
                self.assertEqual(
                    stream.py.getvalue(), "[R]foo\n[/R][G]bar\n[/G][D]baz\n[/D]"
                )

            stream = Stream(StringIO())
            o.auto("foo\nbar\nbaz\n", stream)
            self.assertEqual(
                stream.py.getvalue(), "[R]foo\n[/R][D]bar\n[/D][D]baz\n[/D]"
            )

            stream = Stream(StringIO())
            o.yellow("<a>foo</>bar<a>baz</>", stream)
            self.assertEqual(
                stream.py.getvalue(),
                "[Y][Y][/Y][R]foo[/R][Y][Y]bar[/Y][D]baz[/D][Y][/Y][/Y][/Y]",
            )

    def test_tags(self):
        with TestPrettyOutput.MockPrettyOutput() as o:
            stream = Stream(StringIO())
            o.auto("<r>hi</>", stream)
            self.assertEqual(stream.py.getvalue(), "[D][D][/D][R]hi[/R][D][/D][/D]")

            stream = Stream(StringIO())
            o.auto("<r><y>a</>b</>c", stream)
            self.assertEqual(
                stream.py.getvalue(),
                "[D][D][/D][R][R][/R][Y]a[/Y][R]b[/R][/R][D]c[/D][/D]",
            )

            with self.assertRaisesRegex(Error, "tag mismatch"):
                o.auto("<r>hi", stream)

            with self.assertRaisesRegex(Error, "tag mismatch"):
                o.auto("hi</>", stream)

            with self.assertRaisesRegex(Error, "tag mismatch"):
                o.auto("<r><y>hi</>", stream)

            with self.assertRaisesRegex(Error, "tag mismatch"):
                o.auto("<r><y>hi</><r></>", stream)

            with self.assertRaisesRegex(Error, "tag mismatch"):
                o.auto("</>hi<r>", stream)
