//! Handling of lua types

use std::collections::HashMap;
use std::ffi::{CStr, CString, IntoStringError};
use std::fmt::{Debug, Display};
use std::hash::{Hash, Hasher};
use std::string::FromUtf8Error;

/// Byte marking that the serialized object is a Float
pub const LUA_FLOAT_MARKER: u8 = 0;
/// Byte marking that the serialized object is a String
pub const LUA_STRING_MARKER: u8 = 1;
/// Byte marking that the serialized object is Nil
pub const LUA_NIL_MARKER: u8 = 2;
/// Byte marking that the serialized object is a Boolean
pub const LUA_BOOL_MARKER: u8 = 3;
/// Byte marking that the serialized object is a Table
pub const LUA_TABLE_MARKER: u8 = 4;
/// Byte marking the end of a serialized Table
pub const LUA_END_MARKER: u8 = 5;

pub type LuaTable = HashMap<LuaObject, LuaObject>;

// TODO: Enumerate types for better error messages
#[derive(Debug)]
pub struct LuaTypeError {}

#[derive(Debug)]
pub enum LuaObject {
    Float(f32),
    String(CString),
    Unicode(String),
    Nil,
    Bool(bool),
    Table(LuaTable),
}

impl From<FromUtf8Error> for LuaTypeError {
    fn from(_err: FromUtf8Error) -> LuaTypeError {
        LuaTypeError {}
    }
}

impl From<IntoStringError> for LuaTypeError {
    fn from(_err: IntoStringError) -> LuaTypeError {
        LuaTypeError {}
    }
}

impl From<f32> for LuaObject {
    fn from(data: f32) -> LuaObject {
        LuaObject::Float(data)
    }
}

impl From<CString> for LuaObject {
    fn from(data: CString) -> LuaObject {
        LuaObject::String(data)
    }
}

impl From<String> for LuaObject {
    fn from(data: String) -> LuaObject {
        LuaObject::Unicode(data)
    }
}

impl From<Box<CStr>> for LuaObject {
    fn from(data: Box<CStr>) -> LuaObject {
        LuaObject::String(data.into_c_string())
    }
}

impl From<&str> for LuaObject {
    fn from(data: &str) -> LuaObject {
        LuaObject::Unicode(data.to_string())
    }
}

impl From<bool> for LuaObject {
    fn from(data: bool) -> LuaObject {
        LuaObject::Bool(data)
    }
}

impl Eq for LuaObject {}
impl PartialEq for LuaObject {
    fn eq(&self, other: &LuaObject) -> bool {
        use LuaObject::*;

        match (self, other) {
            (Float(f1), Float(f2)) => f1.eq(f2),
            // String comparisons
            (String(s1), String(s2)) => s1.eq(s2),
            (Unicode(s1), Unicode(s2)) => s1.eq(s2),
            (String(s1), Unicode(s2)) => s1.as_bytes().eq(s2.as_bytes()),
            (Unicode(s1), String(s2)) => s1.as_bytes().eq(s2.as_bytes()),
            // end string comparisons
            (Nil, Nil) => true,
            (Bool(b1), Bool(b2)) => b1.eq(b2),
            (Table(_t1), Table(_t2)) => panic!("Can't compare type 'table' to type 'table'"),
            _ => false,
        }
    }
}

impl Hash for LuaObject {
    fn hash<H: Hasher>(&self, state: &mut H) {
        use LuaObject::*;

        match self {
            Float(f) => unsafe {
                let ptr = if f.is_nan() { &std::f32::NAN } else { f } as *const f32;
                state.write(&*(ptr as *const [u8; 4]))
            },
            String(s) => s.hash(state),
            Unicode(s) => s.hash(state),
            Nil => ().hash(state),
            Bool(b) => b.hash(state),
            Table(_) => panic!("Unhashable type 'table'"),
        }
    }
}

impl Display for LuaObject {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        use LuaObject::*;

