/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use dom_struct::dom_struct;
use ipc_channel::ipc::IpcSender;
use webxr_api::{
    Handedness, InputId, MockButton, MockButtonType, MockDeviceMsg, MockInputMsg, SelectEvent,
    SelectKind, TargetRayMode,
};

use crate::conversions::Convert;
use crate::dom::bindings::codegen::Bindings::FakeXRDeviceBinding::FakeXRRigidTransformInit;
use crate::dom::bindings::codegen::Bindings::FakeXRInputControllerBinding::{
    FakeXRButtonStateInit, FakeXRButtonType, FakeXRInputControllerMethods,
};
use crate::dom::bindings::codegen::Bindings::XRInputSourceBinding::{
    XRHandedness, XRTargetRayMode,
};
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object};
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::fakexrdevice::get_origin;
use crate::dom::globalscope::GlobalScope;
use crate::script_runtime::CanGc;

#[dom_struct]
pub(crate) struct FakeXRInputController {
    reflector: Reflector,
    #[ignore_malloc_size_of = "defined in ipc-channel"]
    #[no_trace]
    sender: IpcSender<MockDeviceMsg>,
    #[ignore_malloc_size_of = "defined in webxr-api"]
    #[no_trace]
    id: InputId,
}

impl FakeXRInputController {
    pub(crate) fn new_inherited(
        sender: IpcSender<MockDeviceMsg>,
        id: InputId,
    ) -> FakeXRInputController {
        FakeXRInputController {
            reflector: Reflector::new(),
            sender,
            id,
        }
    }

    pub(crate) fn new(
        global: &GlobalScope,
        sender: IpcSender<MockDeviceMsg>,
        id: InputId,
        can_gc: CanGc,
    ) -> DomRoot<FakeXRInputController> {
        reflect_dom_object(
            Box::new(FakeXRInputController::new_inherited(sender, id)),
            global,
            can_gc,
        )
    }

    fn send_message(&self, msg: MockInputMsg) {
        let _ = self
            .sender
            .send(MockDeviceMsg::MessageInputSource(self.id, msg));
    }
}

impl FakeXRInputControllerMethods<crate::DomTypeHolder> for FakeXRInputController {
    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-setpointerorigin>
    fn SetPointerOrigin(&self, origin: &FakeXRRigidTransformInit, _emulated: bool) -> Fallible<()> {
        self.send_message(MockInputMsg::SetPointerOrigin(Some(get_origin(origin)?)));
        Ok(())
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-setgriporigin>
    fn SetGripOrigin(&self, origin: &FakeXRRigidTransformInit, _emulated: bool) -> Fallible<()> {
        self.send_message(MockInputMsg::SetGripOrigin(Some(get_origin(origin)?)));
        Ok(())
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-cleargriporigin>
    fn ClearGripOrigin(&self) {
        self.send_message(MockInputMsg::SetGripOrigin(None))
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-disconnect>
    fn Disconnect(&self) {
        self.send_message(MockInputMsg::Disconnect)
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-reconnect>
    fn Reconnect(&self) {
        self.send_message(MockInputMsg::Reconnect)
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-startselection>
    fn StartSelection(&self) {
        self.send_message(MockInputMsg::TriggerSelect(
            SelectKind::Select,
            SelectEvent::Start,
        ))
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-endselection>
    fn EndSelection(&self) {
        self.send_message(MockInputMsg::TriggerSelect(
            SelectKind::Select,
            SelectEvent::End,
        ))
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-simulateselect>
    fn SimulateSelect(&self) {
        self.send_message(MockInputMsg::TriggerSelect(
            SelectKind::Select,
            SelectEvent::Select,
        ))
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-sethandedness>
    fn SetHandedness(&self, handedness: XRHandedness) {
        let h = match handedness {
            XRHandedness::None => Handedness::None,
            XRHandedness::Left => Handedness::Left,
            XRHandedness::Right => Handedness::Right,
        };
        self.send_message(MockInputMsg::SetHandedness(h));
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-settargetraymode>
    fn SetTargetRayMode(&self, target_ray_mode: XRTargetRayMode) {
        let t = match target_ray_mode {
            XRTargetRayMode::Gaze => TargetRayMode::Gaze,
            XRTargetRayMode::Tracked_pointer => TargetRayMode::TrackedPointer,
            XRTargetRayMode::Screen => TargetRayMode::Screen,
            XRTargetRayMode::Transient_pointer => TargetRayMode::TransientPointer,
        };
        self.send_message(MockInputMsg::SetTargetRayMode(t));
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-setprofiles>
    fn SetProfiles(&self, profiles: Vec<DOMString>) {
        let t = profiles.into_iter().map(String::from).collect();
        self.send_message(MockInputMsg::SetProfiles(t));
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-setsupportedbuttons>
    fn SetSupportedButtons(&self, supported_buttons: Vec<FakeXRButtonStateInit>) {
        let supported = init_to_mock_buttons(&supported_buttons);
        self.send_message(MockInputMsg::SetSupportedButtons(supported));
    }

    /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-updatebuttonstate>
    fn UpdateButtonState(&self, button_state: &FakeXRButtonStateInit) -> Fallible<()> {
        // https://immersive-web.github.io/webxr-test-api/#validate-a-button-state
        if (button_state.pressed || *button_state.pressedValue > 0.0) && !button_state.touched {
            return Err(Error::Type("Pressed button must also be touched".into()));
        }
        if *button_state.pressedValue < 0.0 {
            return Err(Error::Type("Pressed value must be non-negative".into()));
        }

        // TODO: Steps 3-5 of updateButtonState
        // Passing the one WPT test that utilizes this will require additional work
        // to specify gamepad button/axes list lengths, as well as passing that info
        // to the constructor of XRInputSource

        Ok(())
    }
}

impl Convert<MockButtonType> for FakeXRButtonType {
    fn convert(self) -> MockButtonType {
        match self {
            FakeXRButtonType::Grip => MockButtonType::Grip,
            FakeXRButtonType::Touchpad => MockButtonType::Touchpad,
            FakeXRButtonType::Thumbstick => MockButtonType::Thumbstick,
            FakeXRButtonType::Optional_button => MockButtonType::OptionalButton,
            FakeXRButtonType::Optional_thumbstick => MockButtonType::OptionalThumbstick,
        }
    }
}

/// <https://immersive-web.github.io/webxr-test-api/#parse-supported-buttons>
pub(crate) fn init_to_mock_buttons(buttons: &[FakeXRButtonStateInit]) -> Vec<MockButton> {
    let supported: Vec<MockButton> = buttons
        .iter()
        .map(|b| MockButton {
            button_type: b.buttonType.convert(),
            pressed: b.pressed,
            touched: b.touched,
            pressed_value: *b.pressedValue,
            x_value: *b.xValue,
            y_value: *b.yValue,
        })
        .collect();
    supported
}