pub mod gesture;

use std::collections::HashSet;
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::io::{BufRead, BufReader, Write};
use std::net::Shutdown;
use std::ops::Deref;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
#[cfg(feature = "clap")]
use std::str::FromStr;
use std::{env, process};

#[cfg(feature = "clap")]
use clap::error::{Error as ClapError, ErrorKind as ClapErrorKind};
#[cfg(feature = "clap")]
use clap::{Subcommand, ValueEnum};
use regex::{Error as RegexError, Regex};
use serde::{Deserialize, Serialize};
#[cfg(feature = "smithay")]
use smithay::input::keyboard::ModifiersState;
#[cfg(feature = "smithay")]
use smithay::utils::{Logical, Point, Size, Transform};
#[cfg(feature = "clap")]
use xkbcommon::xkb;
#[cfg(feature = "clap")]
use xkbcommon::xkb::keysyms;

#[cfg_attr(feature = "clap", derive(Subcommand))]
#[derive(Deserialize, Serialize, Debug)]
pub enum IpcMessage {
    Orientation {
        #[cfg_attr(feature = "clap", clap(long, num_args = 0..=1, conflicts_with = "unlock"))]
        lock: Option<Orientation>,
        #[cfg_attr(feature = "clap", clap(long))]
        unlock: bool,
    },
    Scale {
        scale: WindowScale,
        #[cfg_attr(feature = "clap", clap(long))]
        app_id: Option<String>,
    },
    BindGesture {
        app_id: String,
        start: GestureSector,
        end: GestureSector,
        program: String,
        #[cfg_attr(feature = "clap", clap(allow_hyphen_values = true, trailing_var_arg = true))]
        arguments: Vec<String>,
    },
    BindGestureKey {
        app_id: String,
        start: GestureSector,
        end: GestureSector,
        #[cfg_attr(feature = "clap", clap(long, short))]
        mods: Option<Modifiers>,
        key: Keysym,
    },
    UnbindGesture {
        app_id: String,
        start: GestureSector,
        end: GestureSector,
    },
    BindKey {
        app_id: String,
        #[cfg_attr(feature = "clap", clap(long, short))]
        mods: Option<Modifiers>,
        #[cfg_attr(feature = "clap", clap(long, default_value = "press"))]
        trigger: KeyTrigger,
        #[cfg_attr(feature = "clap", clap(verbatim_doc_comment))]
        keys: Keysyms,
        program: String,
        #[cfg_attr(feature = "clap", clap(allow_hyphen_values = true, trailing_var_arg = true))]
        arguments: Vec<String>,
    },
    UnbindKey {
        app_id: String,
        #[cfg_attr(feature = "clap", clap(long, short))]
        mods: Option<Modifiers>,
        #[cfg_attr(feature = "clap", clap(verbatim_doc_comment))]
        keys: Keysyms,
    },
    KeyboardConfig {
        #[cfg_attr(feature = "clap", clap(long, short))]
        model: Option<String>,
        #[cfg_attr(feature = "clap", clap(long, short))]
        layout: Option<String>,
        #[cfg_attr(feature = "clap", clap(long, short))]
        variant: Option<String>,
        #[cfg_attr(feature = "clap", clap(long, short))]
        options: Option<String>,
    },
    Dpms {
        state: Option<CliToggle>,
    },
    #[cfg_attr(feature = "clap", clap(skip))]
    DpmsReply {
        state: CliToggle,
    },
    Cursor {
        state: CliToggle,
    },
    Launcher,
    Lock,
    Unlock,
    PowerMenu,
    Quit,
    Wallpaper {
        path: String,
    },
    /// Switch to workspace by index (0-based) or relative (+1/-1).
    Workspace {
        /// Target workspace: a number (0-based) or "+1"/"-1" for relative.
        target: String,
    },

