# SPDX-License-Identifier: GPL-2.0-only
# This file is part of Scapy
# See https://scapy.net/ for more information
# Copyright (C) Philippe Biondi <phil@secdev.org>

"""
Color themes for the interactive console.
"""

##################
#  Color themes  #
##################

import html
import sys

from typing import (
    Any,
    List,
    Optional,
    Tuple,
    cast,
)
from scapy.compat import Protocol


class ColorTable:
    colors = {  # Format: (ansi, pygments)
        # foreground
        "black": ("\033[30m", "#ansiblack"),
        "red": ("\033[31m", "#ansired"),
        "green": ("\033[32m", "#ansigreen"),
        "yellow": ("\033[33m", "#ansiyellow"),
        "blue": ("\033[34m", "#ansiblue"),
        "purple": ("\033[35m", "#ansipurple"),
        "cyan": ("\033[36m", "#ansicyan"),
        "white": ("\033[37m", "#ansiwhite"),
        "grey": ("\033[38;5;246m", "#ansiwhite"),
        "reset": ("\033[39m", "noinherit"),
        # background
        "bg_black": ("\033[40m", "bg:#ansiblack"),
        "bg_red": ("\033[41m", "bg:#ansired"),
        "bg_green": ("\033[42m", "bg:#ansigreen"),
        "bg_yellow": ("\033[43m", "bg:#ansiyellow"),
        "bg_blue": ("\033[44m", "bg:#ansiblue"),
        "bg_purple": ("\033[45m", "bg:#ansipurple"),
        "bg_cyan": ("\033[46m", "bg:#ansicyan"),
        "bg_white": ("\033[47m", "bg:#ansiwhite"),
        "bg_reset": ("\033[49m", "noinherit"),
        # specials
        "normal": ("\033[0m", "noinherit"),  # color & brightness
        "bold": ("\033[1m", "bold"),
        "uline": ("\033[4m", "underline"),
        "blink": ("\033[5m", ""),
        "invert": ("\033[7m", ""),
    }
    inv_map = {v[0]: v[1] for k, v in colors.items()}

    def __repr__(self):
        # type: () -> str
        return "<ColorTable>"

    def __getattr__(self, attr):
        # type: (str) -> str
        return self.colors.get(attr, [""])[0]

    def ansi_to_pygments(self, x):
        # type: (str) -> str
        """
        Transform ansi encoded text to Pygments text
        """
        for k, v in self.inv_map.items():
            x = x.replace(k, " " + v)
        return x.strip()


Color = ColorTable()


class _ColorFormatterType(Protocol):
    def __call__(self,
                 val: Any,
                 fmt: Optional[str] = None,
                 fmt2: str = "",
                 before: str = "",
                 after: str = "") -> str:
        pass


def create_styler(fmt=None,  # type: Optional[str]
                  before="",  # type: str
                  after="",  # type: str
                  fmt2="%s"  # type: str
                  ):
    # type: (...) -> _ColorFormatterType
    def do_style(val: Any,
                 fmt: Optional[str] = fmt,
                 fmt2: str = fmt2,
                 before: str = before,
                 after: str = after) -> str:
        if fmt is None:
            sval = str(val)
        else:
            sval = fmt % val
        return fmt2 % (before + sval + after)
    return do_style


class ColorTheme:
    style_normal = ""
    style_prompt = ""
    style_punct = ""
    style_id = ""
    style_not_printable = ""
    style_layer_name = ""
    style_field_name = ""
    style_field_value = ""
    style_emph_field_name = ""
    style_emph_field_value = ""
    style_depreciate_field_name = ""
    style_packetlist_name = ""
    style_packetlist_proto = ""
    style_packetlist_value = ""
    style_fail = ""
    style_success = ""
    style_odd = ""
    style_even = ""
    style_opening = ""
    style_active = ""
    style_closed = ""
    style_left = ""
    style_right = ""
    style_logo = ""

    def __repr__(self):
        # type: () -> str
        return "<%s>" % self.__class__.__name__

    def __reduce__(self):
        # type: () -> Tuple[type, Any, Any]
        return (self.__class__, (), ())

    def __getattr__(self, attr):
        # type: (str) -> _ColorFormatterType
        if attr in ["__getstate__", "__setstate__", "__getinitargs__",
                    "__reduce_ex__"]:
            raise AttributeError()
        return create_styler()

    def format(self, string, fmt):
        # type: (str, str) -> str
        for style in fmt.split("+"):
            string = getattr(self, style)(string)
        return string


