use crate::{ffi, AsRaw, FromRaw};
#[cfg(feature = "libinput_1_26")]
use crate::{DeviceConfigError, DeviceConfigResult};

/// Available tool types for a device with the `DeviceCapability::TabletTool` capability.
///
/// The tool type defines the default usage of the tool as advertised by the
/// manufacturer. Multiple different physical tools may share the same tool type, e.g. a
/// Wacom Classic Pen, Wacom Pro Pen and a Wacom Grip Pen are all of type
/// `TabletToolType::Pen`. Use `TabletTool::tool_id` to get a specific model where
/// applicable.
///
/// Note that on some device, the eraser tool is on the tail end of a pen device. On
/// other devices, e.g. MS Surface 3, the eraser is the pen tip while a button is held
/// down.
///
/// ## Note
///
/// The `TabletToolType` can only describe the default physical type of the device. For
/// devices with adjustable physical properties the tool type remains the same, i.e.
/// putting a Wacom stroke nib into a classic pen leaves the tool type as
/// `TabletToolType::Pen`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TabletToolType {
    /// A generic pen.
    Pen,
    /// Eraser.
    Eraser,
    /// A paintbrush-like tool.
    Brush,
    /// Physical drawing tool, e.g. Wacom Inking Pen
    Pencil,
    /// An airbrush-like tool.
    Airbrush,
    /// A mouse bound to the tablet.
    Mouse,
    /// A mouse tool with a lens.
    Lens,
    /// A rotary device with positional and rotation data
    #[cfg(feature = "libinput_1_14")]
    Totem,
}

ffi_ref_struct! {
    /// An object representing a tool being used by a device with the
    /// `DeviceCapability::TabletTool` capability.
    ///
    /// Tablet events generated by such a device are bound to a specific tool rather than
    /// coming from the device directly. Depending on the hardware it is possible to track
    /// the same physical tool across multiple `Device`s, see
    /// [Tracking unique tools](https://wayland.freedesktop.org/libinput/doc/latest/tablet-support.html#tablet-serial-numbers).
    struct TabletTool, ffi::libinput_tablet_tool, ffi::libinput_tablet_tool_ref, ffi::libinput_tablet_tool_unref
}

impl TabletTool {
    ffi_func!(
    /// Return the serial number of a tool.
    ///
    /// If the tool does not report a serial number, this function returns zero.
    /// See [Tracking unique tools](https://wayland.freedesktop.org/libinput/doc/latest/tablet-support.html#tablet-serial-numbers) for details.
    pub fn serial, ffi::libinput_tablet_tool_get_serial, u64);
    ffi_func!(
    /// Return the tool ID for a tool object.
    ///
    /// If nonzero, this number identifies the specific type of the tool with more
    /// precision than the type returned in `tool_type`,
    /// see [Vendor-specific tablet tool types](https://wayland.freedesktop.org/libinput/doc/latest/tablet-support.html#tablet-tool-types).
    /// Not all tablets support a tool ID.
    ///
    /// Tablets known to support tool IDs include the Wacom Intuos 3, 4, 5, Wacom Cintiq
    /// and Wacom Intuos Pro series.
    pub fn tool_id, ffi::libinput_tablet_tool_get_tool_id, u64);

