"""Floating capsule overlay at bottom-center of screen.

Wispr-style: hidden by default → on press fades in as a slim pill with gold-glow border,
shows live audio waveform while recording, morphs to 'transcribing' shimmer on release,
then fades out when idle.

NSPanel is borderless, click-through, floats above everything (NSStatusWindowLevel),
appears on all spaces. All UI work runs on the main thread; level data is read from
the Recorder's thread-safe deque.
"""

from __future__ import annotations

import math
import time
from typing import Callable

import objc  # type: ignore
from PyObjCTools import AppHelper  # type: ignore
from AppKit import (  # type: ignore
    NSBackingStoreBuffered,
    NSBezierPath,
    NSColor,
    NSGradient,
    NSPanel,
    NSScreen,
    NSStatusWindowLevel,
    NSView,
    NSWindowCollectionBehaviorCanJoinAllSpaces,
    NSWindowCollectionBehaviorStationary,
    NSWindowStyleMaskBorderless,
    NSWindowStyleMaskNonactivatingPanel,
)
from Foundation import NSMakeRect, NSTimer  # type: ignore

# Brand tokens
ACCENT_R, ACCENT_G, ACCENT_B = 0xEB / 255.0, 0xB6 / 255.0, 0x38 / 255.0   # gold core
WARM_R, WARM_G, WARM_B = 0xFF / 255.0, 0x9B / 255.0, 0x50 / 255.0          # amber falloff (firelight)
# Warm coal — premium near-black with the slightest amber tilt (avoids flat #000 feel)
BG_TOP_R, BG_TOP_G, BG_TOP_B = 0x12 / 255.0, 0x0E / 255.0, 0x0A / 255.0
BG_BOT_R, BG_BOT_G, BG_BOT_B = 0x07 / 255.0, 0x05 / 255.0, 0x04 / 255.0
CREAM_R, CREAM_G, CREAM_B = 0xEF / 255.0, 0xEA / 255.0, 0xE4 / 255.0

W_LISTENING = 78.0
H = 18.0
GLOW_PAD = 90.0          # space for the big soft Core Animation shadow bloom
BOTTOM_MARGIN = 110.0    # clears the macOS Dock
ENTRANCE_DURATION = 0.32 # seconds for capsule grow-in
EMA_ALPHA = 0.12         # voice-level smoothing (lower = slower, calmer)


