use super::sanitize_name;
use crate::model;
use std::iter;

pub fn main(paths: Vec<model::Path>) -> model::Result<model::Forest> {
    let mut forest = model::Forest::new();

    for path in paths.into_iter() {
        add_path(&mut forest, path)?;
    }

    let mut index = 0;
    overwrite_indices_in_order(&mut forest, &mut index);

    Ok(forest)
}

fn add_path(forest: &mut model::Forest, path: model::Path) -> model::Result<()> {
    match path.relative.last() {
        None => Err(model::Error::UnexpectedEmptyRelativePath {
            absolute_path: path.absolute.into(),
        }),

        Some(filename) => {
            let file = model::File {
                identifier: sanitize_name::main(
                    filename,
                    sanitize_name::Convention::ScreamingSnakeCase,
                ),
                index: 0,
                relative_path: path.relative.join(NORMALIZED_FOLDER_SEPARATOR),
                absolute_path: path.absolute,
            };

            let mut reverse_path = path.relative;
            reverse_path.reverse();

            add_file(forest, reverse_path, file)
        }
    }
}

const NORMALIZED_FOLDER_SEPARATOR: &str = "/";

fn add_file(
    parent: &mut model::Forest,
    mut reverse_path: Vec<String>,
    file: model::File,
) -> model::Result<()> {
    match reverse_path.pop() {
        None => Err(model::Error::UnexpectedPathCollision(
            file.relative_path.into(),
        )),

        Some(name) => match parent.get_mut(&name) {
            None => {
                let child = get_singleton_tree(reverse_path, file, &name);
                parent.insert(name, child);
                Ok(())
            }

            Some(model::Tree::File(_)) => Err(model::Error::UnexpectedPathCollision(
                file.relative_path.into(),
            )),

            Some(model::Tree::Folder(model::Folder { forest, .. })) => {
                add_file(forest, reverse_path, file)
            }
        },
    }
}

fn get_singleton_tree(reverse_path: Vec<String>, file: model::File, root: &str) -> model::Tree {
    let parents = get_folder_identifiers(
        &reverse_path
            .iter()
            .skip(1)
            .map(|name| name.as_ref())
            .chain(iter::once(root))
            .collect::<Vec<_>>(),
    );

    let mut tree = model::Tree::File(file);

    for (child, parent) in reverse_path.into_iter().zip(parents.into_iter()) {
        let forest = [(child, tree)].into_iter().collect();
        tree = model::Tree::Folder(model::Folder {
            identifier: parent,
            forest,
        });
    }

    tree
}

fn get_folder_identifiers(names: &[&str]) -> Vec<syn::Ident> {
    names
        .iter()
        .map(|name| sanitize_name::main(name, sanitize_name::Convention::SnakeCase))
        .collect()
}

fn overwrite_indices_in_order(forest: &mut model::Forest, index: &mut usize) {
    for tree in forest.values_mut() {
        match tree {
            model::Tree::File(file) => {
                file.index = *index;
                *index += 1;
            }

            model::Tree::Folder(model::Folder { forest, .. }) => {
                overwrite_indices_in_order(forest, index)
            }
        }
    }
}

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

    #[test]
    fn handles_empty_set() {
        let actual = main(vec![]);

        let actual = actual.unwrap();
        let expected = model::Forest::new();
        assert_eq!(actual, expected);
    }

    #[test]
    fn handles_files() {
        let actual = main(vec![
            model::Path {
                relative: vec!["B".into()],
                absolute: "/a/B".into(),
            },
            model::Path {
                relative: vec!["c".into()],
                absolute: "/a/c".into(),
            },
        ]);

        let actual = actual.unwrap();
        let expected = [
            (
                "B".into(),
                model::Tree::File(model::File {
                    identifier: quote::format_ident!("r#B"),
                    index: 0,
                    relative_path: "B".into(),
                    absolute_path: "/a/B".into(),
                }),
            ),
            (
                "c".into(),
                model::Tree::File(model::File {
                    identifier: quote::format_ident!("r#C"),
                    index: 1,
                    relative_path: "c".into(),
                    absolute_path: "/a/c".into(),
                }),
            ),
        ]
        .into_iter()
        .collect();
        assert_eq!(actual, expected);
    }

    #[test]
    fn handles_folders() {
        let actual = main(vec![
            model::Path {
                relative: vec!["a".into()],
                absolute: "/a".into(),
            },
            model::Path {
                relative: vec!["b".into(), "a".into(), "b".into()],
                absolute: "/b/a/b".into(),
            },
            model::Path {
                relative: vec!["b".into(), "c".into()],
                absolute: "/b/c".into(),
            },
        ]);

        let actual = actual.unwrap();
        let expected = [
            (
                "a".into(),
                model::Tree::File(model::File {
                    identifier: quote::format_ident!("r#A"),
                    index: 0,
                    relative_path: "a".into(),
                    absolute_path: "/a".into(),
                }),
            ),
            (
                "b".into(),
                model::Tree::Folder(model::Folder {
                    identifier: quote::format_ident!("r#b"),
                    forest: [
                        (
                            "a".into(),
                            model::Tree::Folder(model::Folder {
                                identifier: quote::format_ident!("r#a"),
                                forest: [(
                                    "b".into(),
                                    model::Tree::File(model::File {
                                        identifier: quote::format_ident!("r#B"),
                                        index: 1,
                                        relative_path: "b/a/b".into(),
                                        absolute_path: "/b/a/b".into(),
                                    }),
                                )]
                                .into_iter()
                                .collect(),
                            }),
                        ),
                        (
                            "c".into(),
                            model::Tree::File(model::File {
                                identifier: quote::format_ident!("r#C"),
                                index: 2,
                                relative_path: "b/c".into(),
                                absolute_path: "/b/c".into(),
                            }),
                        ),
                    ]
                    .into_iter()
                    .collect(),
                }),
            ),
        ]
        .into_iter()
        .collect();
        assert_eq!(actual, expected);
    }

    #[test]
    fn given_empty_relative_path_it_errs() {
        let actual = main(vec![model::Path {
            relative: vec![],
            absolute: "/a/b".into(),
        }]);

        let actual = actual.unwrap_err();
        let expected = model::Error::UnexpectedEmptyRelativePath {
            absolute_path: "/a/b".into(),
        };
        assert_eq!(actual, expected);
    }

    #[test]
    fn given_path_collision_it_errs() {
        let actual = main(vec![
            model::Path {
                relative: vec!["a".into(), "b".into()],
                ..model::stubs::path()
            },
            model::Path {
                relative: vec!["a".into(), "b".into()],
                ..model::stubs::path()
            },
        ]);

        let actual = actual.unwrap_err();
        let expected = model::Error::UnexpectedPathCollision("a/b".into());
        assert_eq!(actual, expected);
    }
}