class NoTheme(ColorTheme):
    pass


class AnsiColorTheme(ColorTheme):
    def __getattr__(self, attr):
        # type: (str) -> _ColorFormatterType
        if attr.startswith("__"):
            raise AttributeError(attr)
        s = "style_%s" % attr
        if s in self.__class__.__dict__:
            before = getattr(self, s)
            after = self.style_normal
        elif not isinstance(self, BlackAndWhite) and attr in Color.colors:
            before = Color.colors[attr][0]
            after = Color.colors["normal"][0]
        else:
            before = after = ""

        return create_styler(before=before, after=after)


class BlackAndWhite(AnsiColorTheme, NoTheme):
    pass


class DefaultTheme(AnsiColorTheme):
    style_normal = Color.normal
    style_prompt = Color.blue + Color.bold
    style_punct = Color.normal
    style_id = Color.blue + Color.bold
    style_not_printable = Color.white
    style_depreciate_field_name = Color.grey
    style_layer_name = Color.red + Color.bold
    style_field_name = Color.blue
    style_field_value = Color.purple
    style_emph_field_name = Color.blue + Color.uline + Color.bold
    style_emph_field_value = Color.purple + Color.uline + Color.bold
    style_packetlist_name = Color.red + Color.bold
    style_packetlist_proto = Color.blue
    style_packetlist_value = Color.purple
    style_fail = Color.red + Color.bold
    style_success = Color.blue + Color.bold
    style_even = Color.black + Color.bold
    style_odd = Color.black
    style_opening = Color.yellow
    style_active = Color.black
    style_closed = Color.white
    style_left = Color.blue + Color.invert
    style_right = Color.red + Color.invert
    style_logo = Color.green + Color.bold


class BrightTheme(AnsiColorTheme):
    style_normal = Color.normal
    style_punct = Color.normal
    style_id = Color.yellow + Color.bold
    style_layer_name = Color.red + Color.bold
    style_field_name = Color.yellow + Color.bold
    style_field_value = Color.purple + Color.bold
    style_emph_field_name = Color.yellow + Color.bold
    style_emph_field_value = Color.green + Color.bold
    style_packetlist_name = Color.red + Color.bold
    style_packetlist_proto = Color.yellow + Color.bold
    style_packetlist_value = Color.purple + Color.bold
    style_fail = Color.red + Color.bold
    style_success = Color.blue + Color.bold
    style_even = Color.black + Color.bold
    style_odd = Color.black
    style_left = Color.cyan + Color.invert
    style_right = Color.purple + Color.invert
    style_logo = Color.green + Color.bold


class RastaTheme(AnsiColorTheme):
    style_normal = Color.normal + Color.green + Color.bold
    style_prompt = Color.yellow + Color.bold
    style_punct = Color.red
    style_id = Color.green + Color.bold
    style_not_printable = Color.green
    style_layer_name = Color.red + Color.bold
    style_field_name = Color.yellow + Color.bold
    style_field_value = Color.green + Color.bold
    style_emph_field_name = Color.green
    style_emph_field_value = Color.green
    style_packetlist_name = Color.red + Color.bold
    style_packetlist_proto = Color.yellow + Color.bold
    style_packetlist_value = Color.green + Color.bold
    style_fail = Color.red
    style_success = Color.red + Color.bold
    style_even = Color.yellow
    style_odd = Color.green
    style_left = Color.yellow + Color.invert
    style_right = Color.red + Color.invert
    style_logo = Color.green + Color.bold


class ColorOnBlackTheme(AnsiColorTheme):
    """Color theme for black backgrounds"""
    style_normal = Color.normal
    style_prompt = Color.green + Color.bold
    style_punct = Color.normal
    style_id = Color.green
    style_not_printable = Color.black + Color.bold
    style_layer_name = Color.yellow + Color.bold
    style_field_name = Color.cyan
    style_field_value = Color.purple + Color.bold
    style_emph_field_name = Color.cyan + Color.bold
    style_emph_field_value = Color.red + Color.bold
    style_packetlist_name = Color.black + Color.bold
    style_packetlist_proto = Color.yellow + Color.bold
    style_packetlist_value = Color.purple + Color.bold
    style_fail = Color.red + Color.bold
    style_success = Color.green
    style_even = Color.black + Color.bold
    style_odd = Color.white
    style_opening = Color.yellow
    style_active = Color.white + Color.bold
    style_closed = Color.black + Color.bold
    style_left = Color.cyan + Color.bold
    style_right = Color.red + Color.bold
    style_logo = Color.green + Color.bold