class CapsuleView(NSView):
    def initWithFrame_levelsProvider_(self, frame, levels_provider: Callable[[int], list[float]]):  # type: ignore
        self = objc.super(CapsuleView, self).initWithFrame_(frame)
        if self is None:
            return None
        self._state = "hidden"  # hidden | listening | transcribing
        self._levels_provider = levels_provider
        self._t0 = time.time()
        self._smooth_level = 0.0
        # CALayer shadow = native Core Animation Gaussian blur — no banding, no rings
        self.setWantsLayer_(True)
        layer = self.layer()
        if layer is not None:
            layer.setMasksToBounds_(False)
            warm = NSColor.colorWithCalibratedRed_green_blue_alpha_(
                WARM_R, WARM_G, WARM_B, 1.0
            )
            layer.setShadowColor_(warm.CGColor())
            layer.setShadowRadius_(72.0)  # bigger soft Gaussian halo around capsule
            layer.setShadowOpacity_(0.0)
            layer.setShadowOffset_((0.0, 0.0))
        return self

    def setState_(self, state):  # noqa: N802
        prev = self._state
        self._state = state
        if state != prev:
            self._t0 = time.time()
            if state == "listening":
                self._smooth_level = 0.0
            if state == "hidden":
                layer = self.layer()
                if layer is not None:
                    layer.setShadowOpacity_(0.0)
        self.setNeedsDisplay_(True)

    def get_halo_opacity(self) -> float:
        """How strong the layer shadow should be right now — driven by entrance scale
        + smoothed voice level. Read from the main-thread tick to update the shadow."""
        if self._state == "hidden":
            return 0.0
        scale = self._entrance_scale()
        loudness = self._smooth_level
        # noticeable baseline halo + bright pulse under voice
        return min(1.0, scale * (0.55 + 0.45 * loudness) * 0.95)

    def _entrance_scale(self) -> float:
        """Ease-out cubic 0→1 over ENTRANCE_DURATION seconds since last state change."""
        if self._state == "hidden":
            return 0.0
        p = min(1.0, (time.time() - self._t0) / ENTRANCE_DURATION)
        return 1.0 - (1.0 - p) ** 3

    def _update_smooth_level(self) -> None:
        recent = self._levels_provider(4)
        instant = max(recent) if recent else 0.0
        # normalize then EMA — gives a slow breathing curve, not jitter
        norm = min(1.0, (instant / 0.07) ** 0.7)
        self._smooth_level = (1.0 - EMA_ALPHA) * self._smooth_level + EMA_ALPHA * norm

    def drawRect_(self, dirty):  # noqa: N802
        if self._state == "hidden":
            return

        bounds = self.bounds()
        full_w = bounds.size.width
        full_h = bounds.size.height

        # entrance scale — grow from a tiny dot to full pill (ease-out cubic)
        scale = self._entrance_scale()
        # min visible scale so it doesn't pop from nothing
        eff_scale_x = 0.15 + 0.85 * scale
        eff_scale_y = 0.5 + 0.5 * scale
        w = full_w * eff_scale_x
        h = full_h * eff_scale_y
        x0 = (full_w - w) / 2
        y0 = (full_h - h) / 2

        # smoothed loudness drives glow intensity (no jitter)
        if self._state == "listening":
            self._update_smooth_level()
        loudness = self._smooth_level if self._state == "listening" else 0.55

        # Halo intentionally NOT drawn here — the view's CALayer shadow renders the smooth
        # Gaussian bloom around the drawn capsule + slit. Updated per-frame from the tick.
        _ = loudness  # reserved; halo intensity is handled by layer.shadowOpacity

        # capsule body — warm-coal vertical gradient (subtle, premium)
        body = NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
            NSMakeRect(x0, y0, w, h), h / 2, h / 2
        )
        top_color = NSColor.colorWithCalibratedRed_green_blue_alpha_(
            BG_TOP_R, BG_TOP_G, BG_TOP_B, 0.97
        )
        bot_color = NSColor.colorWithCalibratedRed_green_blue_alpha_(
            BG_BOT_R, BG_BOT_G, BG_BOT_B, 0.97
        )
        gradient = NSGradient.alloc().initWithStartingColor_endingColor_(top_color, bot_color)
        gradient.drawInBezierPath_angle_(body, 270.0)  # 270° = top→bottom

        # 1px inner gold edge — brightness tied to loudness
        inner = NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
            NSMakeRect(x0 + 0.5, y0 + 0.5, w - 1, h - 1), (h - 1) / 2, (h - 1) / 2
        )
        NSColor.colorWithCalibratedRed_green_blue_alpha_(
            ACCENT_R, ACCENT_G, ACCENT_B, 0.5 + 0.5 * loudness
        ).setStroke()
        inner.setLineWidth_(1.0)
        inner.stroke()

        # inside content (only once entrance is mostly done — avoid weird small artifacts)
        if scale > 0.5:
            content_alpha = min(1.0, (scale - 0.5) * 2.5)
            if self._state == "listening":
                self._draw_slit(x0, y0, w, h, content_alpha)
            elif self._state == "transcribing":
                self._draw_transcribing(x0, y0, w, h, content_alpha)

    def _draw_slit(self, x0: float, y0: float, w: float, h: float, alpha_mul: float) -> None:
        """Horizontal warm 'slit of light' inside the capsule. Width + brightness expand
        from center under voice. Each layer is ONE solid rounded pill (no left/right
        halves — the previous split was causing a dark seam at the center where the
        rounded caps met). The center-emanating glow effect comes from the CALayer
        shadow halo, not from gradient inside the slit."""
        self._update_smooth_level()
        voice = self._smooth_level
        t = time.time() - self._t0

        # tiny ambient float so the slit isn't 0-width in silence
        ambient = 0.5 + 0.5 * math.sin(t * 0.55 * 2 * math.pi)
        intensity = max(0.22 + 0.06 * ambient, 0.30 + 0.70 * voice)

        cx = x0 + w / 2
        cy = y0 + h / 2

        max_w = w * 0.82
        slit_w = max_w * (0.20 + 0.80 * intensity)
        slit_x = cx - slit_w / 2

        # warmth + alpha tied to intensity (single color, no horizontal gradient → no seam)
        mix = 0.20 + 0.55 * intensity
        cr = ACCENT_R * (1 - mix) + WARM_R * mix
        cg = ACCENT_G * (1 - mix) + WARM_G * mix
        cb = ACCENT_B * (1 - mix) + WARM_B * mix

        # stacked thickness layers (outermost = blurriest with low alpha, top = bright core)
        # all centered on the same horizontal line, no horizontal seam anywhere
        layers = [
            (7.0, 0.08),    # outermost soft bloom around the slit
            (4.5, 0.16),
            (2.6, 0.32),
            (1.4, 0.62),
            (0.8, 0.95),    # bright core line
        ]

        for thickness, base_alpha in layers:
            rect = NSMakeRect(slit_x, cy - thickness / 2, slit_w, thickness)
            path = NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
                rect, thickness / 2, thickness / 2
            )
            a = base_alpha * (0.55 + 0.45 * intensity) * alpha_mul
            NSColor.colorWithCalibratedRed_green_blue_alpha_(cr, cg, cb, a).setFill()
            path.fill()

        # Thin horizontal cream highlight at center — same shape as the slit, just brighter.
        # No gradient = no circular artifact. It's a tiny brighter pill ON the slit.
        if voice > 0.10:
            hl_w = (slit_w * 0.18) + (slit_w * 0.18) * voice  # ~18-36% of slit width
            hl_thickness = 0.7
            hl_rect = NSMakeRect(cx - hl_w / 2, cy - hl_thickness / 2, hl_w, hl_thickness)
            hl_path = NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
                hl_rect, hl_thickness / 2, hl_thickness / 2
            )
            NSColor.colorWithCalibratedRed_green_blue_alpha_(
                CREAM_R, CREAM_G, CREAM_B, 0.75 * voice * alpha_mul
            ).setFill()
            hl_path.fill()

    def _draw_blob(self, cx: float, cy: float, radius: float, warmth: float,
                    intensity: float, alpha_mul: float) -> None:
        """Single soft warm radial blob — hot core → amber → transparent edge.
        Clipped to a circular path; gradient fades to alpha 0 so edges feel cloud-like."""
        # core: gold↔amber blend by warmth
        cr_core = ACCENT_R * (1 - warmth) + WARM_R * warmth
        cg_core = ACCENT_G * (1 - warmth) + WARM_G * warmth
        cb_core = ACCENT_B * (1 - warmth) + WARM_B * warmth
        core = NSColor.colorWithCalibratedRed_green_blue_alpha_(
            cr_core, cg_core, cb_core, min(0.95, 0.55 + 0.45 * intensity) * alpha_mul
        )
        edge = NSColor.colorWithCalibratedRed_green_blue_alpha_(
            cr_core, cg_core, cb_core, 0.0
        )
        gradient = NSGradient.alloc().initWithStartingColor_endingColor_(core, edge)
        rect = NSMakeRect(cx - radius, cy - radius, radius * 2, radius * 2)
        path = NSBezierPath.bezierPathWithOvalInRect_(rect)
        # relativeCenterPosition (0,0) = center; gradient radiates outward
        gradient.drawInBezierPath_relativeCenterPosition_(path, (0.0, 0.0))

    def _draw_cloud(self, x0: float, y0: float, w: float, h: float, alpha_mul: float) -> None:
        """Living warm cloud inside the capsule — 3 overlapping radial blobs drift on slow
        sine paths, pulse size with smoothed voice level. Voice modulates intensity and
        size; idle still breathes very gently so the capsule feels alive."""
        self._update_smooth_level()
        voice = self._smooth_level
        t = time.time() - self._t0

        cx_mid = x0 + w / 2
        cy_mid = y0 + h / 2

        # ambient subtle breath (always alive, but quiet)
        ambient = 0.5 + 0.5 * math.sin(t * 0.55 * 2 * math.pi)

        # intensity drives core brightness + radius
        # voice dominates; ambient adds a small floor so capsule isn't dead in silence
        intensity = max(0.22 + 0.10 * ambient, 0.30 + 0.70 * voice)

        # 3 blobs with different drift speeds + phases — overlap → cloud effect
        # each tuple: (base_x_frac, drift_freq, drift_phase, base_radius, warmth)
        blobs = [
            (0.32, 0.40, 0.0,  8.0, 0.05),
            (0.50, 0.55, 1.3, 10.0, 0.55),
            (0.68, 0.36, 2.6,  8.5, 0.95),
        ]

        for base_frac, freq, phase, base_r, warmth in blobs:
            drift_x = 0.10 * w * math.sin(t * freq * 2 * math.pi + phase)
            drift_y = 1.4 * math.sin(t * (freq * 1.3) * 2 * math.pi + phase * 0.7)
            bx = x0 + base_frac * w + drift_x
            by = cy_mid + drift_y
            radius = base_r * (0.85 + 0.55 * intensity)
            self._draw_blob(bx, by, radius, warmth, intensity, alpha_mul)

        # tiny bright highlight in the brightest blob — a "pearl" core that's very small
        # and only visible when speaking; makes the cloud feel like it has a soul
        if voice > 0.05:
            pearl_x = cx_mid + 0.04 * w * math.sin(t * 0.8 * 2 * math.pi)
            pearl_y = cy_mid + 1.0 * math.cos(t * 0.6 * 2 * math.pi)
            pr = 1.5 + 1.5 * voice
            pearl_color = NSColor.colorWithCalibratedRed_green_blue_alpha_(
                CREAM_R, CREAM_G, CREAM_B, 0.85 * voice * alpha_mul
            )
            pearl_edge = NSColor.colorWithCalibratedRed_green_blue_alpha_(
                CREAM_R, CREAM_G, CREAM_B, 0.0
            )
            grad = NSGradient.alloc().initWithStartingColor_endingColor_(pearl_color, pearl_edge)
            path = NSBezierPath.bezierPathWithOvalInRect_(
                NSMakeRect(pearl_x - pr * 2, pearl_y - pr * 2, pr * 4, pr * 4)
            )
            grad.drawInBezierPath_relativeCenterPosition_(path, (0.0, 0.0))

    def _draw_transcribing(self, x0: float, y0: float, w: float, h: float, alpha_mul: float) -> None:
        elapsed = time.time() - self._t0
        bar_count = 3
        gap = 7.0
        total = (bar_count - 1) * gap
        center_x = x0 + w / 2
        center_y = y0 + h / 2
        for i in range(bar_count):
            phase = (elapsed * 1.6 + i * 0.45) % 1.0
            alpha = (0.25 + 0.75 * (0.5 + 0.5 * math.sin(phase * 2 * math.pi))) * alpha_mul
            r = 1.8
            x = center_x - total / 2 + i * gap
            NSColor.colorWithCalibratedRed_green_blue_alpha_(
                ACCENT_R, ACCENT_G, ACCENT_B, alpha
            ).setFill()
            NSBezierPath.bezierPathWithOvalInRect_(
                NSMakeRect(x - r, center_y - r, r * 2, r * 2)
            ).fill()


