//! Error types for Espresso instance management and cube operations

use crate::cover::error::CoverError;
use std::fmt;
use std::io;

/// Errors related to Espresso instance management
///
/// These errors occur when trying to create or use Espresso instances with
/// conflicting dimensions or configurations on the same thread.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum InstanceError {
    /// The requested dimensions don't match the existing thread-local instance
    DimensionMismatch {
        /// The requested dimensions (num_inputs, num_outputs)
        requested: (usize, usize),
        /// The existing instance's dimensions (num_inputs, num_outputs)
        existing: (usize, usize),
    },
    /// The requested configuration doesn't match the existing thread-local instance
    ConfigMismatch {
        /// The requested dimensions (num_inputs, num_outputs)
        requested: (usize, usize),
        /// The existing instance's dimensions (num_inputs, num_outputs)
        existing: (usize, usize),
    },
    /// A requested dimension is too large to represent. The C cube setup casts the counts to a 32-bit
    /// signed `c_int`; an out-of-range value would wrap negative and abort the process inside C, so it
    /// is rejected up front instead.
    DimensionTooLarge {
        /// The requested dimensions (num_inputs, num_outputs)
        requested: (usize, usize),
        /// The maximum value supported for a single dimension
        max: usize,
    },
    /// Allocating the C cube's `part_size` array failed (the underlying `malloc` returned a null
    /// pointer, i.e. the system is out of memory). The thread-local cube state is restored to its
    /// pre-call condition before this error is returned, so a later [`Espresso::try_new`](
    /// crate::espresso::Espresso::try_new) call on the same thread is unaffected.
    AllocationFailure {
        /// The requested dimensions (num_inputs, num_outputs)
        requested: (usize, usize),
    },
}

impl fmt::Display for InstanceError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            InstanceError::DimensionMismatch {
                requested,
                existing,
            } => write!(
                f,
                "Cannot create Espresso instance with dimensions {:?} because a \
                 thread-local instance with dimensions {:?} already exists. \
                 Drop all existing covers and handles first.",
                requested, existing
            ),
            InstanceError::ConfigMismatch {
                requested,
                existing,
            } => write!(
                f,
                "Cannot create Espresso instance with different configuration while a \
                 thread-local instance with dimensions {:?} already exists (requested {:?}). \
                 Drop all existing covers and handles first.",
                existing, requested
            ),
            InstanceError::DimensionTooLarge { requested, max } => write!(
                f,
                "Espresso dimensions {:?} are too large; each must be at most {} (the C core uses \
                 32-bit signed cube indices).",
                requested, max
            ),
            InstanceError::AllocationFailure { requested } => write!(
                f,
                "Failed to allocate the part_size array for an Espresso instance with dimensions \
                 {:?}: out of memory.",
                requested
            ),
        }
    }
}

impl std::error::Error for InstanceError {}

impl From<InstanceError> for io::Error {
    fn from(err: InstanceError) -> Self {
        io::Error::other(err)
    }
}

/// Errors related to cube validation
///
/// These errors occur when invalid cube values are provided during cover creation.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CubeError {
    /// Invalid cube value encountered
    ///
    /// Cube input values must be 0 (low), 1 (high), or 2 (don't care).
    InvalidValue {
        /// The invalid value that was encountered
        value: u8,
        /// The position in the input vector where the invalid value occurred
        position: usize,
    },
    /// A cube's input or output slice length did not match the declared dimensions
    ///
    /// Each cube passed to `EspressoCover::from_cubes` must have exactly `num_inputs` input values
    /// and `num_outputs` output values.
    DimensionMismatch {
        /// Declared number of inputs
        expected_inputs: usize,
        /// Length of the offending cube's input slice
        actual_inputs: usize,
        /// Declared number of outputs
        expected_outputs: usize,
        /// Length of the offending cube's output slice
        actual_outputs: usize,
    },
}

impl fmt::Display for CubeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CubeError::InvalidValue { value, position } => write!(
                f,
                "Invalid cube value {} at position {}. Expected 0 (low), 1 (high), or 2 (don't care).",
                value, position
            ),
            CubeError::DimensionMismatch {
                expected_inputs,
                actual_inputs,
                expected_outputs,
                actual_outputs,
            } => write!(
                f,
                "Cube dimensions (inputs: {}, outputs: {}) don't match declared dimensions (inputs: {}, outputs: {})",
                actual_inputs, actual_outputs, expected_inputs, expected_outputs
            ),
        }
    }
}

