# Copyright 2025-2026 Facundo Batista
# Licensed under the Apache v2 License
# For further info, check https://github.com/facundobatista/nysor

"""Receive, process, and manage all notifications from Neovim."""

import logging
from collections import defaultdict
from typing import Any

from nysor.utils import call_async

logger = logging.getLogger(__name__)


class DynamicCache:
    """A cache that is cleaned up when any of the section labels change."""

    def __init__(self):
        self._data = {}
        self._labels = defaultdict(set)

    def get(self, labels, key):
        """Get the key from the cache indicated by labels.

        This is the critical path. The rest of the methods need to accommodate for this
        to be as fast as possible.
        """
        # the real cache is under the tuple of labels
        cache = self._data.setdefault(labels, {})
        return cache.get(key)

    def set(self, labels, key, value):
        """Store the value under given key in the cache indicated by labels."""
        # store
        cache = self._data.setdefault(labels, {})
        cache[key] = value

        # also annotate the labels for future cleanup
        for label in labels:
            self._labels[label].add(labels)

    def clean(self, label):
        """Clean all caches for all set of labels where this label is present."""
        for section in self._labels[label]:
            self._data[section].clear()


class NvimNotifications:
    """Dance at the rhythm of Neovim.

    Hold the relevant structures and handle all notifications. Some are sent to the TextDisplay
    widgets, other to the main waindow.
    """

    def __init__(self, main_window):
        self.main_window = main_window
        self.text_display = None  # will be set before first usage
        self.options = {}

        # this two currently work "in tandem", we may want to unify them under the same structure
        # in the future
        self.structs = {}
        self.dyncache = DynamicCache()

    def _h__redraw(self, *parameters: tuple[Any]):
        """Handle the 'redraw' notification."""
        for submethod, *args in parameters:
            n_name = "_n_redraw__" + submethod
            n_meth = getattr(self, n_name, None)
            if n_meth is None:
                logger.error(
                    "[NvimNotifications] Submethod {!r} not implemented in 'redraw', params: {}",
                    submethod, args
                )
            else:
                try:
                    logger.debug("[NvimNotifications] Handle 'redraw': {} - {}", submethod, args)
                    n_meth(*args)
                except Exception:
                    logger.exception("Crash when calling {!r} with {!r}", n_name, args)

    def _h__modified_changed(self, is_modified: bool):
        """Handle the notification when the buffer starts/stop having changes."""
        self.main_window.set_buffer_state(is_modified=is_modified)

    def _h__filepath_changed(self, filepath: str):
        """Handle the notification when the buffer has a file associated or not."""
        self.main_window.set_buffer_state(filepath=filepath)

    def handler(self, method: str, parameters: list[Any]):
        """Handle all notifications from Neovim."""
        h_name = "_h__" + method
        h_meth = getattr(self, h_name, None)
        if h_meth is None:
            logger.error(
                "[NvimNotifications] Method {!r} not implemented, params: {}", method, parameters)
            return

        h_meth(*parameters)

    # -- specific notification handlers

    def _n_redraw__default_colors_set(self, colors):
        """Set the default colors."""
        rgb_fg, rgb_bg, rgb_sp, _, _ = colors  # last two are ignored because are for terminals
        self.structs.setdefault("default_colors", {}).update({
            "foreground": rgb_fg,
            "background": rgb_bg,
            "special": rgb_sp,
        })
        self.dyncache.clean("default_colors")

    def _n_redraw__flush(self, _):
        """Clear the grid."""
        self.text_display.flush()

    def _n_redraw__grid_clear(self, args):
        """Clear the grid."""
        (grid_id,) = args
        assert grid_id == 1  # FIXME.90: is it always 1? when do we have more than one?
        self.text_display.clear()

    def _n_redraw__grid_cursor_goto(self, args):
        """Resize a grid."""
        grid_id, row, col = args
        assert grid_id == 1  # FIXME.90: is it always 1? when do we have more than one?
        self.text_display.set_cursor(row, col)

    def _n_redraw__grid_line(self, *args):
        """Expose a line in the grid."""
        for item in args:
            grid, row, col_start, cells, wrap = item
            assert grid == 1  # FIXME.90: same question we do in grid_resize

            # note we ignore "wrap", couldn't find proper utility for it
            self.text_display.write_grid(row, col_start, cells)

    def _n_redraw__grid_resize(self, args):
        """Resize a grid."""
        grid_id, width, height = args
        assert grid_id == 1  # FIXME.90: is it always 1? when do we have more than one?
        self.text_display.resize_view((width, height))

    def _n_redraw__grid_scroll(self, args):
        """Scroll a grid."""
        grid_id, top, bottom, left, right, rows, cols = args
        assert grid_id == 1  # FIXME.90: is it always 1? when do we have more than one?
        self.text_display.scroll((top, bottom, rows), (left, right, cols))

    def _n_redraw__hl_attr_define(self, *args):
        """Add highlights with their attributes.

        E.g.: (
            2,
            {'foreground': 13882323, 'background': 11119017},
            {'foreground': 7, 'background': 242},
            [],
        )
        """
        hl_attrs = self.structs.setdefault("hl-attrs", {})
        for hl_id, rgb_attr, _, info in args:  # third value is ignored as it's for terminals
            assert not info
            hl_attrs[hl_id] = rgb_attr
        self.dyncache.clean("hl-attrs")

    def _n_redraw__hl_group_set(self, *args):
        """Set highlight groups.

        E.g.: [['SpecialKey', 161], ['EndOfBuffer', 161], ...]
        """
        hl_groups = self.structs.setdefault("hl-groups", {})
        for group_name, hl_id in args:
            hl_groups[group_name] = hl_id
        self.dyncache.clean("hl-groups")

    def _n_redraw__mode_change(self, args):
        """Information about cursor mode."""
        mode, mode_idx = args
        # we ignore the mode idx as we stored in the modes in a dict using the name
        mode_info = self.structs["mode-info"][mode]
        self.text_display.change_mode(mode_info)

    def _n_redraw__mode_info_set(self, args):
        """Information about cursor mode."""
        cursor_style_enabled, mode_info = args
        assert cursor_style_enabled  # may it come in False? what do we do? delete previous modes?

        info = {}
        for mi in mode_info:
            # store by name (and remove it from the real data, together with short name)
            name = mi.pop("name")
            del mi["short_name"]
            info[name] = mi

        self.structs.setdefault("mode-info", {}).update(info)
        self.dyncache.clean("mode-info")

    def _n_redraw__mouse_on(self, args):
        """Properly ignored."""

    def _n_redraw__mouse_off(self, args):
        """Properly ignored."""

    def _n_redraw__option_set(self, *options):
        options = dict(options)
        logger.debug("[NvimNotifications] options set: {}", options)
        self.options.update(options)

        # react to some of those options
        if "guifont" in options:
            name, size = options["guifont"].split(":")
            assert size[0] == "h"
            size = float(size[1:])
            self.text_display.set_font(name, size)

    def _n_redraw__set_icon(self, param):
        """Set the icon, if any."""
        (icon,) = param
        if icon:
            logger.warning("[NvimNotifications] need to implement set icon with {!r}", icon)

    def _n_redraw__set_title(self, param):
        """Set title."""
        (title,) = param
        self.main_window.setWindowTitle(title)

    def _n_redraw__win_viewport(self, args):
        """Information for the GUI viewport."""
        grid, objinfo, topline, botline, curline, curcol, line_count, scroll_delta = args
        # FIXME.90: ignore grid and objinfo so far, need to revisit this when multiwindow

        # Note: can't find use to scroll_delta (maybe for smooth scrollbar?)
        call_async(self.main_window.adjust_viewport, topline, botline, line_count, curcol)
