use std::time::Instant;

use imgui::{BackendFlags, ConfigFlags, Context, Io, MouseCursor};
use sdl3::{
    EventPump, Sdl,
    event::Event,
    keyboard::{Mod, Scancode},
    mouse::{Cursor, MouseState, SystemCursor},
    video::Window,
};

/// sdl3 backend platform state.
///
/// A backend platform handles window/input device events and manages their
/// state.
///
/// There are three things you need to do to use this library correctly:
///
/// 1. Initialize a `Platform` instance
/// 2. Pass events to the platform (every frame)
/// 3. Call frame preparation callback (every frame)
pub struct Platform {
    cursor_instance: Option<Cursor>, /* to avoid dropping cursor instances */
    last_frame: Instant,
    /// Last known content (DPI) scale factor to avoid applying scaling
    /// every frame (which would accumulate and produce invalid style values).
    hidpi_factor: f32,
}

impl Platform {
    /// Initializes a SDL platform instance and configures imgui.
    ///
    /// This function configures imgui-rs in the following ways:
    ///
    /// * backend flags are updated
    /// * keys are configured
    /// * platform name is set
    pub fn new(imgui: &mut Context) -> Self {
        let io = imgui.io_mut();

        io.backend_flags.insert(BackendFlags::HAS_MOUSE_CURSORS);
        io.backend_flags.insert(BackendFlags::HAS_SET_MOUSE_POS);

        // io.config_flags.insert(ConfigFlags::DOCKING_ENABLE);

        io.backend_flags.insert(imgui::BackendFlags::RENDERER_HAS_VTX_OFFSET);

        Platform {
            cursor_instance: None,
            last_frame: Instant::now(),
            hidpi_factor: 1.0,
        }
    }

    /// Handles a SDL event.
    ///
    /// This function performs the following actions (depends on the event):
    ///
    /// * keyboard state is updated
    /// * mouse state is updated
    pub fn handle_event(&mut self, context: &mut Context, event: &Event) -> bool {
        let io = context.io_mut();

        match *event {
            Event::MouseWheel { x, y, .. } => {
                io.add_mouse_wheel_event([x, y]);
                true
            }

            Event::MouseButtonDown { mouse_btn, .. } => {
                self.handle_mouse_button(io, &mouse_btn, true);
                true
            }

            Event::MouseButtonUp { mouse_btn, .. } => {
                self.handle_mouse_button(io, &mouse_btn, false);
                true
            }

            Event::TextInput { ref text, .. } => {
                text.chars().for_each(|c| io.add_input_character(c));
                true
            }

            Event::KeyDown {
                scancode: Some(key),
                keymod,
                ..
            } => {
                handle_key_modifier(io, &keymod);
                handle_key(io, &key, true);
                true
            }

            Event::KeyUp {
                scancode: Some(key),
                keymod,
                ..
            } => {
                handle_key_modifier(io, &keymod);
                handle_key(io, &key, false);
                true
            }

            _ => false,
        }
    }

    /// Frame preparation callback.
    ///
    /// Call this before calling the imgui-rs context `frame` function.
    /// This function performs the following actions:
    ///
    /// * display size and the framebuffer scale is set
    /// * mouse cursor is repositioned (if requested by imgui-rs)
    /// * current mouse cursor position is passed to imgui-rs
    /// * changes mouse cursor icon (if requested by imgui-rs)
    pub fn prepare_frame(&mut self, sdl: &mut Sdl, context: &mut Context, window: &Window, event_pump: &EventPump) {
        let content_scale: f32 = window.get_display().and_then(|d| d.get_content_scale()).unwrap_or(1.0);
        // Apply incremental scaling relative to the last known hi-dpi factor to
        // avoid accumulating ScaleAllSizes every frame which can shrink sizes
        // below allowed minima and trigger ImGui sanity checks.
        let scale_factor = content_scale / self.hidpi_factor;
        if (scale_factor - 1.0).abs() > f32::EPSILON {
            context.style_mut().scale_all_sizes(scale_factor);
            self.hidpi_factor = content_scale;
        }

        let mouse_cursor = context.mouse_cursor();
        let io = context.io_mut();

        // Update delta time
        let now = Instant::now();
        io.update_delta_time(now.duration_since(self.last_frame));
        self.last_frame = now;

        let mouse_state = MouseState::new(event_pump);
        let window_size = window.size();
        let window_drawable_size = window.size_in_pixels();

        // Set display size and scale here, since SDL 2 doesn't have
        // any easy way to get the scale factor, and changes in said
        // scale factor
        io.display_size = [window_size.0 as f32, window_size.1 as f32];
        io.display_framebuffer_scale = [
            (window_drawable_size.0 as f32) / (window_size.0 as f32),
            (window_drawable_size.1 as f32) / (window_size.1 as f32),
        ];
        io.font_global_scale = content_scale;

        // Set mouse position if requested by imgui-rs
        if io.want_set_mouse_pos {
            let mouse_util = sdl.mouse();
            mouse_util.warp_mouse_in_window(window, io.mouse_pos[0], io.mouse_pos[1]);
        }

        // Update mouse cursor position
        io.mouse_pos = [mouse_state.x(), mouse_state.y()];

        // Update mouse cursor icon if requested
        if !io.config_flags.contains(ConfigFlags::NO_MOUSE_CURSOR_CHANGE) {
            let mouse_util = sdl.mouse();

            match mouse_cursor {
                Some(mouse_cursor) if !io.mouse_draw_cursor => {
                    let cursor = Cursor::from_system(to_sdl_cursor(mouse_cursor)).unwrap();
                    cursor.set();

                    mouse_util.show_cursor(true);
                    self.cursor_instance = Some(cursor);
                }

                _ => {
                    mouse_util.show_cursor(false);
                    self.cursor_instance = None;
                }
            }
        }
    }
}