    // ── Shell events (forge → crustace via D-Bus or IPC listener) ────
    /// Show the quick settings panel.
    ShowQuickSettings,
    /// Hide the quick settings panel.
    HideQuickSettings,
    /// Show the notification center.
    ShowNotifications,
    /// Hide the notification center.
    HideNotifications,
    /// Request a screenshot (crustace or grim).
    Screenshot,
    /// Toggle overview (app switcher).
    ToggleOverview,
    /// Notify shell of system state change (battery, wifi, etc.).
    #[cfg_attr(feature = "clap", clap(skip))]
    SystemStateUpdate {
        battery_percent: u8,
        battery_charging: bool,
        wifi_connected: bool,
        wifi_ssid: Option<String>,
        cellular_connected: bool,
        bluetooth_enabled: bool,
    },
}

#[cfg_attr(feature = "clap", derive(ValueEnum))]
#[derive(Deserialize, Serialize, Default, PartialEq, Eq, Copy, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum Orientation {
    #[default]
    Portrait,
    InversePortrait,
    Landscape,
    InverseLandscape,
}

#[cfg(feature = "smithay")]
impl Orientation {
    #[must_use]
    pub fn output_transform(&self) -> Transform {
        match self {
            Self::Portrait => Transform::Normal,
            Self::InversePortrait => Transform::_180,
            Self::Landscape => Transform::_90,
            Self::InverseLandscape => Transform::_270,
        }
    }

    #[must_use]
    pub fn surface_transform(&self) -> Transform {
        match self {
            Self::Portrait => Transform::Normal,
            Self::InversePortrait => Transform::_180,
            Self::Landscape => Transform::_270,
            Self::InverseLandscape => Transform::_90,
        }
    }
}

#[cfg_attr(feature = "clap", derive(ValueEnum))]
#[derive(Deserialize, Serialize, PartialEq, Eq, Copy, Clone, Debug)]
pub enum CliToggle {
    On,
    Off,
}

#[cfg_attr(feature = "clap", derive(ValueEnum))]
#[derive(Deserialize, Serialize, PartialEq, Eq, Copy, Clone, Debug)]
pub enum GestureSector {
    #[cfg_attr(feature = "clap", clap(alias = "tl"))]
    TopLeft,
    #[cfg_attr(feature = "clap", clap(alias = "tc"))]
    TopCenter,
    #[cfg_attr(feature = "clap", clap(alias = "tr"))]
    TopRight,
    #[cfg_attr(feature = "clap", clap(alias = "ml"))]
    MiddleLeft,
    #[cfg_attr(feature = "clap", clap(alias = "mc"))]
    MiddleCenter,
    #[cfg_attr(feature = "clap", clap(alias = "mr"))]
    MiddleRight,
    #[cfg_attr(feature = "clap", clap(alias = "bl"))]
    BottomLeft,
    #[cfg_attr(feature = "clap", clap(alias = "bc"))]
    BottomCenter,
    #[cfg_attr(feature = "clap", clap(alias = "br"))]
    BottomRight,
}

impl GestureSector {
    #[cfg(feature = "smithay")]
    pub fn from_point(output_size: Size<f64, Logical>, point: Point<f64, Logical>) -> Self {
        let x_mult = (point.x / (output_size.w / 3.)).max(0.0).floor() as u32;
        let y_mult = (point.y / (output_size.h / 3.)).max(0.0).floor() as u32;

        match (x_mult.min(2), y_mult.min(2)) {
            (0, 0) => Self::TopLeft,
            (1, 0) => Self::TopCenter,
            (2, 0) => Self::TopRight,
            (0, 1) => Self::MiddleLeft,
            (1, 1) => Self::MiddleCenter,
            (2, 1) => Self::MiddleRight,
            (0, 2) => Self::BottomLeft,
            (1, 2) => Self::BottomCenter,
            (2, 2) => Self::BottomRight,
            _ => unreachable!(),
        }
    }
}

#[derive(Deserialize, Serialize, Copy, Clone, PartialEq, Debug)]
pub enum WindowScale {
    Fixed(f64),
    Additive(f64),
    Subtractive(f64),
    Multiplicative(f64),
    Divisive(f64),
}

impl WindowScale {
    pub fn scale(&self, output_scale: f64) -> f64 {
        let mut scale = match self {
            Self::Fixed(scale) => *scale,
            Self::Additive(scale) => output_scale + scale,
            Self::Subtractive(scale) => output_scale - scale,
            Self::Multiplicative(scale) => output_scale * scale,
            Self::Divisive(scale) => output_scale / scale,
        };

        scale = (scale * 120.).round() / 120.;
        scale
    }
}

