use crate::*;
use pstd::veca;
use std::hash::Hasher;

// obj inner enum values
const SCHEMA: usize = 0;
const TABLE: usize = 1;

// obj field numbers
const OBJ_ID: usize = 0;
const _OBJ_SCHEMA: usize = 1;
const OBJ_NAME: usize = 2;
const OBJ_INNER: usize = 3;

/// DataType for sys object ( Schema, Table, Index, Function etc. )
pub fn make_obj_dt() -> DataType {
    use pstd::veca;

    let objenum = DataType::Enum(veca![
        (LString::from("Schema"), DataType::Empty),
        (LString::from("Table"), DataType::TabInfo),
    ]);

    DataType::Struct(veca![
        (LString::from("Id"), DataType::Int),
        (LString::from("Schema"), DataType::Int),
        (LString::from("Name"), DataType::String),
        (LString::from("Inner"), objenum),
    ])
}

/// Can be used for any record which has id as first elem of list.
pub struct IdKey<'a> {
    pub rid: i64,
    pub dt: &'a DataType,
}

impl<'a> IdKey<'a> {
    pub fn insert(&self, r: &Value, main: &mut BuckMapInfo, ps: &mut PageSet) {
        // Store the record.
        let addr = VarValAddr::new(self.dt, r, ps);

        // Associate the id with the addr.
        let mut bm = BuckMap::restore(*main, ps);
        bm.insert(self, addr);
        if bm.root_changed() {
            *main = bm.save();
        }
    }
}

impl<'a> Hash for IdKey<'a> {
    fn hash<H: Hasher>(&self, h: &mut H) {
        self.rid.hash(h);
    }
}

impl<'a> Key<VarValAddr> for IdKey<'a> {
    fn ok(&self, addr: VarValAddr, ps: &mut PageSet) -> Option<(VarValAddr, Value)> {
        let v = addr.get(self.dt, ps);
        let rid = v.list()[0].int();
        if rid == self.rid {
            return Some((addr, v));
        }
        None
    }
}

/// Get obj address and value from specified id.
pub fn get_obj(rid: i64, ps: &mut PageSet) -> Option<(VarValAddr, Value)> {
    let dt = ps.obj_dt.clone();
    let key = IdKey { rid, dt: &dt };
    BuckMap::restore(ps.obj_map, ps).get(&key)
}

#[derive(Hash)]
pub struct ObjNameKey<'a> {
    name: &'a str,
}

impl<'a> Key<i64> for ObjNameKey<'a> {
    fn ok(&self, addr: i64, ps: &mut PageSet) -> Option<(i64, Value)> {
        let (_, v) = get_obj(addr, ps).unwrap();
        let name = v.list()[OBJ_NAME].string();
        if self.name == name {
            return Some((addr, v));
        }
        None
    }
}

/// Print all objects ( debug ).
pub fn list_obj_all(ps: &mut PageSet) {
    let mut x = BuckMap::<VarValAddr>::restore(ps.obj_map, ps).all_addr();

    x.sort();

    let dt = ps.obj_dt.clone();

    for addr in x {
        let r = addr.get(&dt, ps);
        println!("list_obj_all addr={:?} r={:?}", addr, r);
    }
}

/// Save new object and save the address in primary object map.
fn create_obj_inner(rid: i64, r: Value, ps: &mut PageSet) {
    let dt = ps.obj_dt.clone();

    let addr = VarValAddr::new(&dt, &r, ps);

    let key = IdKey { rid, dt: &dt };

    let mut bm = BuckMap::restore(ps.obj_map, ps);
    bm.insert(&key, addr);
    if bm.root_changed() {
        ps.obj_map = bm.save();
    }
}

/// Map the given object name to the object id.
fn index_obj_by_name(id: i64, name: &str, ps: &mut PageSet) {
    let key = ObjNameKey { name };
    let mut bm = BuckMap::restore(ps.obj_by_name, ps);
    bm.insert(&key, id);
    if bm.root_changed() {
        ps.obj_by_name = bm.save();
    }
}

/// Create obj with specified id, name and inner value.
pub fn create_obj(rid: i64, name: &str, inner: Value, ps: &mut PageSet) {
    let r = Value::List(veca![
        Value::Int(rid), // obj id
        Value::Int(0),   // obj schema
        Value::String(LString::from(name)),
        inner
    ]);

    create_obj_inner(rid, r, ps);
    index_obj_by_name(rid, name, ps);
}

