//! Penpot 420×350 material color popup with live preview and reversible dismissal.

mod popup;

use egui;

use super::typography::TypeRole;

pub(super) const POPUP_SIZE: egui::Vec2 = egui::vec2(420.0, 350.0);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PickerMode {
    Wheel,
    Sliders,
    Presets,
}

#[derive(Debug, Clone)]
pub(super) struct PickerSession {
    original: [f32; 4],
    current: [f32; 4],
    mode: PickerMode,
    hex: String,
    opened_at: f64,
    owner_rect: egui::Rect,
    original_intensity: Option<f32>,
    current_intensity: Option<f32>,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ColorPickerResponse {
    pub changed: bool,
    pub applied: bool,
    pub cancelled: bool,
}

pub fn color_control(
    ui: &mut egui::Ui,
    salt: impl std::hash::Hash,
    label: &str,
    rgba: &mut [f32; 4],
) -> ColorPickerResponse {
    color_control_impl(ui, salt, label, rgba, None)
}

pub fn color_control_with_intensity(
    ui: &mut egui::Ui,
    salt: impl std::hash::Hash,
    label: &str,
    rgba: &mut [f32; 4],
    intensity: &mut f32,
) -> ColorPickerResponse {
    color_control_impl(ui, salt, label, rgba, Some(intensity))
}

fn color_control_impl(
    ui: &mut egui::Ui,
    salt: impl std::hash::Hash,
    label: &str,
    rgba: &mut [f32; 4],
    intensity: Option<&mut f32>,
) -> ColorPickerResponse {
    let palette = super::palette(ui);
    let id = ui.make_persistent_id(("penpot_color_picker", salt));
    let width = ui.available_width().max(1.0);
    let (rect, response) = ui.allocate_exact_size(egui::vec2(width, 22.0), egui::Sense::click());
    let swatch_width = 44.0_f32.min(width);
    let swatch_rect = egui::Rect::from_min_size(rect.min, egui::vec2(swatch_width, 22.0));
    let gap = if width > swatch_width + 6.0 { 6.0 } else { 0.0 };
    let hex_rect = egui::Rect::from_min_size(
        rect.min + egui::vec2(swatch_width + gap, 0.0),
        egui::vec2((width - swatch_width - gap).max(0.0), 22.0),
    );
    popup::checker(ui.painter(), swatch_rect, palette.control, palette.elevated);
    let inner_swatch = swatch_rect.shrink(1.0);
    let rgb_rect = egui::Rect::from_min_max(
        inner_swatch.min,
        egui::pos2(inner_swatch.center().x, inner_swatch.max.y),
    );
    let alpha_rect = egui::Rect::from_min_max(
        egui::pos2(inner_swatch.center().x, inner_swatch.min.y),
        inner_swatch.max,
    );
    ui.painter().rect_filled(
        rgb_rect,
        3.0,
        egui::Color32::from_rgb(byte(rgba[0]), byte(rgba[1]), byte(rgba[2])),
    );
    ui.painter().rect_filled(alpha_rect, 3.0, color32(*rgba));
    ui.painter().rect_stroke(
        swatch_rect,
        4.0,
        egui::Stroke::new(1.0_f32, palette.border_strong),
        egui::StrokeKind::Inside,
    );
    if hex_rect.width() > 0.0 {
        ui.painter().rect(
            hex_rect,
            4.0,
            palette.control,
            egui::Stroke::new(1.0_f32, palette.border),
            egui::StrokeKind::Inside,
        );
        ui.painter().text(
            hex_rect.left_center() + egui::vec2(9.0, 0.0),
            egui::Align2::LEFT_CENTER,
            hex(*rgba),
            TypeRole::Body.font(),
            palette.text_primary,
        );
    }

    if response.clicked() {
        let opened_at = ui.input(|input| input.time);
        ui.ctx().data_mut(|data| {
            data.insert_temp(
                id,
                PickerSession {
                    original: *rgba,
                    current: *rgba,
                    mode: PickerMode::Wheel,
                    hex: hex(*rgba),
                    opened_at,
                    owner_rect: ui.clip_rect(),
                    original_intensity: intensity.as_deref().copied(),
                    current_intensity: intensity.as_deref().copied(),
                },
            );
        });
    }

    let before = *rgba;
    let mut result = ColorPickerResponse::default();
    let Some(mut session) = ui.ctx().data_mut(|data| data.get_temp::<PickerSession>(id)) else {
        return result;
    };
    let screen_rect = ui.ctx().content_rect();
    let popup_rect = centered_popup_rect(session.owner_rect, screen_rect);

    let scrim = egui::Area::new(id.with("modal_scrim"))
        .order(egui::Order::Foreground)
        .fixed_pos(session.owner_rect.min)
        .show(ui.ctx(), |ui| {
            let (rect, response) =
                ui.allocate_exact_size(session.owner_rect.size(), egui::Sense::click());
            ui.painter()
                .rect_filled(rect, 0.0, egui::Color32::from_black_alpha(132));
            response
        })
        .inner;

    let popup_response = egui::Area::new(id.with("popup"))
        .order(egui::Order::Foreground)
        .fixed_pos(popup_rect.min)
        .show(ui.ctx(), |ui| {
            ui.set_min_size(POPUP_SIZE);
            ui.set_max_size(POPUP_SIZE);
            popup::show(ui, label, &mut session, &mut result)
        })
        .response;

    let escape = ui.input(|input| input.key_pressed(egui::Key::Escape));
    let outside_click = scrim.clicked()
        || ui.input(|input| {
            input.pointer.any_pressed()
                && input.time > session.opened_at
                && input
                    .pointer
                    .interact_pos()
                    .is_some_and(|position| !popup_response.rect.contains(position))
        });
    if escape || outside_click {
        result.cancelled = true;
    }

    if result.cancelled {
        *rgba = session.original;
        if let (Some(target), Some(original)) = (intensity, session.original_intensity) {
            *target = original;
        }
        ui.ctx().data_mut(|data| data.remove::<PickerSession>(id));
    } else {
        *rgba = session.current;
        if let (Some(target), Some(current)) = (intensity, session.current_intensity) {
            *target = current;
        }
        if result.applied {
            ui.ctx().data_mut(|data| data.remove::<PickerSession>(id));
        } else {
            ui.ctx().data_mut(|data| data.insert_temp(id, session));
        }
    }
    result.changed = *rgba != before;
    result
}

fn centered_popup_rect(owner: egui::Rect, screen: egui::Rect) -> egui::Rect {
    const SCREEN_MARGIN: f32 = 12.0;
    let desired = owner.center() - POPUP_SIZE * 0.5;
    let minimum = screen.min + egui::vec2(SCREEN_MARGIN, SCREEN_MARGIN);
    let maximum = screen.max - POPUP_SIZE - egui::vec2(SCREEN_MARGIN, SCREEN_MARGIN);
    let x = if maximum.x >= minimum.x {
        desired.x.clamp(minimum.x, maximum.x)
    } else {
        screen.center().x - POPUP_SIZE.x * 0.5
    };
    let y = if maximum.y >= minimum.y {
        desired.y.clamp(minimum.y, maximum.y)
    } else {
        screen.center().y - POPUP_SIZE.y * 0.5
    };
    egui::Rect::from_min_size(egui::pos2(x, y), POPUP_SIZE)
}

pub(super) fn set_current(session: &mut PickerSession, value: [f32; 4]) {
    session.current = value.map(|component| component.clamp(0.0, 1.0));
    session.hex = hex(session.current);
}

pub(super) fn color32(value: [f32; 4]) -> egui::Color32 {
    egui::Color32::from_rgba_unmultiplied(
        byte(value[0]),
        byte(value[1]),
        byte(value[2]),
        byte(value[3]),
    )
}

pub(super) fn rgba(value: egui::Color32) -> [f32; 4] {
    [
        value.r() as f32 / 255.0,
        value.g() as f32 / 255.0,
        value.b() as f32 / 255.0,
        value.a() as f32 / 255.0,
    ]
}

pub(super) fn byte(value: f32) -> u8 {
    (value.clamp(0.0, 1.0) * 255.0).round() as u8
}

pub(super) fn hex(value: [f32; 4]) -> String {
    if byte(value[3]) == 255 {
        format!(
            "#{:02X}{:02X}{:02X}",
            byte(value[0]),
            byte(value[1]),
            byte(value[2])
        )
    } else {
        format!(
            "#{:02X}{:02X}{:02X}{:02X}",
            byte(value[0]),
            byte(value[1]),
            byte(value[2]),
            byte(value[3])
        )
    }
}

pub(super) fn parse_hex(value: &str) -> Option<[f32; 4]> {
    let value = value.trim().trim_start_matches('#');
    if !matches!(value.len(), 6 | 8) {
        return None;
    }
    let rgb = u32::from_str_radix(&value[..6], 16).ok()?;
    let alpha = if value.len() == 8 {
        u8::from_str_radix(&value[6..], 16).ok()?
    } else {
        255
    };
    Some([
        ((rgb >> 16) & 0xFF) as f32 / 255.0,
        ((rgb >> 8) & 0xFF) as f32 / 255.0,
        (rgb & 0xFF) as f32 / 255.0,
        alpha as f32 / 255.0,
    ])
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn popup_centers_on_inspector_owner_and_stays_on_screen() {
        let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1440.0, 900.0));
        let inspector =
            egui::Rect::from_min_size(egui::pos2(800.0, 40.0), egui::vec2(640.0, 820.0));
        let popup = centered_popup_rect(inspector, screen);
        assert_eq!(popup.size(), POPUP_SIZE);
        assert_eq!(popup.center(), inspector.center());

        let edge = centered_popup_rect(
            egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(200.0, 200.0)),
            screen,
        );
        assert!(edge.left() >= 12.0);
        assert!(edge.top() >= 12.0);
    }

    #[test]
    fn penpot_hex_round_trip_preserves_rgba() {
        let value = [0.2, 0.4, 0.6, 0.8];
        let parsed = parse_hex(&hex(value)).expect("valid generated hex");
        for (left, right) in value.into_iter().zip(parsed) {
            assert!((left - right).abs() <= 1.0 / 255.0);
        }
    }

    #[test]
    fn cancel_restores_opening_value_and_apply_keeps_live_preview() {
        let original = [0.1, 0.2, 0.3, 1.0];
        let mut session = PickerSession {
            original,
            current: [0.8, 0.7, 0.6, 0.5],
            mode: PickerMode::Wheel,
            hex: String::new(),
            opened_at: 0.0,
            owner_rect: egui::Rect::ZERO,
            original_intensity: None,
            current_intensity: None,
        };
        let applied = session.current;
        set_current(&mut session, applied);
        assert_eq!(session.original, original);
        assert_eq!(session.current, applied);
    }
}
