pub use atom_file::Data;
use std::hash::Hash;
use std::sync::Arc;

/// Value for Hash Map lookup.
pub trait SmallFixed {
    fn size() -> usize;
    fn load(bytes: &[u8]) -> Self;
    fn save(&self, bytes: &mut [u8]);
}

/// Key for Hash Map lookup.
pub trait Key<T>: Hash
where
    T: SmallFixed,
{
    /// Check that hash lookup has found correct value. Returns the checked value (or None if it does not match key).
    fn ok(&self, addr: &T, ps: &mut PageSet) -> Option<Value>;
}

/// [PageSet] - keeps track of changed pages that need saving.
pub mod pageset;
pub use pageset::*;

/// [HashMap] - maps keys to values using 64 bit hash.
pub mod hashmap;
pub use hashmap::*;

/// [Table] -- fixed size record storage. Divided into pages, each page stores up to 64 records.
pub mod table;
pub use table::*;

/// [VarValAddr] -- storage of variable length values.
pub mod varval;
pub use varval::*;

/// ToDo
pub mod sys;
pub use sys::*;

/// [DataType] -- describes data type
pub mod datatype;
pub use datatype::*;

/// [Value] -- generic instance of a data type
pub mod value;
pub use value::*;

// Private modules.

/// [PageTree] - list of pages implemented as tree.
mod pagetree;
use pagetree::*;

/// [TreeVec] -- a variable length list of bytes. Used to implement [Table].
mod treevec;
use treevec::*;

/// Hash Map Bucket - maps keys to values using 64 bit hash.
mod bucket;

const PAGE_SIZE: u64 = 4612;
// pub const PAGE_SIZE: u64 =  4612;

use pstd::localalloc::Local;
/// `StringA<Local>`
pub type LString = pstd::StringA<Local>;
/// `VecA<T, Local>`
pub type LVec<T> = pstd::VecA<T, Local>;
/// `BoxA<T, Local>`
pub type LBox<T> = pstd::BoxA<T, Local>;

#[test]
fn test_main() {
    use page_store::*;

    let limits = Limits::default();
    /*
    let _dt = DataType::String{};
    let _dt = DataType::Binary{};
    let _dt = DataType::Int(4);
    let _dt = DataType::Array( 5, Box::new(DataType::String) );
    let _dt = DataType::Map( Box::new(DataType::String), Box::new(DataType::String) );
    let _dt = DataType::Struct( Vec::new() );
    */

    // Construct BlockPageStg.
    let file = atom_file::MultiFileStorage::new("test.db");
    let upd = atom_file::FastFileStorage::new("test.upd");
    let af = atom_file::AtomicFile::new_with_limits(file, upd, &limits.af_lim);
    let ps = BlockPageStg::new(af, &limits);
    let is_new = ps.is_new();

    let spd = SharedPagedData::new_from_ps(ps);

    println!("max page size={}", spd.psi.max_size_page());

    let mut ps = PageSet::new(spd.new_writer());

    if false {
        let (root, len) = if is_new { (ps.new_page(), 0) } else { (3, 410) };
        test_tv(root, len, &mut ps);
    }

    if false {
        test_table(&mut ps);
    }

    if false {
        test_varval(&mut ps);
    }

    test_cust(&mut ps);

    // ps.save();

    spd.shutdown();
}

#[cfg(test)]
fn tos(s: &[u8]) -> &str {
    str::from_utf8(s).unwrap()
}

/*
   For a table with very fragmented ids, you can make a new table with new compact ids, and map the old ids to the new ones using a PagedHashMap.

   Access is slightly slower, due to the extra Hash lookup.
*/

#[cfg(test)]
#[derive(Hash)] // Not quite right.
struct CustKey<'a> {
    rid: i64,
    cdt: &'a DataType,
}

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

#[cfg(test)]
#[derive(Hash)] // Not quite right.
struct CustEmailKey<'a> {
    email: &'a str,
    cdt: &'a DataType,
}

#[cfg(test)]
impl<'a> Key<VarValAddr> for CustEmailKey<'a> {
    fn ok(&self, addr: &VarValAddr, ps: &mut PageSet) -> Option<Value> { 
        let dt = DataType::IList;

        // Get the list of ids.
        let mut list = addr.get(&dt, ps);

        // Check the email of first element in the list.
        let rid = list.ilist()[0];
        let key = CustKey{ rid, cdt: self.cdt };

        // Use the HashMap to retrieve the cust record.
        let hms = ps.cust_hashmap;
        let mut hm = HashMap::restore(hms, ps);
        let mut cr = hm.get(&key).unwrap();

        // Check the email address in the customer record matches self.email
        let email = cr.list()[2].string();

        if *self.email == **email
        {
            return Some(list);
        }
        None
    }
}

