// xfwl4 -- Wayland compositor for the Xfce Desktop Environment
//
// Copyright (C) 2026 Brian Tarricone <brian@tarricone.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Portions of this file are based on "anvil", an example compositor
// based on the smithay crate, and are licensed under the MIT license
// with the following terms:
//
// Copyright (C) Victor Berger <victor.berger@m4x.org>
// Copyright (C) Drakulix (Victoria Brekenfeld)
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

use smithay::{
    backend::{
        input::{
            AbsolutePositionEvent, Event, GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _,
            InputEvent, KeyboardKeyEvent, PointerButtonEvent, PointerMotionEvent, Switch, SwitchState, TabletToolEvent,
            TabletToolProximityEvent, TabletToolTipState, TouchEvent,
        },
        libinput::LibinputInputBackend,
    },
    input::tablet::TabletDescriptor,
    reexports::{
        calloop::LoopHandle,
        input::{
            Device, DeviceCapability as LibinputDeviceCapability,
            event::{
                switch::{Switch as LibinputSwitch, SwitchState as LibinputSwitchState},
                tablet_tool::TipState as LibinputTipState,
            },
        },
    },
    utils::Size,
};

use crate::{
    backend::{
        DeviceCapabilities, KeyboardInputEvent, PointerInputEvent, SwitchInputEvent, TabletInputEvent, TabletToolAxisData,
        TabletToolButtonData, TabletToolProximityData, TabletToolTipData, TouchInputEvent, TranslatedInput, build_axis_frame,
        build_tablet_axis_frame, udev::UdevData,
    },
    core::{config::PointerConfig, state::Xfwl4State},
    protocols::xfce_input_device_list::{InputDeviceListHandler, InputDeviceListState},
};

impl UdevData {
    pub(crate) fn pointer_config_by_name(&mut self, name: &str) -> Option<&mut PointerConfig> {
        self.pointers
            .iter_mut()
            .find_map(|(device, config)| (device.name() == name).then_some(config))
    }

    fn assigned_monitor(&self, device: &Device) -> Option<String> {
        self.pointers
            .iter()
            .find(|(item, _)| item == device)
            .and_then(|(_, config)| config.assigned_monitor())
            .map(ToOwned::to_owned)
    }

    pub(crate) fn input_device_changed(&mut self, device: &Device) {
        self.input_device_list_state.input_device_changed(device);
    }