/// Returns `true` if the provided event is associated with the provided window.
///
/// # Example
/// ```rust,no_run
/// # let mut event_pump: sdl3::EventPump = unimplemented!();
/// # let window: sdl3::video::Window = unimplemented!();
/// # let mut imgui = imgui::Context::create();
/// # let mut platform = Platform::new(&mut imgui);
/// use imgui_sdl3::platform::{Platform, filter_event};
/// // Assuming there are multiple windows, we only want to provide the events
/// // of the window where we are rendering to imgui-rs
/// for event in event_pump.poll_iter().filter(|event| filter_event(&window, event)) {
///     platform.handle_event(&mut imgui, &event);
/// }
/// ```
pub fn filter_event(window: &Window, event: &Event) -> bool {
    Some(window.id()) == event.get_window_id()
}

impl Platform {
    fn handle_mouse_button(&mut self, io: &mut Io, button: &sdl3::mouse::MouseButton, pressed: bool) {
        match button {
            sdl3::mouse::MouseButton::Left => io.add_mouse_button_event(imgui::MouseButton::Left, pressed),
            sdl3::mouse::MouseButton::Right => io.add_mouse_button_event(imgui::MouseButton::Right, pressed),
            sdl3::mouse::MouseButton::Middle => io.add_mouse_button_event(imgui::MouseButton::Middle, pressed),
            sdl3::mouse::MouseButton::X1 => io.add_mouse_button_event(imgui::MouseButton::Extra1, pressed),
            sdl3::mouse::MouseButton::X2 => io.add_mouse_button_event(imgui::MouseButton::Extra2, pressed),
            _ => {}
        }
    }
}

