use eframe::{egui, egui_glow, egui_wgpu, wgpu, HardwareAcceleration, Renderer};
use std::path::{Path, PathBuf};
use std::process::Command;

const DESKTOP_FILE_NAME: &str = "io.github.tknott95.Guth.desktop";
const LEGACY_DESKTOP_FILE_NAME: &str = "io.knott.KnottFiles.desktop";
const DESKTOP_ENTRY: &str = include_str!("../data/io.github.tknott95.Guth.desktop");
const HELP: &str = "Guth - native Rust file manager\n\nUSAGE:\n    guth [OPTIONS] [DIRECTORY]\n\nOPTIONS:\n    --install-desktop        Install the Guth desktop entry for Open With support\n    --set-default            Install the entry and set Guth as the directory handler\n    --renderer <glow|wgpu>   Select the rendering backend\n    --gpu <MODE>             required, preferred, or off\n    --gles <1|2>             Select a Glow GLES shader fallback\n    --wgpu-backend <LIST>    Comma-separated WGPU backends\n    --wgpu-power <high|low>  Select WGPU power preference\n    --vsync / --no-vsync     Enable or disable vertical sync\n    -h, --help               Print help\n    -V, --version            Print version\n";

fn main() -> eframe::Result<()> {
    let startup = StartupConfig::from_env_and_args();
    if startup.show_help {
        print!("{HELP}");
        return Ok(());
    }
    if startup.show_version {
        println!("guth {}", env!("CARGO_PKG_VERSION"));
        return Ok(());
    }
    if startup.install_desktop {
        match install_desktop_integration(startup.set_default) {
            Ok(path) => {
                println!("Installed Guth desktop integration at {}", path.display());
                return Ok(());
            }
            Err(error) => {
                eprintln!("guth: {error}");
                std::process::exit(1);
            }
        }
    }
    let runtime_info = startup.runtime_info();
    let start_path = startup.start_path.clone();
    let viewport = egui::ViewportBuilder::default()
        .with_title("Guth")
        .with_app_id("io.github.tknott95.Guth")
        .with_inner_size([1240.0, 800.0])
        .with_min_inner_size([760.0, 520.0]);

    let options = eframe::NativeOptions {
        viewport,
        renderer: startup.renderer,
        hardware_acceleration: startup.hardware_acceleration,
        shader_version: startup.shader_version,
        wgpu_options: startup.wgpu_options,
        vsync: startup.vsync,
        dithering: true,
        ..Default::default()
    };

    eframe::run_native(
        "Guth",
        options,
        Box::new(move |cc| {
            Ok(Box::new(guth::app::GuthApp::new(
                cc,
                start_path,
                runtime_info,
            )))
        }),
    )
}

struct StartupConfig {
    start_path: Option<PathBuf>,
    renderer: Renderer,
    hardware_acceleration: HardwareAcceleration,
    shader_version: Option<egui_glow::ShaderVersion>,
    wgpu_options: egui_wgpu::WgpuConfiguration,
    vsync: bool,
    install_desktop: bool,
    set_default: bool,
    show_help: bool,
    show_version: bool,
}

