use crate::bspfile::LumpType;
use crate::data::*;
use std::num::{ParseFloatError, ParseIntError};
use thiserror::Error;
use zip::result::ZipError;

#[non_exhaustive]
#[derive(Debug, Error)]
pub enum BspError {
    #[error("unexpected magic numbers or version: {0:?}")]
    UnexpectedHeader([u8; 4]),
    #[error("bsp lump is out of bounds of the bsp file")]
    LumpOutOfBounds(LumpEntry),
    #[error("bsp game lump is out of bounds of the bsp file")]
    GameLumpOutOfBounds(GameLump),
    #[error("compressed game lump is malformed")]
    MalformedCompressedGameLump,
    #[error("Invalid lump size, lump size {lump_size} is not a multiple of the element size {element_size}")]
    InvalidLumpSize {
        lump: LumpType,
        element_size: usize,
        lump_size: usize,
    },
    #[error("unexpected length of uncompressed lump, got {got} but expected {expected}")]
    UnexpectedUncompressedLumpSize { got: u32, expected: u32 },
    #[error("unexpected length of compressed lump, got {got} but expected {expected}")]
    UnexpectedCompressedLumpSize { got: u32, expected: u32 },
    #[error("error while decompressing lump")]
    LumpDecompressError(lzma_rs::error::Error),
    #[error("io error while reading data: {0}")]
    IO(#[from] std::io::Error),
    #[error(transparent)]
    String(#[from] StringError),
    #[error("Malformed field found while parsing: {0:#}")]
    MalformedData(binrw::Error),
    #[error("bsp file is well-formed but contains invalid data")]
    Validation(#[from] ValidationError),
    #[error(transparent)]
    LumpVersion(UnsupportedLumpVersion),
    #[error(transparent)]
    Zip(#[from] ZipError),
}

impl From<binrw::Error> for BspError {
    fn from(e: binrw::Error) -> Self {
        use binrw::Error;

        // only a few error types should be generated by our code
        match e {
            Error::Backtrace(trace) => Self::from(*trace.error),
            Error::Io(e) => BspError::IO(e),
            Error::Custom { err, .. } => {
                if err.is::<StringError>() {
                    BspError::String(*err.downcast::<StringError>().unwrap())
                } else if err.is::<UnsupportedLumpVersion>() {
                    BspError::LumpVersion(*err.downcast::<UnsupportedLumpVersion>().unwrap())
                } else if err.is::<InvalidNeighbourError>() {
                    BspError::Validation(ValidationError::Neighbour(
                        *err.downcast::<InvalidNeighbourError>().unwrap(),
                    ))
                } else {
                    panic!("unexpected custom error")
                }
            }
            e => BspError::MalformedData(e),
        }
    }
}

impl From<lzma_rs::error::Error> for BspError {
    fn from(e: lzma_rs::error::Error) -> Self {
        use lzma_rs::error::Error;

        match e {
            Error::IoError(e) => BspError::IO(e),
            e => BspError::LumpDecompressError(e),
        }
    }
}

#[derive(Debug, Error)]
pub enum StringError {
    #[error(transparent)]
    NonUTF8(#[from] std::str::Utf8Error),
    #[error("String is not null-terminated")]
    NotNullTerminated,
}

#[derive(Debug, Error)]
#[error("Unsupported lump version {version} for {lump_type} lump")]
pub struct UnsupportedLumpVersion {
    pub lump_type: &'static str,
    pub version: u16,
}

#[derive(Debug, Error)]
pub enum ValidationError {
    #[error(
    "A {source_} indexes into {target} but the index {index} is out of range of the size {size}"
    )]
    ReferenceOutOfRange {
        source_: &'static str,
        target: &'static str,
        index: i64,
        size: usize,
    },
    #[error("bsp contains no root node")]
    NoRootNode,
    #[error("displacement face with {0} edges")]
    NonSquareDisplacement(i16),
    #[error("No static prop lump found")]
    NoStaticPropLump,
    #[error(transparent)]
    Neighbour(InvalidNeighbourError),
}

#[derive(Debug, Error)]
pub enum InvalidNeighbourError {
    #[error("Invalid neighbour span")]
    InvalidNeighbourSpan(u8),
    #[error("Invalid neighbour orientation")]
    InvalidNeighbourOrientation(u8),
}

#[derive(Debug, Error)]
pub enum EntityParseError {
    #[error("wrong number of elements")]
    ElementCount,
    #[error(transparent)]
    Float(#[from] ParseFloatError),
    #[error(transparent)]
    Int(#[from] ParseIntError),
}
