use core::time::Duration;

use crate::layout::{LayoutOffset, LayoutPos, LayoutPx};

use super::InputSnapshot;
use super::key::{KeyChar, KeyCode, KeyEvent, ModifierMask, NamedKey};
use super::pointer::{FrameInstant, PointerButton, PointerButtonMask, PointerSample};

const DEFAULT_TICK: Duration = Duration::from_millis(16);
const DRAG_SAMPLES: usize = 8;

#[derive(Clone, Debug, PartialEq)]
enum Step {
    Move(LayoutPos),
    Press(PointerButton),
    Release(PointerButton),
    Key(KeyEvent),
    TextChunk(String),
    Idle,
}

#[derive(Clone, Debug, PartialEq, Default)]
pub struct Script {
    steps: Vec<Step>,
    cursor: Option<LayoutPos>,
    modifiers: ModifierMask,
}

impl Script {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn with_modifiers(mut self, modifiers: ModifierMask) -> Self {
        self.modifiers = modifiers;
        self
    }

    #[must_use]
    pub fn hover(mut self, pos: LayoutPos) -> Self {
        self.cursor = Some(pos);
        self.steps.push(Step::Move(pos));
        self
    }

    #[must_use]
    pub fn press(mut self, button: PointerButton) -> Self {
        self.steps.push(Step::Press(button));
        self
    }

    #[must_use]
    pub fn release(mut self, button: PointerButton) -> Self {
        self.steps.push(Step::Release(button));
        self
    }

    #[must_use]
    pub fn click(self, button: PointerButton) -> Self {
        self.press(button).release(button)
    }

    #[must_use]
    #[allow(
        clippy::cast_precision_loss,
        reason = "DRAG_SAMPLES and i are small constants, far below f32 precision boundary"
    )]
    pub fn drag(mut self, delta: LayoutOffset) -> Self {
        let from = self.cursor.unwrap_or(LayoutPos::ORIGIN);
        let denom = DRAG_SAMPLES as f32;
        (1..=DRAG_SAMPLES).for_each(|i| {
            let t = i as f32 / denom;
            let pos = LayoutPos::new(
                LayoutPx::saturating(from.x.value() + delta.dx.value() * t),
                LayoutPx::saturating(from.y.value() + delta.dy.value() * t),
            );
            self.steps.push(Step::Move(pos));
            self.cursor = Some(pos);
        });
        self
    }

    #[must_use]
    pub fn key(mut self, code: KeyCode) -> Self {
        let event = KeyEvent::new(code, self.modifiers);
        self.steps.push(Step::Key(event));
        self
    }

    #[must_use]
    pub fn named(self, named: NamedKey) -> Self {
        self.key(KeyCode::Named(named))
    }

    #[must_use]
    pub fn char(self, c: char) -> Self {
        self.key(KeyCode::Char(KeyChar::from_char(c)))
    }

    #[must_use]
    pub fn type_text(mut self, text: &str) -> Self {
        self.steps.push(Step::TextChunk(text.to_owned()));
        self
    }

    #[must_use]
    pub fn idle(mut self) -> Self {
        self.steps.push(Step::Idle);
        self
    }

    #[must_use]
    pub fn frames(&self) -> Vec<InputSnapshot> {
        let acc = FrameAcc {
            cursor: None,
            now: FrameInstant::ZERO,
            modifiers: self.modifiers,
        };
        let (frames, _) = self
            .steps
            .iter()
            .fold((Vec::new(), acc), |(mut frames, acc), step| {
                let (frame, next) = build_frame(acc, step);
                frames.push(frame);
                (frames, next)
            });
        frames
    }
}

#[derive(Copy, Clone, Debug)]
struct FrameAcc {
    cursor: Option<LayoutPos>,
    now: FrameInstant,
    modifiers: ModifierMask,
}

