use std::{
    f32::consts::PI,
    ops::Add,
    process::{self, Child},
    sync::Arc,
    time::{Duration, Instant},
};

use glam::{Quat, Vec4};
use serde::Deserialize;

use crate::{
    backend::{
        common::OverlaySelector,
        input::PointerMode,
        overlay::RelativeTo,
        task::{ColorChannel, SystemTask, TaskType},
    },
    config::{save_layout, save_settings, AStrSetExt},
    hid::VirtualKey,
    overlays::{
        toast::{Toast, ToastTopic},
        watch::WATCH_NAME,
    },
    state::AppState,
};

#[cfg(feature = "wayvr")]
use crate::overlays::wayvr::WayVRAction;

use super::{ExecArgs, ModularControl, ModularData};

#[derive(Deserialize, Clone)]
pub enum PressRelease {
    Release,
    Press,
}

#[derive(Deserialize, Clone, Copy)]
pub enum ViewAngleKind {
    /// The cosine of the angle at which the watch becomes fully transparent
    MinOpacity,
    /// The cosine of the angle at which the watch becomes fully opaque
    MaxOpacity,
}

#[derive(Deserialize, Clone, Copy)]
pub enum Axis {
    X,
    Y,
    Z,
}

#[derive(Deserialize, Clone)]
pub enum HighlightTest {
    AllowSliding,
    AutoRealign,
    NotificationSounds,
    Notifications,
    RorateLock,
}

#[derive(Deserialize, Clone)]
pub enum SystemAction {
    ToggleAllowSliding,
    ToggleAutoRealign,
    ToggleNotificationSounds,
    ToggleNotifications,
    ToggleRotateLock,
    PlayspaceResetOffset,
    PlayspaceFixFloor,
    RecalculateExtent,
    PersistConfig,
    PersistLayout,
}

#[derive(Deserialize, Clone)]
pub enum WatchAction {
    /// Hide the watch until Show/Hide binding is used
    Hide,
    /// Switch the watch to the opposite controller
    SwitchHands,
    /// Change the fade behavior of the watch
    ViewAngle {
        kind: ViewAngleKind,
        delta: f32,
    },
    Rotation {
        axis: Axis,
        delta: f32,
    },
    Position {
        axis: Axis,
        delta: f32,
    },
}

#[derive(Deserialize, Clone)]
pub enum OverlayAction {
    /// Reset the overlay to be in front of the HMD with its original scale
    Reset,
    /// Toggle the visibility of the overlay
    ToggleVisible,
    /// Toggle the ability to grab and recenter the overlay
    ToggleImmovable,
    /// Toggle the ability of the overlay to reacto to laser pointer
    ToggleInteraction,
    /// Change the opacity of the overlay
    Opacity { delta: f32 },
}

#[derive(Deserialize, Clone)]
pub enum WindowAction {
    /// Create a new mirror window, or show/hide an existing one
    ShowMirror,
    /// Create a new UI window, or show/hide an existing one
    ShowUi,
    /// Destroy a previously created window, if it exists
    Destroy,
}

#[derive(Deserialize, Clone)]
#[serde(tag = "type")]
pub enum ButtonAction {
    Exec {
        command: ExecArgs,
        toast: Option<Arc<str>>,
    },
    VirtualKey {
        keycode: VirtualKey,
        action: PressRelease,
    },
    Watch {
        action: WatchAction,
    },
    Overlay {
        target: OverlaySelector,
        action: OverlayAction,
    },
    #[cfg(feature = "wayvr")]
    WayVR(WayVRAction),
    Window {
        target: Arc<str>,
        action: WindowAction,
    },
    Toast {
        message: Arc<str>,
        body: Option<Arc<str>>,
        seconds: Option<f32>,
    },
    ColorAdjust {
        channel: ColorChannel,
        delta: f32,
    },
    DragMultiplier {
        delta: f32,
    },
    System {
        action: SystemAction,
    },
}

pub(super) struct PressData {
    last_down: Instant,
    last_mode: PointerMode,
    child: Option<Child>,
}
impl Clone for PressData {
    fn clone(&self) -> Self {
        Self {
            last_down: self.last_down,
            last_mode: self.last_mode,
            child: None,
        }
    }
}
impl Default for PressData {
    fn default() -> Self {
        Self {
            last_down: Instant::now(),
            last_mode: PointerMode::Left,
            child: None,
        }
    }
}

#[derive(Deserialize, Default, Clone)]
pub struct ButtonData {
    #[serde(skip)]
    pub(super) press: PressData,