impl StartupConfig {
    fn from_env_and_args() -> Self {
        let mut config = Self {
            start_path: None,
            renderer: renderer_from_env(),
            hardware_acceleration: hardware_acceleration_from_env(),
            shader_version: shader_version_from_env(),
            wgpu_options: wgpu_options_from_env(),
            vsync: bool_from_env("GUTH_VSYNC").unwrap_or(true),
            install_desktop: false,
            set_default: false,
            show_help: false,
            show_version: false,
        };

        let mut args = std::env::args_os().skip(1).peekable();
        while let Some(arg) = args.next() {
            let text = arg.to_string_lossy();
            if text == "--renderer" {
                if let Some(value) = args.next() {
                    config.renderer = parse_renderer(&value.to_string_lossy());
                }
            } else if let Some(value) = text.strip_prefix("--renderer=") {
                config.renderer = parse_renderer(value);
            } else if text == "--gpu" {
                if let Some(value) = args.next() {
                    config.hardware_acceleration =
                        parse_hardware_acceleration(&value.to_string_lossy());
                }
            } else if let Some(value) = text.strip_prefix("--gpu=") {
                config.hardware_acceleration = parse_hardware_acceleration(value);
            } else if text == "--gles" {
                if let Some(value) = args.next() {
                    config.shader_version = parse_shader_version(&value.to_string_lossy());
                }
            } else if let Some(value) = text.strip_prefix("--gles=") {
                config.shader_version = parse_shader_version(value);
            } else if text == "--wgpu-backend" {
                if let Some(value) = args.next() {
                    config.wgpu_options.supported_backends =
                        parse_wgpu_backends(&value.to_string_lossy());
                }
            } else if let Some(value) = text.strip_prefix("--wgpu-backend=") {
                config.wgpu_options.supported_backends = parse_wgpu_backends(value);
            } else if text == "--wgpu-power" {
                if let Some(value) = args.next() {
                    config.wgpu_options.power_preference =
                        parse_wgpu_power(&value.to_string_lossy());
                }
            } else if let Some(value) = text.strip_prefix("--wgpu-power=") {
                config.wgpu_options.power_preference = parse_wgpu_power(value);
            } else if text == "--no-vsync" {
                config.vsync = false;
            } else if text == "--vsync" {
                config.vsync = true;
            } else if text == "--install-desktop" {
                config.install_desktop = true;
            } else if text == "--set-default" {
                config.install_desktop = true;
                config.set_default = true;
            } else if text == "--help" || text == "-h" {
                config.show_help = true;
            } else if text == "--version" || text == "-V" {
                config.show_version = true;
            } else if !text.starts_with('-') && config.start_path.is_none() {
                config.start_path = Some(PathBuf::from(arg));
            }
        }

        config
    }

    fn runtime_info(&self) -> guth::app::RuntimeInfo {
        guth::app::RuntimeInfo {
            renderer: match self.renderer {
                Renderer::Glow => "GLOW / OPENGL".to_string(),
                Renderer::Wgpu => "WGPU".to_string(),
            },
            gpu_mode: match self.hardware_acceleration {
                HardwareAcceleration::Required => "REQUIRED".to_string(),
                HardwareAcceleration::Preferred => "PREFERRED".to_string(),
                HardwareAcceleration::Off => "OFF".to_string(),
            },
            shader: match self.shader_version {
                Some(egui_glow::ShaderVersion::Es100) => "GLES 100".to_string(),
                Some(egui_glow::ShaderVersion::Es300) => "GLES 300".to_string(),
                Some(egui_glow::ShaderVersion::Gl120) => "GLSL 120".to_string(),
                Some(egui_glow::ShaderVersion::Gl140) => "GLSL 140".to_string(),
                None => "AUTO".to_string(),
            },
            wgpu_backends: format!("{:?}", self.wgpu_options.supported_backends),
            wgpu_power: format!("{:?}", self.wgpu_options.power_preference),
            frame_latency: self
                .wgpu_options
                .desired_maximum_frame_latency
                .map(|value| value.to_string())
                .unwrap_or_else(|| "AUTO".to_string()),
            vsync: self.vsync,
        }
    }
}

fn renderer_from_env() -> Renderer {
    std::env::var("GUTH_RENDERER")
        .map(|value| parse_renderer(&value))
        .unwrap_or(Renderer::Glow)
}

fn hardware_acceleration_from_env() -> HardwareAcceleration {
    std::env::var("GUTH_GPU")
        .map(|value| parse_hardware_acceleration(&value))
        .unwrap_or(HardwareAcceleration::Required)
}

fn shader_version_from_env() -> Option<egui_glow::ShaderVersion> {
    std::env::var("GUTH_GLES")
        .ok()
        .and_then(|value| parse_shader_version(&value))
}

