#![allow(dead_code, unused)] // crate-level attribute
use hdf5_metno::{Group, Location, Result};
use log::debug;
use std::collections::HashSet as Set;

/// Tree view of an hdf5 file for pretty printing
struct PrettyTree {
    lines: Vec<TreeLine>,
    is_flat: bool,
}

impl PrettyTree {
    /// Commits a line to the tree view
    fn commit_line(&mut self, line: TreeLine) {
        self.lines.push(line);
    }

    /// Prints a line in the tree view
    fn print_lines(self) {
        let mut levels_done: Set<usize> = Set::new();
        for line in self.lines {
            if line.is_last {
                levels_done.insert(line.level);
            }
            let indent = get_indent(line.level, &levels_done);
            let divider = if line.is_last { "└─" } else { "├─" };
            let parent_path_joined = line.path.join(".");
            if self.is_flat {
                println!("{} | {}", parent_path_joined, line.content);
            } else {
                println!("{}{} {}", indent, divider, line.content,);
            }
        }
    }
}

pub mod cli {
    use super::recurse_groups;
    use super::PrettyTree;
    use hdf5_metno::{File, Result};
    use std::env;
    use std::path::PathBuf;

    /// Previews the contents of an hdf5 file
    pub(crate) fn preview(file: &File) -> Result<()> {
        // attributes
        println!("File: {}", file.filename());
        let line = "----------------------------------";
        println!("{}", line);

        // parse command line arguments
        let args: Vec<String> = env::args().collect();
        if args.contains(&"--help".to_string()) {
            help();
            return Ok(());
        }
        let is_flat = args.contains(&"--flat".to_string());

        let root_group = file.group("/")?;
        let mut tree = PrettyTree {
            lines: Vec::new(),
            is_flat,
        };
        let mut path = Vec::new();
        path.push("/".to_string());
        tree = recurse_groups(&root_group, 0, tree, path)?;
        tree.print_lines();

        Ok(())
    }

    /// Prints the help message
    pub(crate) fn help() {
        println!("Usage: hdf5-previewer [options] <[HDF5 file], [HDF5 directory]>");
        println!("Options:");
        println!("  --help: print this help message");
        // println!("  --flat: print in flat mode");
        println!("Examples:");
        println!("  hdf5-previewer /path/to/file.h5");
        println!("  hdf5-previewer /path/to/directory");
        println!("  hdf5-previewer --flat file1.h5 my_hdf5_dir/");
    }

