use std::collections::HashMap;
use std::time::{Duration, Instant};

use halley_core::field::{Field, NodeId, NodeState, Vec2, Visibility};
use halley_core::tiling::Rect;

use super::curves::ease_in_out_cubic;

#[derive(Clone, Copy, Debug)]
pub(crate) struct ClusterTileAnimRect {
    pub(crate) center: Vec2,
    pub(crate) size: Vec2,
    pub(crate) alpha: f32,
}

#[derive(Clone, Copy, Debug)]
pub(crate) struct ClusterTileTrack {
    from: ClusterTileAnimRect,
    to: ClusterTileAnimRect,
    started_at: Instant,
    duration: Duration,
}

pub(crate) type ClusterTileTracks = HashMap<NodeId, ClusterTileTrack>;

#[inline]
fn rect_center(rect: Rect) -> Vec2 {
    Vec2 {
        x: rect.x + rect.w * 0.5,
        y: rect.y + rect.h * 0.5,
    }
}

#[inline]
fn anim_rect_from_tile_rect(rect: Rect, alpha: f32) -> ClusterTileAnimRect {
    ClusterTileAnimRect {
        center: rect_center(rect),
        size: Vec2 {
            x: rect.w.max(1.0),
            y: rect.h.max(1.0),
        },
        alpha: alpha.clamp(0.0, 1.0),
    }
}

#[inline]
fn entry_rect_for_target(target: ClusterTileAnimRect) -> ClusterTileAnimRect {
    ClusterTileAnimRect {
        center: Vec2 {
            x: target.center.x - target.size.x * 0.12,
            y: target.center.y,
        },
        size: Vec2 {
            x: target.size.x * 0.98,
            y: target.size.y * 0.98,
        },
        alpha: 0.0,
    }
}

#[inline]
pub(crate) fn cluster_tile_rect_from_field(
    field: &Field,
    id: NodeId,
) -> Option<ClusterTileAnimRect> {
    let node = field.node(id)?;
    Some(ClusterTileAnimRect {
        center: node.pos,
        size: Vec2 {
            x: node.intrinsic_size.x.max(1.0),
            y: node.intrinsic_size.y.max(1.0),
        },
        alpha: if node.visibility.has(Visibility::HIDDEN_BY_CLUSTER)
            || node.state != NodeState::Active
        {
            0.0
        } else {
            1.0
        },
    })
}

#[inline]
fn nearly_eq(a: f32, b: f32) -> bool {
    (a - b).abs() <= 0.5
}

#[inline]
fn same_rect(a: ClusterTileAnimRect, b: ClusterTileAnimRect) -> bool {
    nearly_eq(a.center.x, b.center.x)
        && nearly_eq(a.center.y, b.center.y)
        && nearly_eq(a.size.x, b.size.x)
        && nearly_eq(a.size.y, b.size.y)
        && nearly_eq(a.alpha, b.alpha)
}

pub(crate) fn cluster_tile_rect_for_track(
    track: &ClusterTileTrack,
    now: Instant,
) -> ClusterTileAnimRect {
    let elapsed = now.saturating_duration_since(track.started_at);
    if elapsed >= track.duration {
        return track.to;
    }
    let t = (elapsed.as_secs_f32() / track.duration.as_secs_f32()).clamp(0.0, 1.0);
    let e = ease_in_out_cubic(t);
    ClusterTileAnimRect {
        center: Vec2 {
            x: track.from.center.x + (track.to.center.x - track.from.center.x) * e,
            y: track.from.center.y + (track.to.center.y - track.from.center.y) * e,
        },
        size: Vec2 {
            x: track.from.size.x + (track.to.size.x - track.from.size.x) * e,
            y: track.from.size.y + (track.to.size.y - track.from.size.y) * e,
        },
        alpha: (track.from.alpha + (track.to.alpha - track.from.alpha) * e).clamp(0.0, 1.0),
    }
}

pub(crate) fn set_cluster_tile_target(
    tracks: &mut ClusterTileTracks,
    current_rect: Option<ClusterTileAnimRect>,
    node_id: NodeId,
    target_rect: Rect,
    now: Instant,
    duration_ms: u64,
) {
    let target = anim_rect_from_tile_rect(target_rect, 1.0);
    let current = tracks
        .get(&node_id)
        .map(|track| cluster_tile_rect_for_track(track, now))
        .or_else(|| {
            current_rect.map(|current| {
                if current.alpha <= 0.01 {
                    entry_rect_for_target(target)
                } else {
                    current
                }
            })
        })
        .unwrap_or_else(|| entry_rect_for_target(target));

    if tracks
        .get(&node_id)
        .is_some_and(|track| same_rect(track.to, target))
    {
        return;
    }
    if same_rect(current, target) {
        tracks.remove(&node_id);
        return;
    }

    tracks.insert(
        node_id,
        ClusterTileTrack {
            from: current,
            to: target,
            started_at: now,
            duration: Duration::from_millis(duration_ms.max(1)),
        },
    );
}

pub(crate) fn cluster_tile_rect_for(
    tracks: &ClusterTileTracks,
    node_id: NodeId,
    now: Instant,
) -> Option<ClusterTileAnimRect> {
    tracks
        .get(&node_id)
        .map(|track| cluster_tile_rect_for_track(track, now))
}

pub(crate) fn retain_live_cluster_tile_tracks(
    tracks: &mut ClusterTileTracks,
    field: &Field,
    now: Instant,
) {
    tracks.retain(|id, track| {
        field.node(*id).is_some()
            && now.saturating_duration_since(track.started_at) < track.duration
    });
}

pub(crate) fn cluster_tile_tracks_animating(tracks: &ClusterTileTracks, now: Instant) -> bool {
    tracks
        .values()
        .any(|track| now.saturating_duration_since(track.started_at) < track.duration)
}
