use std::path::Path;

use inkwell::{
    IntPredicate, OptimizationLevel,
    builder::Builder,
    context::Context,
    module::Module,
    targets::{FileType, InitializationConfig, Target, TargetMachine, TargetMachineOptions},
    types::IntType,
    values::{FunctionValue, PointerValue},
};

use crate::shared::Operation;

pub struct CodeGen<'ctx> {
    context: &'ctx Context,
    module: Module<'ctx>,
    builder: Builder<'ctx>,
    i8_type: IntType<'ctx>,
    i32_type: IntType<'ctx>,
    i64_type: IntType<'ctx>,
    putchar_fn: FunctionValue<'ctx>,
    getchar_fn: FunctionValue<'ctx>,
}

impl<'ctx> CodeGen<'ctx> {
    pub fn build(&self, output_dir: &Path, source_path: &Path) {
        Target::initialize_native(&InitializationConfig {
            base: true,
            machine_code: true,
            asm_printer: true,
            asm_parser: true,
            disassembler: true,
            info: true,
        })
        .unwrap();

        let triple = TargetMachine::get_default_triple();
        let target = Target::from_triple(&triple).unwrap();

        let cpu = TargetMachine::get_host_cpu_name().to_string();
        let features = TargetMachine::get_host_cpu_features().to_string();
        let options = TargetMachineOptions::default()
            .set_cpu(&cpu)
            .set_features(&features)
            .set_abi("sysv")
            .set_level(OptimizationLevel::Aggressive);

        let machine = target
            .create_target_machine_from_options(&triple, options)
            .unwrap();

        let obj_path = std::env::temp_dir().join(format!("barb_{}.o", std::process::id()));
        machine
            .write_to_file(&self.module, FileType::Object, &obj_path)
            .unwrap();

        let binary_name = source_path
            .file_stem()
            .unwrap_or_default()
            .to_str()
            .unwrap_or("a.out");
        let output_path = output_dir.join(binary_name);

        std::fs::create_dir_all(output_dir).unwrap();

        let status = std::process::Command::new("cc")
            .arg(&obj_path)
            .arg("-o")
            .arg(&output_path)
            .status()
            .expect("cc is required for linking");

        let _ = std::fs::remove_file(&obj_path);

        if !status.success() {
            panic!("linker failed");
        }

        println!("Build success! path: {}", output_path.display())
    }

    pub fn new(context: &'ctx Context) -> Self {
        let module = context.create_module("program");
        let builder = context.create_builder();

        let i8_type = context.i8_type();
        let i32_type = context.i32_type();
        let i64_type = context.i64_type();

        let putchar_type = i32_type.fn_type(&[i8_type.into()], false);
        let putchar_fn = module.add_function("putchar", putchar_type, None);

        let getchar_type = i32_type.fn_type(&[], false);
        let getchar_fn = module.add_function("getchar", getchar_type, None);

        CodeGen {
            context,
            module,
            builder,
            i8_type,
            i32_type,
            i64_type,
            putchar_fn,
            getchar_fn,
        }
    }

    pub fn compile(&self, operations: &[Operation]) -> FunctionValue<'ctx> {
        let main_type = self.i32_type.fn_type(&[], false);
        let main_fn = self.module.add_function("main", main_type, None);

        let entry_bb = self.context.append_basic_block(main_fn, "entry");
        self.builder.position_at_end(entry_bb);

        let array_type = self.i8_type.array_type(30000);
        let memory = self.builder.build_alloca(array_type, "memory").unwrap();
        self.builder
            .build_memset(
                memory,
                16,
                self.i8_type.const_zero(),
                self.i64_type.const_int(30000, false),
            )
            .unwrap();

        let data_ptr = self
            .builder
            .build_alloca(self.i64_type, "data_ptr")
            .unwrap();
        self.builder
            .build_store(data_ptr, self.i64_type.const_zero())
            .unwrap();

        let ret_block = self.context.append_basic_block(main_fn, "ret");

