mod array_emitter;
mod ast_emitter;
mod block_emitter;
mod compilation_info;
mod control_structures;
mod dis;
mod emitter;
mod emitter_scope;
mod expression_emitter;
mod function_declaration_emitter;
mod object_emitter;
mod reference_op_emitter;
mod script_emitter;

extern crate jsparagus_ast as ast;
extern crate jsparagus_scope as scope;
extern crate jsparagus_stencil as stencil;

pub use crate::emitter::{EmitError, EmitOptions};
pub use dis::dis;

use crate::compilation_info::CompilationInfo;

use ast::source_atom_set::SourceAtomSet;
use ast::source_slice_list::SourceSliceList;
use scope::{ScopeBuildError, ScopePassResult};
use stencil::result::EmitResult;

pub fn emit<'alloc>(
    ast: &'alloc ast::types::Program<'alloc>,
    options: &EmitOptions,
    atoms: SourceAtomSet<'alloc>,
    slices: SourceSliceList<'alloc>,
) -> Result<EmitResult<'alloc>, EmitError> {
    let ScopePassResult {
        scope_data_map,
        function_declarations,
        function_stencil_indices,
        function_declaration_properties,
        scripts,
        error,
    } = scope::generate_scope_data(ast);

    // Error case for scope analysis will be removed once all syntax is
    // supported. Use field instead of Result type here for simplicity.
    match error {
        Some(ScopeBuildError::NotImplemented(s)) => {
            return Err(EmitError::NotImplemented(s));
        }
        None => {}
    }

    let compilation_info = CompilationInfo::new(
        atoms,
        slices,
        scope_data_map,
        function_declarations,
        function_stencil_indices,
        function_declaration_properties,
        scripts,
    );
    ast_emitter::emit_program(ast, options, compilation_info)
}

#[cfg(test)]
mod tests {
    extern crate jsparagus_parser as parser;

    use super::{emit, EmitOptions};
    use crate::dis::*;
    use ast::source_atom_set::SourceAtomSet;
    use ast::source_slice_list::SourceSliceList;
    use bumpalo::Bump;
    use parser::{parse_script, ParseOptions};
    use std::cell::RefCell;
    use std::convert::TryInto;
    use std::rc::Rc;
    use stencil::opcode::*;
    use stencil::script::SourceExtent;

    fn bytecode(source: &str) -> Vec<u8> {
        let alloc = &Bump::new();
        let parse_options = ParseOptions::new();
        let atoms = Rc::new(RefCell::new(SourceAtomSet::new()));
        let slices = Rc::new(RefCell::new(SourceSliceList::new()));
        let source_len = source.len();
        let parse_result =
            parse_script(alloc, source, &parse_options, atoms.clone(), slices.clone())
                .expect("Failed to parse");
        // println!("{:?}", parse_result);

        let extent = SourceExtent::top_level_script(source_len.try_into().unwrap(), 1, 0);
        let emit_options = EmitOptions::new(extent);

        let result = emit(
            alloc.alloc(ast::types::Program::Script(parse_result.unbox())),
            &emit_options,
            atoms.replace(SourceAtomSet::new_uninitialized()),
            slices.replace(SourceSliceList::new()),
        )
        .expect("Should work!");

        let script_data_index: usize = result.scripts[0]
            .immutable_script_data
            .expect("Top level script should have ImmutableScriptData")
            .into();
        let script_data = &result.script_data_list[script_data_index];
        let bytecode = &script_data.bytecode;

        println!("{}", dis(&bytecode));
        bytecode.to_vec()
    }

    #[test]
    fn it_works() {
        assert_eq!(
            bytecode("2 + 2"),
            vec![
                Opcode::Int8 as u8,
                2,
                Opcode::Int8 as u8,
                2,
                Opcode::Add as u8,
                Opcode::SetRval as u8,
                Opcode::RetRval as u8,
            ]
        )
    }

    #[test]
    fn dis_call() {
        assert_eq!(
            bytecode("dis()"),
            vec![
                Opcode::GetGName as u8,
                1,
                0,
                0,
                0,
                Opcode::Undefined as u8,
                Opcode::Call as u8,
                0,
                0,
                Opcode::SetRval as u8,
                Opcode::RetRval as u8,
            ]
        )
    }

    #[test]
    fn literals() {
        assert_eq!(
            bytecode("true"),
            vec![
                Opcode::True as u8,
                Opcode::SetRval as u8,
                Opcode::RetRval as u8,
            ]
        );
        assert_eq!(
            bytecode("false"),
            vec![
                Opcode::False as u8,
                Opcode::SetRval as u8,
                Opcode::RetRval as u8,
            ]
        );
        //assert_eq!(
        //    bytecode("'hello world'"),
        //    vec![
        //        Opcode::String as u8, 0, 0, 0, 0,
        //        Opcode::SetRval as u8,
        //        Opcode::RetRval as u8,
        //    ]
        //);
    }
}