// 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/>.

use std::{ffi::OsString, fmt, path::PathBuf};

use anyhow::anyhow;
use clap::Parser;

const DEFAULT_SESSION_COMMAND: &str = "xfce4-session";

#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
pub enum ChosenBackend {
    /// Autodetect the backend
    #[default]
    Auto,
    /// Run as a TTY udev client
    #[cfg(feature = "udev")]
    Tty,
    /// Run as an X11 or Wayland client using winit
    #[cfg(feature = "winit")]
    Winit,
    /// Run as an X11 client
    #[cfg(feature = "x11")]
    X11,
}

impl fmt::Display for ChosenBackend {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Auto => f.write_str("auto"),
            #[cfg(feature = "udev")]
            Self::Tty => f.write_str("tty"),
            #[cfg(feature = "winit")]
            Self::Winit => f.write_str("winit"),
            #[cfg(feature = "x11")]
            Self::X11 => f.write_str("x11"),
        }
    }
}

#[derive(Parser)]
#[command(version = xfwl4::build_config::BUILD_VERSION_FULL, about, long_about = None)]
pub struct Cli {
    /// Which backend to use
    #[arg(long, value_enum, default_value_t)]
    pub backend: ChosenBackend,

    /// Start a "session" on startup; that is, export environment details to the user-session
    /// systemd or dbus-daemon instances, and start the session controlling app, which can be
    /// specified as an optional argument (default=xfce4-session).  This is the default behavior
    /// for the tty backend
    #[arg(long, value_name = "COMMAND", num_args = 0..=1, conflicts_with = "no_session")]
    session: Option<Option<OsString>>,

    /// Do not set up a "session" on startup; that is, do not export environment details to the
    /// user-session systemd or dbus-daemon instances, and do not start any session-controlling
    /// app.  This is the default behavior for the winit and x11 backends
    #[arg(long, default_value_t = false)]
    no_session: bool,

    /// GPU DRM device path (backend=tty)
    #[cfg(feature = "udev")]
    #[arg(long, value_name = "PATH")]
    pub drm_device: Option<PathBuf>,

    /// Disable OpenGL ES instancing (allows drawing many objects with a single render call)
    /// (backend=tty)
    #[cfg(feature = "udev")]
    #[arg(long, default_value_t = false)]
    pub disable_gles_instancing: bool,

    /// Disable 10-bit color (backend=tty)
    #[cfg(feature = "udev")]
    #[arg(long, default_value_t = false)]
    pub disable_10bit_color: bool,

    /// Disable direct scanout (backend=tty)
    #[cfg(feature = "udev")]
    #[arg(long, default_value_t = false)]
    pub disable_direct_scanout: bool,

    /// Disable use of Vulkan (backend=x11)
    #[cfg(feature = "x11")]
    #[arg(long, default_value_t = false)]
    pub disable_vulkan: bool,
}

impl Cli {
    pub fn session_command(&self) -> anyhow::Result<Option<(OsString, Vec<OsString>)>> {
        if self.no_session && self.session.is_none() {
            Ok(None)
        } else {
            self.session
                .as_ref()
                .map(|command| {
                    command
                        .clone()
                        .map(|command| {
                            glib::shell_parse_argv(command).map_err(anyhow::Error::from).and_then(|args| {
                                let mut iter = args.into_iter();
                                if let Some(command) = iter.next() {
                                    Ok((command, iter.collect::<Vec<_>>()))
                                } else {
                                    Err(anyhow!("--session command is unparseable"))
                                }
                            })
                        })
                        .unwrap_or_else(|| Ok((OsString::from(DEFAULT_SESSION_COMMAND), Vec::new())))
                })
                .transpose()
        }
    }
}

#[cfg(feature = "x11")]
impl From<Cli> for xfwl4::backend::x11::X11Config {
    fn from(value: Cli) -> Self {
        Self {
            disable_vulkan: value.disable_vulkan,
        }
    }
}

#[cfg(feature = "udev")]
impl From<Cli> for xfwl4::backend::udev::UdevConfig {
    fn from(value: Cli) -> Self {
        Self {
            drm_device: value.drm_device,
            disable_gles_instancing: value.disable_gles_instancing,
            disable_10bit_color: value.disable_10bit_color,
            disable_direct_scanout: value.disable_direct_scanout,
        }
    }
}

pub fn parse() -> anyhow::Result<Cli> {
    let mut cli = Cli::parse();

    if let ChosenBackend::Auto = cli.backend {
        if std::env::var("WAYLAND_DISPLAY").is_ok() || std::env::var("WAYLAND_SOCKET").is_ok() {
            #[cfg(feature = "winit")]
            {
                cli.backend = ChosenBackend::Winit;
            }

            #[cfg(not(feature = "winit"))]
            return Err(anyhow::anyhow!(
                "A Wayland session is already running, but the Winit backend is not enabled"
            ));
        } else if std::env::var("DISPLAY").is_ok() {
            #[cfg(feature = "x11")]
            {
                cli.backend = ChosenBackend::X11;
            }

            #[cfg(all(feature = "winit", not(feature = "x11")))]
            {
                cli.backend = ChosenBackend::Winit;
            }

            #[cfg(not(all(feature = "winit", feature = "x11")))]
            return Err(anyhow::anyhow!(
                "An X11 session is already running, but neither the Winit nor X11 backends are enabled"
            ));
        } else {
            #[cfg(feature = "udev")]
            {
                cli.backend = ChosenBackend::Tty;
            }

            #[cfg(not(feature = "udev"))]
            return Err(anyhow::anyhow!("No suitable backend is availble"));
        }
    }

    cli.no_session = match cli.backend {
        #[cfg(feature = "udev")]
        ChosenBackend::Tty => cli.no_session,
        #[cfg(feature = "winit")]
        ChosenBackend::Winit => cli.session.is_none(),
        #[cfg(feature = "x11")]
        ChosenBackend::X11 => cli.session.is_none(),
        ChosenBackend::Auto => unreachable!(),
    };

    if !cli.no_session && cli.session.as_ref().is_none_or(|session| session.is_none()) {
        cli.session = Some(Some(OsString::from(DEFAULT_SESSION_COMMAND)))
    }

    Ok(cli)
}