#[cfg(feature = "clap")]
impl FromStr for WindowScale {
    type Err = ClapError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.is_empty() {
            return Err(ClapError::raw(ClapErrorKind::InvalidValue, "scale cannot be empty"));
        }

        let (variant, factor): (fn(_) -> _, _) = match &s[..1] {
            "+" => (Self::Additive, &s[1..]),
            "-" => (Self::Subtractive, &s[1..]),
            "*" => (Self::Multiplicative, &s[1..]),
            "/" => (Self::Divisive, &s[1..]),
            _ => (Self::Fixed, s),
        };

        let num = f64::from_str(factor)
            .map_err(|err| ClapError::raw(ClapErrorKind::InvalidValue, err))?;

        Ok(variant(num))
    }
}

impl Display for WindowScale {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let (prefix, scale) = match self {
            Self::Additive(scale) => ("+", scale),
            Self::Subtractive(scale) => ("-", scale),
            Self::Multiplicative(scale) => ("*", scale),
            Self::Divisive(scale) => ("/", scale),
            Self::Fixed(scale) => ("", scale),
        };
        write!(f, "{prefix}{scale}")
    }
}

#[derive(Debug)]
pub struct AppIdMatcher {
    variant: AppIdMatcherVariant,
    base: String,
}

impl AppIdMatcher {
    pub fn matches(&self, app_id: Option<&String>) -> bool {
        match (&self.variant, app_id) {
            (AppIdMatcherVariant::Global, _) => true,
            (AppIdMatcherVariant::Regex(regex), Some(app_id)) => regex.is_match(app_id),
            (AppIdMatcherVariant::Regex(_), None) => false,
        }
    }

    pub fn base(&self) -> &str {
        &self.base
    }
}

impl TryFrom<String> for AppIdMatcher {
    type Error = RegexError;

    fn try_from(base: String) -> Result<Self, Self::Error> {
        let variant = if base == "*" {
            AppIdMatcherVariant::Global
        } else {
            AppIdMatcherVariant::Regex(Regex::new(&base)?)
        };

        Ok(Self { base, variant })
    }
}

#[derive(Debug)]
pub enum AppIdMatcherVariant {
    Regex(Regex),
    Global,
}

#[derive(Deserialize, Serialize, PartialEq, Eq, Default, Copy, Clone, Debug)]
pub struct Modifiers {
    pub control: bool,
    pub shift: bool,
    pub logo: bool,
    pub alt: bool,
}

#[cfg(feature = "smithay")]
impl From<&ModifiersState> for Modifiers {
    fn from(mods: &ModifiersState) -> Self {
        Self { control: mods.ctrl, shift: mods.shift, logo: mods.logo, alt: mods.alt }
    }
}

#[cfg(feature = "clap")]
impl FromStr for Modifiers {
    type Err = ClapError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut modifiers = Self::default();

        let mods = s.split(',');
        for modifier in mods {
            match modifier.trim().to_lowercase().as_str() {
                "control" | "ctrl" => modifiers.control = true,
                "super" | "logo" => modifiers.logo = true,
                "shift" => modifiers.shift = true,
                "alt" => modifiers.alt = true,
                invalid => {
                    return Err(ClapError::raw(
                        ClapErrorKind::InvalidValue,
                        format!(
                            "invalid modifier {invalid:?}, expected one of \"shift\", \
                             \"control\", \"alt\", or \"super\""
                        ),
                    ));
                },
            }
        }

        Ok(modifiers)
    }
}

#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug)]
pub struct Keysyms {
    keysyms: HashSet<Keysym>,
}

#[cfg(feature = "clap")]
impl FromStr for Keysyms {
    type Err = ClapError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut keysyms = HashSet::new();
        for item in s.split('+') {
            keysyms.insert(Keysym::from_str(item)?);
        }
        Ok(Self { keysyms })
    }
}

impl Deref for Keysyms {
    type Target = HashSet<Keysym>;