class FormatTheme(ColorTheme):
    def __getattr__(self, attr: str) -> _ColorFormatterType:
        if attr.startswith("__"):
            raise AttributeError(attr)
        colfmt = self.__class__.__dict__.get("style_%s" % attr, "%s")
        return create_styler(fmt2=colfmt)


class LatexTheme(FormatTheme):
    r"""
    You can prepend the output from this theme with
    \tt\obeyspaces\obeylines\tiny\noindent
    """
    style_prompt = r"\textcolor{blue}{%s}"
    style_not_printable = r"\textcolor{gray}{%s}"
    style_layer_name = r"\textcolor{red}{\bf %s}"
    style_field_name = r"\textcolor{blue}{%s}"
    style_field_value = r"\textcolor{purple}{%s}"
    style_emph_field_name = r"\textcolor{blue}{\underline{%s}}"  # ul
    style_emph_field_value = r"\textcolor{purple}{\underline{%s}}"  # ul
    style_packetlist_name = r"\textcolor{red}{\bf %s}"
    style_packetlist_proto = r"\textcolor{blue}{%s}"
    style_packetlist_value = r"\textcolor{purple}{%s}"
    style_fail = r"\textcolor{red}{\bf %s}"
    style_success = r"\textcolor{blue}{\bf %s}"
    style_left = r"\textcolor{blue}{%s}"
    style_right = r"\textcolor{red}{%s}"
#    style_even = r"}{\bf "
#    style_odd = ""
    style_logo = r"\textcolor{green}{\bf %s}"

    def __getattr__(self, attr: str) -> _ColorFormatterType:
        from scapy.utils import tex_escape
        styler = super(LatexTheme, self).__getattr__(attr)
        return cast(
            _ColorFormatterType,
            lambda x, *args, **kwargs: styler(tex_escape(str(x)), *args, **kwargs),
        )


class LatexTheme2(FormatTheme):
    style_prompt = r"@`@textcolor@[@blue@]@@[@%s@]@"
    style_not_printable = r"@`@textcolor@[@gray@]@@[@%s@]@"
    style_layer_name = r"@`@textcolor@[@red@]@@[@@`@bfseries@[@@]@%s@]@"
    style_field_name = r"@`@textcolor@[@blue@]@@[@%s@]@"
    style_field_value = r"@`@textcolor@[@purple@]@@[@%s@]@"
    style_emph_field_name = r"@`@textcolor@[@blue@]@@[@@`@underline@[@%s@]@@]@"
    style_emph_field_value = r"@`@textcolor@[@purple@]@@[@@`@underline@[@%s@]@@]@"  # noqa: E501
    style_packetlist_name = r"@`@textcolor@[@red@]@@[@@`@bfseries@[@@]@%s@]@"
    style_packetlist_proto = r"@`@textcolor@[@blue@]@@[@%s@]@"
    style_packetlist_value = r"@`@textcolor@[@purple@]@@[@%s@]@"
    style_fail = r"@`@textcolor@[@red@]@@[@@`@bfseries@[@@]@%s@]@"
    style_success = r"@`@textcolor@[@blue@]@@[@@`@bfseries@[@@]@%s@]@"
    style_even = r"@`@textcolor@[@gray@]@@[@@`@bfseries@[@@]@%s@]@"
#    style_odd = r"@`@textcolor@[@black@]@@[@@`@bfseries@[@@]@%s@]@"
    style_left = r"@`@textcolor@[@blue@]@@[@%s@]@"
    style_right = r"@`@textcolor@[@red@]@@[@%s@]@"
    style_logo = r"@`@textcolor@[@green@]@@[@@`@bfseries@[@@]@%s@]@"


class HTMLTheme(FormatTheme):
    style_prompt = "<span class=prompt>%s</span>"
    style_not_printable = "<span class=not_printable>%s</span>"
    style_layer_name = "<span class=layer_name>%s</span>"
    style_field_name = "<span class=field_name>%s</span>"
    style_field_value = "<span class=field_value>%s</span>"
    style_emph_field_name = "<span class=emph_field_name>%s</span>"
    style_emph_field_value = "<span class=emph_field_value>%s</span>"
    style_packetlist_name = "<span class=packetlist_name>%s</span>"
    style_packetlist_proto = "<span class=packetlist_proto>%s</span>"
    style_packetlist_value = "<span class=packetlist_value>%s</span>"
    style_fail = "<span class=fail>%s</span>"
    style_success = "<span class=success>%s</span>"
    style_even = "<span class=even>%s</span>"
    style_odd = "<span class=odd>%s</span>"
    style_left = "<span class=left>%s</span>"
    style_right = "<span class=right>%s</span>"