/// Handle changes in the key states.
fn handle_key(io: &mut Io, key: &Scancode, pressed: bool) {
    let igkey = match key {
        Scancode::A => imgui::Key::A,
        Scancode::B => imgui::Key::B,
        Scancode::C => imgui::Key::C,
        Scancode::D => imgui::Key::D,
        Scancode::E => imgui::Key::E,
        Scancode::F => imgui::Key::F,
        Scancode::G => imgui::Key::G,
        Scancode::H => imgui::Key::H,
        Scancode::I => imgui::Key::I,
        Scancode::J => imgui::Key::J,
        Scancode::K => imgui::Key::K,
        Scancode::L => imgui::Key::L,
        Scancode::M => imgui::Key::M,
        Scancode::N => imgui::Key::N,
        Scancode::O => imgui::Key::O,
        Scancode::P => imgui::Key::P,
        Scancode::Q => imgui::Key::Q,
        Scancode::R => imgui::Key::R,
        Scancode::S => imgui::Key::S,
        Scancode::T => imgui::Key::T,
        Scancode::U => imgui::Key::U,
        Scancode::V => imgui::Key::V,
        Scancode::W => imgui::Key::W,
        Scancode::X => imgui::Key::X,
        Scancode::Y => imgui::Key::Y,
        Scancode::Z => imgui::Key::Z,
        Scancode::_1 => imgui::Key::Keypad1,
        Scancode::_2 => imgui::Key::Keypad2,
        Scancode::_3 => imgui::Key::Keypad3,
        Scancode::_4 => imgui::Key::Keypad4,
        Scancode::_5 => imgui::Key::Keypad5,
        Scancode::_6 => imgui::Key::Keypad6,
        Scancode::_7 => imgui::Key::Keypad7,
        Scancode::_8 => imgui::Key::Keypad8,
        Scancode::_9 => imgui::Key::Keypad9,
        Scancode::_0 => imgui::Key::Keypad0,
        Scancode::Return => imgui::Key::Enter, // TODO: Should this be treated as alias?
        Scancode::Escape => imgui::Key::Escape,
        Scancode::Backspace => imgui::Key::Backspace,
        Scancode::Tab => imgui::Key::Tab,
        Scancode::Space => imgui::Key::Space,
        Scancode::Minus => imgui::Key::Minus,
        Scancode::Equals => imgui::Key::Equal,
        Scancode::LeftBracket => imgui::Key::LeftBracket,
        Scancode::RightBracket => imgui::Key::RightBracket,
        Scancode::Backslash => imgui::Key::Backslash,
        Scancode::Semicolon => imgui::Key::Semicolon,
        Scancode::Apostrophe => imgui::Key::Apostrophe,
        Scancode::Grave => imgui::Key::GraveAccent,
        Scancode::Comma => imgui::Key::Comma,
        Scancode::Period => imgui::Key::Period,
        Scancode::Slash => imgui::Key::Slash,
        Scancode::CapsLock => imgui::Key::CapsLock,
        Scancode::F1 => imgui::Key::F1,
        Scancode::F2 => imgui::Key::F2,
        Scancode::F3 => imgui::Key::F3,
        Scancode::F4 => imgui::Key::F4,
        Scancode::F5 => imgui::Key::F5,
        Scancode::F6 => imgui::Key::F6,
        Scancode::F7 => imgui::Key::F7,
        Scancode::F8 => imgui::Key::F8,
        Scancode::F9 => imgui::Key::F9,
        Scancode::F10 => imgui::Key::F10,
        Scancode::F11 => imgui::Key::F11,
        Scancode::F12 => imgui::Key::F12,
        Scancode::PrintScreen => imgui::Key::PrintScreen,
        Scancode::ScrollLock => imgui::Key::ScrollLock,
        Scancode::Pause => imgui::Key::Pause,
        Scancode::Insert => imgui::Key::Insert,
        Scancode::Home => imgui::Key::Home,
        Scancode::PageUp => imgui::Key::PageUp,
        Scancode::Delete => imgui::Key::Delete,
        Scancode::End => imgui::Key::End,
        Scancode::PageDown => imgui::Key::PageDown,
        Scancode::Right => imgui::Key::RightArrow,
        Scancode::Left => imgui::Key::LeftArrow,
        Scancode::Down => imgui::Key::DownArrow,
        Scancode::Up => imgui::Key::UpArrow,
        Scancode::KpDivide => imgui::Key::KeypadDivide,
        Scancode::KpMultiply => imgui::Key::KeypadMultiply,
        Scancode::KpMinus => imgui::Key::KeypadSubtract,
        Scancode::KpPlus => imgui::Key::KeypadAdd,
        Scancode::KpEnter => imgui::Key::KeypadEnter,
        Scancode::Kp1 => imgui::Key::Keypad1,
        Scancode::Kp2 => imgui::Key::Keypad2,
        Scancode::Kp3 => imgui::Key::Keypad3,
        Scancode::Kp4 => imgui::Key::Keypad4,
        Scancode::Kp5 => imgui::Key::Keypad5,
        Scancode::Kp6 => imgui::Key::Keypad6,
        Scancode::Kp7 => imgui::Key::Keypad7,
        Scancode::Kp8 => imgui::Key::Keypad8,
        Scancode::Kp9 => imgui::Key::Keypad9,
        Scancode::Kp0 => imgui::Key::Keypad0,
        Scancode::KpPeriod => imgui::Key::KeypadDecimal,
        Scancode::Application => imgui::Key::Menu,
        Scancode::KpEquals => imgui::Key::KeypadEqual,
        Scancode::Menu => imgui::Key::Menu,
        Scancode::LCtrl => imgui::Key::LeftCtrl,
        Scancode::LShift => imgui::Key::LeftShift,
        Scancode::LAlt => imgui::Key::LeftAlt,
        Scancode::LGui => imgui::Key::LeftSuper,
        Scancode::RCtrl => imgui::Key::RightCtrl,
        Scancode::RShift => imgui::Key::RightShift,
        Scancode::RAlt => imgui::Key::RightAlt,
        Scancode::RGui => imgui::Key::RightSuper,
        _ => {
            // Ignore unknown keys
            return;
        }
    };

    io.add_key_event(igkey, pressed);
}

/// Handle changes in the key modifier states.
fn handle_key_modifier(io: &mut Io, keymod: &Mod) {
    // handle the actual modifiers:
    io.add_key_event(imgui::Key::ModShift, keymod.intersects(Mod::LSHIFTMOD | Mod::RSHIFTMOD));
    io.add_key_event(imgui::Key::ModCtrl, keymod.intersects(Mod::LCTRLMOD | Mod::RCTRLMOD));
    io.add_key_event(imgui::Key::ModAlt, keymod.intersects(Mod::LALTMOD | Mod::RALTMOD));
    io.add_key_event(imgui::Key::ModSuper, keymod.intersects(Mod::LGUIMOD | Mod::RGUIMOD));
}

/// Map an imgui::MouseCursor to an equivalent sdl3::mouse::SystemCursor.
fn to_sdl_cursor(cursor: MouseCursor) -> SystemCursor {
    match cursor {
        MouseCursor::Arrow => SystemCursor::Arrow,
        MouseCursor::TextInput => SystemCursor::IBeam,
        MouseCursor::ResizeAll => SystemCursor::SizeAll,
        MouseCursor::ResizeNS => SystemCursor::SizeNS,
        MouseCursor::ResizeEW => SystemCursor::SizeWE,
        MouseCursor::ResizeNESW => SystemCursor::SizeNESW,
        MouseCursor::ResizeNWSE => SystemCursor::SizeNWSE,
        MouseCursor::Hand => SystemCursor::Hand,
        MouseCursor::NotAllowed => SystemCursor::No,
    }
}
