# SPDX-FileCopyrightText: Copyright (c) 2025 Dana Kamp
#
# SPDX-License-Identifier: MIT

import math
import displayio
from adafruit_display_shapes.rect import Rect
from adafruit_display_shapes.roundrect import RoundRect
from adafruit_display_shapes.circle import Circle
from adafruit_display_shapes.line import Line
from adafruit_display_text.bitmap_label import Label


class Thermometer:
    """Reusable thermometer graphic with update method for actual/apparent temps.

    Builds a displayio.Group containing the frame, outline, bulb, tick marks,
    labels, and two fills (actual in red, apparent in white). Use ``update`` to
    redraw the fills at new temperatures.

    > Copy thermometer.py into the lib folder.

    > anchor_point is the top-left corner of the thermometer graphic. Default is
    (550, 0). Adjust as needed to fit your display.

    The default graphic is sizeed 100 px wide by 440 px high. The scale runs
    from temp_min (default 0F) to temp_max (default 100F). Each degree is
    represented by px_per_degree pixels (default 4), so the scale height is
    px_per_degree * (temp_max - temp_min) pixels high.

    Default min/max temperature is 0-100°F. You can adjust temp_min and temp_max
    to fit your expected temperature range. If you set px_per_degree to None,
    the scale will stretch to fill the full 400px height between the top and
    bottom of the thermometer graphic.

    Colors can be customized by passing a dictionary of color values (0xRRGGBB)
    for any of these keys:
        "frame", "bg", "outline", "bulb_outline", "bulb_fill",
        "tick_major", "tick_minor", "tick_c", "tick_c_zero", "freezing_tick",
        "fill_actual", "fill_apparent", "fill_placeholder",
        "label_f", "label_c"
    If omitted, defaults will be used.

   
    
    from thermometer import Thermometer
    therm = Thermometer(
        anchor=(550, 0),
        display=display,
        tick_font=font15,
        therm = Thermometer(
        temp_min=20,
        temp_max=80,    # scale stretches to fill
                        # px_per_degree=None  # implied; leave it out to auto-fill
        frame_height=320,      # optional shrink total height from detault 440 if needed
        frame_width=80,       # optional
        scale_margin_top=8,    # optional
        scale_margin_bottom=22, # optional
        colors={
            "frame": 0x001122,
            "outline": 0xFFFFFF,
            "bulb_fill": 0x00FF00,
            "fill_actual": 0x00CCFF,
            "fill_apparent": 0xFFFF00,
            "tick_major": 0xEEEEEE,
            "label_f": 0xFFAA00,
        },
    )

    group.append(therm.group) where you want it layered.

    > Call 
    therm.update(current_temp, apparent_temp) whenever new data arrives.

    """

    def __init__(
        self,
        anchor=(550, 0),
        *,
        display=None,
        colors=None,
        tick_font=None,
        px_per_degree=None,
        temp_min=0,
        temp_max=100,
        frame_height=440,
        frame_width=100,
        scale_margin_top=10,
        scale_margin_bottom=30,
        log_fn=None,
        initial_temp=50,
    ):
        self.anchor = anchor
        self.display = display
        self.tick_font = tick_font
        self.temp_min = temp_min
        self.temp_max = temp_max
        self.frame_height = frame_height
        self.frame_width = frame_width
        self.scale_margin_top = scale_margin_top
        self.scale_margin_bottom = scale_margin_bottom
        self.log = log_fn or (lambda msg: None)

        # Core geometry: frame is 100x440 by default with a scale that fills the
        # available vertical space between the top and bottom margins. If
        # px_per_degree is not provided, it is derived so the given temp range
        # fills the entire scale height (e.g., 20-80°F will stretch to the full
        # height). Reduce frame_height if your display is shorter than 440 px.
        self.scale_height_px = max(
            1, self.frame_height - self.scale_margin_top - self.scale_margin_bottom
        )
        self.px_per_degree = (
            px_per_degree
            if px_per_degree is not None
            else self.scale_height_px / max(1, (self.temp_max - self.temp_min))
        )

        self.colors = {
            "frame": 0x000066,
            "bg": 0x000066,
            "outline": 0xFFFFFF,
            "bulb_outline": 0xFFFFFF,
            "bulb_fill": 0xFF0000,
            "tick_major": 0xFFFFFF,
            "tick_minor": 0xFFFFFF,
            "tick_c": 0xAAAAFF,
            "tick_c_zero": 0xFF0000,
            "freezing_tick": 0xFF0000,
            "fill_actual": 0xFF0000,
            "fill_apparent": 0xFFFFFF,
            "fill_placeholder": 0x000066,
            "label_f": 0xFFFFFF,
            "label_c": 0xAAAAFF,
        }
        if colors:
            self.colors.update(colors)

        self.group = displayio.Group()
        self._build_static_parts()
        self._append_fills(initial_temp, initial_temp, placeholder=True)

    # --- public API -----------------------------------------------------
    def update(self, temperature, apparent_temperature):
        """Update the thermometer fills based on current and apparent temps."""
        temp = self._parse_temp(temperature, default=50)
        app = self._parse_temp(apparent_temperature, default=temp)

        if temp < self.temp_min:
            temp = self.temp_min
        if temp > self.temp_max:
            temp = self.temp_max
        if app < self.temp_min:
            app = self.temp_min
        if app > self.temp_max:
            app = self.temp_max

        if self.display:
            self.display.auto_refresh = False
        try:
            self._pop_fills()
            self._append_fills(temp, app, placeholder=False)
            self.log(
                f"Thermometer updated - actual: {temp}F, apparent: {app}F"
            )
        finally:
            if self.display:
                self.display.auto_refresh = True

    # --- internals ------------------------------------------------------
    def _build_static_parts(self):
        x, y = self.anchor
        self.top_y = y + self.scale_margin_top
        self.bottom_y = self.top_y + self.scale_height_px

        # Background frame and outline
        frame_fill = self.colors.get("bg", self.colors["frame"])
        frame = Rect(x, y, self.frame_width, self.frame_height, fill=frame_fill)
        outline = RoundRect(
            x=x + 40,
            y=self.top_y,
            width=20,
            height=self.scale_height_px,
            r=10,
            outline=self.colors["outline"],
            stroke=1,
        )
        bulb = Circle(
            x + 50,
            y + self.frame_height - (self.scale_margin_bottom + 10),
            15,
            outline=self.colors["bulb_outline"],
            fill=self.colors["bulb_fill"],
            stroke=1,
        )

        self.group.append(frame)
        self.group.append(outline)
        self.group.append(bulb)

        self._build_ticks(x, y)
        self._build_labels(x)

    def _build_ticks(self, x, y):
        # Fahrenheit ticks (respect the configured min/max range)
        start_major = int(math.ceil(self.temp_min / 10.0) * 10)
        for temp in range(start_major, self.temp_max + 1, 10):
            y_pos = self._temp_to_y(temp)
            self.group.append(
                Line(x + 30, y_pos, x + 40, y_pos, color=self.colors["tick_major"])
            )

        start_minor = int(math.ceil((self.temp_min - 5) / 10.0) * 10 + 5)
        for temp in range(start_minor, self.temp_max, 10):
            if temp < self.temp_min:
                continue
            y_pos = self._temp_to_y(temp)
            self.group.append(
                Line(x + 35, y_pos, x + 40, y_pos, color=self.colors["tick_minor"])
            )

        # Freezing tick (32F) only if in range
        if self.temp_min <= 32 <= self.temp_max:
            freezing_y = self._temp_to_y(32)
            self.group.append(
                Line(x + 30, freezing_y, x + 40, freezing_y, color=self.colors["freezing_tick"])
            )

        # Celsius ticks (-10 to 35) filtered to range
        for c_temp in (-10, 0, 10, 20, 30):
            f_temp = self._c_to_f(c_temp)
            if not (self.temp_min <= f_temp <= self.temp_max):
                continue
            y_pos = self._temp_to_y(f_temp)
            color = self.colors["tick_c_zero"] if c_temp == 0 else self.colors["tick_c"]
            self.group.append(Line(x + 60, y_pos, x + 65, y_pos, color=color))
        for c_temp in (-5, 5, 15, 25, 35):
            f_temp = self._c_to_f(c_temp)
            if not (self.temp_min <= f_temp <= self.temp_max):
                continue
            y_pos = self._temp_to_y(f_temp)
            self.group.append(Line(x + 60, y_pos, x + 65, y_pos, color=self.colors["tick_c"]))

    def _build_labels(self, x):
        if not self.tick_font:
            return

        # Fahrenheit labels (right-justified on the left side of the graphic)
        start_major = int(math.ceil(self.temp_min / 10.0) * 10)
        for temp in range(start_major, self.temp_max + 1, 10):
            y_pos = self._temp_to_y(temp) - 4
            self.group.append(
                Label(
                    self.tick_font,
                    text=str(temp),
                    anchor_point=(1.0, 0.5),
                    anchored_position=(x + 20, y_pos),
                    color=self.colors["label_f"],
                )
            )

        # Celsius labels (left-justified on the right side)
        for c_temp in (-10, -5, 0, 5, 10, 15, 20, 25, 30, 35):
            f_temp = self._c_to_f(c_temp)
            if not (self.temp_min <= f_temp <= self.temp_max):
                continue
            y_pos = self._temp_to_y(f_temp) - 3
            color = self.colors["tick_c_zero"] if c_temp == 0 else self.colors["label_c"]
            self.group.append(
                Label(
                    self.tick_font,
                    text=str(c_temp),
                    anchor_point=(0.0, 0.5),
                    anchored_position=(x + 70, y_pos),
                    color=color,
                )
            )

    def _append_fills(self, temp, app_temp, *, placeholder):
        actual_fill_x = self.anchor[0] + 42
        apparent_fill_x = self.anchor[0] + 48

        actual_height, actual_y = self._height_and_y(temp)
        apparent_height, apparent_y = self._height_and_y(app_temp)

        actual_radius = min(7, actual_height // 2) if actual_height > 0 else 0
        app_radius = min(2, apparent_height // 2) if apparent_height > 0 else 0

        actual_color = (
            self.colors["fill_placeholder"] if placeholder else self.colors["fill_actual"]
        )
        app_color = (
            self.colors["fill_placeholder"] if placeholder else self.colors["fill_apparent"]
        )

        actual_rect = RoundRect(
            x=actual_fill_x,
            y=actual_y,
            width=15,
            height=max(1, actual_height),
            r=actual_radius,
            fill=actual_color,
        )
        apparent_rect = RoundRect(
            x=apparent_fill_x,
            y=apparent_y,
            width=4,
            height=max(1, apparent_height),
            r=app_radius,
            fill=app_color,
        )

        self.group.append(actual_rect)
        self.group.append(apparent_rect)

    def _pop_fills(self):
        if len(self.group) >= 2:
            self.group.pop()
            self.group.pop()

    def _parse_temp(self, value, default):
        try:
            txt = str(value).replace("°F", "").replace("°", "").strip()
            return float(txt)
        except Exception:
            return float(default)

    def _height_and_y(self, temp):
        delta = temp - self.temp_min
        height = int(delta * self.px_per_degree)
        y_pos = self.bottom_y - height
        return height, y_pos

    def _temp_to_y(self, temp_f):
        delta = temp_f - self.temp_min
        return int(self.bottom_y - delta * self.px_per_degree)

    @staticmethod
    def _c_to_f(temp_c):
        return temp_c * 9 / 5 + 32