        match self {
            Float(f_) => Display::fmt(f_, f),
            String(s) => Debug::fmt(s, f),
            Unicode(s) => Display::fmt(s, f),
            Nil => write!(f, "Nil"),
            Bool(b) => Display::fmt(b, f),
            Table(ref t) => {
                if t.is_empty() {
                    return write!(f, "{{}}");
                }
                match f.alternate() {
                    false => format_table(t, f),
                    true => format_table_pretty(t, f),
                }
            }
        }
    }
}

fn format_table(table: &LuaTable, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "{{")?;

    let mut iter = table.iter();
    for _ in 0..table.len() - 1 {
        let (k, v) = iter.next().unwrap();
        write!(f, "{}: {}, ", k, v)?;
    }

    let (k, v) = iter.next().unwrap();
    write!(f, "{}: {}}}", k, v)
}

fn format_table_pretty(table: &LuaTable, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    let width = f.width().unwrap_or(4);
    let fill = f.fill();
    let mut pad_str = String::with_capacity(width);
    for _ in 0..width {
        pad_str.push(fill);
    }

    write!(f, "{{\n")?;
    let mut iter = table.iter();
    for _ in 0..table.len() - 1 {
        let (k, v) = iter.next().unwrap();
        write!(f, "{}{:>#4}: {:#},\n", pad_str, k, v)?;
    }
    let (k, v) = iter.next().unwrap();
    write!(f, "{}{:>#4}: {:#}\n}}", pad_str, k, v)
}

impl LuaObject {
    /// Evaluate the contents of this object.
    /// For String and Table, returns false if the object is empty.
    /// For Number returns false on `0`.
    /// For Bool returns the bool.
    /// For Nil returns false.
    pub fn evaluate_as_bool(&self) -> bool {
        use LuaObject::*;
        match self {
            Float(f) => *f == 0.0,
            String(s) => !s.as_bytes().is_empty(),
            Unicode(s) => !s.is_empty(),
            Nil => false,
            Bool(b) => *b,
            Table(t) => !t.is_empty(),
        }
    }

    pub fn as_float(&self) -> Result<f32, LuaTypeError> {
        match self {
            LuaObject::Float(f) => Ok(*f),
            _ => Err(LuaTypeError {}),
        }
    }

    pub fn into_string(self) -> Result<String, LuaTypeError> {
        match self {
            LuaObject::String(s) => Ok(s.into_string()?),
            LuaObject::Unicode(s) => Ok(s),
            _ => Err(LuaTypeError {}),
        }
    }

    pub fn to_string(&self) -> Result<String, LuaTypeError> {
        match self {
            LuaObject::String(s) => Ok(String::from_utf8(s.as_bytes().to_vec())?),
            LuaObject::Unicode(s) => Ok(s.clone()),
            _ => Err(LuaTypeError {}),
        }
    }

    pub fn to_string_lossy(&self) -> Result<String, LuaTypeError> {
        match self {
            LuaObject::String(s) => Ok(String::from_utf8_lossy(s.as_bytes()).to_string()),
            LuaObject::Unicode(s) => Ok(s.clone()),
            _ => Err(LuaTypeError {}),
        }
    }

    pub fn as_nil(&self) -> Result<(), LuaTypeError> {
        match self {
            LuaObject::Nil => Ok(()),
            _ => Err(LuaTypeError {}),
        }
    }

    pub fn as_bool(&self) -> Result<bool, LuaTypeError> {
        match self {
            LuaObject::Bool(b) => Ok(*b),
            _ => Err(LuaTypeError {}),
        }
    }

    pub fn into_hashmap(self) -> Result<LuaTable, LuaTypeError> {
        match self {
            LuaObject::Table(t) => Ok(t),
            _ => Err(LuaTypeError {}),
        }
    }

    pub fn as_hashmap(&self) -> Result<&LuaTable, LuaTypeError> {
        match self {
            LuaObject::Table(ref t) => Ok(t),
            _ => Err(LuaTypeError {}),
        }
    }
}
