mod error;

use std::{
    borrow::Cow,
    collections::{HashMap, VecDeque},
    fs,
    path::{Path, PathBuf},
    time::Instant,
};

use crate::error::{Error, ErrorKind};

#[derive(Debug)]
enum FileKind {
    Static,
    Minifiable,
    Templated(String),
}

const PUBLIC_PATH: &str = "public/";
const DIST_PATH: &str = "dist/";
const TEMPLATES_PATH: &str = "templates/";

fn main() -> Result<(), Error> {
    let start = Instant::now();

    let dist_path = Path::new(DIST_PATH);
    if dist_path.is_dir() {
        fs::remove_dir_all(dist_path)
            .map_err(ErrorKind::RemoveDir)
            .map_err(Error::Unspecified)?;
    }
    fs::create_dir(dist_path)
        .map_err(ErrorKind::CreateDir)
        .map_err(Error::Unspecified)?;

    let templates = compile_templates().map_err(Error::Templates)?;
    println!("{}", templates["blogpost"]);
    compile_public(templates).map_err(Error::Public)?;

    println!("built in {:.1?}", start.elapsed());

    Ok(())
}

fn compile_public(templates: HashMap<String, String>) -> Result<(), ErrorKind> {
    let dist_path = Path::new(DIST_PATH);

    let mut stack = vec![PathBuf::from(PUBLIC_PATH)];
    while let Some(dir) = stack.pop() {
        for entry in fs::read_dir(dir).map_err(ErrorKind::ReadDir)? {
            let entry = entry.map_err(ErrorKind::ReadDir)?;
            let src = entry.path();
            let dest = dist_path.join(
                src.strip_prefix(PUBLIC_PATH)
                    .map_err(ErrorKind::StripPrefix)?,
            );

            if src.is_dir() {
                stack.push(src);
                fs::create_dir(dest).map_err(ErrorKind::CreateDir)?;
            } else {
                compile_file(src, dest, &templates)?;
            }
        }
    }

    Ok(())
}

fn compile_templates() -> Result<HashMap<String, String>, ErrorKind> {
    let mut templates = HashMap::<String, String>::new();

    for entry in fs::read_dir(TEMPLATES_PATH).map_err(ErrorKind::ReadDir)? {
        let entry = entry.map_err(ErrorKind::ReadDir)?;
        let src = entry.path();

        let stem = src
            .file_stem()
            .and_then(|s| s.to_str())
            .ok_or(ErrorKind::PathManip)?
            .to_string();

        let content = minify(fs::read_to_string(src).map_err(ErrorKind::ReadFile)?);

        templates.insert(stem, content);
    }

    // not optimized but in the worst case scenario, has a
    // time complexity of O(n^2) where n is the number of
    // templates, which should be quite low.
    let mut stack = templates
        .keys()
        .filter(|n| n.contains('.'))
        .cloned()
        .collect::<VecDeque<_>>();
    while let Some(current) = stack.pop_front() {
        if let Some((name, template)) = current.rsplit_once('.') {
            if let Some(template_content) = templates.get(template) {
                let content = compile_templated(&templates[&current], template_content);

                templates.remove(&current);
                templates.insert(name.to_string(), content);
            } else {
                stack.push_back(current);
            }
        }
    }

    Ok(templates)
}

fn compile_file(
    src: PathBuf,
    mut dest: PathBuf,
    templates: &HashMap<String, String>,
) -> Result<(), ErrorKind> {
    let mut kind = FileKind::Static;

    let ext = src.extension().and_then(|e| e.to_str());

    if matches!(ext, Some("html") | Some("css")) {
        kind = FileKind::Minifiable;
    }

    // check if file is templated
    if let Some("html") = ext {
        let stem = src
            .file_stem()
            .and_then(|s| s.to_str())
            .ok_or(ErrorKind::PathManip)?;

        if let Some((name, template_name)) = stem.rsplit_once('.') {
            dest = dest.with_file_name([name, ".html"].concat());
            kind = FileKind::Templated(template_name.to_string());
        }
    }

    if let FileKind::Static = kind {
        // copy directly if the file is not minifiable or templated
        fs::copy(src, dest).map_err(ErrorKind::CopyFile)?;
    } else {
        let mut content_buf = fs::read_to_string(src).map_err(ErrorKind::ReadFile)?;

        // minify before operations to save time (hopefully)
        content_buf = minify(content_buf);

        // compile templated
        if let FileKind::Templated(template_name) = kind {
            content_buf = compile_templated(&content_buf, &templates[&template_name]);
        }

        fs::write(dest, content_buf).map_err(ErrorKind::WriteFile)?;
    }

    Ok(())
}

fn compile_templated(content: &str, template_content: &str) -> String {
    let mut blocks = HashMap::with_capacity(10);

    // parse def blocks
    for block in content.split("<!---def:").skip(1) {
        if let Some((label, content)) = block.split_once("--->") {
            blocks.insert(label, Cow::Borrowed(content));
        }
    }

    // parse pipe blocks
    for block in content.split("<!---pipe:").skip(1) {
        if let Some((label, _)) = block.split_once("--->") {
            blocks.insert(label, Cow::Owned(format!("<!---put:{}--->", label)));
        }
    }

    let mut compiled_content = String::new();

    // replace put blocks with associated content from def
    // and pipe blocks
    let mut iter = template_content.split("<!---put:");
    if let Some(first) = iter.next() {
        compiled_content.push_str(first);
    }
    for part in iter {
        if let Some((label, rest)) = part.split_once("--->") {
            if let Some(content) = blocks.get(label) {
                compiled_content.push_str(content);
            }
            compiled_content.push_str(rest);
        } else {
            compiled_content.push_str(part);
        }
    }

    compiled_content
}

fn minify(content: String) -> String {
    content.split_whitespace().collect::<Vec<_>>().join(" ")
}
