# ------------------------------------------------------------------------------
#  es7s/core
#  (c) 2022-2023 A. Shavykin <0.delameter@gmail.com>
# ------------------------------------------------------------------------------
from __future__ import annotations

import os
import pydoc
import re
import signal
import threading as th
from abc import abstractmethod
from collections import deque
from functools import reduce
from io import StringIO

import click
import psutil
import pytermor as pt
from pytermor import RT

from ._base import _catch_and_print, _catch_and_log_and_exit
from ._base_monitor import CoreMonitor, MonitorCliCommand
from ..shared import (
    ShutdownableThread,
    get_stdout,
    get_logger,
    get_config,
    init_config,
    Styles,
    get_color,
)


class Source:
    @abstractmethod
    def read(self) -> str:
        ...


class SourceStatic(Source):
    def __init__(self, src: Separator):
        self._src = get_stdout().render(src.fragment)

    def read(self) -> str:
        return self._src


class SourceInterceptor(Source):
    def __init__(self, io: StringIO, length: int):
        self._io = io
        self._length = length

    def read(self) -> str:
        if not self._io.readable():
            return " " * self._length
        result = self._io.read().replace(os.linesep, "")
        self._io.seek(0)
        return result


@click.command(
    name=__file__,
    cls=MonitorCliCommand,
    short_help="monitors defined in LAYOUT config var as single line",
    output_examples=[],
)
@click.argument("layout", type=str, required=False, default="layout1")
@click.pass_context
@_catch_and_log_and_exit
@_catch_and_print
class CombinedMonitor(ShutdownableThread):
    """
    ;.
    """

    _UPDATE_TIMEOUTS = {
        "layout1": 1.0,
        "layout2": 2.0,
    }

    def __init__(self, ctx: click.Context, demo: bool, layout: str, **kwargs):
        super().__init__(command_name=ctx.command.name, thread_name="ui")

        self._update_ui_event = th.Event()
        self._reset_ui_event = th.Event()
        self._init_ui_event = th.Event()
        self._init_ui_event.set()

        self._sources: deque[Source] = deque[Source]()
        self._monitors: deque[CoreMonitor] = deque[CoreMonitor]()

        config = get_config()
        layout_cfg = filter(None, config.get("monitor.combined", layout).strip().split("\n"))
        self._debug_mode = config.get_monitor_debug_mode()
        self._force_cache = config.getboolean("monitor", "force-cache", fallback=False)
        self._update_timeout = self._UPDATE_TIMEOUTS.get(layout)
        self.start()

        for el in list(layout_cfg):
            module_name, origin_name = el.rsplit(".", 1)
            if (module := pydoc.safeimport(module_name)) is None:
                continue

            origin = getattr(module, origin_name)
            if isinstance(origin, Separator):
                self._sources.append(SourceStatic(origin))
                continue

            interceptor = StringIO()
            monitor = origin(
                ctx,
                demo=demo,
                interceptor=interceptor,
                ui_update=self._update_ui_event,
                debug_mode=self._debug_mode,
                force_cache=self._force_cache,
                **kwargs,
            )
            self._sources.append(SourceInterceptor(interceptor, monitor.get_output_width()))
            self._monitors.append(monitor)
            self._update_ui_event.set()

        signal.signal(signal.SIGUSR1, self._preview_alt_mode)
        signal.signal(signal.SIGUSR2, self._update_settings_request)

        self.join()

    def _preview_alt_mode(self, *args):
        get_logger().debug("Switching to alt mode")
        for monitor in self._monitors:
            monitor._preview_alt_mode(*args)
            self._update_ui_event.set()

    def _update_settings_request(self, signal_code: int, *args):
        get_logger().info(f"Updating the setup: {signal.Signals(signal_code).name} ({signal_code})")
        init_config()
        for monitor in self._monitors:
            monitor._update_settings_request(signal_code, *args)
        self._reset_ui_event.set()

    def run(self):
        super().run()

        stdout = get_stdout()
        process = psutil.Process()

        def stats() -> str:
            result = ""
            for s in _stats():
                result += pt.render(LINE.fragment) + s
            return result

        def _stats() -> RT:
            yield from [
                stdout.render(p, Styles.DEBUG_SEP_INT)
                for p in [
                    f"{render_tick:>5d}/W{100*wasted_ticks/render_tick:>2.0f}%",
                    f"T{process.num_threads():>2d} "
                    + f"{process.cpu_percent():>3.0f}% "
                    + f"{pt.format_bytes_human(process.memory_info().rss):>5s}",
                ]
            ]

        render_tick = 0
        wasted_ticks = 0
        combined_line_prev = ""

        while True:
            if self.is_shutting_down():
                self.destroy()
                break

            if self._init_ui_event.is_set():
                stdout.echo_rendered("[...]", Styles.TEXT_DISABLED)

            if self._reset_ui_event.is_set():
                combined_line_prev = ""
                self._reset_ui_event.clear()

            if self._update_ui_event.wait(self._update_timeout):
                get_logger().debug("Received update ui event")
            else:
                get_logger().debug("Update ui timeout")
            self._update_ui_event.clear()

            combined_line = reduce(lambda t, s: t + s.read(), self._sources, "")
            render_tick += 1

            if not combined_line or combined_line == combined_line_prev:
                wasted_ticks += 1
                continue

            self._init_ui_event.clear()
            stdout.echo(combined_line + (stats() if self._debug_mode else ""))
            combined_line_prev = combined_line


class Separator:
    def __init__(self, label: str):
        self._label = label

    def _apply_style(self, label: str) -> pt.Fragment:
        st = pt.Style(
            bg=Styles.STATUSBAR_BG,
            fg=get_color().get_monitor_separator_color(),
        )
        if get_config().get_monitor_debug_mode():
            if {*label} == {" "}:
                label = label.replace(" ", "␣")
            else:
                left_pad, label, right_pad = re.split(r"(\S+)", label)
                label = len(left_pad) * ">" + label + len(right_pad) * "<"
            st = Styles.DEBUG_SEP_EXT
        return pt.Fragment(label, st)

    @property
    def fragment(self) -> pt.Fragment:
        return self._apply_style(self._label)


EMPTY = Separator("")

SPACE = Separator(" ")
SPACE_2 = Separator(" " * 2)
SPACE_3 = Separator(" " * 3)

LINE = Separator("│")
LINE_2 = Separator(" ▏")
LINE_3 = Separator(" │ ")

EDGE_LEFT = Separator("▏")
EDGE_RIGHT = Separator("▕")
