use crate::*;

use log::{info, warn};
use vagabond::*;


/// Compiles multiple source code files into one.
pub struct Compiler {
    pub source_path: Option<PathBuf>,
    pub resolver: Resolver,
    pub parse_symbols: ParseFn,
    pub push_code: PushFn,
}

impl Compiler {
    pub fn new(parse_symbols: ParseFn, push_code: PushFn) -> Self {
        let resolver = Resolver::new();
        Self { source_path: None, resolver, parse_symbols, push_code }
    }

    pub fn root_from_string<P: AsRef<Path>>(&mut self, source_code: String, path: P) {
        let source_unit = SourceUnit::from_string(source_code, &path, self.parse_symbols);
        self.source_path = Some(path.as_ref().to_path_buf());
        self.resolver.include_source_unit(source_unit, None);
    }

    pub fn root_from_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), FileError> {
        let source_unit = SourceUnit::from_path(&path, None, self.parse_symbols)?;
        self.source_path = Some(path.as_ref().to_path_buf());
        self.resolver.include_source_unit(source_unit, None);
        return Ok(());
    }

    /// Find library files descending from the parent directory.
    pub fn include_libs_from_parent(&mut self, ext: Option<&str>) {
        if let Some(path) = &self.source_path {
            if let Some(parent_path) = path.parent() {
                let parent_path = parent_path.to_owned();
                self.include_libs_from_path(&parent_path, ext);
            }
        }
    }

    /// Find library files at or descending from a path.
    pub fn include_libs_from_path(&mut self, path: &Path, ext: Option<&str>) {
        let libraries = gather_from_path(path, ext, self.parse_symbols);
        self.resolver.add_library_source_units(libraries);
        self.resolver.resolve();
    }

    /// Find library files from a PATH-style environment variable.
    pub fn include_libs_from_path_variable(&mut self, name: &str, ext: Option<&str>) {
        let libraries = gather_from_path_variable(name, ext, self.parse_symbols);
        self.resolver.add_library_source_units(libraries);
        self.resolver.resolve();
    }

    pub fn error(&self) -> Option<ResolverError> {
        self.resolver.error()
    }

    pub fn hierarchy(&self) -> SourceHierarchy {
        self.resolver.hierarchy()
    }

    pub fn symbols(&self) -> SourceSymbols {
        self.resolver.symbols()
    }

    pub fn unused(&self) -> UnusedSymbols {
        self.resolver.unused()
    }

    pub fn get_compiled_source(&mut self) -> Result<String, MergeError> {
        self.resolver.calculate_hierarchy();
        self.resolver.get_merged_source_code(self.push_code)
    }
}


/// Gather all source units with a given extension using a PATH-style environment variable.
pub fn gather_from_path_variable(variable: &str, extension: Option<&str>, parse: ParseFn) -> Vec<SourceUnit> {
    let mut source_units = Vec::new();
    if let Ok(string) = std::env::var(variable) {
        for path in string.split(":").map(PathBuf::from) {
            info!("Found path {path:?} in environment variable {variable:?}");
            source_units.extend(gather_from_path(&path, extension, parse));
        }
    };
    return source_units;
}

/// Gather source units with a given extension at or descending from a path.
pub fn gather_from_path(path: &Path, extension: Option<&str>, parse: ParseFn) -> Vec<SourceUnit> {
    let mut source_units = Vec::new();
    let check_optional_file = |file: &Option<SourceFile>| -> bool {
        match file {
            Some(file) => match file.symbols {
                Some(_) => { info!("Found source file at {:?}", file.path); true }
                None => { warn!("Could not parse source file at {:?}", file.path); false }
            }
            None => true,
        }
    };
    let mut gather_source_unit = |path: &Path| {
        if let Ok(unit) = SourceUnit::from_path(&path, extension, parse) {
            if unit.main.symbols.is_some() {
                info!("Found source file at {:?}", unit.main.path);
                let head_good = check_optional_file(&unit.head);
                let tail_good = check_optional_file(&unit.tail);
                if head_good && tail_good {
                    source_units.push(unit);
                }
            } else {
                warn!("Could not parse source file at {path:?}");
                check_optional_file(&unit.head);
                check_optional_file(&unit.tail);
            }
        }
    };
    if let Ok(entry) = Entry::from_path(path) {
        if EntryType::File == entry.entry_type {
            gather_source_unit(&entry.path)
        } else if EntryType::Directory == entry.entry_type {
            info!("Traversing directory {path:?} for source files");
            if let Ok(entries) = traverse_directory(entry.path) {
                for entry in entries {
                    gather_source_unit(&entry.path)
                }
            }
        }
    };
    return source_units;
}