    /// Return the tool type for a tool object,
    /// see [Vendor-specific tablet tool types](https://wayland.freedesktop.org/libinput/doc/latest/tablet-support.html#tablet-tool-types)
    /// for details.
    ///
    /// A return value of `None` means the tool type is not known.
    pub fn tool_type(&self) -> Option<TabletToolType> {
        match unsafe { ffi::libinput_tablet_tool_get_type(self.as_raw_mut()) } {
            ffi::libinput_tablet_tool_type_LIBINPUT_TABLET_TOOL_TYPE_PEN => {
                Some(TabletToolType::Pen)
            }
            ffi::libinput_tablet_tool_type_LIBINPUT_TABLET_TOOL_TYPE_ERASER => {
                Some(TabletToolType::Eraser)
            }
            ffi::libinput_tablet_tool_type_LIBINPUT_TABLET_TOOL_TYPE_BRUSH => {
                Some(TabletToolType::Brush)
            }
            ffi::libinput_tablet_tool_type_LIBINPUT_TABLET_TOOL_TYPE_PENCIL => {
                Some(TabletToolType::Pencil)
            }
            ffi::libinput_tablet_tool_type_LIBINPUT_TABLET_TOOL_TYPE_AIRBRUSH => {
                Some(TabletToolType::Airbrush)
            }
            ffi::libinput_tablet_tool_type_LIBINPUT_TABLET_TOOL_TYPE_MOUSE => {
                Some(TabletToolType::Mouse)
            }
            ffi::libinput_tablet_tool_type_LIBINPUT_TABLET_TOOL_TYPE_LENS => {
                Some(TabletToolType::Lens)
            }
            #[cfg(feature = "libinput_1_14")]
            ffi::libinput_tablet_tool_type_LIBINPUT_TABLET_TOOL_TYPE_TOTEM => {
                Some(TabletToolType::Totem)
            }
            _x => {
                #[cfg(feature = "log")]
                log::warn!("Unknown `TabletToolType` returned by libinput: {}", _x);
                None
            }
        }
    }

    /// Check if a tablet tool has a button with the passed-in code (see linux/input.h).
    pub fn has_button(&self, button: u32) -> bool {
        unsafe { ffi::libinput_tablet_tool_has_button(self.as_raw_mut(), button) != 0 }
    }

    ffi_func!(
    /// Return whether the tablet tool supports distance.
    pub fn has_distance, ffi::libinput_tablet_tool_has_distance, bool);
    ffi_func!(
    /// Return whether the tablet tool supports pressure.
    pub fn has_pressure, ffi::libinput_tablet_tool_has_pressure, bool);
    ffi_func!(
    /// Return whether the tablet tool supports z-rotation.v
    pub fn has_rotation, ffi::libinput_tablet_tool_has_rotation, bool);
    ffi_func!(
    /// Return whether the tablet tool has a slider axis.
    pub fn has_slider, ffi::libinput_tablet_tool_has_slider, bool);
    ffi_func!(
    /// Return whether the tablet tool supports tilt.
    pub fn has_tilt, ffi::libinput_tablet_tool_has_tilt, bool);
    ffi_func!(
    /// Return whether the tablet tool has a relative wheel.
    pub fn has_wheel, ffi::libinput_tablet_tool_has_wheel, bool);
    ffi_func!(
    /// Returns `true` if the physical tool can be uniquely identified by libinput, or
    /// `false` otherwise.
    ///
    /// If a tool can be uniquely identified, keeping a reference to the tool allows
    /// tracking the tool across proximity out sequences and across compatible tablets.
    /// See [Tracking unique tools](https://wayland.freedesktop.org/libinput/doc/latest/tablet-support.html#tablet-serial-numbers)
    /// for more details.
    pub fn is_unique, ffi::libinput_tablet_tool_is_unique, bool);
    #[cfg(feature = "libinput_1_14")]
    ffi_func!(
    /// Returns whether the tablet tool has a ellipsis major and minor.
    ///
    /// Where the underlying hardware only supports one of either major or minor,
    /// libinput emulated the other axis as a cicular contact, i.e. major == minor
    /// for all values of major.
    pub fn tablet_tool_has_size, ffi::libinput_tablet_tool_has_size, bool);