fn build_frame(acc: FrameAcc, step: &Step) -> (InputSnapshot, FrameAcc) {
    let mut input = InputSnapshot::idle(acc.now);
    input.modifiers = acc.modifiers;
    let mut next = acc;
    match step {
        Step::Move(pos) => {
            next.cursor = Some(*pos);
            input.pointer = Some(PointerSample::new(*pos));
        }
        Step::Press(button) => {
            input.pointer = acc.cursor.map(PointerSample::new);
            input.buttons_pressed = PointerButtonMask::just(*button);
        }
        Step::Release(button) => {
            input.pointer = acc.cursor.map(PointerSample::new);
            input.buttons_released = PointerButtonMask::just(*button);
        }
        Step::Key(event) => {
            input.pointer = acc.cursor.map(PointerSample::new);
            input.keys_pressed.push(*event);
        }
        Step::TextChunk(text) => {
            input.pointer = acc.cursor.map(PointerSample::new);
            text.clone_into(&mut input.text_committed);
        }
        Step::Idle => {
            input.pointer = acc.cursor.map(PointerSample::new);
        }
    }
    next.now = FrameInstant::from_duration(acc.now.duration() + DEFAULT_TICK);
    (input, next)
}

#[cfg(test)]
mod tests {
    use super::{DEFAULT_TICK, KeyChar, KeyCode, ModifierMask, NamedKey, PointerButton, Script};
    use crate::input::FrameInstant;
    use crate::layout::{LayoutOffset, LayoutPos, LayoutPx};

    #[test]
    fn empty_script_produces_no_frames() {
        let script = Script::new();
        assert!(script.frames().is_empty());
    }

    #[test]
    fn click_emits_press_then_release() {
        let frames = Script::new().click(PointerButton::Primary).frames();
        assert_eq!(frames.len(), 2);
        assert!(frames[0].buttons_pressed.contains(PointerButton::Primary));
        assert!(frames[0].buttons_released.is_empty());
        assert!(frames[1].buttons_pressed.is_empty());
        assert!(frames[1].buttons_released.contains(PointerButton::Primary));
    }

    #[test]
    fn hover_then_drag_carries_cursor_forward() {
        let frames = Script::new()
            .hover(LayoutPos::new(LayoutPx::new(10.0), LayoutPx::new(20.0)))
            .press(PointerButton::Primary)
            .drag(LayoutOffset::new(LayoutPx::new(8.0), LayoutPx::new(0.0)))
            .release(PointerButton::Primary)
            .frames();
        assert_eq!(frames.len(), 1 + 1 + super::DRAG_SAMPLES + 1);
        let drag_xs: Vec<f32> = frames[2..2 + super::DRAG_SAMPLES]
            .iter()
            .map(|f| {
                let Some(s) = f.pointer else {
                    panic!("drag sample carries pointer");
                };
                s.position.x.value()
            })
            .collect();
        assert!(
            drag_xs.windows(2).all(|w| w[1] > w[0]),
            "drag samples must be monotonically increasing in x, got {drag_xs:?}",
        );
        let Some(last) = frames.last() else {
            panic!("frames produced");
        };
        let Some(p) = last.pointer else {
            panic!("release frame must carry pointer position");
        };
        assert!((p.position.x.value() - 18.0).abs() < f32::EPSILON);
        assert!(last.buttons_released.contains(PointerButton::Primary));
    }

    #[test]
    fn key_steps_attach_modifiers() {
        let frames = Script::new()
            .with_modifiers(ModifierMask::CTRL)
            .char('a')
            .frames();
        assert_eq!(frames.len(), 1);
        assert_eq!(frames[0].keys_pressed.len(), 1);
        let event = frames[0].keys_pressed[0];
        assert_eq!(event.code, KeyCode::Char(KeyChar::from_char('a')));
        assert!(event.modifiers.contains(ModifierMask::CTRL));
    }

    #[test]
    fn named_key_step_records_named_code() {
        let frames = Script::new().named(NamedKey::Escape).frames();
        assert_eq!(frames.len(), 1);
        assert_eq!(
            frames[0].keys_pressed[0].code,
            KeyCode::Named(NamedKey::Escape)
        );
    }

    #[test]
    fn type_text_emits_text_committed_on_one_frame() {
        let frames = Script::new().type_text("hi").frames();
        assert_eq!(frames.len(), 1);
        assert_eq!(frames[0].text_committed, "hi");
    }

    #[test]
    fn ticks_advance_frame_time_by_default_interval() {
        let frames = Script::new().idle().idle().frames();
        assert_eq!(frames[0].frame, FrameInstant::ZERO);
        assert_eq!(frames[1].frame, FrameInstant::from_duration(DEFAULT_TICK));
    }
}