class Overlay:
    def __init__(self, levels_provider: Callable[[int], list[float]]):
        self._levels_provider = levels_provider
        self._panel = None
        self._view = None
        self._timer = None
        self._state = "hidden"

    def _ensure_panel(self) -> None:
        if self._panel is not None:
            return
        screen = NSScreen.mainScreen()
        if screen is None:
            return
        sf = screen.frame()
        w = W_LISTENING + 2 * GLOW_PAD
        h = H + 2 * GLOW_PAD
        x = sf.origin.x + (sf.size.width - w) / 2
        y = sf.origin.y + BOTTOM_MARGIN - GLOW_PAD
        rect = NSMakeRect(x, y, w, h)

        style = NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel
        panel = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
            rect, style, NSBackingStoreBuffered, False
        )
        panel.setOpaque_(False)
        panel.setBackgroundColor_(NSColor.clearColor())
        panel.setHasShadow_(False)
        panel.setLevel_(NSStatusWindowLevel)
        panel.setIgnoresMouseEvents_(True)
        panel.setCollectionBehavior_(
            NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary
        )
        panel.setHidesOnDeactivate_(False)
        panel.setMovableByWindowBackground_(False)
        panel.setReleasedWhenClosed_(False)

        content_rect = NSMakeRect(GLOW_PAD, GLOW_PAD, W_LISTENING, H)
        view = CapsuleView.alloc().initWithFrame_levelsProvider_(content_rect, self._levels_provider)
        panel.contentView().addSubview_(view)
        self._panel = panel
        self._view = view

    def show_listening(self) -> None:
        AppHelper.callAfter(self._show_listening_main)

    def show_transcribing(self) -> None:
        AppHelper.callAfter(self._show_transcribing_main)

    def hide(self) -> None:
        AppHelper.callAfter(self._hide_main)

    def _show_listening_main(self) -> None:
        self._ensure_panel()
        if self._panel is None:
            return
        self._state = "listening"
        self._view.setState_("listening")
        self._panel.orderFrontRegardless()
        self._start_timer()

    def _show_transcribing_main(self) -> None:
        if self._panel is None:
            return
        self._state = "transcribing"
        self._view.setState_("transcribing")

    def _hide_main(self) -> None:
        if self._panel is None:
            return
        self._state = "hidden"
        self._view.setState_("hidden")
        self._panel.orderOut_(None)
        self._stop_timer()

    def _start_timer(self) -> None:
        if self._timer is not None:
            return
        self._timer = NSTimer.scheduledTimerWithTimeInterval_repeats_block_(
            1.0 / 30.0, True, lambda _t: self._tick()
        )

    def _stop_timer(self) -> None:
        if self._timer is not None:
            self._timer.invalidate()
            self._timer = None

    def _tick(self) -> None:
        if self._state == "hidden":
            self._stop_timer()
            return
        if self._view is not None:
            # update layer shadow opacity for the smooth Core Animation halo
            layer = self._view.layer()
            if layer is not None:
                layer.setShadowOpacity_(self._view.get_halo_opacity())
            self._view.setNeedsDisplay_(True)