    fn deref(&self) -> &Self::Target {
        &self.keysyms
    }
}

#[derive(Deserialize, Serialize, Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub enum Keysym {
    EnableVirtualKeyboard,
    DisableVirtualKeyboard,
    AutoVirtualKeyboard,
    Xkb(u32),
}

#[cfg(feature = "clap")]
impl FromStr for Keysym {
    type Err = ClapError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "enablevirtualkeyboard" => return Ok(Self::EnableVirtualKeyboard),
            "disablevirtualkeyboard" => return Ok(Self::DisableVirtualKeyboard),
            "autovirtualkeyboard" => return Ok(Self::AutoVirtualKeyboard),
            _ => (),
        }

        match xkb::keysym_from_name(s, xkb::KEYSYM_NO_FLAGS).raw() {
            keysyms::KEY_NoSymbol => {
                Err(ClapError::raw(ClapErrorKind::InvalidValue, format!("invalid keysym {s:?}")))
            },
            keysym => Ok(Self::Xkb(keysym)),
        }
    }
}

#[cfg_attr(feature = "clap", derive(ValueEnum))]
#[derive(Deserialize, Serialize, PartialEq, Eq, Copy, Clone, Debug)]
pub enum KeyTrigger {
    Press,
    Repeat,
    Release,
}

pub fn send_message(message: &IpcMessage) -> Result<Option<IpcMessage>, Box<dyn Error>> {
    validate_message(message)?;

    let socket_name = match env::var("WAYLAND_DISPLAY") {
        Ok(socket_name) => socket_name,
        Err(_) => {
            eprintln!("Error: WAYLAND_DISPLAY is not set");
            process::exit(101);
        },
    };

    let socket_path = socket_path(&socket_name);

    if !socket_path.exists() {
        eprintln!("Error: IPC socket not found, ensure Forge is running");
        process::exit(102);
    }

    let mut stream = UnixStream::connect(&socket_path)?;

    let json = serde_json::to_string(&message)?;
    stream.write_all(json.as_bytes())?;
    stream.flush()?;

    stream.shutdown(Shutdown::Write)?;

    listen_for_reply(&stream, message)
}

fn listen_for_reply(
    stream: &UnixStream,
    message: &IpcMessage,
) -> Result<Option<IpcMessage>, Box<dyn Error>> {
    // Bounded wait: a wedged compositor must not hang the CLI forever.
    let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(1)));
    let mut buffer = String::new();
    let mut reader = BufReader::new(stream);
    if let Ok(0) | Err(_) = reader.read_line(&mut buffer) {
        return Ok(None);
    }

    let reply: IpcMessage = match serde_json::from_str(&buffer) {
        Ok(reply) => reply,
        Err(_) => return Ok(None),
    };

    match (message, &reply) {
        (IpcMessage::Dpms { .. }, IpcMessage::DpmsReply { .. }) => Ok(Some(reply)),
        (IpcMessage::Dpms { .. }, unexpected_reply) => {
            eprintln!("Error: Invalid IPC reply\n  {unexpected_reply:?}");
            Ok(None)
        },
        _ => Ok(None),
    }
}

pub fn socket_path(socket_name: &str) -> PathBuf {
    dirs::runtime_dir().unwrap_or_else(env::temp_dir).join(format!("forge-{socket_name}.sock"))
}

fn validate_message(message: &IpcMessage) -> Result<(), Box<dyn Error>> {
    match message {
        IpcMessage::Scale { app_id: Some(app_id), .. }
        | IpcMessage::BindGesture { app_id, .. }
        | IpcMessage::BindKey { app_id, .. } => {
            AppIdMatcher::try_from(app_id.clone())?;
        },
        IpcMessage::Scale { scale, app_id: None } if !matches!(scale, WindowScale::Fixed(_)) => {
            return Err(format!("global scale must be fixed, got \"{scale}\"").into());
        },
        IpcMessage::KeyboardConfig { model: None, layout: None, variant: None, options: None } => {
            eprintln!("Resetting keyboard configuration to default");
        },
        _ => (),
    }

    Ok(())
}
