use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;

fn run_pnpm(args: &[&str], cwd: &str) {
    let mut cmd = if cfg!(target_os = "windows") {
        let mut c = Command::new("cmd");
        c.args(["/C", "pnpm"]);
        c
    } else {
        Command::new("pnpm")
    };

    let status = cmd
        .args(args)
        .current_dir(cwd)
        .status()
        .expect("failed to run pnpm");

    if !status.success() {
        panic!("pnpm command failed: {:?}", args);
    }
}

fn find_all_files(path: PathBuf) -> Vec<PathBuf> {
    let mut files = vec![];
    for entry in fs::read_dir(path).unwrap() {
        let entry = entry.unwrap();
        let path = entry.path();
        if path.is_file() {
            files.push(path);
        } else if path.is_dir() {
            files.extend(find_all_files(path));
        }
    }
    files
}

/// Returns true if the file type is already compressed and not worth re-compressing.
fn is_compressed_asset(path: &str) -> bool {
    path.ends_with(".png")
        || path.ends_with(".jpg")
        || path.ends_with(".jpeg")
        || path.ends_with(".gif")
        || path.ends_with(".webp")
        || path.ends_with(".avif")
        || path.ends_with(".ico")
        || path.ends_with(".woff")
        || path.ends_with(".woff2")
        || path.ends_with(".ttf")
        || path.ends_with(".otf")
        || path.ends_with(".eot")
        || path.ends_with(".zip")
        || path.ends_with(".gz")
        || path.ends_with(".br")
        || path.ends_with(".xz")
        || path.ends_with(".mp4")
        || path.ends_with(".mp3")
        || path.ends_with(".webm")
        || path.ends_with(".ogg")
        || path.ends_with(".wasm")
}

/// Minimum file size (bytes) before compression is applied.
const MIN_COMPRESS_SIZE: usize = 256;

fn main() {
    println!("cargo:rerun-if-changed=../../package.json");
    println!("cargo:rerun-if-changed=../../pnpm-lock.yaml");
    println!("cargo:rerun-if-changed=../../tsconfig.json");
    println!("cargo:rerun-if-changed=../../src/");

    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let project_root = manifest_dir.parent().unwrap().parent().unwrap();

    println!("cargo:warning=Building frontend...");
    run_pnpm(&["run", "build"], project_root.to_str().unwrap());

    let dist = project_root.join("dist");
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
    let blob_dir = out_dir.join("dist_blobs");
    fs::create_dir_all(&blob_dir).unwrap();

    let mut strs = vec![];
    for file in find_all_files(dist.clone()) {
        let key = file
            .strip_prefix(&dist)
            .unwrap()
            .components()
            .collect::<PathBuf>()
            .to_string_lossy()
            .replace('\\', "/");
        let safe_name = key.replace('/', "_").replace('\\', "_");
        let blob_path = blob_dir.join(&safe_name);
        let content = fs::read(&file).unwrap();
        fs::write(&blob_path, &content).unwrap();

        let hash = md5::compute(&content);
        let etag_literal = format!("\"{:x}\"", hash);
        let key_literal = format!("\"{}\"", key.replace('"', "\\\""));

        let (gz_literal, br_literal) =
            if !is_compressed_asset(&key) && content.len() >= MIN_COMPRESS_SIZE {
                let gz_path = blob_dir.join(format!("{}.gz", safe_name));
                let br_path = blob_dir.join(format!("{}.br", safe_name));

                let gz_data = {
                    let mut encoder =
                        flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
                    let _ = encoder.write_all(&content);
                    encoder.finish().unwrap()
                };
                let br_data = {
                    let mut encoder = brotli::CompressorWriter::new(Vec::new(), 4096, 11, 22);
                    let _ = encoder.write_all(&content);
                    encoder.flush().unwrap();
                    encoder.into_inner()
                };

                if gz_data.len() < content.len() {
                    fs::write(&gz_path, &gz_data).unwrap();
                }
                if br_data.len() < content.len() {
                    fs::write(&br_path, &br_data).unwrap();
                }

                let gz = if gz_data.len() < content.len() {
                    format!(
                        "Some((include_bytes!(\"dist_blobs/{}.gz\").to_vec(), \"gzip\"))",
                        safe_name
                    )
                } else {
                    "None".to_string()
                };
                let br = if br_data.len() < content.len() {
                    format!(
                        "Some((include_bytes!(\"dist_blobs/{}.br\").to_vec(), \"br\"))",
                        safe_name
                    )
                } else {
                    "None".to_string()
                };
                (gz, br)
            } else {
                ("None".to_string(), "None".to_string())
            };

        strs.push(format!(
            r#"    (
        {},
        include_bytes!("dist_blobs/{}"),
        {},
        {},
        {},
    ),"#,
            key_literal,
            safe_name,
            etag_literal,
            gz_literal,
            br_literal
        ));
    }

    let out_file = out_dir.join("frontend.rs");
    let generated = format!(
        "lazy_static::lazy_static! {{\n    pub static ref FRONTEND: Vec<(\n        &'static str,\n        &'static [u8],\n        &'static str,\n        Option<(Vec<u8>, &'static str)>,\n        Option<(Vec<u8>, &'static str)>,\n    )> = vec![\n{}\n    ];\n}}\n",
        strs.join("\n")
    );
    fs::write(&out_file, generated).unwrap();
    println!("cargo:include={}", out_file.display());
}