    /// Set the pressure range for this tablet tool.
    ///
    /// This maps the given logical pressure range into the available hardware
    /// pressure range so that a hardware pressure of the given minimum value
    /// maps into a logical pressure of 0.0 (as returned by
    /// [`TabletToolEventTrait::get_pressure`](crate::event::tablet_tool::TabletToolEventTrait::pressure))
    /// and the hardware pressure of the given maximum value is mapped into
    /// the logical pressure of 1.0 (as returned by
    /// [`TabletToolEventTrait::get_pressure`](crate::event::tablet_tool::TabletToolEventTrait::pressure))
    ///
    /// The minimum value must be less than the maximum value, libinput may
    /// require the values to have a specific distance to each other,
    /// i.e. that (maximium - minimum > N) for an implementation-defined value of N.
    #[cfg(feature = "libinput_1_26")]
    pub fn config_pressure_range_set(&self, minimum: f64, maximum: f64) -> DeviceConfigResult {
        match unsafe {
            ffi::libinput_tablet_tool_config_pressure_range_set(self.as_raw_mut(), minimum, maximum)
        } {
            ffi::libinput_config_status_LIBINPUT_CONFIG_STATUS_SUCCESS => Ok(()),
            ffi::libinput_config_status_LIBINPUT_CONFIG_STATUS_UNSUPPORTED => {
                Err(DeviceConfigError::Unsupported)
            }
            ffi::libinput_config_status_LIBINPUT_CONFIG_STATUS_INVALID => {
                Err(DeviceConfigError::Invalid)
            }
            _ => panic!("libinput returned invalid 'libinput_config_status'"),
        }
    }

    #[cfg(feature = "libinput_1_26")]
    ffi_func!(
    /// Check if a tablet tool can have a custom pressure range.
    pub fn config_pressure_range_is_available, ffi::libinput_tablet_tool_config_pressure_range_is_available, bool);

    #[cfg(feature = "libinput_1_26")]
    ffi_func!(
    /// Get the minimum pressure value for this tablet tool, normalized to the
    /// range [0.0, 1.0] of the available hardware pressure.
    ///
    /// If the tool does not support pressure range configuration, the return
    /// value of this function is always 0.0.
    pub fn config_pressure_range_get_minimum, ffi::libinput_tablet_tool_config_pressure_range_get_minimum, f64);

    #[cfg(feature = "libinput_1_26")]
    ffi_func!(
    /// Get the maximum pressure value for this tablet tool, normalized to the range [0.0, 1.0] of the available hardware pressure.
    ///
    /// If the tool does not support pressure range configuration, the return value of this function is always 1.0.
    pub fn config_pressure_range_get_maximum, ffi::libinput_tablet_tool_config_pressure_range_get_maximum, f64);

    #[cfg(feature = "libinput_1_26")]
    ffi_func!(
    /// Get the minimum pressure value for this tablet tool, normalized to the range [0.0, 1.0] of the available hardware pressure.
    ///
    /// If the tool does not support pressure range configuration, the return value of this function is always 0.0.
    pub fn config_pressure_range_get_default_minimum, ffi::libinput_tablet_tool_config_pressure_range_get_default_minimum, f64);

    #[cfg(feature = "libinput_1_26")]
    ffi_func!(
    /// Get the maximum pressure value for this tablet tool, normalized to the range [0.0, 1.0] of the available hardware pressure.
    ///
    /// If the tool does not support pressure range configuration, the return value of this function is always 1.0.
    pub fn config_pressure_range_get_default_maximum, ffi::libinput_tablet_tool_config_pressure_range_get_default_maximum, f64);
}

/// Used to change tablet tool eraser mode using [`TabletTool::config_eraser_button_set_mode`]
#[cfg(feature = "libinput_1_29")]
#[doc(alias = "libinput_config_eraser_button_mode")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EraserButtonMode {
    /// The eraser button on the tool sends a button event instead.
    /// If this tool comes into proximity as an eraser,
    /// a button event on the pen is emulated instead.
    ///
    /// See [`TabletTool::config_eraser_button_set_mode`] for details.
    Button,
    /// Use the default hardware behavior of the tool.
    /// libinput does not modify the behavior of the eraser button (if any).
    Default,
}