    pub(super) click_down: Option<Vec<ButtonAction>>,
    pub(super) click_up: Option<Vec<ButtonAction>>,
    pub(super) long_click_up: Option<Vec<ButtonAction>>,
    pub(super) right_down: Option<Vec<ButtonAction>>,
    pub(super) right_up: Option<Vec<ButtonAction>>,
    pub(super) long_right_up: Option<Vec<ButtonAction>>,
    pub(super) middle_down: Option<Vec<ButtonAction>>,
    pub(super) middle_up: Option<Vec<ButtonAction>>,
    pub(super) long_middle_up: Option<Vec<ButtonAction>>,
    pub(super) scroll_down: Option<Vec<ButtonAction>>,
    pub(super) scroll_up: Option<Vec<ButtonAction>>,
    pub(super) highlight: Option<HighlightTest>,
}

pub fn modular_button_init(button: &mut ModularControl, data: &ButtonData) {
    button.state = Some(ModularData::Button(Box::new(data.clone())));
    button.on_press = Some(modular_button_dn);
    button.on_release = Some(modular_button_up);
    button.on_scroll = Some(modular_button_scroll);
    button.test_highlight = Some(modular_button_highlight);
}

fn modular_button_dn(
    button: &mut ModularControl,
    _: &mut (),
    app: &mut AppState,
    mode: PointerMode,
) {
    // want panic
    let ModularData::Button(data) = button.state.as_mut().unwrap() else {
        panic!("modular_button_dn: button state is not Button");
    };

    data.press.last_down = Instant::now();
    data.press.last_mode = mode;

    let actions = match mode {
        PointerMode::Left => data.click_down.as_ref(),
        PointerMode::Right => data.right_down.as_ref(),
        PointerMode::Middle => data.middle_down.as_ref(),
        _ => None,
    };

    if let Some(actions) = actions {
        for action in actions {
            handle_action(action, &mut data.press, app);
        }
    }
}

fn modular_button_up(button: &mut ModularControl, _: &mut (), app: &mut AppState) {
    // want panic
    let ModularData::Button(data) = button.state.as_mut().unwrap() else {
        panic!("modular_button_up: button state is not Button");
    };

    let now = Instant::now();
    let duration = now - data.press.last_down;
    let long_press = duration.as_secs_f32() > app.session.config.long_press_duration;

    let actions = match data.press.last_mode {
        PointerMode::Left => {
            if long_press {
                data.long_click_up.as_ref()
            } else {
                data.click_up.as_ref()
            }
        }
        PointerMode::Right => {
            if long_press {
                data.long_right_up.as_ref()
            } else {
                data.right_up.as_ref()
            }
        }
        PointerMode::Middle => {
            if long_press {
                data.long_middle_up.as_ref()
            } else {
                data.middle_up.as_ref()
            }
        }
        _ => None,
    };

    if let Some(actions) = actions {
        for action in actions {
            handle_action(action, &mut data.press, app);
        }
    }
}

fn modular_button_scroll(button: &mut ModularControl, _: &mut (), app: &mut AppState, delta: f32) {
    // want panic
    let ModularData::Button(data) = button.state.as_mut().unwrap() else {
        panic!("modular_button_scroll: button state is not Button");
    };

    let actions = if delta < 0.0 {
        data.scroll_down.as_ref()
    } else {
        data.scroll_up.as_ref()
    };

    if let Some(actions) = actions {
        for action in actions {
            handle_action(action, &mut data.press, app);
        }
    }
}

fn modular_button_highlight(
    button: &ModularControl,
    _: &mut (),
    app: &mut AppState,
) -> Option<Vec4> {
    // want panic
    let ModularData::Button(data) = button.state.as_ref().unwrap() else {
        panic!("modular_button_highlight: button state is not Button");
    };

    if let Some(test) = &data.highlight {
        let lit = match test {
            HighlightTest::AllowSliding => app.session.config.allow_sliding,
            HighlightTest::AutoRealign => app.session.config.realign_on_showhide,
            HighlightTest::NotificationSounds => app.session.config.notifications_sound_enabled,
            HighlightTest::Notifications => app.session.config.notifications_enabled,
            HighlightTest::RorateLock => !app.session.config.space_rotate_unlocked,
        };

        if lit {
            return Some(Vec4::new(1.0, 1.0, 1.0, 0.5));
        }
    }
    None
}

fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppState) {
    match action {
        ButtonAction::Exec { command, toast } => run_exec(command, toast, press, app),
        ButtonAction::Watch { action } => run_watch(action, app),
        ButtonAction::Overlay { target, action } => run_overlay(target, action, app),
        ButtonAction::Window { target, action } => run_window(target, action, app),
        #[cfg(feature = "wayvr")]
        ButtonAction::WayVR(action) => {
            app.tasks.enqueue(TaskType::WayVR(action.clone()));
        }
        ButtonAction::VirtualKey { keycode, action } => app
            .hid_provider
            .send_key(*keycode, matches!(*action, PressRelease::Press)),
        ButtonAction::Toast {
            message,
            body,
            seconds,
        } => {
            Toast::new(
                ToastTopic::System,
                message.clone(),
                body.clone().unwrap_or_else(|| "".into()),
            )
            .with_timeout(seconds.unwrap_or(5.))
            .submit(app);
        }
        ButtonAction::ColorAdjust { channel, delta } => {
            let channel = *channel;
            let delta = *delta;
            app.tasks
                .enqueue(TaskType::System(SystemTask::ColorGain(channel, delta)));
        }
        ButtonAction::System { action } => run_system(action, app),
        ButtonAction::DragMultiplier { delta } => {
            app.session.config.space_drag_multiplier += delta;
        }
    }
}

const ENABLED_DISABLED: [&str; 2] = ["enabled", "disabled"];

fn run_system(action: &SystemAction, app: &mut AppState) {
    match action {
        SystemAction::ToggleAllowSliding => {
            app.session.config.allow_sliding = !app.session.config.allow_sliding;
            Toast::new(
                ToastTopic::System,
                format!(
                    "Sliding is {}.",
                    ENABLED_DISABLED[app.session.config.allow_sliding as usize]
                )
                .into(),
                "".into(),
            )
            .submit(app);
        }
        SystemAction::ToggleAutoRealign => {
            app.session.config.realign_on_showhide = !app.session.config.realign_on_showhide;
            Toast::new(
                ToastTopic::System,
                format!(
                    "Auto realign is {}.",
                    ENABLED_DISABLED[app.session.config.realign_on_showhide as usize]
                )
                .into(),
                "".into(),
            )
            .submit(app);
        }
        SystemAction::ToggleRotateLock => {
            app.session.config.space_rotate_unlocked = !app.session.config.space_rotate_unlocked;
            Toast::new(
                ToastTopic::System,
                format!(
                    "Space rotate axis lock now {}.",
                    ENABLED_DISABLED[!app.session.config.space_rotate_unlocked as usize]
                )
                .into(),
                "".into(),
            )
            .submit(app);
        }
        SystemAction::PlayspaceResetOffset => {
            app.tasks
                .enqueue(TaskType::System(SystemTask::ResetPlayspace));
        }
        SystemAction::PlayspaceFixFloor => {
            let now = Instant::now();
            let sec = Duration::from_secs(1);
            for i in 0..5 {
                let at = now.add(i * sec);
                let display = 5 - i;
                Toast::new(
                    ToastTopic::System,
                    format!("Fixing floor in {}", display).into(),
                    "Place either controller on the floor.".into(),
                )
                .with_timeout(1.0)
                .submit_at(app, at);
            }
            app.tasks
                .enqueue_at(TaskType::System(SystemTask::FixFloor), now.add(5 * sec));
        }
        SystemAction::RecalculateExtent => {
            todo!()
        }
        SystemAction::ToggleNotifications => {
            app.session.config.notifications_enabled = !app.session.config.notifications_enabled;
            Toast::new(
                ToastTopic::System,
                format!(
                    "Notifications are {}.",
                    ENABLED_DISABLED[app.session.config.notifications_enabled as usize]
                )
                .into(),
                "".into(),
            )
            .submit(app);
        }
        SystemAction::ToggleNotificationSounds => {
            app.session.config.notifications_sound_enabled =
                !app.session.config.notifications_sound_enabled;
            Toast::new(
                ToastTopic::System,
                format!(
                    "Notification sounds are {}.",
                    ENABLED_DISABLED[app.session.config.notifications_sound_enabled as usize]
                )
                .into(),
                "".into(),
            )
            .submit(app);
        }
        SystemAction::PersistConfig => {
            if let Err(e) = save_settings(&app.session.config) {
                log::error!("Failed to save config: {:?}", e);
            }
        }
        SystemAction::PersistLayout => {
            if let Err(e) = save_layout(&app.session.config) {
                log::error!("Failed to save layout: {:?}", e);
            }
        }
    }
}

