"""Composite color strip stream — blends multiple sub-streams as layers."""

import threading
import time
from typing import Dict, List, Optional

import numpy as np

from wled_controller.core.processing.color_strip_stream import ColorStripStream
from wled_controller.storage.bindable import bfloat
from wled_controller.utils import get_logger

logger = get_logger(__name__)

# Blend-mode dispatch keys
_BLEND_NORMAL = "normal"
_BLEND_ADD = "add"
_BLEND_MULTIPLY = "multiply"
_BLEND_SCREEN = "screen"
_BLEND_OVERRIDE = "override"
_BLEND_OVERLAY = "overlay"
_BLEND_SOFT_LIGHT = "soft_light"
_BLEND_HARD_LIGHT = "hard_light"
_BLEND_DIFFERENCE = "difference"
_BLEND_EXCLUSION = "exclusion"


class CompositeColorStripStream(ColorStripStream):
    """Blends multiple ColorStripStreams as layers with blend modes and opacity.

    Each layer references an existing (non-composite) ColorStripSource.
    Sub-streams are acquired from the ColorStripStreamManager so picture
    sources share their existing capture pipeline.

    Processing runs in a background thread at 30 FPS, polling each
    sub-stream's latest colors and blending bottom-to-top.
    """

    def __init__(self, source, css_manager, value_stream_manager=None, cspt_store=None):
        import uuid as _uuid

        self._source_id: str = source.id
        self._instance_id: str = _uuid.uuid4().hex[:8]  # unique per instance to avoid release races
        self._layers: List[dict] = list(source.layers)
        self._led_count: int = source.led_count
        self._auto_size: bool = source.led_count == 0
        self._css_manager = css_manager
        self._value_stream_manager = value_stream_manager
        self._cspt_store = cspt_store
        self._fps: int = 30
        self._frame_time: float = 1.0 / 30

        self._running = False
        self._thread: Optional[threading.Thread] = None
        self._latest_colors: Optional[np.ndarray] = None
        self._latest_layer_colors: Optional[List[np.ndarray]] = None
        self._colors_lock = threading.Lock()
        self._need_layer_snapshots: bool = False  # set True when get_layer_colors() is called

        # (src_len, dst_len) -> (src_x, dst_x, buffer) cache for zone resizing
        self._resize_cache: Dict[tuple, tuple] = {}
        # layer_index -> (source_id, consumer_id, stream)
        self._sub_streams: Dict[int, tuple] = {}
        # layer_index -> (vs_id, value_stream)
        self._brightness_streams: Dict[int, tuple] = {}
        self._sub_lock = threading.Lock()  # guards _sub_streams and _brightness_streams
        self._sub_streams_version: int = 0  # bumped when _sub_streams changes
        self._sub_snapshot_version: int = -1  # version of cached snapshot
        self._sub_snapshot_cache: Dict[int, tuple] = {}  # cached dict(self._sub_streams)

        # Pre-resolved blend methods: blend_mode_str -> bound method
        self._blend_methods = {k: getattr(self, v) for k, v in self._BLEND_DISPATCH.items()}
        self._default_blend_method = self._blend_normal

        # Pre-allocated scratch (rebuilt when LED count changes)
        self._pool_n = 0
        self._result_a: Optional[np.ndarray] = None
        self._result_b: Optional[np.ndarray] = None
        self._use_a = True
        self._u16_a: Optional[np.ndarray] = None
        self._u16_b: Optional[np.ndarray] = None
        self._resize_buf: Optional[np.ndarray] = None

    # ── ColorStripStream interface ──────────────────────────────

    @property
    def target_fps(self) -> int:
        return self._fps

    def set_capture_fps(self, fps: int) -> None:
        self._fps = max(1, min(90, fps))
        self._frame_time = 1.0 / self._fps

    @property
    def led_count(self) -> int:
        return self._led_count

    @property
    def is_animated(self) -> bool:
        return True

    def start(self) -> None:
        with self._sub_lock:
            self._acquire_sub_streams()
        self._running = True
        self._thread = threading.Thread(
            target=self._processing_loop,
            daemon=True,
            name=f"CompositeCSS-{self._source_id[:12]}",
        )
        self._thread.start()
        logger.info(
            f"CompositeColorStripStream started: {self._source_id} "
            f"({len(self._sub_streams)} layers, {self._led_count} LEDs)"
        )

    def stop(self) -> None:
        self._running = False
        if self._thread is not None:
            self._thread.join(timeout=5.0)
            self._thread = None
        with self._sub_lock:
            self._release_sub_streams()
        logger.info(f"CompositeColorStripStream stopped: {self._source_id}")

    def get_latest_colors(self) -> Optional[np.ndarray]:
        with self._colors_lock:
            return self._latest_colors

    def get_layer_colors(self) -> Optional[List[np.ndarray]]:
        """Return per-layer color snapshots (after resize/brightness, before blending)."""
        self._need_layer_snapshots = True
        with self._colors_lock:
            return self._latest_layer_colors

    def get_layer_brightness(self) -> List[Optional[float]]:
        """Return per-layer brightness values (0.0-1.0) from value sources, or None if no source."""
        enabled = [layer for layer in self._layers if layer.get("enabled", True)]
        result: List[Optional[float]] = []
        with self._sub_lock:
            for i in range(len(enabled)):
                if i in self._brightness_streams:
                    _vs_id, vs = self._brightness_streams[i]
                    try:
                        result.append(vs.get_value())
                    except Exception:
                        result.append(None)
                else:
                    result.append(None)
        return result

    def configure(self, device_led_count: int) -> None:
        if self._auto_size and device_led_count > 0 and device_led_count != self._led_count:
            self._led_count = device_led_count
            # Re-configure sub-streams that support auto-sizing
            with self._sub_lock:
                snapshot = dict(self._sub_streams)
            for _idx, (src_id, consumer_id, stream) in snapshot.items():
                if hasattr(stream, "configure"):
                    stream.configure(device_led_count)
            logger.debug(f"CompositeColorStripStream auto-sized to {device_led_count} LEDs")

    def update_source(self, source) -> None:
        """Hot-update: rebuild sub-streams if layer config changed."""
        new_layers = list(source.layers)
        old_layer_ids = [
            (
                layer.get("source_id"),
                layer.get("blend_mode"),
                layer.get("opacity"),
                layer.get("enabled"),
                layer.get("brightness_source_id"),
                layer.get("start", 0),
                layer.get("end", 0),
                layer.get("reverse", False),
            )
            for layer in self._layers
        ]
        new_layer_ids = [
            (
                layer.get("source_id"),
                layer.get("blend_mode"),
                layer.get("opacity"),
                layer.get("enabled"),
                layer.get("brightness_source_id"),
                layer.get("start", 0),
                layer.get("end", 0),
                layer.get("reverse", False),
            )
            for layer in new_layers
        ]

        self._layers = new_layers

        if source.led_count != 0:
            self._led_count = source.led_count
            self._auto_size = False

        # If layer composition changed, rebuild sub-streams
        if old_layer_ids != new_layer_ids:
            with self._sub_lock:
                self._release_sub_streams()
                self._acquire_sub_streams()
            logger.info(f"CompositeColorStripStream rebuilt sub-streams: {self._source_id}")

    # ── Sub-stream lifecycle ────────────────────────────────────

    def _acquire_sub_streams(self) -> None:
        self._sub_streams_version += 1
        for i, layer in enumerate(self._layers):
            if not layer.get("enabled", True):
                continue
            src_id = layer.get("source_id", "")
            if not src_id:
                continue
            consumer_id = f"{self._source_id}__{self._instance_id}__layer_{i}"
            try:
                stream = self._css_manager.acquire(src_id, consumer_id)
                if hasattr(stream, "configure") and self._led_count > 0:
                    # Configure with zone length if layer has a range, else full strip
                    layer_start = layer.get("start", 0)
                    layer_end = layer.get("end", 0)
                    if layer_start > 0 or layer_end > 0:
                        eff_end = layer_end if layer_end > 0 else self._led_count
                        zone_len = max(0, eff_end - layer_start)
                        stream.configure(zone_len if zone_len > 0 else self._led_count)
                    else:
                        stream.configure(self._led_count)
                self._sub_streams[i] = (src_id, consumer_id, stream)
            except Exception as e:
                logger.warning(f"Composite layer {i} (source {src_id}) failed to acquire: {e}")
            # Acquire brightness value stream if configured
            vs_id = layer.get("brightness_source_id")
            if vs_id and self._value_stream_manager:
                try:
                    vs = self._value_stream_manager.acquire(vs_id)
                    self._brightness_streams[i] = (vs_id, vs)
                except Exception as e:
                    logger.warning(f"Composite layer {i} brightness source {vs_id} failed: {e}")

    def _release_sub_streams(self) -> None:
        self._sub_streams_version += 1
        for _idx, (src_id, consumer_id, _stream) in list(self._sub_streams.items()):
            try:
                self._css_manager.release(src_id, consumer_id)
            except Exception as e:
                logger.warning(f"Composite layer release error ({src_id}): {e}")
        self._sub_streams.clear()
        # Release brightness value streams
        if self._value_stream_manager:
            for _idx, (vs_id, _vs) in list(self._brightness_streams.items()):
                try:
                    self._value_stream_manager.release(vs_id)
                except Exception as e:
                    logger.warning(f"Composite brightness release error ({vs_id}): {e}")
        self._brightness_streams.clear()

    # ── Scratch pool ────────────────────────────────────────────

    def _ensure_pool(self, n: int) -> None:
        if n == self._pool_n or n <= 0:
            return
        self._pool_n = n
        self._result_a = np.empty((n, 3), dtype=np.uint8)
        self._result_b = np.empty((n, 3), dtype=np.uint8)
        self._u16_a = np.empty((n, 3), dtype=np.uint16)
        self._u16_b = np.empty((n, 3), dtype=np.uint16)
        self._resize_buf = np.empty((n, 3), dtype=np.uint8)

    # ── Resize helper ───────────────────────────────────────────

    def _resize_to_target(self, colors: np.ndarray, target_n: int) -> np.ndarray:
        """Resize (N, 3) uint8 array to (target_n, 3) via linear interpolation."""
        n_src = len(colors)
        if n_src == target_n:
            return colors
        src_x = np.linspace(0, 1, n_src)
        dst_x = np.linspace(0, 1, target_n)
        buf = self._resize_buf
        for ch in range(3):
            np.copyto(
                buf[:, ch],
                np.interp(dst_x, src_x, colors[:, ch]),
                casting="unsafe",
            )
        return buf

    # ── Blend operations (integer math, pre-allocated) ──────────

    def _blend_normal(
        self, bottom: np.ndarray, top: np.ndarray, alpha: int, out: np.ndarray
    ) -> None:
        """Normal blend: out = (bottom * (256-a) + top * a) >> 8"""
        u16a, u16b = self._u16_a, self._u16_b
        np.copyto(u16a, bottom, casting="unsafe")
        np.copyto(u16b, top, casting="unsafe")
        u16a *= 256 - alpha
        u16b *= alpha
        u16a += u16b
        u16a >>= 8
        np.copyto(out, u16a, casting="unsafe")

    def _blend_add(self, bottom: np.ndarray, top: np.ndarray, alpha: int, out: np.ndarray) -> None:
        """Additive blend: out = min(255, bottom + top * alpha >> 8)"""
        u16a, u16b = self._u16_a, self._u16_b
        np.copyto(u16a, bottom, casting="unsafe")
        np.copyto(u16b, top, casting="unsafe")
        u16b *= alpha
        u16b >>= 8
        u16a += u16b
        np.clip(u16a, 0, 255, out=u16a)
        np.copyto(out, u16a, casting="unsafe")

    def _blend_multiply(
        self, bottom: np.ndarray, top: np.ndarray, alpha: int, out: np.ndarray
    ) -> None:
        """Multiply blend: blended = bottom*top>>8, then lerp with alpha."""
        u16a, u16b = self._u16_a, self._u16_b
        # blended = (bottom * top) >> 8
        np.copyto(u16a, bottom, casting="unsafe")
        np.copyto(u16b, top, casting="unsafe")
        u16a *= u16b
        u16a >>= 8
        # lerp: result = (bottom * (256-a) + blended * a) >> 8
        np.copyto(u16b, bottom, casting="unsafe")
        u16b *= 256 - alpha
        u16a *= alpha
        u16a += u16b
        u16a >>= 8
        np.copyto(out, u16a, casting="unsafe")

    def _blend_screen(
        self, bottom: np.ndarray, top: np.ndarray, alpha: int, out: np.ndarray
    ) -> None:
        """Screen blend: blended = 255 - (255-bottom)*(255-top)>>8, then lerp."""
        u16a, u16b = self._u16_a, self._u16_b
        # blended = 255 - ((255 - bottom) * (255 - top)) >> 8
        np.copyto(u16a, bottom, casting="unsafe")
        np.copyto(u16b, top, casting="unsafe")
        u16a[:] = 255 - u16a
        u16b[:] = 255 - u16b
        u16a *= u16b
        u16a >>= 8
        u16a[:] = 255 - u16a
        # lerp: result = (bottom * (256-a) + blended * a) >> 8
        np.copyto(u16b, bottom, casting="unsafe")
        u16b *= 256 - alpha
        u16a *= alpha
        u16a += u16b
        u16a >>= 8
        np.copyto(out, u16a, casting="unsafe")

    def _blend_override(
        self, bottom: np.ndarray, top: np.ndarray, alpha: int, out: np.ndarray
    ) -> None:
        """Override blend: per-pixel alpha derived from top brightness.

        Black pixels are fully transparent (bottom shows through),
        bright pixels fully opaque (top replaces bottom). Layer opacity
        scales the per-pixel alpha.
        """
        u16a, u16b = self._u16_a, self._u16_b
        # Per-pixel brightness = max(R, G, B) for each LED
        per_px_alpha = np.max(top, axis=1, keepdims=True).astype(np.uint16)
        # Scale by layer opacity
        per_px_alpha = (per_px_alpha * alpha) >> 8
        # Lerp: out = (bottom * (256 - per_px_alpha) + top * per_px_alpha) >> 8
        np.copyto(u16a, bottom, casting="unsafe")
        np.copyto(u16b, top, casting="unsafe")
        u16a *= 256 - per_px_alpha
        u16b *= per_px_alpha
        u16a += u16b
        u16a >>= 8
        np.copyto(out, u16a, casting="unsafe")

    def _blend_overlay(
        self, bottom: np.ndarray, top: np.ndarray, alpha: int, out: np.ndarray
    ) -> None:
        """Overlay blend: multiply darks, screen lights, then lerp with alpha.

        if bottom < 128: blended = 2*bottom*top >> 8
        else:            blended = 255 - 2*(255-bottom)*(255-top) >> 8
        """
        u16a, u16b = self._u16_a, self._u16_b
        np.copyto(u16a, bottom, casting="unsafe")
        np.copyto(u16b, top, casting="unsafe")
        # Multiply path: 2*b*t >> 8
        mul = (u16a * u16b) >> 7  # * 2 >> 8 == >> 7
        # Screen path: 255 - 2*(255-b)*(255-t) >> 8
        scr = 255 - (((255 - u16a) * (255 - u16b)) >> 7)
        # Select based on bottom < 128
        mask = u16a < 128
        blended = np.where(mask, mul, scr)
        np.clip(blended, 0, 255, out=blended)
        # Lerp: result = (bottom * (256-a) + blended * a) >> 8
        np.copyto(u16a, bottom, casting="unsafe")
        u16a *= 256 - alpha
        blended *= alpha
        u16a += blended
        u16a >>= 8
        np.copyto(out, u16a, casting="unsafe")

    def _blend_soft_light(
        self, bottom: np.ndarray, top: np.ndarray, alpha: int, out: np.ndarray
    ) -> None:
        """Soft light blend (Pegtop formula), then lerp with alpha.

        blended = (1 - 2*t/255) * b*b/255 + 2*t*b/255
        """
        u16a, u16b = self._u16_a, self._u16_b
        np.copyto(u16a, bottom, casting="unsafe")
        np.copyto(u16b, top, casting="unsafe")
        # term1 = (255 - 2*t) * b * b / (255*255)
        # term2 = 2 * t * b / 255
        # Use intermediate 32-bit to avoid overflow
        b32 = u16a.astype(np.uint32)
        t32 = u16b.astype(np.uint32)
        blended = ((255 - 2 * t32) * b32 * b32 + 2 * t32 * b32 * 255) // (255 * 255)
        np.clip(blended, 0, 255, out=blended)
        # Lerp
        np.copyto(u16a, bottom, casting="unsafe")
        u16a *= 256 - alpha
        blended_u16 = blended.astype(np.uint16)
        blended_u16 *= alpha
        u16a += blended_u16
        u16a >>= 8
        np.copyto(out, u16a, casting="unsafe")

    def _blend_hard_light(
        self, bottom: np.ndarray, top: np.ndarray, alpha: int, out: np.ndarray
    ) -> None:
        """Hard light blend: overlay with top/bottom roles swapped.

        if top < 128: blended = 2*bottom*top >> 8
        else:         blended = 255 - 2*(255-bottom)*(255-top) >> 8
        """
        u16a, u16b = self._u16_a, self._u16_b
        np.copyto(u16a, bottom, casting="unsafe")
        np.copyto(u16b, top, casting="unsafe")
        mul = (u16a * u16b) >> 7
        scr = 255 - (((255 - u16a) * (255 - u16b)) >> 7)
        # Select based on top < 128 (differs from overlay)
        mask = u16b < 128
        blended = np.where(mask, mul, scr)
        np.clip(blended, 0, 255, out=blended)
        # Lerp
        np.copyto(u16a, bottom, casting="unsafe")
        u16a *= 256 - alpha
        blended *= alpha
        u16a += blended
        u16a >>= 8
        np.copyto(out, u16a, casting="unsafe")

    def _blend_difference(
        self, bottom: np.ndarray, top: np.ndarray, alpha: int, out: np.ndarray
    ) -> None:
        """Difference blend: |bottom - top|, then lerp with alpha."""
        u16a, u16b = self._u16_a, self._u16_b
        np.copyto(u16a, bottom, casting="unsafe")
        np.copyto(u16b, top, casting="unsafe")
        # abs diff using signed subtraction
        blended = np.abs(u16a.astype(np.int16) - u16b.astype(np.int16)).astype(np.uint16)
        # Lerp
        np.copyto(u16a, bottom, casting="unsafe")
        u16a *= 256 - alpha
        blended *= alpha
        u16a += blended
        u16a >>= 8
        np.copyto(out, u16a, casting="unsafe")

    def _blend_exclusion(
        self, bottom: np.ndarray, top: np.ndarray, alpha: int, out: np.ndarray
    ) -> None:
        """Exclusion blend: bottom + top - 2*bottom*top/255, then lerp with alpha."""
        u16a, u16b = self._u16_a, self._u16_b
        np.copyto(u16a, bottom, casting="unsafe")
        np.copyto(u16b, top, casting="unsafe")
        # blended = b + t - 2*b*t/255
        blended = u16a + u16b - ((u16a * u16b) >> 7)
        np.clip(blended, 0, 255, out=blended)
        # Lerp
        np.copyto(u16a, bottom, casting="unsafe")
        u16a *= 256 - alpha
        blended *= alpha
        u16a += blended
        u16a >>= 8
        np.copyto(out, u16a, casting="unsafe")

    _BLEND_DISPATCH = {
        _BLEND_NORMAL: "_blend_normal",
        _BLEND_ADD: "_blend_add",
        _BLEND_MULTIPLY: "_blend_multiply",
        _BLEND_SCREEN: "_blend_screen",
        _BLEND_OVERRIDE: "_blend_override",
        _BLEND_OVERLAY: "_blend_overlay",
        _BLEND_SOFT_LIGHT: "_blend_soft_light",
        _BLEND_HARD_LIGHT: "_blend_hard_light",
        _BLEND_DIFFERENCE: "_blend_difference",
        _BLEND_EXCLUSION: "_blend_exclusion",
    }

    # ── Processing loop ─────────────────────────────────────────

    def _processing_loop(self) -> None:
        # Per-layer CSPT filter cache: layer_index -> (template_id, [PostprocessingFilter, ...])
        _layer_cspt_cache: Dict[int, tuple] = {}

        try:
            while self._running:
                loop_start = time.perf_counter()
                frame_time = self._frame_time

                try:
                    target_n = self._led_count
                    if target_n <= 0:
                        time.sleep(frame_time)
                        continue

                    self._ensure_pool(target_n)

                    result_buf = self._result_a if self._use_a else self._result_b
                    self._use_a = not self._use_a
                    has_result = False
                    layer_snapshots: List[np.ndarray] = []

                    with self._sub_lock:
                        if self._sub_streams_version != self._sub_snapshot_version:
                            self._sub_snapshot_cache = dict(self._sub_streams)
                            self._sub_snapshot_version = self._sub_streams_version
                        sub_snapshot = self._sub_snapshot_cache

                    for i, layer in enumerate(self._layers):
                        if not layer.get("enabled", True):
                            continue
                        if i not in sub_snapshot:
                            continue

                        _src_id, _consumer_id, stream = sub_snapshot[i]
                        colors = stream.get_latest_colors()
                        if colors is None:
                            continue

                        # Apply per-layer CSPT filters
                        _layer_tmpl_id = layer.get("processing_template_id") or ""
                        if _layer_tmpl_id and self._cspt_store:
                            cached = _layer_cspt_cache.get(i)
                            if cached is None or cached[0] != _layer_tmpl_id:
                                # Resolve and cache filters for this layer
                                try:
                                    from wled_controller.core.filters.registry import FilterRegistry

                                    _resolved = self._cspt_store.resolve_filter_instances(
                                        self._cspt_store.get_template(_layer_tmpl_id).filters
                                    )
                                    _filters = [
                                        FilterRegistry.create_instance(fi.filter_id, fi.options)
                                        for fi in _resolved
                                        if getattr(
                                            FilterRegistry.get(fi.filter_id), "supports_strip", True
                                        )
                                    ]
                                    _layer_cspt_cache[i] = (_layer_tmpl_id, _filters)
                                    logger.info(
                                        f"Composite layer {i} CSPT resolved {len(_filters)} filters "
                                        f"from template {_layer_tmpl_id}"
                                    )
                                except Exception as e:
                                    logger.warning(
                                        f"Failed to resolve layer {i} CSPT {_layer_tmpl_id}: {e}"
                                    )
                                    _layer_cspt_cache[i] = (_layer_tmpl_id, [])
                            _layer_filters = _layer_cspt_cache[i][1]
                            if _layer_filters:
                                for _flt in _layer_filters:
                                    _result = _flt.process_strip(colors)
                                    if _result is not None:
                                        colors = _result

                        # Determine layer range
                        layer_start = layer.get("start", 0)
                        layer_end = layer.get("end", 0)
                        has_range = layer_start > 0 or layer_end > 0

                        if has_range:
                            # Clamp range to strip bounds
                            eff_start = max(0, min(layer_start, target_n))
                            eff_end = max(
                                eff_start, min(layer_end if layer_end > 0 else target_n, target_n)
                            )
                            zone_len = eff_end - eff_start
                            if zone_len <= 0:
                                continue
                            # Resize to zone length
                            if len(colors) != zone_len:
                                rkey = (len(colors), zone_len)
                                cached = self._resize_cache.get(rkey)
                                if cached is None:
                                    cached = (
                                        np.linspace(0, 1, len(colors)),
                                        np.linspace(0, 1, zone_len),
                                        np.empty((zone_len, 3), dtype=np.uint8),
                                    )
                                    self._resize_cache[rkey] = cached
                                src_x, dst_x, resized = cached
                                for ch in range(3):
                                    np.copyto(
                                        resized[:, ch],
                                        np.interp(dst_x, src_x, colors[:, ch]),
                                        casting="unsafe",
                                    )
                                colors = resized
                        else:
                            # Full-strip layer: resize to target LED count
                            if len(colors) != target_n:
                                colors = self._resize_to_target(colors, target_n)

                        # Reverse if requested
                        if layer.get("reverse", False):
                            colors = colors[::-1].copy()

                        # Apply per-layer brightness from value source
                        if i in self._brightness_streams:
                            _vs_id, vs = self._brightness_streams[i]
                            bri = vs.get_value()
                            if bri < 1.0:
                                colors = (colors.astype(np.uint16) * int(bri * 256) >> 8).astype(
                                    np.uint8
                                )

                        # Snapshot layer colors before blending (copy — may alias shared buf)
                        if self._need_layer_snapshots:
                            layer_snapshots.append(colors.copy())

                        opacity = bfloat(layer.get("opacity", 1.0), 1.0)
                        blend_mode = layer.get("blend_mode", _BLEND_NORMAL)
                        alpha = int(opacity * 256)
                        alpha = max(0, min(256, alpha))

                        if not has_result:
                            result_buf[:] = 0
                            has_result = True

                        if has_range:
                            # Blend only into the target range — use scratch sub-slices
                            rng = result_buf[eff_start:eff_end]
                            u16a_rng = self._u16_a[:zone_len]
                            u16b_rng = self._u16_b[:zone_len]
                            blend_fn = self._blend_methods.get(
                                blend_mode, self._default_blend_method
                            )
                            # Temporarily swap scratch buffers for the range size
                            orig_u16a, orig_u16b = self._u16_a, self._u16_b
                            self._u16_a, self._u16_b = u16a_rng, u16b_rng
                            blend_fn(rng, colors, alpha, rng)
                            self._u16_a, self._u16_b = orig_u16a, orig_u16b
                        else:
                            blend_fn = self._blend_methods.get(
                                blend_mode, self._default_blend_method
                            )
                            blend_fn(result_buf, colors, alpha, result_buf)

                    if has_result:
                        with self._colors_lock:
                            self._latest_colors = result_buf
                            self._latest_layer_colors = (
                                layer_snapshots if len(layer_snapshots) > 1 else None
                            )

                except Exception as e:
                    logger.error(f"CompositeColorStripStream processing error: {e}", exc_info=True)

                elapsed = time.perf_counter() - loop_start
                time.sleep(max(frame_time - elapsed, 0.001))
        except Exception as e:
            logger.error(f"Fatal CompositeColorStripStream loop error: {e}", exc_info=True)
        finally:
            self._running = False
