use crate::model;
use ignore::overrides;
use std::path;

pub fn main(
    configuration: &model::Configuration,
    base_folder: &path::Path,
) -> model::Result<Vec<path::PathBuf>> {
    iterate_entries(configuration, base_folder)?
        .filter_map(|entry| match entry {
            Err(error) => Some(Err(error.into())),
            Ok(entry) => match entry.metadata() {
                Err(error) => Some(Err(error.into())),
                Ok(metadata) => {
                    if metadata.is_dir() {
                        None
                    } else {
                        Some(Ok(entry.into_path()))
                    }
                }
            },
        })
        .collect()
}

fn iterate_entries(
    configuration: &model::Configuration,
    base_folder: &path::Path,
) -> model::Result<ignore::Walk> {
    let filter = get_filter(configuration, base_folder)?;
    Ok(ignore::WalkBuilder::new(base_folder)
        .standard_filters(false)
        .overrides(filter)
        .build())
}

fn get_filter(
    configuration: &model::Configuration,
    base_folder: &path::Path,
) -> model::Result<overrides::Override> {
    let mut builder = overrides::OverrideBuilder::new(base_folder);
    for pattern in configuration.paths.lines() {
        builder.add(pattern)?;
    }
    let filter = builder.build()?;

    if filter.is_empty() {
        ignore_everything(base_folder)
    } else {
        Ok(filter)
    }
}

fn ignore_everything(base_folder: &path::Path) -> model::Result<overrides::Override> {
    Ok(overrides::OverrideBuilder::new(base_folder)
        .add("!*")?
        .build()?)
}

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

    #[test]
    fn handles_empty_paths() {
        let actual = main(
            &model::Configuration {
                paths: String::new(),
                ..model::stubs::configuration()
            },
            path::Path::new("."),
        );

        let actual = actual.unwrap();
        assert!(actual.is_empty());
    }

    #[test]
    fn handles_single_path() {
        let actual = main(
            &model::Configuration {
                paths: "/examples/assets/**".into(),
                ..model::stubs::configuration()
            },
            path::Path::new("."),
        );

        let mut actual = actual.unwrap();
        actual.sort();
        let expected = Vec::<path::PathBuf>::from([
            "./examples/assets/.env".into(),
            "./examples/assets/configuration/menu.json".into(),
            "./examples/assets/configuration/translations.csv".into(),
            "./examples/assets/credits.md".into(),
            "./examples/assets/world/levels/tutorial.json".into(),
            "./examples/assets/world/physical_constants.json".into(),
        ]);
        assert_eq!(actual, expected);
    }

    #[test]
    fn handles_multiple_paths() {
        let actual = main(
            &model::Configuration {
                paths: "/examples/assets/configuration/**
/examples/assets/world/**"
                    .into(),
                ..model::stubs::configuration()
            },
            path::Path::new("."),
        );

        let mut actual = actual.unwrap();
        actual.sort();
        let expected = Vec::<path::PathBuf>::from([
            "./examples/assets/configuration/menu.json".into(),
            "./examples/assets/configuration/translations.csv".into(),
            "./examples/assets/world/levels/tutorial.json".into(),
            "./examples/assets/world/physical_constants.json".into(),
        ]);
        assert_eq!(actual, expected);
    }

    #[test]
    fn handles_negated_patterns() {
        let actual = main(
            &model::Configuration {
                paths: "/examples/assets/**/*.json
!/examples/assets/world/levels/"
                    .into(),
                ..model::stubs::configuration()
            },
            path::Path::new("."),
        );

        let mut actual = actual.unwrap();
        actual.sort();
        let expected = Vec::<path::PathBuf>::from([
            "./examples/assets/configuration/menu.json".into(),
            "./examples/assets/world/physical_constants.json".into(),
        ]);
        assert_eq!(actual, expected);
    }

    #[test]
    fn handles_without_hidden_files() {
        let actual = main(
            &model::Configuration {
                paths: "/examples/assets/*
!.*"
                .into(),
                ..model::stubs::configuration()
            },
            path::Path::new("."),
        );

        let mut actual = actual.unwrap();
        actual.sort();
        let expected = Vec::<path::PathBuf>::from(["./examples/assets/credits.md".into()]);
        assert_eq!(actual, expected);
    }
}
