// db created and exported from here
extern crate lmdb_rs as lmdb;

use lmdb::{
    DbFlags,
    DbHandle,
    EnvBuilder,
    Environment,
};
use log::{
    debug,
    error,
};

use crate::utils;

/// LMDB Interface allows access to the env
///
/// and handle for the write, read and delete
///
/// functionality.
pub struct Interface {
    pub env: Environment,
    pub handle: DbHandle,
}

impl Interface {
    /// Instantiation of ```Environment``` and ```DbHandle```
    pub fn open() -> Self {
        let release_env = utils::get_release_env();
        let file_path = format!(
            "/home/{}/.{}/",
            std::env::var("USER").unwrap_or(String::from("user")),
            crate::APP_NAME,
        );
        let mut env_str: &str = "test-lmdb";
        if release_env != utils::ReleaseEnvironment::Development {
            env_str = "lmdb";
        };
        let env = EnvBuilder::new()
            .open(format!("{}/{}", file_path, env_str), 0o777)
            .expect(&format!("could not open LMDB at {}", file_path));
        let handle = env.get_default_db(DbFlags::empty()).unwrap();
        Interface { env, handle }
    }
    pub async fn async_open() -> Self {
        tokio::time::sleep(std::time::Duration::from_micros(1)).await;
        self::Interface::open()
    }
    /// Write a key-value to LMDB. NEVEKO does not currently support
    ///
    /// writing multiple key value pairs.
    pub fn write(e: &Environment, h: &DbHandle, k: &str, v: &str) {
        let txn = e.new_transaction().unwrap();
        {
            // get a database bound to this transaction
            let db = txn.bind(&h);
            let pair = vec![(k, v)];
            for &(key, value) in pair.iter() {
                db.set(&key, &value).unwrap();
            }
        }
        match txn.commit() {
            Err(_) => error!("failed to commit!"),
            Ok(_) => (),
        }
    }
    pub async fn async_write(e: &Environment, h: &DbHandle, k: &str, v: &str) {
        tokio::time::sleep(std::time::Duration::from_micros(1)).await;
        self::Interface::write(e, h, k, v)
    }
    /// Read a value from LMDB by passing the key as a static
    ///
    /// string. If the value does not exist an empty string is
    ///
    /// returned. NEVEKO does not currently support duplicate keys.
    pub fn read(e: &Environment, h: &DbHandle, k: &str) -> String {
        let reader = e.get_reader().unwrap();
        let db = reader.bind(&h);
        let value = db.get::<&str>(&k).unwrap_or_else(|_| "");
        let r = String::from(value);
        {
            if r == utils::empty_string() {
                debug!("Failed to read from db.")
            }
        }
        r
    }
    pub async fn async_read(e: &Environment, h: &DbHandle, k: &str) -> String {
        tokio::time::sleep(std::time::Duration::from_micros(1)).await;
        self::Interface::read(e, h, k)
    }
    /// Delete a value from LMDB by passing the key as a
    ///
    /// static string. If the value does not exist then an
    ///
    /// error will be logged.
    pub fn delete(e: &Environment, h: &DbHandle, k: &str) {
        let txn = e.new_transaction().unwrap();
        {
            // get a database bound to this transaction
            let db = txn.bind(&h);
            db.del(&k).unwrap_or_else(|_| error!("failed to delete"));
        }
        match txn.commit() {
            Err(_) => error!("failed to commit!"),
            Ok(_) => (),
        }
    }
    pub async fn async_delete(e: &Environment, h: &DbHandle, k: &str) {
        tokio::time::sleep(std::time::Duration::from_micros(1)).await;
        self::Interface::delete(e, h, k)
    }
}

// Tests
//-------------------------------------------------------------------------------

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

    #[test]
    fn async_write_and_read_test() {
        // run and async cleanup so the test doesn't fail when deleting test data
        use tokio::runtime::Runtime;
        let rt = Runtime::new().expect("Unable to create Runtime for test");
        let _enter = rt.enter();
        tokio::spawn(async move {
            let s = Interface::async_open().await;
            let k = "async-test-key";
            let v = "async-test-value";
            Interface::async_write(&s.env, &s.handle, k, v).await;
            let expected = String::from(v);
            let actual = Interface::async_read(&s.env, &s.handle, k).await;
            assert_eq!(expected, actual);
            Interface::async_delete(&s.env, &s.handle, &k).await;
        });
    }

    #[test]
    fn async_write_and_delete_test() {
        // run and async cleanup so the test doesn't fail when deleting test data
        use tokio::runtime::Runtime;
        let rt = Runtime::new().expect("Unable to create Runtime for test");
        let _enter = rt.enter();
        tokio::spawn(async move {
            let s = Interface::open();
            let k = "write_and_delete_test_test-key";
            let v = "write_and_delete_test_test-value";
            Interface::async_write(&s.env, &s.handle, k, v).await;
            let expected = utils::empty_string();
            Interface::async_delete(&s.env, &s.handle, &k).await;
            let actual = Interface::async_read(&s.env, &s.handle, k).await;
            assert_eq!(expected, actual);
        });
    }
}