use crate::*;


/// Compiles multiple source code files into one.
pub struct Compiler {
    pub source_path: PathBuf,
    pub resolver: Resolver,
}

impl Compiler {
    pub fn from_string<P: AsRef<Path>>(source_code: String, path: P) -> Self {
        let source_unit = SourceUnit::from_string(source_code, &path, parse_symbols);
        Self {
            source_path: path.as_ref().to_path_buf(),
            resolver: Resolver::new(source_unit)
        }
    }

    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, FileError> {
        let source_unit = SourceUnit::from_path(&path, None, parse_symbols)?;
        Ok(Self {
            source_path: path.as_ref().to_path_buf(),
            resolver: Resolver::new(source_unit)
        })
    }

    /// Find library files descending from the parent directory.
    pub fn include_libs_from_parent(&mut self, ext: &str) {
        if let Some(parent_path) = self.source_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: &str) {
        let libraries = gather_from_path(path, Some(ext), 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: &str) {
        let libraries = gather_from_path_variable(name, Some(ext), parse_symbols);
        self.resolver.add_library_source_units(libraries);
        self.resolver.resolve();
    }

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

    pub fn get_compiled_source(&self) -> Result<String, MergeError> {
        self.resolver.get_merged_source_code(push_source_code)
    }
}


/// Parse all symbols from a source code string.
fn parse_symbols(source_code: &str, path: Option<&Path>) -> Vec<Symbol> {
    use SyntacticTokenVariant as SynVar;
    use DefinitionType::*;
    use SymbolRole::*;
    let mut symbols = Vec::new();
    let mut macro_name: Option<String> = None;
    let mut parse_arg_list = false;   // true if parsing macro argument list
    let mut after_separator = false;  // true if prev token was separator

    macro_rules! push {
        ($name:expr, $source:expr, $role:expr) => {
            symbols.push(Symbol {
                name: $name,
                source: $source,
                role: $role,
                namespace: match &macro_name {
                    Some(name) => vec![name.to_owned()],
                    None => vec![],
                }
            })
        }
    }

    let syntactic_tokens = SyntacticParser::new(&source_code, path).parse();
    for token in syntactic_tokens {
        match token.variant {
            SynVar::MacroDefinition(name) => {
                push!(name.clone(), token.source, Definition(MustPrecedeReference));
                macro_name = Some(name);
                parse_arg_list = true;
            }
            SynVar::MacroDefinitionTerminator => {
                macro_name = None;
            }
            SynVar::LabelDefinition(name) => {
                push!(name.clone(), token.source, Definition(CanFollowReference));
            }
            SynVar::Symbol(name) => if parse_arg_list && after_separator {
                push!(name, token.source, Definition(MustPrecedeReference));
            } else {
                parse_arg_list = false;
                push!(name, token.source, Reference);
            }
            SynVar::Separator => {
                after_separator = true;
                continue;
            }
            SynVar::BlockOpen | SynVar::BlockClose => {
                continue;
            }
            SynVar::PackedBinaryLiteral(pbl) => {
                for field in pbl.fields {
                    push!(field.name.to_string(), field.source, Reference)
                }
            }
            SynVar::Expression(expr) => {
                for token in expr.tokens {
                    if let ExpressionTokenVariant::Invocation(name) = token.variant {
                        push!(name, token.source, Reference);
                    }
                }
            }
            _ => ()
        };
        after_separator = false;
    }
    return symbols;
}

/// Push source code to a source compilation string.
fn push_source_code(compilation: &mut String, source_file: &SourceFile) {
    // Skip blank files.
    let source_code = &source_file.source_code;
    if source_code.chars().all(|c| c.is_whitespace()) { return; }
    // Ensure that the previous section is followed by two newline characters.
    if !compilation.is_empty() {
        if !compilation.ends_with('\n') { compilation.push('\n'); }
        if !compilation.ends_with("\n\n") { compilation.push('\n'); }
    }
    // Push a path comment and the source code.
    let path_str = source_file.path.as_os_str().to_string_lossy();
    let path_comment = format!("(: {path_str} )\n");
    compilation.push_str(&path_comment);
    compilation.push_str(&source_code);
}