/// Get the object id and Value for the supplied object name.
pub fn get_obj_from_name(name: &str, ps: &mut PageSet) -> Option<(i64, Value)> {
    let key = ObjNameKey { name };

    let mut bm = BuckMap::restore(ps.obj_by_name, ps);
    if let Some(x) = bm.get(&key) {
        return Some(x);
    }
    None
}

/// Save ( update ) the Value associated with the given address.
pub fn save_obj(addr: VarValAddr, obj: Value, ps: &mut PageSet) {
    let dt = ps.obj_dt.clone();
    let addr2 = addr.update(&dt, &obj, ps);
    if addr2 != addr {
        let rid = obj.list()[OBJ_ID].int();
        let key = IdKey { rid, dt: &dt };

        let mut bm = BuckMap::restore(ps.obj_map, ps);
        bm.remove(&key);
        bm.insert(&key, addr2);
        if bm.root_changed() {
            ps.obj_map = bm.save();
        }
    }
}

/// Create schema.
pub fn create_schema(name: &str, ps: &mut PageSet) -> Option<(i64, Value)> {
    // Check if obj already exists.
    let obj = get_obj_from_name(name, ps);

    if obj.is_some() {
        obj
    } else {
        let rid = ps.next_obj_id;
        ps.next_obj_id = rid + 1;
        let inner = Value::Enum(SCHEMA, LBox::new(Value::Empty));
        create_obj(rid, name, inner, ps);
        None
    }
}

fn set_table_datatype(rid: i64, tdt: DataType, ps: &mut PageSet) {
    let dt = DataType::Tuple(veca![DataType::Int, DataType::DataType]);
    let v = Value::List(veca![Value::Int(rid), Value::DataType(tdt)]);

    let addr = VarValAddr::new(&dt, &v, ps);
    let key = IdKey { rid, dt: &dt };
    let mut bm = BuckMap::restore(ps.table_datatype_map, ps);
    bm.insert(&key, addr);
    if bm.root_changed() {
        ps.table_datatype_map = bm.save();
    }
}

/// Get table DataType given table id.
pub fn get_table_datatype(rid: i64, ps: &mut PageSet) -> DataType {
    let dt = DataType::Tuple(veca![DataType::Int, DataType::DataType]);
    let key = IdKey { rid, dt: &dt };
    let mut bm = BuckMap::restore(ps.table_datatype_map, ps);
    let mut av = bm.get(&key).unwrap();
    let dt = av.1.list_mut()[1].datatype_mut();
    std::mem::take(dt)
}

/// Create a table. If it already exists, return id and value.
pub fn create_table(
    name: &str,
    dt: DataType,
    ix_count: usize,
    ps: &mut PageSet,
) -> Option<(i64, Value)> {
    // Check if obj already exists.
    let obj = get_obj_from_name(name, ps);

    if obj.is_some() {
        obj
    } else {
        let rid = ps.next_obj_id;
        ps.next_obj_id = rid + 1;

        let bm = BuckMap::<VarValAddr>::new(1, ps);
        let main = bm.save();

        let mut index = LVec::new();
        for _ in 0..ix_count {
            let bm = BuckMap::<VarValAddr>::new(1, ps);
            index.push(bm.save());
        }

        let tableinfo = Value::TabInfo(TabInfo {
            alloc: 0,
            main,
            index,
        });

        let inner = Value::Enum(TABLE, LBox::new(tableinfo));

        create_obj(rid, name, inner, ps);

        set_table_datatype(rid, dt, ps);

        None
    }
}

#[derive(
    Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct TabInfo {
    alloc: i64,
    pub main: BuckMapInfo,
    pub index: LVec<BuckMapInfo>,
}

impl TabInfo {
    pub fn id(&mut self) -> i64 {
        let result = self.alloc;
        self.alloc += 1;
        result
    }
    pub fn to_bytes(&self) -> Vec<u8> {
        postcard::to_stdvec(self).unwrap()
    }
    pub fn from_bytes(b: &[u8]) -> Self {
        postcard::from_bytes(b).unwrap()
    }
}

/// Get mut ref to TabInfo from Value.
pub fn decode_table(obj: &mut Value) -> &mut TabInfo {
    let (tag, inner) = obj.list_mut()[OBJ_INNER].en_mut();
    assert!(*tag == TABLE);
    inner.tabinfo_mut()
}

/// Update table data for the specified id.
pub fn save_table(rid: i64, obj: Value, ps: &mut PageSet) {
    let (addr, _) = get_obj(rid, ps).unwrap();
    save_obj(addr, obj, ps);
}
