use crate::errors::{ProblemError, ProblemResult};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::fmt;

/// type of decision variable
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum VariableType {
    /// continuous var (any real value within bounds)
    Continuous,
    /// integer var (only int values within bounds)  
    Integer,
    /// binary var (only 0 or 1)
    Binary,
}

/// decision variable in optimization problem
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Variable {
    pub name: String,
    pub lower_bound: f64,
    pub upper_bound: f64,
    pub var_type: VariableType,
}

impl Variable {
    /// creates new continuous var w/ given name & bounds
    pub fn new_continuous(name: impl Into<String>, lower_bound: f64, upper_bound: f64) -> ProblemResult<Self> {
        let name = name.into();
        if lower_bound > upper_bound {
            return Err(ProblemError::InvalidBounds {
                lower: lower_bound,
                upper: upper_bound,
            });
        }
        
        Ok(Variable {
            name,
            lower_bound,
            upper_bound,
            var_type: VariableType::Continuous,
        })
    }

    /// Creates a new integer variable with the given name and bounds
    pub fn new_integer(name: impl Into<String>, lower_bound: f64, upper_bound: f64) -> ProblemResult<Self> {
        let name = name.into();
        if lower_bound > upper_bound {
            return Err(ProblemError::InvalidBounds {
                lower: lower_bound,
                upper: upper_bound,
            });
        }
        
        Ok(Variable {
            name,
            lower_bound,
            upper_bound,
            var_type: VariableType::Integer,
        })
    }

    /// Creates a new binary variable with the given name
    pub fn new_binary(name: impl Into<String>) -> Self {
        Variable {
            name: name.into(),
            lower_bound: 0.0,
            upper_bound: 1.0,
            var_type: VariableType::Binary,
        }
    }

    /// Creates a new non-negative continuous variable
    pub fn new_non_negative(name: impl Into<String>) -> Self {
        Variable {
            name: name.into(),
            lower_bound: 0.0,
            upper_bound: f64::INFINITY,
            var_type: VariableType::Continuous,
        }
    }

    /// Creates a new free (unbounded) continuous variable
    pub fn new_free(name: impl Into<String>) -> Self {
        Variable {
            name: name.into(),
            lower_bound: f64::NEG_INFINITY,
            upper_bound: f64::INFINITY,
            var_type: VariableType::Continuous,
        }
    }

    /// Checks if the variable is bounded from below
    pub fn is_lower_bounded(&self) -> bool {
        self.lower_bound.is_finite()
    }

    /// Checks if the variable is bounded from above
    pub fn is_upper_bounded(&self) -> bool {
        self.upper_bound.is_finite()
    }

    /// Checks if the variable is bounded (both lower and upper bounds are finite)
    pub fn is_bounded(&self) -> bool {
        self.is_lower_bounded() && self.is_upper_bounded()
    }

    /// Checks if the variable is free (unbounded in both directions)
    pub fn is_free(&self) -> bool {
        !self.is_lower_bounded() && !self.is_upper_bounded()
    }

    /// Checks if a value is feasible for this variable
    pub fn is_feasible(&self, value: f64) -> bool {
        value >= self.lower_bound - f64::EPSILON && 
        value <= self.upper_bound + f64::EPSILON &&
        (self.var_type != VariableType::Integer || (value - value.round()).abs() < f64::EPSILON) &&
        (self.var_type != VariableType::Binary || (value == 0.0 || value == 1.0))
    }

    /// Projects a value onto the feasible region of this variable
    pub fn project(&self, value: f64) -> f64 {
        let projected = value.max(self.lower_bound).min(self.upper_bound);
        
        match self.var_type {
            VariableType::Continuous => projected,
            VariableType::Integer => projected.round(),
            VariableType::Binary => {
                if projected < 0.5 { 0.0 } else { 1.0 }
            }
        }
    }
}

impl fmt::Display for Variable {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let type_str = match self.var_type {
            VariableType::Continuous => "continuous",
            VariableType::Integer => "integer",
            VariableType::Binary => "binary",
        };
        
        write!(f, "{} ({}, [{}, {}])", 
               self.name, type_str, self.lower_bound, self.upper_bound)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use approx::assert_abs_diff_eq;

    #[test]
    fn test_continuous_variable_creation() {
        let var = Variable::new_continuous("x1", 0.0, 10.0).unwrap();
        assert_eq!(var.name, "x1");
        assert_eq!(var.lower_bound, 0.0);
        assert_eq!(var.upper_bound, 10.0);
        assert_eq!(var.var_type, VariableType::Continuous);
    }

    #[test]
    fn test_invalid_bounds() {
        let result = Variable::new_continuous("x1", 10.0, 5.0);
        assert!(result.is_err());
        if let Err(ProblemError::InvalidBounds { lower, upper }) = result {
            assert_eq!(lower, 10.0);
            assert_eq!(upper, 5.0);
        }
    }

    #[test]
    fn test_binary_variable() {
        let var = Variable::new_binary("b1");
        assert_eq!(var.name, "b1");
        assert_eq!(var.lower_bound, 0.0);
        assert_eq!(var.upper_bound, 1.0);
        assert_eq!(var.var_type, VariableType::Binary);
    }

    #[test]
    fn test_feasibility_checks() {
        let cont_var = Variable::new_continuous("x", 0.0, 10.0).unwrap();
        assert!(cont_var.is_feasible(5.0));
        assert!(cont_var.is_feasible(0.0));
        assert!(cont_var.is_feasible(10.0));
        assert!(!cont_var.is_feasible(-1.0));
        assert!(!cont_var.is_feasible(11.0));

        let int_var = Variable::new_integer("i", 0.0, 10.0).unwrap();
        assert!(int_var.is_feasible(5.0));
        assert!(!int_var.is_feasible(5.5));

        let bin_var = Variable::new_binary("b");
        assert!(bin_var.is_feasible(0.0));
        assert!(bin_var.is_feasible(1.0));
        assert!(!bin_var.is_feasible(0.5));
    }

    #[test]
    fn test_projection() {
        let var = Variable::new_continuous("x", 0.0, 10.0).unwrap();
        assert_abs_diff_eq!(var.project(-1.0), 0.0);
        assert_abs_diff_eq!(var.project(5.0), 5.0);
        assert_abs_diff_eq!(var.project(11.0), 10.0);

        let int_var = Variable::new_integer("i", 0.0, 10.0).unwrap();
        assert_abs_diff_eq!(int_var.project(5.7), 6.0);

        let bin_var = Variable::new_binary("b");
        assert_abs_diff_eq!(bin_var.project(0.3), 0.0);
        assert_abs_diff_eq!(bin_var.project(0.7), 1.0);
    }
}