#-----------------------------------------------------------------------------------------------------------------------

# Draw OHD layers: background, frame, symbols ($ , .) via symbol_slots, scrolling digit columns (LSB right).

# No leading zeros; $ hugs the most significant digit shown.

#-----------------------------------------------------------------------------------------------------------------------

from typing import Dict, List, Optional



import pygame



from ohd_aligned_draw import draw_aligned_odometer
from ohd_assets import OhdGameAssets, build_glyph_surfaces, build_symbol_glyphs

from ohd_layout import (

    COMMA_LEFT_POWER,

    COMMA_RIGHT_POWER,

    DigitSlot,

    OhdLayout,

    SymbolSlot,

    default_symbol_slots,

    slot_for_power,

    symbol_slots_for_render,

)

from odometer_engine import NUM_COLUMNS, OdometerEngine, leftmost_significant_power



# Fallback when symbol_slots missing: comma between 1000s and 100s only.

_COMMA_GAPS = ((COMMA_LEFT_POWER, COMMA_RIGHT_POWER),)

_SYMBOL_GAP_PX = 4





class OhdRenderer:

    def __init__(self, layout: OhdLayout):

        self.layout = layout

        self._glyphs: Optional[List[pygame.Surface]] = None

        self._symbols: Optional[Dict[str, pygame.Surface]] = None

        self._scaled_glyphs: Optional[dict] = None  # (w,h) -> [surfaces]

        self._scaled_symbols: Optional[dict] = None  # slot_h -> {name: surface}

        self._static_layer: Optional[pygame.Surface] = None

        self._static_layer_key: Optional[tuple] = None



    def set_assets(self, assets: OhdGameAssets) -> None:

        self.layout = assets.layout

        self._glyphs = build_glyph_surfaces(assets)

        self._symbols = build_symbol_glyphs(assets)

        self.invalidate_glyph_cache()

        self._static_layer = None

        self._static_layer_key = None



    def invalidate_glyph_cache(self) -> None:

        self._scaled_glyphs = {}

        self._scaled_symbols = {}

    def _ensure_static_layer(
        self, target: pygame.Surface, assets: OhdGameAssets
    ) -> pygame.Surface:
        """Compose background + frame once; both are static for the life of an install."""
        sw, sh = self.layout.screen_size
        fx, fy = self.layout.frame_anchor
        key = (
            sw,
            sh,
            fx,
            fy,
            id(assets.background),
            id(assets.blocking) if assets.blocking is not None else 0,
            id(assets.frame),
            assets.background.get_width(),
            assets.background.get_height(),
            assets.frame.get_width(),
            assets.frame.get_height(),
        )
        if self._static_layer is not None and self._static_layer_key == key:
            return self._static_layer

        layer = pygame.Surface((sw, sh)).convert()
        if assets.background.get_size() == (sw, sh):
            layer.blit(assets.background, (0, 0))
        else:
            layer.blit(
                pygame.transform.smoothscale(assets.background, (sw, sh)),
                (0, 0),
            )
        if assets.blocking is not None:
            layer.blit(assets.blocking, (fx, fy))
        layer.blit(assets.frame, (fx, fy))
        self._static_layer = layer
        self._static_layer_key = key
        return layer

    # FINAL / slip reference: display_scale 2.0 matches preview digit height (not 2× slot box).
    _TEMPLATE_REFERENCE_SCALE = 2.0

    def _template_digit_target_height(self) -> int:
        top = self.layout.digit_top_y
        base = self.layout.digit_baseline_y
        if top is not None and base is not None:
            ref_h = max(1, base - top)
        elif self.layout.digit_slots:
            ref_h = self.layout.digit_slots[0].h
        else:
            ref_h = self.layout.digit_cell_h
        s = self.layout.display_scale / self._TEMPLATE_REFERENCE_SCALE
        return max(1, int(round(ref_h * s)))

    def _symbol_target_height(self, name: str) -> int:
        """Scale strip punctuation / dollar with digits, keeping strip height ratios."""
        digit_h = self._template_digit_target_height()
        if not self.layout.use_template_screen_coords:
            return digit_h
        if self._symbols is None or name not in self._symbols:
            return digit_h
        sym_h = self._symbols[name].get_height()
        scale = digit_h / max(1, self.layout.digit_cell_h)
        return max(1, int(round(sym_h * scale)))

    def _glyph_pixel_size(self, w: int, h: int) -> tuple:
        """Rendered glyph size (template uses measured top→baseline, not slot×display_scale)."""
        if self.layout.use_template_screen_coords:
            nh = self._template_digit_target_height()
            if self._glyphs and self._glyphs[0].get_height() > 0:
                g = self._glyphs[0]
                scale = nh / max(1, g.get_height())
                return (
                    max(1, int(round(g.get_width() * scale))),
                    nh,
                )
            return (max(1, int(round(w * nh / max(1, h)))), nh)
        if self.layout.scale_slot_positions and not self.layout.use_template_screen_coords:
            return w, h
        s = self.layout.display_scale
        if self._glyphs and self._glyphs[0].get_height() > 0:
            g = self._glyphs[0]
            return (
                max(1, int(round(g.get_width() * s))),
                max(1, int(round(g.get_height() * s))),
            )
        return (
            max(1, int(round(w * s))),
            max(1, int(round(h * s))),
        )

    def _glyphs_for_size(self, w: int, h: int) -> List[pygame.Surface]:

        key = (w, h)

        if self._scaled_glyphs is not None and key in self._scaled_glyphs:

            return self._scaled_glyphs[key]

        if self._glyphs is None:

            return []

        scaled = [

            pygame.transform.smoothscale(g, (w, h)) if (g.get_width(), g.get_height()) != (w, h) else g

            for g in self._glyphs

        ]

        if self._scaled_glyphs is not None:

            self._scaled_glyphs[key] = scaled

        return scaled



    def _symbols_for_slot_height(self, slot_h: int) -> Dict[str, pygame.Surface]:

        if self._symbols is None or not self._symbols:

            return {}

        if self._scaled_symbols is not None and slot_h in self._scaled_symbols:

            return self._scaled_symbols[slot_h]

        if self._glyphs is None or not self.layout.digit_slots:

            return self._symbols

        ref_h = self._glyph_pixel_size(self.layout.digit_slots[0].w, self.layout.digit_slots[0].h)[1]

        if ref_h == slot_h:

            scaled = self._symbols

        else:

            scale = slot_h / max(1, ref_h)

            scaled = {}

            for name, surf in self._symbols.items():

                nw = max(1, int(round(surf.get_width() * scale)))

                nh = max(1, int(round(surf.get_height() * scale)))

                scaled[name] = (

                    pygame.transform.smoothscale(surf, (nw, nh))

                    if (nw, nh) != (surf.get_width(), surf.get_height())

                    else surf

                )

        if self._scaled_symbols is not None:

            self._scaled_symbols[slot_h] = scaled

        return scaled



    @staticmethod

    def _should_show_comma(leftmost_power: int) -> bool:

        return leftmost_power >= COMMA_LEFT_POWER



    def draw(

        self,

        target: pygame.Surface,

        assets: OhdGameAssets,

        engine: OdometerEngine,

    ) -> None:

        if self._glyphs is None:

            self.set_assets(assets)



        sw, sh = self.layout.screen_size

        target.blit(self._ensure_static_layer(target, assets), (0, 0))

        fx, fy = self.layout.frame_anchor



        digits = engine.get_display_digits()

        leftmost = leftmost_significant_power(digits)

        if assets.aligned:
            draw_aligned_odometer(
                target,
                self.layout,
                assets.aligned,
                engine,
                frame_x=fx,
                frame_w=assets.frame.get_width(),
            )
            return

        if self.layout.digit_slots:

            if self.layout.use_template_screen_coords:
                symbols = self._symbols or {}
            else:
                _, glyph_h = self._glyph_pixel_size(
                    self.layout.digit_slots[0].w, self.layout.digit_slots[0].h
                )
                symbols = self._symbols_for_slot_height(glyph_h)

            if symbols:

                sym_slots = (

                    symbol_slots_for_render(self.layout)

                    if self.layout.symbol_slots

                    else default_symbol_slots(self.layout)

                )

                if sym_slots:

                    self._draw_symbols(

                        target, symbols, sym_slots, leftmost, show_comma=self._should_show_comma(leftmost)

                    )

                else:

                    self._draw_symbols_gap_fallback(

                        target, symbols, leftmost, show_comma=self._should_show_comma(leftmost)

                    )

            for power in range(NUM_COLUMNS):

                if power > leftmost:

                    continue

                slot = slot_for_power(self.layout, power)

                if slot is None:

                    continue

                if not self._glyphs:

                    continue

                d, scroll = engine.get_column_draw(power)

                if scroll > 0.001:

                    self._draw_scrolling_column(target, slot, self._glyphs, d, scroll)

                else:
                    glyph = self._fit_digit_glyph(self._glyphs[d], slot)
                    gx, gy = self._digit_blit_xy(slot, glyph)
                    target.blit(glyph, (gx, gy))

        else:

            self._draw_digits_fallback(target, engine, digits, leftmost)



    def _fit_digit_glyph(self, glyph: pygame.Surface, slot: DigitSlot) -> pygame.Surface:
        """Template: uniform scale to FINAL top→baseline height; else display_scale / slot fit."""
        gw, gh = glyph.get_width(), glyph.get_height()
        if gw <= 0 or gh <= 0:
            return glyph
        if self.layout.use_template_screen_coords:
            nh = self._template_digit_target_height()
            scale = nh / gh
            nw = max(1, int(round(gw * scale)))
            if (nw, nh) == (gw, gh):
                return glyph
            return pygame.transform.smoothscale(glyph, (nw, nh))
        s = self.layout.display_scale
        if abs(s - 1.0) > 1e-6:
            nw = max(1, int(round(gw * s)))
            nh = max(1, int(round(gh * s)))
            if (nw, nh) != (gw, gh):
                return pygame.transform.smoothscale(glyph, (nw, nh))
            return glyph
        if slot.w <= 0 or slot.h <= 0:
            return glyph
        scale = min(slot.w / gw, slot.h / gh)
        nw = max(1, int(round(gw * scale)))
        nh = max(1, int(round(gh * scale)))
        if (nw, nh) == (gw, gh):
            return glyph
        return pygame.transform.smoothscale(glyph, (nw, nh))

    def _digit_baseline_y(self) -> int:
        if self.layout.digit_baseline_y is not None:
            return self.layout.digit_baseline_y
        row = self.layout.digit_slots[0]
        return row.y + row.h

    def _digit_blit_xy(self, slot: DigitSlot, glyph: pygame.Surface, y_offset: int = 0) -> tuple:
        """Bottom-align on digit_baseline_y when set; else slot bottom."""
        x = slot.x + (slot.w - glyph.get_width()) // 2
        base = self._digit_baseline_y()
        y = base - glyph.get_height() + y_offset
        return x, y

    def _fit_symbol_glyph(
        self,
        glyph: pygame.Surface,
        slot: SymbolSlot,
        *,
        height_only: bool = False,
        target_h: Optional[int] = None,
    ) -> pygame.Surface:
        """Fit symbols; template punctuation uses digit height, not narrow slot width."""
        ow, oh = glyph.get_width(), glyph.get_height()
        if ow <= 0 or oh <= 0:
            return glyph
        if target_h is None and self.layout.use_template_screen_coords:
            target_h = self._symbol_target_height(slot.name)
        narrow_punct = self.layout.use_template_screen_coords and slot.name in (
            "comma",
            "decimal",
        )
        if height_only or narrow_punct:
            nh = target_h or oh
            scale = nh / oh
            nw = max(1, int(round(ow * scale)))
            if self.layout.use_template_screen_coords:
                if slot.name == "decimal":
                    nw = min(nw, max(12, nh))
                elif slot.name == "comma":
                    nw = min(nw, max(16, int(round(nh * 1.15))))
        elif slot.w <= 0 or slot.h <= 0:
            return glyph
        else:
            scale = min(slot.w / ow, slot.h / oh)
            nw = max(1, int(round(ow * scale)))
            nh = max(1, int(round(oh * scale)))
        if (nw, nh) == (ow, oh):
            return glyph
        return pygame.transform.smoothscale(glyph, (nw, nh))



    def _draw_dynamic_dollar(

        self,

        target: pygame.Surface,

        dollar: pygame.Surface,

        leftmost_power: int,

        bottom_y: int,

    ) -> None:

        msd = slot_for_power(self.layout, leftmost_power)

        if msd is None:

            return

        ref = SymbolSlot("dollar", msd.x, msd.y, msd.w, msd.h)

        glyph = self._fit_symbol_glyph(dollar, ref)

        x = msd.x - glyph.get_width() - _SYMBOL_GAP_PX

        self._blit_bottom_aligned(target, glyph, x, bottom_y)



    def _draw_symbols(

        self,

        target: pygame.Surface,

        symbols: Dict[str, pygame.Surface],

        sym_slots: List[SymbolSlot],

        leftmost_power: int,

        show_comma: bool,

    ) -> None:

        bottom = self._digit_baseline_y()

        dec_slot = next((s for s in sym_slots if s.name == "decimal"), None)
        dec_glyph = None
        if dec_slot is not None and symbols.get("decimal"):
            dec_glyph = self._fit_symbol_glyph(symbols["decimal"], dec_slot)



        dollar = symbols.get("dollar")

        if dollar and not self.layout.use_template_screen_coords:

            self._draw_dynamic_dollar(target, dollar, leftmost_power, bottom)



        for slot in sym_slots:

            if slot.name == "dollar" and not self.layout.use_template_screen_coords:

                continue

            if slot.name == "comma" and not show_comma:

                continue

            glyph = symbols.get(slot.name)

            if glyph is None:

                continue

            if slot.name == "comma" and dec_glyph is not None:
                self._blit_punctuation_matched(target, glyph, slot, dec_glyph, bottom)
            else:
                self._blit_symbol_in_slot(target, glyph, slot, bottom=bottom)



    def _blit_symbol_in_slot(
        self,
        target: pygame.Surface,
        glyph: pygame.Surface,
        slot: SymbolSlot,
        *,
        bottom: Optional[int] = None,
    ) -> None:
        glyph = self._fit_symbol_glyph(glyph, slot)

        x = slot.x + (slot.w - glyph.get_width()) // 2

        if bottom is not None:
            y = bottom - glyph.get_height()
        else:
            y = slot.y + slot.h - glyph.get_height()

        target.blit(glyph, (x, y))

    def _blit_punctuation_matched(
        self,
        target: pygame.Surface,
        glyph: pygame.Surface,
        slot: SymbolSlot,
        ref_glyph: pygame.Surface,
        bottom: int,
    ) -> None:
        """Scale comma to decimal height (not squeezed to narrow slot width)."""
        ref_h = ref_glyph.get_height()
        scaled = self._fit_symbol_glyph(glyph, slot, height_only=True, target_h=ref_h)
        x = slot.x + (slot.w - scaled.get_width()) // 2
        target.blit(scaled, (x, bottom - scaled.get_height()))



    def _draw_symbols_gap_fallback(

        self,

        target: pygame.Surface,

        symbols: Dict[str, pygame.Surface],

        leftmost_power: int,

        show_comma: bool,

    ) -> None:

        """Legacy placement when layout.json has no symbol_slots."""

        dollar = symbols.get("dollar")

        comma = symbols.get("comma")

        decimal = symbols.get("decimal")

        if not any((dollar, comma, decimal)):

            return



        bottom = self._digit_baseline_y()



        if dollar:

            self._draw_dynamic_dollar(target, dollar, leftmost_power, bottom)



        if decimal:

            dollars = slot_for_power(self.layout, 2)

            dimes = slot_for_power(self.layout, 1)

            if dollars and dimes:

                x = self._gap_center_x(dollars, dimes, decimal.get_width())

                self._blit_bottom_aligned(target, decimal, x, bottom)



        if comma and show_comma:

            for left_power, right_power in _COMMA_GAPS:

                left_slot = slot_for_power(self.layout, left_power)

                right_slot = slot_for_power(self.layout, right_power)

                if left_slot and right_slot:

                    x = self._gap_center_x(left_slot, right_slot, comma.get_width())

                    self._blit_bottom_aligned(target, comma, x, bottom)



    @staticmethod

    def _gap_center_x(left_slot: DigitSlot, right_slot: DigitSlot, sym_w: int) -> int:

        gap_left = left_slot.x + left_slot.w

        gap_right = right_slot.x

        return gap_left + max(0, (gap_right - gap_left - sym_w) // 2)



    @staticmethod

    def _blit_bottom_aligned(

        target: pygame.Surface, surf: pygame.Surface, x: int, bottom_y: int

    ) -> None:

        target.blit(surf, (x, bottom_y - surf.get_height()))



    def _draw_digits_fallback(

        self,

        target: pygame.Surface,

        engine: OdometerEngine,

        digits: List[int],

        leftmost: int,

    ) -> None:

        """Legacy row if layout.json has no digit_slots."""

        cell_w, cell_h = 90, 120

        gap = 8

        cols = NUM_COLUMNS

        row_w = cols * cell_w + (cols - 1) * gap

        fx, fy = self.layout.frame_anchor

        fw = 1872

        ox = fx + max(0, (fw - row_w) // 2)

        oy = fy + 260

        gw, gh = self._glyph_pixel_size(cell_w, cell_h)
        glyphs = self._glyphs_for_size(gw, gh)

        for screen_col in range(cols):

            power = NUM_COLUMNS - 1 - screen_col

            if power > leftmost:

                continue

            x = ox + screen_col * (cell_w + gap)

            d, scroll = engine.get_column_draw(power)

            if scroll > 0.001:

                slot = DigitSlot("pennies", power, x, oy, cell_w, cell_h)

                self._draw_scrolling_column(target, slot, glyphs, d, scroll)

            else:
                slot = DigitSlot("col", power, x, oy, cell_w, cell_h)
                glyph = self._fit_digit_glyph(glyphs[d], slot)
                gx, gy = self._digit_blit_xy(slot, glyph)
                target.blit(glyph, (gx, gy))



    def _draw_scrolling_column(

        self,

        target: pygame.Surface,

        slot: DigitSlot,

        glyphs: List[pygame.Surface],

        digit: int,

        scroll: float,

    ) -> None:

        next_digit = (digit + 1) % 10

        g0 = self._fit_digit_glyph(glyphs[digit], slot)
        g1 = self._fit_digit_glyph(glyphs[next_digit], slot)
        step = max(g0.get_height(), g1.get_height(), slot.h)
        offset_y = -int(round(scroll * step))

        clip = pygame.Rect(slot.x, slot.y, slot.w, slot.h)

        prev_clip = target.get_clip()

        target.set_clip(clip)

        try:

            x0, y0 = self._digit_blit_xy(slot, g0, offset_y)
            x1, y1 = self._digit_blit_xy(slot, g1, offset_y + step)
            target.blit(g0, (x0, y0))
            target.blit(g1, (x1, y1))

        finally:

            target.set_clip(prev_clip)


