1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
use crate::*;
use std::path::{Path, PathBuf};
use std::fs;

/// Used for reading from a `LazyDB` with less boiler-plate
#[macro_export]
macro_rules! search_database {
    (($ldb:expr) /$($($con:ident)?$(($can:expr))?)/ *) => {(|| {
        let database = $ldb;
        let container = database.as_container()?;
        $(
            $(let container = container.read_container(stringify!($con))?;)?
            $(let container = container.read_container($can)?;)?
        )*
        let result: Result<LazyContainer, LDBError> = Ok(container);
        result
    })()};

    (($ldb:expr) /$($($con:ident)?$(($can:expr))?)/ *::$($item:ident)?$(($obj:expr))?) => {(|| {
        let container = search_database!(($ldb) /$($($con)?$(($can))?)/ *)?;
        $(let result: Result<LazyData, LDBError> = container.read_data(stringify!($item));)?
        $(let result: Result<LazyData, LDBError> = container.read_data($obj);)?
        result
    })()};

    (($ldb:expr) $($item:ident)?$(($obj:expr))?) => {(|| {
        let database = $ldb;
        let container = database.as_container()?;
        $(let result: Result<LazyData, LDBError> = container.read_data(stringify!($item));)?
        $(let result: Result<LazyData, LDBError> = container.read_data($obj);)?
        result
    })()};
}

/// Used for reading from a `LazyDB` with less boiler-plate
#[macro_export]
macro_rules! write_database {
    (($ldb:expr) $($item:ident)?$(($obj:expr))? = $func:ident($value:expr)) => {(|| {
        let database = $ldb;
        let container = database.as_container()?;
        $(LazyData::$func(container.data_writer(stringify!($item))?, $value)?;)?
        $(LazyData::$func(container.data_writer($obj)?, $value)?;)?
        Result::<(), LDBError>::Ok(())
    })()};

    (($ldb:expr) /$($($con:ident)?$(($can:expr))?)/ *::$($item:ident)?$(($obj:expr))? = $func:ident($value:expr)) => {(|| {
        let database = $ldb;
        let mut container = database.as_container()?;
        $({
            let con = $(stringify!($con))?$($can)?;
            container = match container.read_container(con) {
                Ok(x) => x,
                Err(LDBError::DirNotFound(_)) => container.new_container(con)?,
                Err(e) => return Err(e),
            }
        };)*

        $(LazyData::$func(container.data_writer(stringify!($item))?, $value)?;)?
        $(LazyData::$func(container.data_writer($obj)?, $value)?;)?
        Result::<(), LDBError>::Ok(())
    })()}
}

pub struct LazyDB {
    path: PathBuf,
    compressed: bool,
}

impl LazyDB {
    /// Initialises a new LazyDB directory at a specified path.
    /// 
    /// It will create the path if it doesn't already exist and initialise a metadata file with the current version of `lazy-db` if one doesn't exist already.
    /// 
    /// **WARNING:** if you initialise the database this way, you cannot compile it in future without errors being thrown!
    /// If you want to compile it, then use `LazyDB::init_db` instead.
    pub fn init(path: impl AsRef<Path>) -> Result<Self, LDBError> {
        let path = path.as_ref();

        // Check if path exists or not if init it
        if !path.is_dir() { unwrap_result!((fs::create_dir_all(path)) err => LDBError::IOError(err)) };
        
        { // Check if `.meta` file exists if not 
            let meta = path.join(".meta");
            if !meta.is_file() {
                // Write version
                LazyData::new_binary(
                    FileWrapper::new_writer(
                        unwrap_result!((fs::File::create(meta)) err => LDBError::IOError(err))
                    ), &[VERSION.major, VERSION.minor, VERSION.build],
                )?;
            }
        };

        // Construct Self
        Ok(Self {
            path: path.to_path_buf(),
            compressed: false,
        })
    }

