//! Helpers de mutación/render que tocan estado de VISTA además del dominio.
//! El ciclo de vida de la simulación (avance, reseed, snapshots, trails,
//! clusters) vive en `dominium_sim::Sim` (regla #2); acá quedan los que
//! cruzan con `selected`, `cfg`, `iso` u otros campos del frontend.

use dominium_core::{Concepto, LayerMods};
use dominium_render_plan::{Color, Quad, RenderMode, RenderPlan};

use crate::consts::GRID;
use crate::model::Model;

/// Accede mutable al Concepto seleccionado, si lo hay.
pub(crate) fn selected_mut(m: &mut Model) -> Option<&mut Concepto> {
    let i = m.selected?;
    m.sim.world.conceptos.items.get_mut(i)
}

/// Agrega un Concepto en `(x, y)` (clamp al grid), lo nombra
/// `nuevo-N` y queda seleccionado para edición inmediata.
pub(crate) fn spawn_concepto_at(m: &mut Model, x: f32, y: f32) {
    let max = (GRID as f32) - 1.0;
    let n = m.sim.world.conceptos.len();
    let new = Concepto {
        id: format!("nuevo-{}", n + 1),
        sprite_id: 0,
        pos_x: x.clamp(0.0, max),
        pos_y: y.clamp(0.0, max),
        radius: 4.0,
        mods: LayerMods::default(),
        hack: None,
        persuasion: None,
    };
    let i = m.sim.world.conceptos.add(new);
    m.selected = Some(i);
}

/// Copia `ZWeights` (relieve visual) al array `[f32; 5]` que SimParams
/// usa como relieve físico, manteniendo el orden de capas del `Grid`.
pub(crate) fn mirror_zweights_to_relieve(
    z: &dominium_iso::ZWeights,
    relieve: &mut [f32; 5],
) {
    relieve[dominium_core::RELIEVE_MATERIA] = z.materia;
    relieve[dominium_core::RELIEVE_PSIQUE] = z.psique;
    relieve[dominium_core::RELIEVE_PODER] = z.poder;
    relieve[dominium_core::RELIEVE_ORO] = z.oro;
    relieve[dominium_core::RELIEVE_DEGRADACION] = z.degradacion;
}

/// Tres colores fijos del paleta de clusters — orden de aparición en el
/// resultado de `kmeans_psi`. Magenta / cian / amarillo: los más fáciles de
/// distinguir sobre cualquier fondo de bioma.
pub(crate) const CLUSTER_COLORS: [Color; 3] = [
    [0.96, 0.30, 0.72, 1.0], // magenta
    [0.30, 0.90, 0.90, 1.0], // cian
    [0.96, 0.92, 0.30, 1.0], // amarillo
];

/// Color para el lemming `i` según el `RenderMode` actual y las
/// asignaciones de cluster vigentes. Se usa como override de
/// `build_plan_with_overrides`.
pub(crate) fn lemming_color_for(m: &Model, i: usize) -> Color {
    if matches!(m.cfg.render_mode, RenderMode::PsiCluster)
        && i < m.sim.cluster_assignments.len()
    {
        let c = m.sim.cluster_assignments[i] as usize;
        if c < CLUSTER_COLORS.len() {
            return CLUSTER_COLORS[c];
        }
    }
    m.cfg.palette.lemming
}

/// Pinta las posiciones históricas de los lemmings como quads diminutos
/// con alpha decreciente — los más viejos casi transparentes. Va después
/// del `build_plan` para que los trails queden por encima del suelo pero
/// por debajo del HUD; depth pequeño constante negativo para no romper el
/// orden de pintor de las celdas.
///
/// Se llama sólo en vivo (no en rewind), porque en rewind el `World` que
/// se renderiza no necesariamente tiene los mismos índices de lemming que
/// el frame de trails — y mezclarlos confundiría al ojo más que ayudar.
pub(crate) fn overlay_trails(plan: &mut RenderPlan, m: &Model) {
    let n_frames = m.sim.trails.len();
    if n_frames == 0 {
        return;
    }
    let lemming_color = m.cfg.palette.lemming;
    // Tamaño de la moteta: la mitad del marker del lemming, así no compite
    // visualmente con la posición actual.
    let size = m.cfg.lemming_size * 0.45;
    for (k, frame) in m.sim.trails.iter().enumerate() {
        // k=0 es el más viejo → alpha bajo; k=n-1 el más nuevo → alpha alto.
        // No incluyo el último frame: ya está pintado por el lemming actual.
        if k + 1 == n_frames {
            break;
        }
        let t = (k + 1) as f32 / n_frames as f32; // ∈ (0, 1)
        let alpha = 0.10 + 0.40 * t;
        let color: Color = [
            lemming_color[0],
            lemming_color[1],
            lemming_color[2],
            alpha,
        ];
        for &(x, y) in frame {
            let (sx, sy) = m.iso.project(x, y, m.cfg.lemming_lift * 0.5);
            plan.quads.push(Quad {
                x: sx - size * 0.5,
                y: sy - size * 0.5,
                w: size,
                h: size,
                color,
                // Detrás de los Lemmings vivos (que pintan a depth ≈ x+y+0.5)
                // pero delante de la celda (depth x+y).
                depth: x + y + 0.25,
            });
        }
    }
    // Mantengo el plan ordenado: insert al final desordena. Re-ordeno por
    // depth — coste O(N log N) pero N es del orden de 50·24 = 1200 quads.
    plan.quads.sort_by(|a, b| {
        a.depth.partial_cmp(&b.depth).unwrap_or(std::cmp::Ordering::Equal)
    });
    // Re-extender la bounding box por si los trails caen fuera.
    for q in &plan.quads {
        plan.min_x = plan.min_x.min(q.x);
        plan.min_y = plan.min_y.min(q.y);
        plan.max_x = plan.max_x.max(q.x + q.w);
        plan.max_y = plan.max_y.max(q.y + q.h);
    }
}
