use std::{error::Error as StdError, fmt, io, rc::Rc};

use crate::{
    compile::CompileError,
    vm::{
        Value,
        instruction::{Instruction, InstructionDecodeError},
        value::{BytecodeFunction, IdentifierValue, StringValue},
    },
};

#[derive(Debug, thiserror::Error)]
#[error("{error}")]
pub struct MachineErrorAt {
    // TODO ip where the error occured
    pub error: MachineError,
    pub location: Option<MachineErrorLocation>,
}
#[derive(Debug, PartialEq)]
pub struct MachineErrorLocation {
    pub function: Rc<BytecodeFunction>,
    pub offset: usize,
}

#[derive(Debug, PartialEq, thiserror::Error)]
#[error("expected {expected}, got {got}")]
pub struct ValueConversionError {
    pub expected: String,
    pub got: Value,
}

#[derive(Debug, thiserror::Error)]
pub enum ReadError {
    #[error("{0}")]
    Lexical(nom::Err<nom::error::Error<String>, nom::error::Error<String>>),
    #[error("{0}")]
    Io(io::Error),
}

#[derive(Debug, PartialEq)]
pub struct ArgumentCountError {
    pub function: Rc<BytecodeFunction>,
    pub actual: usize,
}

#[derive(Debug, thiserror::Error)]
pub enum MachineError {
    // VM itself
    #[error("instruction pointer is undefined")]
    InstructionPointerUndefined,
    #[error("instruction pointer is out of bounds")]
    InstructionPointerOutOfBounds,
    #[error("instruction fetch failed")]
    InstructionFetch,
    #[error("instruction decode error: {0}")]
    InstructionDecode(#[from] InstructionDecodeError),
    #[error("data stack overflowed")]
    DataStackOverflow,
    #[error("data stack underflowed")]
    DataStackUnderflow,
    #[error("call stack overflowed")]
    CallStackOverflow,
    #[error("call stack underflowed")]
    CallStackUnderflow,
    #[error("undefined upvalue reference")]
    UndefinedUpvalueReference,
    #[error("undefined local reference")]
    UndefinedLocalReference,
    #[error("undefined constant reference")]
    UndefinedConstantReference,
    #[error("unbound identifier reference: {0}")]
    UnboundIdentifier(IdentifierValue),
    #[error("invalid {0} argument: {1}")]
    InvalidInstructionArgument(Instruction, ValueConversionError),
    #[error("invalid branch target: {0}{1:+}")]
    InvalidBranchTarget(usize, isize),
    #[error("GET_TEMP with an empty temp register")]
    TempRegisterEmpty,
    #[error("{0}")]
    ArgumentCount(ArgumentCountError),

    // Syntax+evaluation
    // #[error("evaluation error: {0}")]
    // EvaluationError(EvalError),
    #[error("invalid argument count")]
    InvalidArgumentCount,
    #[error("aborted: {0}")]
    Abort(Rc<str>),
    #[error("{0}: {1}")]
    LoadError(StringValue, Box<MachineErrorAt>),
    #[error("{0}")]
    Custom(Box<dyn StdError>),

    #[error("value conversion error: {0}")]
    ValueConversion(#[from] ValueConversionError),
    #[error("syntax error: {0}")]
    Read(ReadError),
    #[error("compile error: {0}")]
    Compile(#[from] CompileError),
}

impl MachineError {
    pub fn at(self, location: Option<MachineErrorLocation>) -> MachineErrorAt {
        MachineErrorAt {
            error: self,
            location,
        }
    }

    pub fn at_unknown(self) -> MachineErrorAt {
        self.at(None)
    }
}

impl MachineErrorAt {
    pub fn at_unknown(error: MachineError) -> Self {
        Self::at(error, None)
    }

    pub fn at(error: MachineError, location: Option<MachineErrorLocation>) -> Self {
        Self { error, location }
    }
}

impl MachineErrorLocation {
    pub fn disassemble_chunk(&self, address: usize, before: usize, after: usize, arrow: bool) {
        self.function.disassemble(address, before, after, arrow);
    }
}

impl fmt::Display for MachineErrorLocation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:p}+{}", self.function, self.offset)
    }
}

impl PartialEq for ReadError {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Lexical(a), Self::Lexical(b)) => a == b,
            (Self::Io(a), Self::Io(b)) => a.raw_os_error() == b.raw_os_error(),
            _ => false,
        }
    }
}

impl fmt::Display for ArgumentCountError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let too_few = self.actual < self.function.required_count;
        write!(
            f,
            "too {} arguments for function {}: expected ",
            if too_few { "few" } else { "many" },
            self.function
        )?;

        if self.function.has_rest {
            let singular = self.function.required_count == 1;

            write!(
                f,
                "at least {} argument{}",
                self.function.required_count,
                if singular { "" } else { "s" }
            )?;
        } else if self.function.optional_count > 0 {
            write!(
                f,
                "{}-{} arguments",
                self.function.required_count,
                self.function.required_count + self.function.optional_count
            )?;
        } else {
            let singular = self.function.required_count == 1;

            write!(
                f,
                "{} argument{}",
                self.function.required_count,
                if singular { "" } else { "s" }
            )?;
        }

        write!(f, ", got {}", self.actual)?;

        Ok(())
    }
}
