use crate::*;

use vagabond::*;


/// Gather all library units from the given path.
pub fn gather_project_libraries(path: &Path, extension: &str) -> Vec<SourceUnit> {
    match path.parent() {
        Some(parent_path) => gather_source_units(parent_path, extension),
        None => Vec::new(),
    }
}


/// Gather all library units from the paths specified in an environment variable.
pub fn gather_environment_libraries(extension: &str) -> Vec<Vec<SourceUnit>> {
    let mut environment_libraries = Vec::new();
    if let Ok(lib_var) = std::env::var("BEDROCK_LIBS") {
        for path_str in lib_var.split(":") {
            let lib_path = PathBuf::from(path_str);
            let source_units = gather_source_units(&lib_path, extension);
            if !source_units.is_empty() {
                environment_libraries.push(source_units);
            }
        }
    };
    return environment_libraries;
}


/// Gather all source units at or descended from the given entry.
fn gather_source_units(path: &Path, extension: &str) -> Vec<SourceUnit> {
    let mut source_units = Vec::new();
    if let Ok(entry) = Entry::from_path(path) {
        match entry.entry_type {
            EntryType::File => {
                if let Ok(source) = SourceUnit::from_path(entry.path, extension) {
                    source_units.push(source);
                }
            }
            EntryType::Directory => {
                if let Ok(entries) = traverse_directory(entry.path) {
                    for entry in entries {
                        if let Ok(source) = SourceUnit::from_path(entry.path, extension) {
                            source_units.push(source);
                        }
                    }
                }
            }
        }
    };
    return source_units;
}


pub struct SourceUnit {
    pub main: SourceFile,
    pub head: Option<SourceFile>,
    pub tail: Option<SourceFile>,
}


impl SourceUnit {
    /// Load from a source file and an associated head and tail file.
    pub fn from_path<P: Into<PathBuf>>(path: P, extension: &str) -> Result<Self, ParseError> {
        let main_path = canonicalize_path(path);
        let main_path_str = main_path.as_os_str().to_string_lossy().to_string();
        let head_extension = format!("head.{extension}");
        let tail_extension = format!("tail.{extension}");
        let is_head = main_path_str.ends_with(&head_extension);
        let is_tail = main_path_str.ends_with(&tail_extension);
        let is_not_main = !main_path_str.ends_with(extension);
        if is_not_main || is_head || is_tail { return Err(ParseError::InvalidExtension); }

        let symbols = parse_symbols_from_file(&main_path)?;
        let head_path = main_path.with_extension(head_extension);
        let tail_path = main_path.with_extension(tail_extension);

        let main = SourceFile { path: main_path, symbols };
        let head = match parse_symbols_from_file(&head_path) {
            Ok(symbols) => Some(SourceFile { path: head_path, symbols }),
            Err(_) => None,
        };
        let tail = match parse_symbols_from_file(&tail_path) {
            Ok(symbols) => Some(SourceFile { path: tail_path, symbols }),
            Err(_) => None,
        };
        Ok( SourceUnit { main, head, tail } )
    }

    /// Load from a string of source code.
    pub fn from_source_code<P: Into<PathBuf>>(source_code: String, path: P) -> Self {
        let path = canonicalize_path(path);
        let symbols = parse_symbols_from_source(source_code, Some(&path));
        Self {
            main: SourceFile { path, symbols },
            head: None,
            tail: None,
        }
    }
}


/// Read and parse all symbols from a source file.
fn parse_symbols_from_file(path: &Path) -> Result<Symbols, ParseError> {
    let source = read_source_from_file(path)?;
    Ok(parse_symbols_from_source(source, Some(path)))
}


/// Parse all symbols from a source code string.
fn parse_symbols_from_source(source_code: String, path: Option<&Path>) -> Symbols {
    use SyntacticTokenVariant as SynVar;

    let token_iter = SyntacticParser::from_source_code(&source_code, path);
    let mut definitions = Vec::new();
    let mut references = Vec::new();

    for token in token_iter {
        let source = token.source;
        match token.variant {
            SynVar::LabelDefinition(name) => {
                let variant = SymbolVariant::LabelDefinition;
                definitions.push(Symbol { name, source, variant });
            },
            SynVar::MacroDefinition(name) => {
                let variant = SymbolVariant::MacroDefinition;
                definitions.push(Symbol { name, source, variant });
            }
            SynVar::Symbol(name) => {
                let variant = SymbolVariant::Reference;
                references.push(Symbol { name, source, variant });
            },
            _ => (),
        }
    }

    Symbols {
        definitions: Some(definitions),
        references: Some(references),
        source_code,
    }
}


/// Attempt to read program source from a file.
pub fn read_source_from_file(path: &Path) -> Result<String, ParseError> {
    match std::fs::read(&path) {
        Ok(bytes) => match String::from_utf8(bytes) {
            Ok(source) => Ok(source),
            Err(_) => return Err(ParseError::InvalidUtf8),
        }
        Err(err) => return Err( match err.kind() {
            std::io::ErrorKind::NotFound => ParseError::NotFound,
            std::io::ErrorKind::PermissionDenied => ParseError::NotReadable,
            std::io::ErrorKind::IsADirectory => ParseError::IsADirectory,
            _ => ParseError::Unknown,
        } )
    }
}


fn canonicalize_path<P: Into<PathBuf>>(path: P) -> PathBuf {
    let pathbuf = path.into();
    match pathbuf.canonicalize() {
        Ok(canonical) => canonical,
        Err(_) => pathbuf,
    }
}



pub struct SourceFile {
    pub path: PathBuf,
    pub symbols: Symbols,
}


pub struct Symbols {
    pub definitions: Option<Vec<Symbol>>,
    pub references: Option<Vec<Symbol>>,
    pub source_code: String,
}


pub struct Symbol {
    pub name: String,
    pub variant: SymbolVariant,
    pub source: SourceSpan,
}


#[derive(PartialEq)]
pub enum SymbolVariant {
    LabelDefinition,
    MacroDefinition,
    Reference,
}