use std::collections::HashMap;

use super::chunk;
use super::errors::*;
use super::interner::Interner;
use super::opcode::OpCode;
use super::value::{LoxString, Value};

pub struct VM {
    chunk: chunk::Chunk,

    // TODO(tazjin): Accessing array elements constantly is not ideal,
    // lets see if something clever can be done with iterators.
    ip: usize,

    stack: Vec<Value>,
    strings: Interner,

    globals: HashMap<LoxString, Value>,

    // Operations that consume values from the stack without pushing
    // anything leave their last value in this slot, which makes it
    // possible to return values from interpreters that ran code which
    // ended with a statement.
    last_drop: Option<Value>,
}

impl VM {
    fn push(&mut self, value: Value) {
        self.stack.push(value)
    }

    fn pop(&mut self) -> Value {
        self.stack.pop().expect("fatal error: stack empty!")
    }
}

macro_rules! with_type {
    ( $self:ident, $val:ident, $type:pat, $body:expr ) => {
        match $val {
            $type => $body,
            _ => {
                return Err(Error {
                    line: $self.chunk.get_line($self.ip - 1),
                    kind: ErrorKind::TypeError(format!(
                        "Expected type {}, but found value: {:?}",
                        stringify!($type),
                        $val,
                    )),
                })
            }
        }
    };
}

macro_rules! binary_op {
    ( $vm:ident, $type:tt, $op:tt ) => {
        binary_op!($vm, $type, $type, $op)
    };

    ( $vm:ident, $in_type:tt, $out_type:tt, $op:tt ) => {{
        let b = $vm.pop();
        let a = $vm.pop();

        with_type!($vm, b, Value::$in_type(val_b), {
            with_type!($vm, a, Value::$in_type(val_a), {
                $vm.push(Value::$out_type(val_a $op val_b))
            })
        })
    }};
}

impl VM {
    fn run(&mut self) -> LoxResult<Value> {
        loop {
            let op = &self.chunk.code[self.ip];

            #[cfg(feature = "disassemble")]
            chunk::disassemble_instruction(&self.chunk, self.ip);

            self.ip += 1;

            match op {
                OpCode::OpReturn => {
                    if !self.stack.is_empty() {
                        let val = self.pop();
                        return Ok(self.return_value(val));
                    } else if self.last_drop.is_some() {
                        let val = self.last_drop.take().unwrap();
                        return Ok(self.return_value(val));
                    } else {
                        return Ok(Value::Nil);
                    }
                }

                OpCode::OpConstant(idx) => {
                    let c = self.chunk.constant(*idx).clone();
                    self.push(c);
                }

                OpCode::OpNil => self.push(Value::Nil),
                OpCode::OpTrue => self.push(Value::Bool(true)),
                OpCode::OpFalse => self.push(Value::Bool(false)),

                OpCode::OpNot => {
                    let v = self.pop();
                    self.push(Value::Bool(v.is_falsey()));
                }

                OpCode::OpEqual => {
                    let b = self.pop();
                    let a = self.pop();
                    self.push(Value::Bool(a == b));
                }

                OpCode::OpLess => binary_op!(self, Number, Bool, <),
                OpCode::OpGreater => binary_op!(self, Number, Bool, >),

                OpCode::OpNegate => {
                    let v = self.pop();
                    with_type!(self, v, Value::Number(num), self.push(Value::Number(-num)));
                }

                OpCode::OpSubtract => binary_op!(self, Number, -),
                OpCode::OpMultiply => binary_op!(self, Number, *),
                OpCode::OpDivide => binary_op!(self, Number, /),

                OpCode::OpAdd => {
                    let b = self.pop();
                    let a = self.pop();

                    match (a, b) {
                        (Value::String(s_a), Value::String(s_b)) => {
                            let mut new_s = self.resolve_str(&s_a).to_string();
                            new_s.push_str(self.resolve_str(&s_b));
                            self.push(Value::String(new_s.into()));
                        }

                        (Value::Number(n_a), Value::Number(n_b)) => {
                            self.push(Value::Number(n_a + n_b))
                        }

                        _ => {
                            return Err(Error {
                                line: self.chunk.get_line(self.ip - 1),
                                kind: ErrorKind::TypeError(
                                    "'+' operator only works on strings and numbers".into(),
                                ),
                            })
                        }
                    }
                }

                OpCode::OpPrint => {
                    let val = self.pop();
                    println!("{}", self.print_value(val));
                }

                OpCode::OpPop => {
                    self.last_drop = Some(self.pop());
                }

                OpCode::OpDefineGlobal(name_idx) => {
                    let name = self.chunk.constant(*name_idx);
                    with_type!(self, name, Value::String(name), {
                        let name = name.clone();
                        let val = self.pop();
                        self.globals.insert(name, val);
                    });
                }

                OpCode::OpGetGlobal(name_idx) => {
                    let name = self.chunk.constant(*name_idx);
                    with_type!(self, name, Value::String(name), {
                        let val = match self.globals.get(name) {
                            None => unimplemented!("variable not found error"),
                            Some(val) => val.clone(),
                        };
                        self.push(val)
                    });
                }

                OpCode::OpSetGlobal(name_idx) => {
                    let name = self.chunk.constant(*name_idx).clone();
                    let new_val = self.pop();
                    with_type!(self, name, Value::String(name), {
                        match self.globals.get_mut(&name) {
                            None => unimplemented!("variable not found error"),
                            Some(val) => {
                                *val = new_val;
                            }
                        }
                    });
                }

                OpCode::OpGetLocal(local_idx) => {
                    let value = self.stack[local_idx.0].clone();
                    self.push(value);
                }

                OpCode::OpSetLocal(local_idx) => {
                    debug_assert!(
                        self.stack.len() > local_idx.0,
                        "stack is not currently large enough for local"
                    );
                    self.stack[local_idx.0] = self.stack.last().unwrap().clone();
                }

                OpCode::OpJumpPlaceholder(_) => {
                    panic!("unpatched jump detected - this is a fatal compiler error!");
                }

                OpCode::OpJump(offset) => {
                    self.ip += offset.0;
                }

                OpCode::OpJumpIfFalse(offset) => {
                    if self
                        .stack
                        .last()
                        .expect("condition should leave a value on the stack")
                        .is_falsey()
                    {
                        self.ip += offset.0;
                    }
                }
            }

            #[cfg(feature = "disassemble")]
            println!("=> {:?}", self.stack);
        }
    }

    // For some types of values (e.g. interned strings), returns
    // should no longer include any references into the interpreter.
    fn return_value(&self, val: Value) -> Value {
        match val {
            Value::String(string @ LoxString::Interned(_)) => {
                Value::String(self.resolve_str(&string).to_string().into())
            }
            _ => val,
        }
    }

    fn resolve_str<'a>(&'a self, string: &'a LoxString) -> &'a str {
        match string {
            LoxString::Heap(s) => s.as_str(),
            LoxString::Interned(id) => self.strings.lookup(*id),
        }
    }

    fn print_value(&self, val: Value) -> String {
        match val {
            Value::String(LoxString::Heap(s)) => s,
            Value::String(LoxString::Interned(id)) => self.strings.lookup(id).into(),
            _ => format!("{:?}", val),
        }
    }
}

pub fn interpret(strings: Interner, chunk: chunk::Chunk) -> LoxResult<Value> {
    let mut vm = VM {
        chunk,
        strings,
        globals: HashMap::new(),
        ip: 0,
        stack: vec![],
        last_drop: None,
    };

    vm.run()
}