#[cfg(test)]
fn init(ps: &mut PageSet) {
    let hm = HashMap::<VarValAddr>::new(ps, 1);
    ps.cust_hashmap = hm.save();

    let hm = HashMap::<VarValAddr>::new(ps, 1);
    ps.cust_by_email = hm.save();
}

#[cfg(test)]
fn cust_dt() -> DataType {
    use pstd::veca;
    DataType::Struct(veca![
        (LString::from("Id"), DataType::Int),
        (LString::from("Name"), DataType::String),
        (LString::from("Email"), DataType::String),
        (LString::from("Postal"), DataType::String)
    ])
}

#[cfg(test)]
fn make_cust(ps: &mut PageSet, cdt: &DataType, rid: i64, name: &str, email: &str, postal: &str) {
    use pstd::veca;

    // Set up a customer record in memory.
    let mut cr = Value::List(veca![
        Value::Int(rid),
        Value::String(LString::from(name)),
        Value::String(LString::from(email)),
        Value::String(LString::from(postal)),
    ]);

    println!("make_cust cr={:?}", &cr);

    // Store the customer record ( which has variable length ).
    let addr = VarValAddr::new(cdt, &mut cr, ps);

    {
        // Insert the mapping rid -> addr into the HashMap.
        let key = CustKey{rid, cdt};
        let mut hm = HashMap::restore(ps.cust_hashmap, ps);
        hm.insert(&key, addr);
        if hm.root_changed() {
            ps.cust_hashmap = hm.save();
        }
    }

    let emkey = CustEmailKey { email, cdt };

    // Get list of ids associated with cust email.
    let list = {
        let mut hm = HashMap::restore(ps.cust_by_email, ps);
        let list = hm.remove(&emkey);
        if hm.root_changed() {
            ps.cust_by_email = hm.save();
        }
        list
    };

    let dt2 = DataType::IList;
    let addr = if let Some(mut list) = list {
        // List already exists, append to it.
        list.ilist().push(rid);
        addr.update(&dt2, &mut list, ps)
    } else {
        // Create list with single element and store it as VarVal.
        let mut list = Value::IList(veca![rid]);
        VarValAddr::new(&dt2, &mut list, ps)
    };

    // Associate addr with emkey.
    let mut hm = HashMap::restore(ps.cust_by_email, ps);
    hm.insert(&emkey, addr);
    if hm.root_changed() {
        ps.cust_by_email = hm.save();
    }
}

#[cfg(test)]
fn list_cust(ps: &mut PageSet, cdt: &DataType, email: &str) {
    // Print customer ids with specified email address.

    let key = CustEmailKey { email, cdt };
    let mut hm = HashMap::restore(ps.cust_by_email, ps);

    if let Some(mut list) = hm.get(&key) {
        println!("List of cust ids for {} = {:?}", email, list.ilist());
    } else {
        println!("No cust ids found for email {}", email);
    }
}

#[cfg(test)]
fn test_cust(ps: &mut PageSet) {
    init(ps);
    let cdt = cust_dt();

    make_cust(
        ps,
        &cdt,
        99,
        "George Barwood",
        "george@gmail.com",
        "33 Sandpipe Close, GL2 4LZ",
    );
    // Make a duplicate
    make_cust(
        ps,
        &cdt,
        100,
        "George Barwood",
        "george@gmail.com",
        "33 Sandpipe Close, GL2 4LZ",
    );
    // Another duplicate
    make_cust(
        ps,
        &cdt,
        101,
        "George Barwood",
        "george@gmail.com",
        "33 Sandpipe Close, GL2 4LZ",
    );

    make_cust(
        ps,
        &cdt,
        102,
        "Marilyn Barwood",
        "maz.barwood@gmail.com",
        "33 Sandpipe Close, GL2 4LZ",
    );

    list_cust(ps, &cdt, "george@gmail.com");
    list_cust(ps, &cdt, "maz.barwood@gmail.com");
    list_cust(ps, &cdt, "mary@gmail.com");
}