#[cfg(feature = "libinput_1_29")]
impl EraserButtonMode {
    fn from_ffi(v: ffi::libinput_config_eraser_button_mode) -> Self {
        match v {
            ffi::libinput_config_eraser_button_mode_LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON => {
                Self::Button
            }
            ffi::libinput_config_eraser_button_mode_LIBINPUT_CONFIG_ERASER_BUTTON_DEFAULT => {
                Self::Default
            }
            _ => unreachable!(),
        }
    }

    fn as_ffi(&self) -> ffi::libinput_config_eraser_button_mode {
        match self {
            EraserButtonMode::Button => {
                ffi::libinput_config_eraser_button_mode_LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON
            }
            EraserButtonMode::Default => {
                ffi::libinput_config_eraser_button_mode_LIBINPUT_CONFIG_ERASER_BUTTON_DEFAULT
            }
        }
    }
}

#[cfg(feature = "libinput_1_29")]
impl TabletTool {
    /// Get the button configured to emulate an eraser for this tool.
    #[doc(alias = "libinput_tablet_tool_config_eraser_button_get_button")]
    pub fn config_eraser_button_get_button(&self) -> Option<u32> {
        let btn = unsafe {
            ffi::libinput_tablet_tool_config_eraser_button_get_button(self.as_raw_mut()) as u32
        };
        (btn != 0).then_some(btn)
    }

    /// Get the default button configured to emulate an eraser for this tool.
    #[doc(alias = "libinput_tablet_tool_config_eraser_button_get_default_button")]
    pub fn config_eraser_button_get_default_button(&self) -> Option<u32> {
        let btn = unsafe {
            ffi::libinput_tablet_tool_config_eraser_button_get_default_button(self.as_raw_mut())
                as u32
        };
        (btn != 0).then_some(btn)
    }

    /// Get the mode for the eraser button.
    #[doc(alias = "libinput_tablet_tool_config_eraser_button_get_mode")]
    pub fn config_eraser_button_get_mode(&self) -> EraserButtonMode {
        EraserButtonMode::from_ffi(unsafe {
            ffi::libinput_tablet_tool_config_eraser_button_get_mode(self.as_raw_mut())
        })
    }

    /// Get the default mode for the eraser button.
    #[doc(alias = "libinput_tablet_tool_config_eraser_button_get_default_mode")]
    pub fn config_eraser_button_get_default_mode(&self) -> EraserButtonMode {
        EraserButtonMode::from_ffi(unsafe {
            ffi::libinput_tablet_tool_config_eraser_button_get_default_mode(self.as_raw_mut())
        })
    }

    ffi_func!(
    /// Check if a tool can change the behavior of or to a firmware eraser button.
    ///
    /// returns: Non-zero if the device can be set to change to an eraser on button
    #[doc(alias = "libinput_tablet_tool_config_eraser_button_get_modes")]
    pub fn config_eraser_button_get_modes, ffi::libinput_tablet_tool_config_eraser_button_get_modes, u32);

    /// Set a button to be the eraser button for this tool.
    /// This configuration has no effect unless the caller also sets
    /// the eraser mode to [`ConfigEraserButtonMode::Button`] via
    /// [`TabletTool::config_eraser_button_set_mode()`].
    #[doc(alias = "libinput_tablet_tool_config_eraser_button_set_button")]
    pub fn config_eraser_button_set_button(&self, button: u32) -> DeviceConfigResult {
        DeviceConfigError::from_ffi(unsafe {
            ffi::libinput_tablet_tool_config_eraser_button_set_button(self.as_raw_mut(), button)
        })
    }

    /// Change the eraser button behavior on a tool.
    #[doc(alias = "libinput_tablet_tool_config_eraser_button_set_mode")]
    pub fn config_eraser_button_set_mode(&self, mode: EraserButtonMode) -> DeviceConfigResult {
        DeviceConfigError::from_ffi(unsafe {
            ffi::libinput_tablet_tool_config_eraser_button_set_mode(
                self.as_raw_mut(),
                mode.as_ffi(),
            )
        })
    }
}