class HTMLTheme2(HTMLTheme):
    style_prompt = "#[#span class=prompt#]#%s#[#/span#]#"
    style_not_printable = "#[#span class=not_printable#]#%s#[#/span#]#"
    style_layer_name = "#[#span class=layer_name#]#%s#[#/span#]#"
    style_field_name = "#[#span class=field_name#]#%s#[#/span#]#"
    style_field_value = "#[#span class=field_value#]#%s#[#/span#]#"
    style_emph_field_name = "#[#span class=emph_field_name#]#%s#[#/span#]#"
    style_emph_field_value = "#[#span class=emph_field_value#]#%s#[#/span#]#"
    style_packetlist_name = "#[#span class=packetlist_name#]#%s#[#/span#]#"
    style_packetlist_proto = "#[#span class=packetlist_proto#]#%s#[#/span#]#"
    style_packetlist_value = "#[#span class=packetlist_value#]#%s#[#/span#]#"
    style_fail = "#[#span class=fail#]#%s#[#/span#]#"
    style_success = "#[#span class=success#]#%s#[#/span#]#"
    style_even = "#[#span class=even#]#%s#[#/span#]#"
    style_odd = "#[#span class=odd#]#%s#[#/span#]#"
    style_left = "#[#span class=left#]#%s#[#/span#]#"
    style_right = "#[#span class=right#]#%s#[#/span#]#"


def apply_ipython_style(shell):
    # type: (Any) -> None
    """Updates the specified IPython console shell with
    the conf.color_theme scapy theme."""
    try:
        from IPython.terminal.prompts import Prompts, Token
    except Exception:
        from scapy.error import log_loading
        log_loading.warning(
            "IPython too old. Shell color won't be handled."
        )
        return
    from scapy.config import conf
    scapy_style = {}
    # Overwrite colors
    if isinstance(conf.color_theme, NoTheme):
        shell.colors = 'nocolor'
    elif isinstance(conf.color_theme, BrightTheme):
        # lightbg is optimized for light backgrounds
        shell.colors = 'lightbg'
    elif isinstance(conf.color_theme, ColorOnBlackTheme):
        # linux is optimised for dark backgrounds
        shell.colors = 'linux'
    else:
        # default
        shell.colors = 'neutral'
    try:
        get_ipython()  # type: ignore
        # This function actually contains tons of hacks
        color_magic = shell.magics_manager.magics["line"]["colors"]
        color_magic(shell.colors)
    except NameError:
        pass
    # Prompt Style
    if isinstance(conf.prompt, Prompts):
        # Set custom prompt style
        shell.prompts_class = conf.prompt
    else:
        if isinstance(conf.color_theme, (FormatTheme, NoTheme)):
            # Formatable
            if isinstance(conf.color_theme, HTMLTheme):
                prompt = html.escape(conf.prompt)
            elif isinstance(conf.color_theme, LatexTheme):
                from scapy.utils import tex_escape
                prompt = tex_escape(conf.prompt)
            else:
                prompt = conf.prompt
            prompt = conf.color_theme.prompt(prompt)
        else:
            # Needs to be manually set
            prompt = str(conf.prompt)
            scapy_style[Token.Prompt] = Color.ansi_to_pygments(
                conf.color_theme.style_prompt
            )

        class ClassicPrompt(Prompts):
            def in_prompt_tokens(self, cli=None):
                # type: (Any) -> List[Tuple[Any, str]]
                return [(Token.Prompt, prompt), ]

            def out_prompt_tokens(self):
                # type: () -> List[Tuple[Any, str]]
                return [(Token.OutPrompt, ''), ]
        # Apply classic prompt style
        shell.prompts_class = ClassicPrompt
        sys.ps1 = prompt
    # Register scapy color style
    shell.highlighting_style_overrides = scapy_style
    # Apply if Live
    try:
        get_ipython().refresh_style()  # type: ignore
    except NameError:
        pass