    /// Initialise a new compiled `LazyDB` (compressed tarball) at the specified path.
    ///
    /// It will create the path if it doesn't already exist and initialise a metadata file with the current version of `lazy-db` if one doesn't exist already.
    pub fn init_db(path: impl AsRef<Path>) -> Result<Self, LDBError> {
        let dir_path = path.as_ref().with_extension("modb");
        let mut this = Self::init(dir_path)?;
        this.compressed = true;
        Ok(this)
    }

    /// Loads a pre-existing LazyDB directory at a specified path.
    /// 
    /// Loads LazyDB as `read-write` allowing for modification of the data within it.
    /// 
    /// If the LazyDB is invalid, it will return an error.
    pub fn load_dir(path: impl AsRef<Path>) -> Result<Self, LDBError> {
        let path = path.as_ref();

        // Checks if path exists
        if !path.is_dir() { return Err(LDBError::DirNotFound(path.to_path_buf())) };

        // Checks if `.meta` file exists or not
        let meta = path.join(".meta");
        if !meta.is_file() { return Err(LDBError::FileNotFound(meta)) };

        // Checks validity of version
        let read_version = LazyData::load(&meta)?.collect_binary()?;
        if read_version.len() != 3 { return Err(LDBError::InvalidMetaVersion(meta)) };
        let read_version = version::Version::new(read_version[0], read_version[1], read_version[2]);
        if !VERSION.is_compatible(&read_version) { return Err(LDBError::IncompatibleVersion(read_version)) };

        // Constructs Self
        Ok(Self {
            path: path.to_path_buf(),
            compressed: false,
        })
    }

    /// Loads a pre-existing LazyDB file (compressed tarball) at a specified path
    /// 
    /// Loads LazyDB as `read-write` allowing for modification of the data within it.
    /// 
    /// If a directory version of the LazyDatabase exists, it will load the directory version instead of decompiling.
    /// 
    /// If the LazyDB is invalid, it will return an error.
    pub fn load_db(path: impl AsRef<Path>) -> Result<Self, LDBError> {
        let path = path.as_ref();

        { // Checks if other loaded version exists
            let dir_path = path.with_extension("modb");
            if dir_path.is_dir() { return Self::load_dir(dir_path) }
        }

        // Decompiles database
        let path = Self::decompile(path)?;
        let mut ldb = Self::load_dir(path)?;
        ldb.compressed = true;

        Ok(ldb)
    }

    /// Gets the 'root' container of the `LazyDB`
    #[inline]
    pub fn as_container(&self) -> Result<LazyContainer, LDBError> {
        LazyContainer::load(&self.path)
    }

    /// Compiles a modifiable `LazyDatabase` directory into a compressed tarball (doesn't delete the modifable directory).
    pub fn compile(&self) -> Result<PathBuf, std::io::Error> {
        use lazy_archive::*; // imports
        let tar = self.path.with_extension("tmp.tar");
        let new = self.path.with_extension("ldb");

        // Build and compress tarball
        build_tar(&self.path, &tar)?; // build tar
        compress_file(&tar, &new)?;

        // Clean-up
        fs::remove_file(tar)?;

        Ok(new)
    }

    /// Decompiles a compressed tarball `LazyDatabase` into a modifiable directory (doesn't remove the compressed tarball)
    pub fn decompile(path: impl AsRef<Path>) -> Result<PathBuf, LDBError> {
        use lazy_archive::*; // imports
        let path = path.as_ref();

        // Checks if the path exists
        if !path.is_file() { return Err(LDBError::FileNotFound(path.to_path_buf())) };

        // Decompress and unpack
        let tar = path.with_extension("tmp.tar");
        let unpacked = path.with_extension("modb");
        unwrap_result!((decompress_file(path, &tar)) err => LDBError::IOError(err));
        unwrap_result!((unpack_tar(&tar, &unpacked)) err => LDBError::IOError(err));

        // Clean-up
        unwrap_result!((fs::remove_file(tar)) err => LDBError::IOError(err));
        
        Ok(unpacked)
    }
}

impl Drop for LazyDB {
    fn drop(&mut self) {
        if !self.compressed { return }; // If not compressed do nothing
        let ok = self.compile().is_ok();
        if !ok { return }; // Don't delete if not ok
        let _ = fs::remove_dir_all(&self.path);
    }
}