use std::f32::consts::PI;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;

pub use forge_ipc::Orientation;
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{LoopHandle, RegistrationToken};
use udev::{Device, Enumerator};

use crate::forge::Forge;
use crate::geometry::{Matrix3x3, Vector, Vector3D};

const POLL_RATE: Duration = Duration::from_millis(500);
const THRESHOLD: f32 = 35.0;
const DEADZONE: f32 = 5.0;
const RADIANS_TO_DEGREES: f32 = 180.0 / PI;
const GRAVITY_SCALE: f64 = 256.0 / 9.81;

pub trait AccelerometerSource {
    fn subscribe<'a, F>(self, loop_handle: &LoopHandle<'a, Forge>, fun: F) -> RegistrationToken
    where
        F: FnMut(Orientation, &mut Forge) + 'a;
}

pub enum Accelerometer {
    Sensor(SensorAccelerometer),
    Dummy(DummyAccelerometer),
}

impl Accelerometer {
    pub fn new() -> Self {
        SensorAccelerometer::new()
            .map(Self::Sensor)
            .unwrap_or_else(|| Self::Dummy(DummyAccelerometer))
    }
}

impl AccelerometerSource for Accelerometer {
    fn subscribe<'a, F>(self, loop_handle: &LoopHandle<'a, Forge>, fun: F) -> RegistrationToken
    where
        F: FnMut(Orientation, &mut Forge) + 'a,
    {
        match self {
            Self::Sensor(accel) => accel.subscribe(loop_handle, fun),
            Self::Dummy(accel) => accel.subscribe(loop_handle, fun),
        }
    }
}

pub struct SensorAccelerometer {
    syspath: PathBuf,
    last: Option<Orientation>,
    mount_matrix: Matrix3x3<f32>,
}

impl SensorAccelerometer {
    fn new() -> Option<Self> {
        let mut enumerator = Enumerator::new().ok()?;
        enumerator.match_is_initialized().ok()?;
        enumerator.match_subsystem("iio").ok()?;

        let accel = enumerator
            .scan_devices()
            .ok()?
            .find(|d| d.attribute_value("in_accel_x_raw").is_some())?;

        let mount_matrix = udev_attr::<Matrix3x3<f32>>(&accel, "in_mount_matrix")
            .or_else(|| udev_attr::<Matrix3x3<f32>>(&accel, "in_accel_mount_matrix"))
            .or_else(|| udev_attr::<Matrix3x3<f32>>(&accel, "mount_matrix"))?;

        Some(Self { syspath: accel.syspath().to_path_buf(), last: None, mount_matrix })
    }

    fn orientation(&self) -> Option<Orientation> {
        let device = Device::from_syspath(&self.syspath).ok()?;

        let x = udev_attr::<i32>(&device, "in_accel_x_raw")? as f32;
        let y = udev_attr::<i32>(&device, "in_accel_y_raw")? as f32;
        let z = udev_attr::<i32>(&device, "in_accel_z_raw")? as f32;

        let accel = &self.mount_matrix * Vector3D::new(x, y, z);
        let scale = udev_attr::<f32>(&device, "in_accel_scale")? as f64;
        let Vector3D { x, y, z } = accel.scale(scale * GRAVITY_SCALE);

        let portrait = (x.atan2((y * y + z * z).sqrt()) * RADIANS_TO_DEGREES).round();
        let landscape = (y.atan2((x * x + z * z).sqrt()) * RADIANS_TO_DEGREES).round();

        let portrait_abs = portrait.abs();
        let landscape_abs = landscape.abs();

        if portrait_abs + DEADZONE > THRESHOLD && landscape_abs + DEADZONE > THRESHOLD {
            return None;
        }

        if portrait_abs > THRESHOLD {
            Some(if portrait > 0.0 {
                Orientation::InverseLandscape
            } else {
                Orientation::Landscape
            })
        } else if landscape_abs > THRESHOLD {
            Some(if landscape > 0.0 { Orientation::InversePortrait } else { Orientation::Portrait })
        } else {
            None
        }
    }
}

impl AccelerometerSource for SensorAccelerometer {
    fn subscribe<'a, F>(
        mut self,
        loop_handle: &LoopHandle<'a, Forge>,
        mut fun: F,
    ) -> RegistrationToken
    where
        F: FnMut(Orientation, &mut Forge) + 'a,
    {
        loop_handle
            .insert_source(Timer::immediate(), move |_, _, forge| {
                let fallback = self.last.unwrap_or(Orientation::Portrait);
                let orientation = self.orientation().unwrap_or(fallback);

                if Some(orientation) != self.last {
                    self.last = Some(orientation);
                    fun(orientation, forge);
                }

                TimeoutAction::ToDuration(POLL_RATE)
            })
            .expect("orientation timer")
    }
}

pub struct DummyAccelerometer;

impl AccelerometerSource for DummyAccelerometer {
    fn subscribe<'a, F>(self, loop_handle: &LoopHandle<'a, Forge>, mut fun: F) -> RegistrationToken
    where
        F: FnMut(Orientation, &mut Forge) + 'a,
    {
        loop_handle
            .insert_source(Timer::immediate(), move |_, _, forge| {
                fun(Orientation::Portrait, forge);
                TimeoutAction::Drop
            })
            .expect("dummy orientation timer")
    }
}

fn udev_attr<T: FromStr>(device: &Device, attr: &str) -> Option<T> {
    device.attribute_value(attr)?.to_string_lossy().parse().ok()
}
