use std::ffi::OsStr;
use std::mem::MaybeUninit;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{ExitCode, Stdio};
use std::{env, io, ptr};

use clap::{Parser, Subcommand};
use forge_ipc::{CliToggle, IpcMessage};
#[cfg(feature = "profiling")]
use profiling::puffin;
#[cfg(feature = "profiling")]
use puffin_http::Server;
use tracing_subscriber::{EnvFilter, FmtSubscriber};

mod app_switcher;
mod autobrightness;
mod backend;
mod backlight;
#[allow(dead_code)]
mod canvas;
mod config;
mod corner_action;
mod cursor;
mod display_config_dbus;
mod drawing;
mod edge_action;
mod forge;
mod geoclue_dbus;
mod geometry;
mod geometry_store;
mod gesture_recognizer;
mod gnome_shell_dbus;
mod hostname_dbus;
mod icons;
mod idle_monitor_dbus;
mod input;
mod ipc_server;
mod kms;
mod layer;
mod logind_dbus;
mod mount_monitor;
mod mpris;
mod night_light;
mod orientation;
mod osd;
mod output;
mod overview;
mod pam_verify;
mod polkit;
mod portal;
mod portal_dbus;
mod powermenu;
mod present;
mod protocols;
mod rfkill_dbus;
mod rounded;
mod scheduling;
mod screensaver_dbus;
mod session_manager_dbus;
mod socket;
mod ssd;
mod sysmon;
mod text;
mod topbar;
mod udev;
mod windows;
#[cfg(feature = "winit")]
mod winit;
mod x11_window;

const DEFAULT_LOG_DIRECTIVES: &str = "warn,forge=info";

#[derive(Parser)]
#[clap(author, about, version, max_term_width = 80)]
struct Cli {
    /// Run in a window using the winit backend (for testing)
    #[cfg(feature = "winit")]
    #[clap(long)]
    winit: bool,

    /// Window size for winit backend (e.g. 480x900)
    #[cfg(feature = "winit")]
    #[clap(long, value_name = "WxH")]
    size: Option<String>,

    /// Run this executable after the Wayland socket is up, instead of
    /// searching for an `initrc` in `~/.config/forge/` or `/etc/forge/`.
    /// To pass arguments, point at a wrapper script.
    #[clap(long, value_name = "PATH")]
    exec: Option<PathBuf>,

    #[clap(subcommand)]
    command: Option<SubCommand>,
}

#[derive(Subcommand)]
enum SubCommand {
    #[clap(subcommand)]
    Msg(IpcMessage),
}

fn main() -> ExitCode {
    #[cfg(feature = "profiling")]
    let _profiler = init_profiler();

    init_tracing();

    let cli = Cli::parse();

    if let Some(SubCommand::Msg(msg)) = &cli.command {
        return dispatch_ipc(msg);
    }

    scheduling::boost_main_priority();

    #[cfg(feature = "winit")]
    if cli.winit {
        let force_size = cli.size.is_some();
        let size = cli.size.as_deref().map(parse_size).unwrap_or((1280, 800));
        winit::run(size, force_size, cli.exec);
        return ExitCode::SUCCESS;
    }

    udev::run(cli.exec);
    ExitCode::SUCCESS
}

#[cfg(feature = "winit")]
fn parse_size(s: &str) -> (u32, u32) {
    let parts: Vec<&str> = s.split('x').collect();
    if parts.len() == 2 {
        if let (Ok(w), Ok(h)) = (parts[0].parse(), parts[1].parse()) {
            return (w, h);
        }
    }
    eprintln!(
        "Invalid size format '{}', expected WxH (e.g. 480x900). Using default.",
        s
    );
    (1280, 800)
}

#[cfg(feature = "profiling")]
fn init_profiler() -> Option<Server> {
    puffin::set_scopes_on(true);
    Server::new(&format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT)).ok()
}

fn init_tracing() {
    let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_DIRECTIVES.into());
    FmtSubscriber::builder()
        .with_env_filter(EnvFilter::builder().parse_lossy(directives))
        .with_line_number(true)
        .init();
}

fn dispatch_ipc(msg: &IpcMessage) -> ExitCode {
    match forge_ipc::send_message(msg) {
        Ok(Some(IpcMessage::DpmsReply { state })) => {
            println!(
                "{}",
                match state {
                    CliToggle::On => "on",
                    CliToggle::Off => "off",
                }
            );
            ExitCode::SUCCESS
        }
        Ok(_) => ExitCode::SUCCESS,
        Err(err) => {
            eprintln!("\x1b[1;31merror\x1b[0m: {err}");
            ExitCode::FAILURE
        }
    }
}

pub fn daemon<I, S>(program: S, args: I) -> io::Result<()>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let mut cmd = std::process::Command::new(program);
    cmd.args(args)
        .stdin(Stdio::null())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit());

    unsafe {
        cmd.pre_exec(|| {
            match libc::fork() {
                -1 => return Err(io::Error::last_os_error()),
                0 => {}
                _ => libc::_exit(0),
            }

            if libc::setsid() == -1 {
                return Err(io::Error::last_os_error());
            }

            let mut sigset = MaybeUninit::<libc::sigset_t>::uninit();
            libc::sigemptyset(sigset.as_mut_ptr());
            libc::sigprocmask(libc::SIG_SETMASK, sigset.as_mut_ptr(), ptr::null_mut());

            Ok(())
        });
    }

    cmd.spawn()?.wait().map(drop)
}

#[macro_export]
macro_rules! trace_error {
    ($result:expr) => {
        if let Err(err) = &$result {
            tracing::error!("{err}");
        }
    };
}
