#!/usr/bin/env python

# Copyright (C) 2017-2025 Alex Manuskin, Gil Tsuker
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA


import contextlib
import logging
import math

from s_tui.sturwid.complex_bar_graph import LabeledBarGraphVector, ScalableBarGraph

logger = logging.getLogger(__name__)


class BarGraphVector(LabeledBarGraphVector):
    @staticmethod
    def append_latest_value(values, new_val):
        values.append(new_val)
        return values[1:]

    MAX_SAMPLES = 1000
    SCALE_DENSITY = 5

    def __init__(
        self,
        source,
        regular_colors,
        graph_count,
        visible_graph_list,
        alert_colors=None,
        bar_width=1,
    ):
        self.source = source
        self.graph_count = graph_count
        self.graph_name = self.source.get_source_name()
        self.measurement_unit = self.source.get_measurement_unit()

        self.num_samples = self.MAX_SAMPLES

        self.graph_data = []
        # We must create new instances for each list
        for _ in range(graph_count):
            self.graph_data.append([0] * self.num_samples)

        # Init from get_top(); never use `== 1` as an uninitialized sentinel
        # since Fan/Rapl legitimately return 1.
        self.graph_max = max(self.source.get_top(), 1)
        # Force one rebuild after init/reset so the first update()'s fresh
        # y_label reaches the screen (the init render used a placeholder).
        self._needs_rebuild = True

        self.color_a = regular_colors[0]
        self.color_b = regular_colors[1]
        self.smooth_a = regular_colors[2]
        self.smooth_b = regular_colors[3]

        if alert_colors:
            self.alert_colors = alert_colors
        else:
            self.alert_colors = regular_colors
        self.regular_colors = regular_colors

        self.satt = None

        y_label = []

        graph_title = self.graph_name + " [" + self.measurement_unit + "]"
        sub_title_list = self.source.get_sensor_list()

        # create several different instances of scalable bar graph
        w = []
        for _ in range(graph_count):
            graph = ScalableBarGraph(["bg background", self.color_a, self.color_b])
            w.append(graph)
        super().__init__(graph_title, sub_title_list, y_label, w, visible_graph_list)

        for graph in self.bar_graph_vector:
            graph.set_bar_width(bar_width)

        self.color_counter_vector = [0] * graph_count

    def _set_colors(self, graph, colors):
        self.color_a = colors[0]
        self.color_b = colors[1]
        self.smooth_a = colors[2]
        self.smooth_b = colors[3]
        if self.satt:
            self.satt = {(1, 0): self.smooth_a, (2, 0): self.smooth_b}

        graph.set_segment_attributes(
            ["bg background", self.color_a, self.color_b], satt=self.satt
        )

    def get_graph_name(self):
        return self.graph_name

    def get_measurement_unit(self):
        return self.measurement_unit

    def get_is_available(self):
        return self.source.get_is_available()

    def get_label_scale(self, min_val, max_val, size):
        """Dynamically change the scale of the graph (y label)"""
        label_cnt = 1 if size < self.SCALE_DENSITY else int(size / self.SCALE_DENSITY)
        try:
            if max_val >= 100:
                label = [
                    int(min_val + i * (max_val - min_val) / label_cnt)
                    for i in range(label_cnt + 1)
                ]
            else:
                label = [
                    round((min_val + i * (max_val - min_val) / label_cnt), 1)
                    for i in range(label_cnt + 1)
                ]
            return label
        except ZeroDivisionError:
            logging.debug("Side label creation divided by 0")
            return ""

    def set_smooth_colors(self, smooth):
        self.smooth_mode = smooth
        if smooth:
            self.satt = {(1, 0): self.smooth_a, (2, 0): self.smooth_b}
        else:
            self.satt = None

        for graph in self.bar_graph_vector:
            graph.set_segment_attributes(
                ["bg background", self.color_a, self.color_b], satt=self.satt
            )

    def update(self):
        if not self.get_is_available():
            return

        # NOTE setting edge trigger causes overhead
        triggered = False
        with contextlib.suppress(NotImplementedError):
            triggered = self.source.get_edge_triggered()

        current_reading = self.source.get_reading_list()
        current_thresholds = self.source.get_threshold_list()
        logging.info("Reading %s", current_reading)

        y_label_size_max = 0
        local_top_value = []
        # Store per-graph data for the second pass
        graph_display_data = []

        # First pass: update graph data, collect local maximums, and prepare display data
        for graph_idx, graph in enumerate(self.bar_graph_vector):
            # Check if this graph has a threshold defined
            has_threshold = (
                graph_idx < len(current_thresholds)
                and current_thresholds[graph_idx] is not None
            )
            if (
                # Case 1: no per-sensor threshold, fall back to global trigger
                (not has_threshold and triggered)
                # Case 2: per-sensor threshold exists and this sensor exceeds it,
                #         even if `triggered` is False
                or (
                    has_threshold
                    and current_reading[graph_idx] > current_thresholds[graph_idx]
                )
            ):
                self._set_colors(graph, self.alert_colors)
            else:
                self._set_colors(graph, self.regular_colors)
            try:
                _ = self.visible_graph_list[graph_idx]
            except IndexError:
                # If a new graph "Appears", append it to visible
                self.visible_graph_list.append(True)

            if self.visible_graph_list[graph_idx]:
                self.graph_data[graph_idx] = self.append_latest_value(
                    self.graph_data[graph_idx], current_reading[graph_idx]
                )

                # Get the graph width (dimension 1) - cache for reuse
                num_displayed_bars = graph.get_size()[1]
                start_idx = self.MAX_SAMPLES - num_displayed_bars

                visible_graph_data = self.graph_data[graph_idx][start_idx - 1 :]
                local_top_value.append(max(visible_graph_data))
                graph_display_data.append(
                    (graph_idx, graph, start_idx, num_displayed_bars)
                )
            else:
                graph_display_data.append(None)

        update_max = False
        local_max = math.ceil(max(local_top_value))
        if local_max > self.graph_max:
            update_max = True
            self.graph_max = local_max

        # Second pass: generate bars using cached data
        for entry in graph_display_data:
            if entry is None:
                continue
            graph_idx, graph, start_idx, num_displayed_bars = entry
            bars = []
            # Determine bar color alternation pattern
            swap = self.color_counter_vector[graph_idx] % 2 == 1
            for n in range(start_idx, self.MAX_SAMPLES):
                value = round(self.graph_data[graph_idx][n], 1)
                # toggle between two bar types
                first = (n & 1) ^ swap
                if first:
                    bars.append([0, value])
                else:
                    bars.append([value, 0])
            self.color_counter_vector[graph_idx] += 1

            graph.set_data(bars, float(self.graph_max))
            y_label_size_max = max(y_label_size_max, graph.get_size()[0])

        self.set_y_label(
            self.get_label_scale(0, self.graph_max, float(y_label_size_max))
        )

        # Sync sensor availability from source and rebuild graphs if changed
        need_rebuild = update_max or self._needs_rebuild
        source_available = self.source.sensor_available
        if source_available:
            new_available = source_available[: len(self.sensor_available)]
            if new_available != self.sensor_available[: len(new_available)]:
                self.sensor_available[: len(new_available)] = new_available
                need_rebuild = True

        if need_rebuild:
            self.set_visible_graphs()
            self._needs_rebuild = False

    def reset(self):
        self.graph_data = []
        # Like in init, we create new instances for each list
        for _ in range(self.graph_count):
            self.graph_data.append([0] * self.num_samples)
        self.graph_max = max(self.source.get_top(), 1)
        # Let the next update() repaint with a fresh y_label.
        self._needs_rebuild = True