fn run_exec(args: &ExecArgs, toast: &Option<Arc<str>>, press: &mut PressData, app: &mut AppState) {
    if let Some(proc) = press.child.as_mut() {
        match proc.try_wait() {
            Ok(Some(code)) => {
                if !code.success() {
                    log::error!("Child process exited with code: {}", code);
                }
                press.child = None;
            }
            Ok(None) => {
                log::warn!("Unable to launch child process: previous child not exited yet");
                return;
            }
            Err(e) => {
                press.child = None;
                log::error!("Error checking child process: {:?}", e);
            }
        }
    }
    let args = args.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
    match process::Command::new(args[0]).args(&args[1..]).spawn() {
        Ok(proc) => {
            press.child = Some(proc);
            if let Some(toast) = toast.as_ref() {
                Toast::new(ToastTopic::System, toast.clone(), "".into()).submit(app);
            }
        }
        Err(e) => {
            log::error!("Failed to spawn process {:?}: {:?}", args, e);
        }
    };
}

fn run_watch(data: &WatchAction, app: &mut AppState) {
    match data {
        WatchAction::Hide => {
            app.tasks.enqueue(TaskType::Overlay(
                OverlaySelector::Name(WATCH_NAME.into()),
                Box::new(|app, o| {
                    if o.saved_transform.is_none() {
                        o.want_visible = false;
                        o.saved_transform = Some(o.transform);
                        Toast::new(
                            ToastTopic::System,
                            "Watch hidden".into(),
                            "Use show/hide binding to restore.".into(),
                        )
                        .with_timeout(3.)
                        .submit(app);
                    } else {
                        o.want_visible = true;
                        o.saved_transform = None;
                        Toast::new(ToastTopic::System, "Watch restored".into(), "".into())
                            .submit(app);
                    }
                }),
            ));
            audio_thump(app);
        }
        WatchAction::SwitchHands => {
            app.tasks.enqueue(TaskType::Overlay(
                OverlaySelector::Name(WATCH_NAME.into()),
                Box::new(|app, o| {
                    if let RelativeTo::Hand(0) = o.relative_to {
                        o.relative_to = RelativeTo::Hand(1);
                        o.spawn_rotation = app.session.config.watch_rot
                            * Quat::from_rotation_x(PI)
                            * Quat::from_rotation_z(PI);
                        o.spawn_point = app.session.config.watch_pos;
                        o.spawn_point.x *= -1.;
                    } else {
                        o.relative_to = RelativeTo::Hand(0);
                        o.spawn_rotation = app.session.config.watch_rot;
                        o.spawn_point = app.session.config.watch_pos;
                    }
                    o.dirty = true;
                    Toast::new(
                        ToastTopic::System,
                        "Watch switched".into(),
                        "Check your other hand".into(),
                    )
                    .with_timeout(3.)
                    .submit(app);
                }),
            ));
            audio_thump(app);
        }
        WatchAction::ViewAngle { kind, delta } => match kind {
            ViewAngleKind::MinOpacity => {
                let diff = (app.session.config.watch_view_angle_max
                    - app.session.config.watch_view_angle_min)
                    + delta;

                app.session.config.watch_view_angle_min = (app.session.config.watch_view_angle_max
                    - diff)
                    .clamp(0.0, app.session.config.watch_view_angle_max - 0.05);
            }
            ViewAngleKind::MaxOpacity => {
                let diff = app.session.config.watch_view_angle_max
                    - app.session.config.watch_view_angle_min;

                app.session.config.watch_view_angle_max =
                    (app.session.config.watch_view_angle_max + delta).clamp(0.05, 1.0);

                app.session.config.watch_view_angle_min = (app.session.config.watch_view_angle_max
                    - diff)
                    .clamp(0.0, app.session.config.watch_view_angle_max - 0.05);
            }
        },
        WatchAction::Rotation { axis, delta } => {
            let rot = match axis {
                Axis::X => Quat::from_rotation_x(delta.to_radians()),
                Axis::Y => Quat::from_rotation_y(delta.to_radians()),
                Axis::Z => Quat::from_rotation_z(delta.to_radians()),
            };
            app.tasks.enqueue(TaskType::Overlay(
                OverlaySelector::Name(WATCH_NAME.into()),
                Box::new(move |app, o| {
                    o.spawn_rotation *= rot;
                    app.session.config.watch_rot = o.spawn_rotation;
                    o.dirty = true;
                }),
            ));
        }
        WatchAction::Position { axis, delta } => {
            let delta = *delta;
            let axis = match axis {
                Axis::X => 0,
                Axis::Y => 1,
                Axis::Z => 2,
            };
            app.tasks.enqueue(TaskType::Overlay(
                OverlaySelector::Name(WATCH_NAME.into()),
                Box::new(move |app, o| {
                    o.spawn_point[axis] += delta;
                    app.session.config.watch_pos = o.spawn_point;
                    o.dirty = true;
                }),
            ));
        }
    }
}

fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppState) {
    match action {
        OverlayAction::Reset => {
            app.tasks.enqueue(TaskType::Overlay(
                overlay.clone(),
                Box::new(|app, o| {
                    o.reset(app, true);
                    Toast::new(
                        ToastTopic::System,
                        format!("{} has been reset!", o.name).into(),
                        "".into(),
                    )
                    .submit(app);
                }),
            ));
        }
        OverlayAction::ToggleVisible => {
            app.tasks.enqueue(TaskType::Overlay(
                overlay.clone(),
                Box::new(|app, o| {
                    o.want_visible = !o.want_visible;
                    if o.recenter {
                        o.show_hide = o.want_visible;
                        o.reset(app, false);
                    }

                    let mut state_dirty = false;
                    if !o.want_visible {
                        state_dirty |= app.session.config.show_screens.arc_rm(o.name.as_ref());
                    } else if o.want_visible {
                        state_dirty |= app.session.config.show_screens.arc_set(o.name.clone());
                    }

                    if state_dirty {
                        match save_layout(&app.session.config) {
                            Ok(_) => log::debug!("Saved state"),
                            Err(e) => log::error!("Failed to save state: {:?}", e),
                        }
                    }
                }),
            ));
        }
        OverlayAction::ToggleImmovable => {
            app.tasks.enqueue(TaskType::Overlay(
                overlay.clone(),
                Box::new(|app, o| {
                    o.recenter = !o.recenter;
                    o.grabbable = o.recenter;
                    o.show_hide = o.recenter;
                    if !o.recenter {
                        Toast::new(
                            ToastTopic::System,
                            format!("{} is now locked in place!", o.name).into(),
                            "".into(),
                        )
                        .submit(app);
                    } else {
                        Toast::new(
                            ToastTopic::System,
                            format!("{} is now unlocked!", o.name).into(),
                            "".into(),
                        )
                        .submit(app);
                    }
                }),
            ));
            audio_thump(app);
        }
        OverlayAction::ToggleInteraction => {
            app.tasks.enqueue(TaskType::Overlay(
                overlay.clone(),
                Box::new(|app, o| {
                    o.interactable = !o.interactable;
                    if !o.interactable {
                        Toast::new(
                            ToastTopic::System,
                            format!("{} is now non-interactable!", o.name).into(),
                            "".into(),
                        )
                        .submit(app);
                    } else {
                        Toast::new(
                            ToastTopic::System,
                            format!("{} is now interactable!", o.name).into(),
                            "".into(),
                        )
                        .submit(app);
                    }
                }),
            ));
            audio_thump(app);
        }
        OverlayAction::Opacity { delta } => {
            let delta = *delta;
            app.tasks.enqueue(TaskType::Overlay(
                overlay.clone(),
                Box::new(move |_, o| {
                    o.alpha = (o.alpha + delta).clamp(0.1, 1.0);
                    o.dirty = true;
                    log::debug!("{}: alpha {}", o.name, o.alpha);
                }),
            ));
        }
    }
}

fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
    use crate::overlays::custom;

    match action {
        WindowAction::ShowMirror => {
            #[cfg(feature = "wayland")]
            app.tasks.enqueue(TaskType::CreateOverlay(
                OverlaySelector::Name(window.clone()),
                Box::new({
                    let name = window.clone();
                    move |app| {
                        Toast::new(
                            ToastTopic::System,
                            "Check your desktop for popup.".into(),
                            "".into(),
                        )
                        .with_sound(true)
                        .submit(app);
                        crate::overlays::mirror::new_mirror(name.clone(), false, &app.session)
                    }
                }),
            ));
            #[cfg(not(feature = "wayland"))]
            log::warn!("Mirror not available without Wayland feature.");
        }
        WindowAction::ShowUi => {
            app.tasks.enqueue(TaskType::CreateOverlay(
                OverlaySelector::Name(window.clone()),
                Box::new({
                    let name = window.clone();
                    move |app| custom::create_custom(app, name)
                }),
            ));
        }
        WindowAction::Destroy => {
            app.tasks
                .enqueue(TaskType::DropOverlay(OverlaySelector::Name(window.clone())));
        }
    }
}

const THUMP_AUDIO_WAV: &[u8] = include_bytes!("../../res/380885.wav");

fn audio_thump(app: &mut AppState) {
    app.audio.play(THUMP_AUDIO_WAV);
}