    /// Reads an hdf5 file
    pub(crate) fn read_hdf5(filename: &PathBuf) -> Result<File> {
        let file = File::open(filename); // open for reading
        file
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ctor::ctor; // ctor is used for initialization and teardown functions
    use hdf5_metno::File;
    use std::path::PathBuf;

    #[ctor]
    fn init_color_backtrace() {
        color_backtrace::install();
    }

    fn get_samples_dir() -> PathBuf {
        let script_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        script_dir.join("samples")
    }

    fn get_sample_drf_hdf5() -> PathBuf {
        let samples_dir = get_samples_dir();
        let filename = samples_dir.join("digital-rf/capture/drf_properties.h5");
        filename
    }

    #[test]
    fn test_read_hdf5_runs() {
        let filename = get_sample_drf_hdf5();
        let file = cli::read_hdf5(&filename);
        // print error
        match file {
            Ok(_) => println!("File opened successfully"),
            Err(e) => {
                println!("Error: {:?}", e);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_main_runs() {
        let filename = get_sample_drf_hdf5();
        let file = cli::read_hdf5(&filename).unwrap();
        let main = cli::preview(&file);
        assert!(main.is_ok());
    }
}

/// Lists the attributes of a group or dataset
fn list_attributes(
    group_or_ds: &Location,
    level: usize,
    mut tree: PrettyTree,
    parent_path: Vec<String>,
) -> Result<PrettyTree> {
    let attr_names = group_or_ds.attr_names()?;
    debug!("attributes:");
    let length = attr_names.len();
    for (idx, name) in attr_names.iter().enumerate() {
        let mut this_path = parent_path.clone();
        this_path.push(name.clone().replace("/", ""));
        let value = group_or_ds.attr(name)?;
        let val_type = value.dtype()?;
        let thing = value.is_scalar();
        let value = if value.is_scalar() {
            value.read_raw::<i64>()
        } else {
            Err("Not scalar".into())
        };
        let content: String = match value {
            Ok(val) => format!("{} | {:?}", name, val),
            Err(_) => format!("{} | {:?}", name, val_type),
        };
        let line = TreeLine {
            content: content,
            is_last: idx == length - 1,
            path: this_path,
            level,
        };
        tree.commit_line(line);
    }
    Ok(tree)
}

/// Iterates over the datasets in a group and prints tree lines
fn iterate_datasets(
    group: &Group,
    level: usize,
    mut tree: PrettyTree,
    parent_path: Vec<String>,
) -> Result<PrettyTree> {
    let datasets = group.datasets()?;
    debug!("datasets:");
    let length = datasets.len();
    for (idx, ds) in datasets.iter().enumerate() {
        let mut content = ds.name();
        content.push_str(" | dataset");
        let mut this_path = parent_path.clone();
        this_path.push(ds.name().replace("/", ""));
        let line = TreeLine {
            content,
            level,
            path: this_path.clone(),
            is_last: idx == length - 1,
        };
        tree.commit_line(line);
        tree = list_attributes(&ds, level + 1, tree, this_path.clone())?;
    }
    Ok(tree)
}

// struct with content and indentation level
#[derive(Clone)]
struct TreeLine {
    content: String,
    is_last: bool,
    level: usize,
    path: Vec<String>,
}

/// Recursively lists groups and datasets in a group
fn recurse_groups(
    root_group: &Group,
    level: usize,
    mut tree: PrettyTree,
    path: Vec<String>,
) -> Result<PrettyTree> {
    let hdf5_groups = root_group.groups()?;
    let indent = get_indent(level, &Set::new());
    debug!("groups:");
    let mut parent_path;
    let clean_name = if root_group.name() == "/" {
        parent_path = Vec::new();
        "".to_string()
    } else {
        parent_path = path;
        root_group.name().replace("/", "")
    };
    if clean_name.is_empty() {
        println!("{}/", indent);
    } else {
        println!("{}{} | group", indent, clean_name);
    }
    parent_path.push(clean_name);
    let length = hdf5_groups.len();
    for (idx, child_group) in hdf5_groups.iter().enumerate() {
        let mut this_path = parent_path.clone();
        this_path.push(child_group.name().replace("/", ""));
        // recurse into the group
        for grand_children_groups in child_group.groups()? {
            let mut grand_children_path_root = this_path.clone();
            let gc_path = grand_children_groups.name().replace("/", "");
            grand_children_path_root.push(gc_path);
            tree = recurse_groups(&child_group, level + 1, tree, grand_children_path_root)?;
        }
        // list attributes of the group
        {
            let line = TreeLine {
                content: child_group.name(),
                level,
                path: this_path.clone(),
                is_last: idx == length - 1,
            };
            tree.commit_line(line);
            tree = list_attributes(&child_group, level + 1, tree, this_path.clone())?;
        }
    }
    // list attributes and datasets of the root group
    tree = list_attributes(&root_group, level, tree, parent_path.clone())?;
    tree = iterate_datasets(&root_group, level, tree, parent_path.clone())?;
    Ok(tree)
}

/// Returns the indentation for a given level
fn get_indent(level: usize, levels_done: &Set<usize>) -> String {
    let mut indent = String::new();
    for level in 0..level {
        indent.push_str(if levels_done.contains(&level) {
            "   "
        } else {
            "│  "
        });
    }
    indent
}