impl std::error::Error for CubeError {}

impl From<CubeError> for io::Error {
    fn from(err: CubeError) -> Self {
        io::Error::new(io::ErrorKind::InvalidData, err)
    }
}

/// Errors that can occur during minimisation operations
///
/// This error type is returned by `Cover::minimize()` and `BoolExpr::minimize()`.
#[derive(Debug)]
#[non_exhaustive]
pub enum MinimizationError {
    /// Instance management error
    Instance(InstanceError),
    /// Cube validation error
    Cube(CubeError),
    /// Cover operation error
    Cover(CoverError),
    /// IO error during minimisation
    Io(io::Error),
    /// The cover's ON-set and OFF-set overlap: some minterm is asserted as both 1 and 0 for the same
    /// output. Such a cover is contradictory; the C core's `expand` aborts the process on it, so the
    /// pre-minimisation check rejects it with this error instead.
    NonOrthogonal {
        /// An output index at which the ON-set and OFF-set overlap.
        output: usize,
    },
    /// The C minimiser reported a fatal condition and was caught before it could terminate the
    /// process. This surfaces at the low-level [`Espresso`](crate::espresso::Espresso) /
    /// [`EspressoCover`](crate::espresso::EspressoCover) `try_minimize` entry points, which — unlike
    /// the high-level [`Cover`](crate::Cover) path — do not pre-validate their inputs. The most common
    /// cause is an explicit OFF-set that overlaps the ON-set (a non-orthogonal cover), for which the C
    /// core aborts inside `expand`/`complement`.
    EspressoFatal {
        /// The diagnostic captured from the C core (the text it would otherwise have printed to
        /// stderr before exiting).
        message: String,
    },
}

impl From<CoverError> for MinimizationError {
    fn from(err: CoverError) -> Self {
        MinimizationError::Cover(err)
    }
}

impl fmt::Display for MinimizationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MinimizationError::Instance(e) => write!(f, "Instance error: {}", e),
            MinimizationError::Cube(e) => write!(f, "Cube error: {}", e),
            MinimizationError::Cover(e) => write!(f, "Cover error: {}", e),
            MinimizationError::Io(e) => write!(f, "IO error: {}", e),
            MinimizationError::NonOrthogonal { output } => write!(
                f,
                "ON-set and OFF-set are not orthogonal (they overlap at output {})",
                output
            ),
            MinimizationError::EspressoFatal { message } => {
                write!(f, "Espresso reported a fatal error: {}", message)
            }
        }
    }
}

impl std::error::Error for MinimizationError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            MinimizationError::Instance(e) => Some(e),
            MinimizationError::Cube(e) => Some(e),
            MinimizationError::Cover(e) => Some(e),
            MinimizationError::Io(e) => Some(e),
            MinimizationError::NonOrthogonal { .. } => None,
            MinimizationError::EspressoFatal { .. } => None,
        }
    }
}

impl From<InstanceError> for MinimizationError {
    fn from(err: InstanceError) -> Self {
        MinimizationError::Instance(err)
    }
}

impl From<CubeError> for MinimizationError {
    fn from(err: CubeError) -> Self {
        MinimizationError::Cube(err)
    }
}

impl From<io::Error> for MinimizationError {
    fn from(err: io::Error) -> Self {
        MinimizationError::Io(err)
    }
}