    pub(crate) fn translate_input_event(
        &mut self,
        event: InputEvent<LibinputInputBackend>,
        loop_handle: LoopHandle<'_, Xfwl4State<UdevData>>,
    ) -> Option<TranslatedInput> {
        match event {
            InputEvent::DeviceAdded { device } => {
                let mut caps = DeviceCapabilities {
                    has_keyboard: false,
                    has_pointer: false,
                    has_touch: false,
                    tablet_descriptor: None,
                };

                if device.has_capability(LibinputDeviceCapability::Keyboard) {
                    caps.has_keyboard = true;
                    self.keyboards.push(device.clone());
                }

                caps.has_pointer = device.has_capability(LibinputDeviceCapability::Pointer);
                caps.has_touch = device.has_capability(LibinputDeviceCapability::Touch);

                if device.has_capability(LibinputDeviceCapability::TabletTool) {
                    caps.tablet_descriptor = Some(TabletDescriptor::from(&device));
                }

                if caps.has_pointer || caps.has_touch || caps.tablet_descriptor.is_some() {
                    let config = PointerConfig::new(device.clone(), loop_handle);
                    self.pointers.push((device.clone(), config));
                }

                self.input_device_list_state
                    .input_device_added::<Xfwl4State<UdevData>>(device.clone());

                Some(TranslatedInput::DeviceAdded(caps))
            }

            InputEvent::DeviceRemoved { ref device } => {
                let mut caps = DeviceCapabilities {
                    has_keyboard: false,
                    has_pointer: false,
                    has_touch: false,
                    tablet_descriptor: None,
                };

                if device.has_capability(LibinputDeviceCapability::Keyboard) {
                    caps.has_keyboard = true;
                    self.keyboards.retain(|item| item != device);
                }

                caps.has_pointer = device.has_capability(LibinputDeviceCapability::Pointer);
                caps.has_touch = device.has_capability(LibinputDeviceCapability::Touch);

                if device.has_capability(LibinputDeviceCapability::TabletTool) {
                    caps.tablet_descriptor = Some(TabletDescriptor::from(device));
                }

                if let Some(pos) = self.pointers.iter().position(|(item, _)| item == device) {
                    let (_, config) = self.pointers.remove(pos);
                    let token = config.shutdown();
                    loop_handle.remove(token);
                }

                self.input_device_list_state.input_device_removed(device);

                Some(TranslatedInput::DeviceRemoved(caps))
            }

            InputEvent::Keyboard { event } => Some(TranslatedInput::Keyboard(KeyboardInputEvent::Key {
                keycode: event.key_code().into(),
                state: event.state(),
                time: event.time_msec(),
            })),

            InputEvent::PointerMotion { event } => Some(TranslatedInput::Pointer(PointerInputEvent::MotionRelative {
                delta: event.delta(),
                delta_unaccel: event.delta_unaccel(),
                utime: event.time(),
            })),

            InputEvent::PointerMotionAbsolute { event } => Some(TranslatedInput::Pointer(PointerInputEvent::MotionAbsolute {
                position: event.position_transformed(Size::from((1, 1))),
                time: event.time_msec(),
            })),

            InputEvent::PointerButton { event } => Some(TranslatedInput::Pointer(PointerInputEvent::Button {
                button: event.button_code(),
                state: event.state(),
                time: event.time_msec(),
            })),

            InputEvent::PointerAxis { event } => Some(TranslatedInput::Pointer(PointerInputEvent::Axis {
                frame: build_axis_frame::<LibinputInputBackend>(&event),
            })),

            InputEvent::TabletToolProximity { event } => {
                Some(TranslatedInput::Tablet(TabletInputEvent::ToolProximity(TabletToolProximityData {
                    descriptor: event.tool(),
                    tablet: TabletDescriptor::from(&event.device()),
                    state: event.state(),
                    position: event.position_transformed(Size::from((1, 1))),
                    axis: build_tablet_axis_frame::<LibinputInputBackend>(&event),
                    time: event.time_msec(),
                })))
            }

            InputEvent::TabletToolAxis { event } => Some(TranslatedInput::Tablet(TabletInputEvent::ToolAxis(TabletToolAxisData {
                descriptor: event.tool(),
                position: event.position_transformed(Size::from((1, 1))),
                axis: build_tablet_axis_frame::<LibinputInputBackend>(&event),
                time: event.time_msec(),
            }))),

            InputEvent::TabletToolTip { event } => Some(TranslatedInput::Tablet(TabletInputEvent::ToolTip(TabletToolTipData {
                descriptor: event.tool(),
                position: event.position_transformed(Size::from((1, 1))),
                tip_state: match event.tip_state() {
                    LibinputTipState::Up => TabletToolTipState::Up,
                    LibinputTipState::Down => TabletToolTipState::Down,
                },
                time: event.time_msec(),
            }))),

            InputEvent::TabletToolButton { event } => Some(TranslatedInput::Tablet(TabletInputEvent::ToolButton(TabletToolButtonData {
                descriptor: event.tool(),
                button: event.button(),
                state: event.button_state().into(),
                time: event.time_msec(),
            }))),

            InputEvent::GestureSwipeBegin { event } => Some(TranslatedInput::Pointer(PointerInputEvent::GestureSwipeBegin {
                time: event.time_msec(),
                fingers: event.fingers(),
            })),

            InputEvent::GestureSwipeUpdate { event } => Some(TranslatedInput::Pointer(PointerInputEvent::GestureSwipeUpdate {
                time: event.time_msec(),
                delta: event.delta(),
            })),

            InputEvent::GestureSwipeEnd { event } => Some(TranslatedInput::Pointer(PointerInputEvent::GestureSwipeEnd {
                time: event.time_msec(),
                cancelled: event.cancelled(),
            })),

            InputEvent::GesturePinchBegin { event } => Some(TranslatedInput::Pointer(PointerInputEvent::GesturePinchBegin {
                time: event.time_msec(),
                fingers: event.fingers(),
            })),

            InputEvent::GesturePinchUpdate { event } => Some(TranslatedInput::Pointer(PointerInputEvent::GesturePinchUpdate {
                time: event.time_msec(),
                delta: event.delta(),
                scale: event.scale(),
                rotation: event.rotation(),
            })),

            InputEvent::GesturePinchEnd { event } => Some(TranslatedInput::Pointer(PointerInputEvent::GesturePinchEnd {
                time: event.time_msec(),
                cancelled: event.cancelled(),
            })),

            InputEvent::GestureHoldBegin { event } => Some(TranslatedInput::Pointer(PointerInputEvent::GestureHoldBegin {
                time: event.time_msec(),
                fingers: event.fingers(),
            })),

            InputEvent::GestureHoldEnd { event } => Some(TranslatedInput::Pointer(PointerInputEvent::GestureHoldEnd {
                time: event.time_msec(),
                cancelled: event.cancelled(),
            })),

            InputEvent::TouchDown { event } => Some(TranslatedInput::Touch(TouchInputEvent::Down {
                slot: event.slot(),
                position: event.position_transformed(Size::from((1, 1))),
                assigned_monitor: self.assigned_monitor(&event.device()),
                time: event.time_msec(),
            })),

            InputEvent::TouchUp { event } => Some(TranslatedInput::Touch(TouchInputEvent::Up {
                slot: event.slot(),
                time: event.time_msec(),
            })),

            InputEvent::TouchMotion { event } => Some(TranslatedInput::Touch(TouchInputEvent::Motion {
                slot: event.slot(),
                position: event.position_transformed(Size::from((1, 1))),
                assigned_monitor: self.assigned_monitor(&event.device()),
                time: event.time_msec(),
            })),

            InputEvent::TouchFrame { .. } => Some(TranslatedInput::Touch(TouchInputEvent::Frame)),

            InputEvent::TouchCancel { .. } => Some(TranslatedInput::Touch(TouchInputEvent::Cancel)),

            InputEvent::SwitchToggle { event } => match event.switch() {
                Some(LibinputSwitch::Lid) => Some(Switch::Lid),
                Some(LibinputSwitch::TabletMode) => Some(Switch::TabletMode),
                _ => None,
            }
            .map(|switch| {
                TranslatedInput::Switch(SwitchInputEvent {
                    switch,
                    state: match event.switch_state() {
                        LibinputSwitchState::On => SwitchState::On,
                        LibinputSwitchState::Off => SwitchState::Off,
                    },
                })
            }),

            InputEvent::Special(_) => None,
        }
    }
}

impl InputDeviceListHandler for Xfwl4State<UdevData> {
    fn input_device_list_state(&mut self) -> &mut InputDeviceListState {
        &mut self.backend.input_device_list_state
    }
}
