use crate::{Session, SJFLError};
use crate::parse::{Variable, List, InternedString, Table};
use std::collections::HashMap;
use hmath::Ratio;

#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum Value {
    None,
    Boolean(bool),
    String(InternedString),
    Number(Ratio),
    Variable(Variable),
    List(List),
    Table(Table),
}

impl Value {

    pub(crate) fn substitute_vars(&mut self, session: &Session) -> Result<(), SJFLError> {

        match self {
            Value::List(l) => {

                for e in l.data.iter_mut() {
                    e.substitute_vars(session)?;
                }

            }
            Value::Table(t) => {

                for (k, v) in t.data.iter_mut() {
                    k.substitute_vars(session)?;
                    v.substitute_vars(session)?;
                }

            }
            Value::Variable(v) => {

                match session.get_value_from_var_index(v.index) {
                    Some(v) => {
                        *self = v;
                        self.substitute_vars(session)?;  // some vars may be nested
                    }
                    _ => {
                        return Err(SJFLError::NotDefinedVar(v.clone()))
                    }
                }

            }
            _ => {}
        }

        Ok(())
    }

    /// It returns a valid sjfl string. That means it may include quotation marks.
    /// If you want to unwrap a string value, use `try_unwrap_string` function.
    pub fn to_string(&self, session: &Session) -> String {

        match self {
            Value::None => format!("null"),
            Value::Boolean(b) => format!("{b}"),
            Value::String(s) => format!(
                "{:?}",
                match session.get_string_from_index(s.index) {
                    Some(s) => String::from_utf8_lossy(&s).to_string(),
                    None => String::from("\"\"")
                }
            ),
            Value::Number(n) => format!("{}", n.to_approx_string(100)),
            Value::List(l) => format!(
                "[{}]",
                l.data.iter().map(|e| e.to_string(session)).collect::<Vec<String>>().join(", ")
            ),
            Value::Table(t) => format!(
                "{}{}{}",
                "{",
                t.data.iter().map(
                    |(key, value)|
                    format!("{}: {}", key.to_string(session), value.to_string(session))
                ).collect::<Vec<String>>().join(", "),
                "}"
            ),
            Value::Variable(var) => format!(
                "{}",
                match session.get_string_from_index(var.index) {
                    Some(s) => String::from_utf8_lossy(&s).to_string(),
                    None => String::from("UNKNOWN_VARIABLE")
                }
            ),
        }
    }

    pub fn is_none(&self) -> bool {
        match self {
            Value::None => true,
            _ => false
        }
    }

    pub fn is_bool(&self) -> bool {
        match self {
            Value::Boolean(_) => true,
            _ => false
        }
    }

    pub fn try_unwrap_bool(&self) -> Option<&bool> {
        match self {
            Value::Boolean(b) => Some(b),
            _ => None
        }
    }

    pub fn is_string(&self) -> bool {
        match self {
            Value::String(_) => true,
            _ => false
        }
    }

    pub fn is_valid_string(&self, session: &Session) -> bool {
        match self {
            Value::String(InternedString { index }) if session.is_valid_index(*index) => true,
            _ => false
        }
    }

    pub fn try_unwrap_string(&self, session: &Session) -> Option<String> {
        match self {
            Value::String(InternedString { index }) => match session.get_string_from_index(*index) {
                Some(buf) => Some(String::from_utf8_lossy(&buf).to_string()),
                _ => None
            },
            _ => None
        }
    }

    pub fn new_string(s: &str, session: &mut Session) -> Self {
        let index = session.intern_string(s.as_bytes());

        Value::String(InternedString { index })
    }

    pub fn is_number(&self) -> bool {
        match self {
            Value::Number(_) => true,
            _ => false
        }
    }

    pub fn try_unwrap_number(&self) -> Option<&Ratio> {
        match self {
            Value::Number(n) => Some(n),
            _ => None
        }
    }

    /// using strings is more accurate than f32 and f64
    pub fn number_from_string(s: &str) -> Result<Self, hmath::ConversionError> {
        let n = Ratio::from_string(s)?;

        Ok(Value::Number(n))
    }

    pub fn is_list(&self) -> bool {
        match self {
            Value::List(_) => true,
            _ => false
        }
    }

    pub fn try_unwrap_list(&self) -> Option<&Vec<Box<Value>>> {
        match self {
            Value::List(l) => Some(&l.data),
            _ => None
        }
    }

    pub fn is_table(&self) -> bool {
        match self {
            Value::Table(_) => true,
            _ => false
        }
    }

    pub fn try_unwrap_table(&self) -> Option<&Vec<(Box<Value>, Box<Value>)>> {
        match self {
            Value::Table(t) => Some(&t.data),
            _ => None
        }
    }

}

impl From<bool> for Value {

    fn from(b: bool) -> Self {
        Value::Boolean(b)
    }

}

impl<T: Into<Value>> From<Vec<T>> for Value {

    fn from(v: Vec<T>) -> Self {
        Value::List(List { data: v.into_iter().map(|v| Box::new(v.into())).collect() })
    }

}

impl<K: Into<Value>, V: Into<Value>> From<HashMap<K, V>> for Value {

    fn from(t: HashMap<K, V>) -> Self {
        Value::Table(Table { data: t.into_iter().map(|(k, v)| (Box::new(k.into()), Box::new(v.into()))).collect() })
    }

}

impl<T1: Into<Value>, T2: Into<Value>> From<(T1, T2)> for Value {

    fn from(t: (T1, T2)) -> Self {
        Value::List(List { data: vec![Box::new(t.0.into()), Box::new(t.1.into())] })
    }

}

macro_rules! impl_into_number {
    ($t: ty) => (
        impl From<$t> for Value {
            fn from(n: $t) -> Self {
                Value::Number(n.into())
            }
        }
    );
    ($t: ty, $($u: ty), +) => (
        impl_into_number!($t);
        impl_into_number!($($u),+);
    )
}

impl_into_number!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize);

impl From<f32> for Value {
    fn from(n: f32) -> Self {
        Value::Number(n.try_into().unwrap_or(Ratio::zero()))
    }
}

impl From<f64> for Value {
    fn from(n: f64) -> Self {
        Value::Number(n.try_into().unwrap_or(Ratio::zero()))
    }
}