fn wgpu_options_from_env() -> egui_wgpu::WgpuConfiguration {
    let mut options = egui_wgpu::WgpuConfiguration::default();
    if let Ok(value) = std::env::var("GUTH_WGPU_BACKEND") {
        options.supported_backends = parse_wgpu_backends(&value);
    }
    if let Ok(value) = std::env::var("GUTH_WGPU_POWER") {
        options.power_preference = parse_wgpu_power(&value);
    }
    if let Ok(value) = std::env::var("GUTH_FRAME_LATENCY") {
        options.desired_maximum_frame_latency =
            value.parse::<u32>().ok().filter(|value| *value > 0);
    }
    options
}

fn install_desktop_integration(set_default: bool) -> Result<PathBuf, String> {
    let data_home = std::env::var_os("XDG_DATA_HOME")
        .map(PathBuf::from)
        .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/share")))
        .ok_or_else(|| "Cannot determine XDG data directory".to_string())?;
    let applications = data_home.join("applications");
    std::fs::create_dir_all(&applications)
        .map_err(|error| format!("Cannot create {}: {error}", applications.display()))?;
    let executable = std::env::current_exe()
        .map_err(|error| format!("Cannot locate the Guth executable: {error}"))?;
    let desktop_path = applications.join(DESKTOP_FILE_NAME);
    let default_handler = default_directory_handler();
    let legacy_was_default = matches!(
        &default_handler,
        Ok(Some(handler)) if handler == LEGACY_DESKTOP_FILE_NAME
    );
    std::fs::write(&desktop_path, desktop_entry_for_executable(&executable)?).map_err(|error| {
        format!(
            "Cannot write desktop entry {}: {error}",
            desktop_path.display()
        )
    })?;
    let legacy_path = applications.join(LEGACY_DESKTOP_FILE_NAME);
    refresh_desktop_database(&applications)?;
    if set_default || legacy_was_default {
        set_default_directory_handler()?;
    }
    let can_remove_legacy = set_default || default_handler.is_ok();
    if legacy_path.is_file() && can_remove_legacy {
        std::fs::remove_file(&legacy_path).map_err(|error| {
            format!(
                "Cannot remove legacy desktop entry {}: {error}",
                legacy_path.display()
            )
        })?;
        refresh_desktop_database(&applications)?;
    } else if legacy_path.is_file() {
        eprintln!(
            "guth: preserving {} because the current directory handler could not be queried",
            legacy_path.display()
        );
    }
    Ok(desktop_path)
}

fn desktop_entry_for_executable(executable: &Path) -> Result<String, String> {
    let executable = executable
        .to_str()
        .ok_or_else(|| "Guth executable path is not valid UTF-8".to_string())?;
    let escaped = escape_desktop_exec_arg(executable);
    Ok(DESKTOP_ENTRY.replace("Exec=guth %f", &format!("Exec=\"{escaped}\" %f")))
}

fn escape_desktop_exec_arg(value: &str) -> String {
    let mut escaped = String::with_capacity(value.len());
    for character in value.chars() {
        match character {
            '\\' => escaped.push_str("\\\\\\\\"),
            '"' => escaped.push_str("\\\\\\\""),
            '`' => escaped.push_str("\\\\`"),
            '$' => escaped.push_str("\\\\$"),
            '%' => escaped.push_str("%%"),
            _ => escaped.push(character),
        }
    }
    escaped
}

fn default_directory_handler() -> Result<Option<String>, String> {
    let output = Command::new("xdg-mime")
        .args(["query", "default", "inode/directory"])
        .output()
        .map_err(|error| format!("Cannot query the default directory handler: {error}"))?;
    if !output.status.success() {
        return Err(format!(
            "xdg-mime query exited with status {}",
            output.status
        ));
    }
    let handler = String::from_utf8_lossy(&output.stdout).trim().to_string();
    Ok((!handler.is_empty()).then_some(handler))
}

fn set_default_directory_handler() -> Result<(), String> {
    let status = Command::new("xdg-mime")
        .args(["default", DESKTOP_FILE_NAME, "inode/directory"])
        .status()
        .map_err(|error| format!("Cannot run xdg-mime: {error}"))?;
    if status.success() {
        Ok(())
    } else {
        Err(format!("xdg-mime exited with status {status}"))
    }
}

