use path_slash::PathExt;
use std::{
    fs::{self, File, Metadata},
    io::{self, Write},
    path::{Path, PathBuf},
    time::SystemTime,
};

/// Static files resource.
pub struct Resource {
    pub data: &'static [u8],
    pub modified: u64,
    pub mime_type: &'static str,
}

/// Used internally in generated functions.
#[inline]
pub fn new_resource(data: &'static [u8], modified: u64, mime_type: &'static str) -> Resource {
    Resource {
        data,
        modified,
        mime_type,
    }
}

pub(crate) const DEFAULT_VARIABLE_NAME: &str = "r";

/// Generate resources for `project_dir` using `filter`.
/// Result saved in `generated_filename` and function named as `fn_name`.
///
/// in `build.rs`:
/// ```rust
/// use std::{env, path::Path};
/// use pagetop_statics::resource::generate_resources;
///
/// fn main() {
///     let out_dir = env::var("OUT_DIR").unwrap();
///     let generated_filename = Path::new(&out_dir).join("generated.rs");
///     generate_resources("./tests", None, generated_filename, "generate", "pagetop_statics").unwrap();
/// }
/// ```
///
/// in `main.rs`:
/// ```rust
/// include!(concat!(env!("OUT_DIR"), "/generated.rs"));
///
/// fn main() {
///     let generated_file = generate();
///
///     assert_eq!(generated_file.len(), 4);
/// }
/// ```
pub fn generate_resources<P: AsRef<Path>, G: AsRef<Path>>(
    project_dir: P,
    filter: Option<fn(p: &Path) -> bool>,
    generated_filename: G,
    fn_name: &str,
    crate_name: &str,
) -> io::Result<()> {
    let resources = collect_resources(&project_dir, filter)?;

    let mut f = File::create(&generated_filename)?;

    generate_function_header(&mut f, fn_name, crate_name)?;
    generate_uses(&mut f, crate_name)?;

    generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?;
    generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, resources)?;
    generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?;

    generate_function_end(&mut f)?;

    Ok(())
}

/// Generate resource mapping for `project_dir` using `filter`.
/// Result saved in `generated_filename` as anonymous block which returns HashMap<&'static str, Resource>.
///
/// in `build.rs`:
/// ```rust
///
/// use std::{env, path::Path};
/// use pagetop_statics::resource::generate_resources_mapping;
///
/// fn main() {
///     let out_dir = env::var("OUT_DIR").unwrap();
///     let generated_filename = Path::new(&out_dir).join("generated_mapping.rs");
///     generate_resources_mapping("./tests", None, generated_filename, "pagetop_statics").unwrap();
/// }
/// ```
///
/// in `main.rs`:
/// ```rust
/// use std::collections::HashMap;
///
/// use pagetop_statics::StaticResource;
///
/// fn generate_mapping() -> HashMap<&'static str, StaticResource> {
///   include!(concat!(env!("OUT_DIR"), "/generated_mapping.rs"))
/// }
///
/// fn main() {
///     let generated_file = generate_mapping();
///
///     assert_eq!(generated_file.len(), 4);
///
/// }
/// ```
pub fn generate_resources_mapping<P: AsRef<Path>, G: AsRef<Path>>(
    project_dir: P,
    filter: Option<fn(p: &Path) -> bool>,
    generated_filename: G,
    crate_name: &str,
) -> io::Result<()> {
    let resources = collect_resources(&project_dir, filter)?;

    let mut f = File::create(&generated_filename)?;
    writeln!(f, "{{")?;

    generate_uses(&mut f, crate_name)?;

    generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?;

    generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, resources)?;

    generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?;

    writeln!(f, "}}")?;
    Ok(())
}

#[cfg(not(feature = "sort"))]
pub(crate) fn collect_resources<P: AsRef<Path>>(
    path: P,
    filter: Option<fn(p: &Path) -> bool>,
) -> io::Result<Vec<(PathBuf, Metadata)>> {
    collect_resources_nested(path, filter)
}

#[cfg(feature = "sort")]
pub(crate) fn collect_resources<P: AsRef<Path>>(
    path: P,
    filter: Option<fn(p: &Path) -> bool>,
) -> io::Result<Vec<(PathBuf, Metadata)>> {
    let mut resources = collect_resources_nested(path, filter)?;
    resources.sort_by(|a, b| a.0.cmp(&b.0));
    Ok(resources)
}

#[inline]
fn collect_resources_nested<P: AsRef<Path>>(
    path: P,
    filter: Option<fn(p: &Path) -> bool>,
) -> io::Result<Vec<(PathBuf, Metadata)>> {
    let mut result = vec![];

    for entry in fs::read_dir(&path)? {
        let entry = entry?;
        let path = entry.path();

        if let Some(ref filter) = filter {
            if !filter(path.as_ref()) {
                continue;
            }
        }

        if path.is_dir() {
            let nested = collect_resources(path, filter)?;
            result.extend(nested);
        } else {
            result.push((path, entry.metadata()?));
        }
    }

    Ok(result)
}

pub(crate) fn generate_resource_inserts<P: AsRef<Path>, W: Write>(
    f: &mut W,
    project_dir: &P,
    variable_name: &str,
    resources: Vec<(PathBuf, Metadata)>,
) -> io::Result<()> {
    for resource in &resources {
        generate_resource_insert(f, project_dir, variable_name, resource)?;
    }
    Ok(())
}

#[allow(clippy::unnecessary_debug_formatting)]
pub(crate) fn generate_resource_insert<P: AsRef<Path>, W: Write>(
    f: &mut W,
    project_dir: &P,
    variable_name: &str,
    resource: &(PathBuf, Metadata),
) -> io::Result<()> {
    let (path, metadata) = resource;
    let abs_path = path.canonicalize()?;
    let key_path = path.strip_prefix(project_dir).unwrap().to_slash().unwrap();

    let modified = if let Ok(Ok(modified)) = metadata
        .modified()
        .map(|x| x.duration_since(SystemTime::UNIX_EPOCH))
    {
        modified.as_secs()
    } else {
        0
    };
    let mime_type = mime_guess::MimeGuess::from_path(path).first_or_octet_stream();
    writeln!(
        f,
        "{}.insert({:?},n(i!({:?}),{:?},{:?}));",
        variable_name, &key_path, &abs_path, modified, &mime_type,
    )
}

pub(crate) fn generate_function_header<F: Write>(
    f: &mut F,
    fn_name: &str,
    crate_name: &str,
) -> io::Result<()> {
    writeln!(
        f,
        "#[allow(clippy::unreadable_literal)] pub fn {fn_name}() -> ::std::collections::HashMap<&'static str, ::{crate_name}::StaticResource> {{",
    )
}

pub(crate) fn generate_function_end<F: Write>(f: &mut F) -> io::Result<()> {
    writeln!(f, "}}")
}

pub(crate) fn generate_uses<F: Write>(f: &mut F, crate_name: &str) -> io::Result<()> {
    writeln!(
        f,
        "use ::{crate_name}::resource::new_resource as n;
use ::std::include_bytes as i;",
    )
}

pub(crate) fn generate_variable_header<F: Write>(f: &mut F, variable_name: &str) -> io::Result<()> {
    writeln!(
        f,
        "let mut {variable_name} = ::std::collections::HashMap::new();",
    )
}

pub(crate) fn generate_variable_return<F: Write>(f: &mut F, variable_name: &str) -> io::Result<()> {
    writeln!(f, "{variable_name}")
}