impl From<MinimizationError> for io::Error {
    fn from(err: MinimizationError) -> Self {
        match err {
            // If it's already an IO error, return it directly
            MinimizationError::Io(e) => e,
            // Otherwise, wrap it as Other
            MinimizationError::Instance(e) => io::Error::other(e),
            MinimizationError::Cube(e) => io::Error::new(io::ErrorKind::InvalidData, e),
            MinimizationError::Cover(e) => io::Error::new(io::ErrorKind::InvalidData, e),
            e @ MinimizationError::NonOrthogonal { .. } => {
                io::Error::new(io::ErrorKind::InvalidData, e.to_string())
            }
            e @ MinimizationError::EspressoFatal { .. } => {
                io::Error::new(io::ErrorKind::InvalidData, e.to_string())
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error;

    #[test]
    fn test_instance_error_dimension_mismatch() {
        let err = InstanceError::DimensionMismatch {
            requested: (2, 1),
            existing: (3, 2),
        };
        let msg = err.to_string();
        assert!(msg.contains("Cannot create Espresso instance"));
        assert!(msg.contains("(2, 1)"));
        assert!(msg.contains("(3, 2)"));
    }

    #[test]
    fn test_instance_error_config_mismatch() {
        let err = InstanceError::ConfigMismatch {
            requested: (2, 1),
            existing: (2, 1),
        };
        let msg = err.to_string();
        assert!(msg.contains("different configuration"));
        assert!(msg.contains("(2, 1)"));
    }

    #[test]
    fn test_cube_error_invalid_value() {
        let err = CubeError::InvalidValue {
            value: 5,
            position: 2,
        };
        let msg = err.to_string();
        assert!(msg.contains("Invalid cube value 5"));
        assert!(msg.contains("position 2"));
    }

    #[test]
    fn test_minimization_error_from_instance_error() {
        let inst_err = InstanceError::DimensionMismatch {
            requested: (2, 1),
            existing: (3, 2),
        };
        let min_err: MinimizationError = inst_err.into();
        assert!(matches!(min_err, MinimizationError::Instance(_)));
        assert!(min_err.source().is_some());
    }

    #[test]
    fn test_minimization_error_from_cube_error() {
        let cube_err = CubeError::InvalidValue {
            value: 5,
            position: 2,
        };
        let min_err: MinimizationError = cube_err.into();
        assert!(matches!(min_err, MinimizationError::Cube(_)));
    }

    #[test]
    fn test_minimization_error_from_io_error() {
        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
        let min_err: MinimizationError = io_err.into();
        assert!(matches!(min_err, MinimizationError::Io(_)));
    }

    #[test]
    fn test_instance_error_to_io_error() {
        let err = InstanceError::DimensionMismatch {
            requested: (2, 1),
            existing: (3, 2),
        };
        let io_err: io::Error = err.into();
        assert_eq!(io_err.kind(), io::ErrorKind::Other);
    }

    #[test]
    fn test_cube_error_to_io_error() {
        let err = CubeError::InvalidValue {
            value: 5,
            position: 2,
        };
        let io_err: io::Error = err.into();
        assert_eq!(io_err.kind(), io::ErrorKind::InvalidData);
    }

    #[test]
    fn test_minimization_error_to_io_error_preserves_io_error() {
        let original_io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
        let min_err = MinimizationError::Io(original_io_err);
        let io_err: io::Error = min_err.into();
        assert_eq!(io_err.kind(), io::ErrorKind::NotFound);
        assert_eq!(io_err.to_string(), "file not found");
    }

    #[test]
    fn test_minimization_error_instance_to_io_error() {
        let inst_err = InstanceError::DimensionMismatch {
            requested: (2, 1),
            existing: (3, 2),
        };
        let min_err = MinimizationError::Instance(inst_err);
        let io_err: io::Error = min_err.into();
        assert_eq!(io_err.kind(), io::ErrorKind::Other);
    }

    #[test]
    fn test_minimization_error_cube_to_io_error() {
        let cube_err = CubeError::InvalidValue {
            value: 5,
            position: 2,
        };
        let min_err = MinimizationError::Cube(cube_err);
        let io_err: io::Error = min_err.into();
        assert_eq!(io_err.kind(), io::ErrorKind::InvalidData);
    }

    #[test]
    fn test_instance_error_allocation_failure() {
        let err = InstanceError::AllocationFailure { requested: (4, 2) };
        let msg = err.to_string();
        assert!(msg.contains("Failed to allocate"));
        assert!(msg.contains("(4, 2)"));
        assert!(msg.contains("out of memory"));
    }

    #[test]
    fn test_instance_error_allocation_failure_to_io_error() {
        let err = InstanceError::AllocationFailure { requested: (4, 2) };
        let io_err: io::Error = err.into();
        assert_eq!(io_err.kind(), io::ErrorKind::Other);
    }

    #[test]
    fn test_minimization_error_from_allocation_failure() {
        let inst_err = InstanceError::AllocationFailure { requested: (4, 2) };
        let min_err: MinimizationError = inst_err.into();
        assert!(matches!(min_err, MinimizationError::Instance(_)));
        assert!(min_err.source().is_some());
    }
}