fn refresh_desktop_database(applications: &Path) -> Result<(), String> {
    match Command::new("update-desktop-database")
        .arg(applications)
        .status()
    {
        Ok(status) if status.success() => Ok(()),
        Ok(status) => Err(format!(
            "update-desktop-database exited with status {status}"
        )),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(error) => Err(format!("Cannot refresh desktop database: {error}")),
    }
}

fn parse_renderer(value: &str) -> Renderer {
    match value.to_ascii_lowercase().as_str() {
        "wgpu" | "vulkan" | "metal" | "dx12" => Renderer::Wgpu,
        _ => Renderer::Glow,
    }
}

fn parse_hardware_acceleration(value: &str) -> HardwareAcceleration {
    match value.to_ascii_lowercase().as_str() {
        "off" | "0" | "false" | "software" => HardwareAcceleration::Off,
        "preferred" | "auto" => HardwareAcceleration::Preferred,
        _ => HardwareAcceleration::Required,
    }
}

fn parse_shader_version(value: &str) -> Option<egui_glow::ShaderVersion> {
    match value.to_ascii_lowercase().as_str() {
        "1" | "100" | "es100" | "gles1" => Some(egui_glow::ShaderVersion::Es100),
        "2" | "3" | "300" | "es300" | "gles2" | "gles3" => Some(egui_glow::ShaderVersion::Es300),
        _ => None,
    }
}

fn parse_wgpu_backends(value: &str) -> wgpu::Backends {
    let mut backends = wgpu::Backends::empty();
    for part in value.split(',').map(str::trim) {
        match part.to_ascii_lowercase().as_str() {
            "primary" => backends |= wgpu::Backends::PRIMARY,
            "secondary" => backends |= wgpu::Backends::SECONDARY,
            "vulkan" | "vk" => backends |= wgpu::Backends::VULKAN,
            "gl" | "gles" | "opengl" | "opengles" => backends |= wgpu::Backends::GL,
            "metal" => backends |= wgpu::Backends::METAL,
            "dx12" | "d3d12" => backends |= wgpu::Backends::DX12,
            _ => {}
        }
    }
    if backends.is_empty() {
        wgpu::Backends::PRIMARY | wgpu::Backends::GL
    } else {
        backends
    }
}

fn parse_wgpu_power(value: &str) -> wgpu::PowerPreference {
    match value.to_ascii_lowercase().as_str() {
        "low" | "low-power" | "integrated" => wgpu::PowerPreference::LowPower,
        _ => wgpu::PowerPreference::HighPerformance,
    }
}

fn bool_from_env(name: &str) -> Option<bool> {
    std::env::var(name).ok().map(|value| {
        !matches!(
            value.to_ascii_lowercase().as_str(),
            "0" | "false" | "off" | "no"
        )
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_renderer_modes() {
        assert_eq!(parse_renderer("glow"), Renderer::Glow);
        assert_eq!(parse_renderer("wgpu"), Renderer::Wgpu);
        assert_eq!(parse_renderer("vulkan"), Renderer::Wgpu);
    }

    #[test]
    fn parses_wgpu_backend_list() {
        let backends = parse_wgpu_backends("vulkan,gl");
        assert!(backends.contains(wgpu::Backends::VULKAN));
        assert!(backends.contains(wgpu::Backends::GL));
        assert!(!backends.contains(wgpu::Backends::METAL));
    }

    #[test]
    fn desktop_entry_uses_exact_executable_and_directory_mime() {
        let entry = desktop_entry_for_executable(Path::new("/tmp/Guth Bin/guth"))
            .expect("render desktop entry");

        assert!(entry.contains("Name=Guth"));
        assert!(entry.contains("Exec=\"/tmp/Guth Bin/guth\" %f"));
        assert!(entry.contains("MimeType=inode/directory;"));
        assert!(entry.contains("StartupWMClass=io.github.tknott95.Guth"));
    }

    #[test]
    fn desktop_exec_escaping_handles_both_parsing_layers() {
        assert_eq!(escape_desktop_exec_arg("$`\\\"%"), r#"\\$\\`\\\\\\\"%%"#);
    }
}