        if operations.is_empty() {
            self.builder.build_unconditional_branch(ret_block).unwrap();
        } else {
            let mut op_blocks = Vec::with_capacity(operations.len());
            for i in 0..operations.len() {
                let bb = self
                    .context
                    .append_basic_block(main_fn, &format!("op_{}", i));
                op_blocks.push(bb);
            }

            self.builder
                .build_unconditional_branch(op_blocks[0])
                .unwrap();

            for (i, op) in operations.iter().enumerate() {
                self.builder.position_at_end(op_blocks[i]);

                let next_block = if i + 1 < operations.len() {
                    op_blocks[i + 1]
                } else {
                    ret_block
                };

                match op {
                    Operation::Next => {
                        let dp = self
                            .builder
                            .build_load(self.i64_type, data_ptr, "dp")
                            .unwrap()
                            .into_int_value();
                        let inc = self
                            .builder
                            .build_int_add(dp, self.i64_type.const_int(1, false), "dp_inc")
                            .unwrap();
                        self.builder.build_store(data_ptr, inc).unwrap();
                        self.builder.build_unconditional_branch(next_block).unwrap();
                    }
                    Operation::Prev => {
                        let dp = self
                            .builder
                            .build_load(self.i64_type, data_ptr, "dp")
                            .unwrap()
                            .into_int_value();
                        let dec = self
                            .builder
                            .build_int_sub(dp, self.i64_type.const_int(1, false), "dp_dec")
                            .unwrap();
                        self.builder.build_store(data_ptr, dec).unwrap();
                        self.builder.build_unconditional_branch(next_block).unwrap();
                    }
                    Operation::Increment => {
                        let cell = self.build_cell_ptr(memory, data_ptr);
                        let val = self
                            .builder
                            .build_load(self.i8_type, cell, "val")
                            .unwrap()
                            .into_int_value();
                        let inc = self
                            .builder
                            .build_int_add(val, self.i8_type.const_int(1, false), "inc")
                            .unwrap();
                        self.builder.build_store(cell, inc).unwrap();
                        self.builder.build_unconditional_branch(next_block).unwrap();
                    }
                    Operation::Decrement => {
                        let cell = self.build_cell_ptr(memory, data_ptr);
                        let val = self
                            .builder
                            .build_load(self.i8_type, cell, "val")
                            .unwrap()
                            .into_int_value();
                        let dec = self
                            .builder
                            .build_int_sub(val, self.i8_type.const_int(1, false), "dec")
                            .unwrap();
                        self.builder.build_store(cell, dec).unwrap();
                        self.builder.build_unconditional_branch(next_block).unwrap();
                    }
                    Operation::Output => {
                        let cell = self.build_cell_ptr(memory, data_ptr);
                        let val = self.builder.build_load(self.i8_type, cell, "val").unwrap();
                        self.builder
                            .build_call(self.putchar_fn, &[val.into()], "putchar")
                            .unwrap();
                        self.builder.build_unconditional_branch(next_block).unwrap();
                    }
                    Operation::Input => {
                        let cell = self.build_cell_ptr(memory, data_ptr);
                        let call = self
                            .builder
                            .build_call(self.getchar_fn, &[], "getchar")
                            .unwrap();
                        let val = call.try_as_basic_value().basic().unwrap().into_int_value();
                        let trunc = self
                            .builder
                            .build_int_truncate(val, self.i8_type, "char")
                            .unwrap();
                        self.builder.build_store(cell, trunc).unwrap();
                        self.builder.build_unconditional_branch(next_block).unwrap();
                    }
                    Operation::JumpIfZero(target) => {
                        let cell = self.build_cell_ptr(memory, data_ptr);
                        let val = self
                            .builder
                            .build_load(self.i8_type, cell, "val")
                            .unwrap()
                            .into_int_value();
                        let is_zero = self
                            .builder
                            .build_int_compare(
                                IntPredicate::EQ,
                                val,
                                self.i8_type.const_zero(),
                                "iszero",
                            )
                            .unwrap();
                        self.builder
                            .build_conditional_branch(is_zero, op_blocks[*target], next_block)
                            .unwrap();
                    }
                    Operation::JumpIfNonzero(target) => {
                        let cell = self.build_cell_ptr(memory, data_ptr);
                        let val = self
                            .builder
                            .build_load(self.i8_type, cell, "val")
                            .unwrap()
                            .into_int_value();
                        let is_nz = self
                            .builder
                            .build_int_compare(
                                IntPredicate::NE,
                                val,
                                self.i8_type.const_zero(),
                                "isnz",
                            )
                            .unwrap();
                        self.builder
                            .build_conditional_branch(is_nz, op_blocks[*target], next_block)
                            .unwrap();
                    }
                }
            }
        }

        self.builder.position_at_end(ret_block);
        self.builder
            .build_return(Some(&self.i32_type.const_int(0, false)))
            .unwrap();

        main_fn
    }

    fn build_cell_ptr(
        &self,
        memory: PointerValue<'ctx>,
        data_ptr_ptr: PointerValue<'ctx>,
    ) -> PointerValue<'ctx> {
        let data = self
            .builder
            .build_load(self.i64_type, data_ptr_ptr, "data")
            .unwrap()
            .into_int_value();
        let array_type = self.i8_type.array_type(30000);
        let indices = &[self.i64_type.const_zero(), data];
        unsafe {
            self.builder
                .build_in_bounds_gep(array_type, memory, indices, "cell")
        }
        .unwrap()
    }

    pub fn jit_run(self) {
        let engine = self
            .module
            .create_jit_execution_engine(inkwell::OptimizationLevel::None)
            .unwrap();
        unsafe {
            let address = engine.get_function_address("main").unwrap();
            let func: fn() -> i32 = std::mem::transmute(address);
            func();
        }
    }

    pub fn print_ir(&self) {
        println!("{}", self.module.print_to_string().to_string());
    }
}
