/* Requires */
	const fs = require('fs');
	const path = require('path');
	const chalk = require('chalk');
	const stdout = process.stdout;
	const ImageDB = require('./imagedb');
	const Randoma = require('randoma');
	const Context = require('./context');
	const convert = require('convert-units');
	const Serialize = require('./serialize');
	const Serializer = require('./serializer');
	const { LSB, Image } = require('../fs');


class Database {

	constructor (options) { /* Constructor */
		this.directory = options.directory;
		this.bitrate = options.bitrate;
		this.entry = options.entry;
		this.key = options.key;
		this.lsb = new LSB(this.key, this.bitrate);
		this.context = new Context(this.directory, this.key);
		this.cache = {};
		this.data = {};
	}

	async unload () {
		await this.save();

		delete this.cache;
	}

	async destroy () {
		let context = new Context(this.directory, this.key);

		let files = await context.loadFiles();
		let rand_buf = [];
		let to_write = new Buffer(rand_buf);

		for(let i=0, l=files.length;i<l;i++) {
			try {
				let path = context.file(files[i]);
				let img  = await Image.read(path);
				let size = this.lsb.sizeOf(img.data);

				if(rand_buf.length <= size) {
					while(rand_buf.length <= size) rand_buf.push(0);
					to_write = new Buffer(rand_buf);
				}

				this.lsb.hide(img.data, to_write);

				await Image.write(path, img);
			} catch (exc) {}
		}
	}

	async checkDB () {
		let img;
		try {
			img = (await ImageDB.load(this.context.file(this.entry), this.key, this.bitrate));
		} catch (exc) {
			return false;
		}

		return img.tag === "DBF";
	}

	async loadImage (route) {
		return this.cache[route] = await ImageDB.load(route);
	}

	cacheImage (img) {
		return this.cache[img.path] = img;
	}

	async decacheImage (img) {
		await this.cache[img.path].save();
		delete this.cache[img.path];
	}

	async changeCypher (new_pass, new_bitrate) {
		this.log("Changing password...");

		let all = {};
		let context = new Context(this.directory, this.key);
		let files = await context.loadFiles((file) => file.substring(file.lastIndexOf(".")) === ".png");

		for(let i=0, l=files.length;i<l;i++) {
			let f = files[i];
			let img = new ImageDB(context.file(f), this.key, this.bitrate);

			await img.load();
			if(img.tag !== "DBF") continue;

			Serializer.merge(all, img.data);
		}

		console.log(all);

		this.log("Database Backup done...");
		this.key = new_pass;
		this.bitrate = new_bitrate;
		this.lsb = new LSB(this.key, this.bitrate);
		this.context = new Context(this.directory, this.key);

		this.cache = [];

		await this.initDirectory();
		this.log("Saving Data...")
		await this.set("", all);

		this.log("Done!");
	}

	async save () {
		this.benchmark();
		this.log("Saving Database...");
		
		for(let i in this.cache) await this.cache[i].save();

		this.log("Done!");
	}

	async set (k, v) {
		await this.remove(k);

		if(typeof v === "object") for(let i in v) await this.set((k? k + "/" : "") + i, v[i]);

		let img = await this.getFreeSpace(Serializer.stringifyPrimitive(k, v).length);

		if(!img || !img.file) return false;

		await img.file.set(k, v);

		return true;
	}

	async get (k) {
		let img = await this.search(k);

		if(!img) return null;

		let obj = img && img.get(k);

		if(typeof obj === "object") return this.getObject(k);

		return obj;
	}

	async search (k) {
		let ret = null;

		await this.each((img) => {
			if(img.get(k) !== undefined) {
				ret = img;
				return false;
			}
		});

		if(ret) this.cacheImage(ret);

		return ret;
	}

	async remove (k) {
		let img = await this.search(k);

		if(!img) return false;

		img.remove(k);
		return true;
	}

	async initDirectory () {
		this.log("Initializing directory...");
		this.benchmark();

		let files = await this.context.loadFiles((file) => file.substring(file.lastIndexOf(".")) === ".png" && file !== this.entry);

		files.unshift(this.entry);

		for(let i=0, l=files.length;i<l;i++) {

			await ((new ImageDB(this.context.file(files[i]), this.key, this.bitrate)).init({
				tag: "DBF",
				next: files[i + 1] || "",
				data: {}
			}));
		}

		this.log("Total Files Initialized: " + files.length);
	}

	file (f) {
		return this.context.file(f);
	}

	async each (cb) {
		let img;
		let next = this.entry;

		for(let i in this.cache) {
			img = this.cache[i];
			if(cb(img) === false) return true;
		}

		while(next) {
			let file = this.file(next);

			img = this.cache[file];
			if(!img){
				try {
					img = new ImageDB(file, this.key, this.bitrate);
					await img.load();
				} catch (exc) {
					break;
				}
				if(cb(img) === false) break;
			}
			next = img.next;
		}

		return this;
	}

	async getObject (path="") {
		let obj = {};

		await this.each((img) => {
			let data = img.get(path);

			if(typeof data === "object") {
				this.cacheImage(img);
				Serializer.merge(obj, data);
			}
		});

		return obj;
	} 

	async keys (path="") {
		let obj = [];

		await this.each((img) => {
			let data = img.get(path);

			if(typeof data === "object") {
				obj.push(...(Object.keys(data)));
			}
		});

		return obj;
	} 

	async removeObject (path) {
		await this.each((img) => {
			img.remove(path);
		});
	}

	benchmark () {
		this.time = Date.now();
	}

	log (msg) {
		let str = msg;

		if(this.silent) return;
		if(this.time) {
			let now = Date.now();
			let unit = convert(now - this.time).from("ms").toBest();

			str += " " + chalk.green(unit.val + unit.unit);

			if(this.time > 1000) str = "\n" + str;

			this.time = now;
		}

		console.log(str);
	}

	async size () {
		let size = 0;

		await this.each((img) => size += img.available_length);

		return size;
	}

	async freeSpace () {
		let size = 0;

		await this.each((img) => size += img.freeSize());

		return size;
	}

	async files () {
		let files = [];

		await this.each((img) => files.push(img.path));

		return files;
	}

	async getFreeSpace (bytes) {
		let max = 0;
		let file = null;

		await this.each((img) =>{
			let size = img.freeSize();
			if(size >= bytes){
				max  = size;
				file = img;

				return false;
			}
		});

		if(file) this.cacheImage(file);

		return max? { size: max, file: file } : null;
	}

	async maxBlockSize () {
		let max = 0;
		let file = "";

		await this.each((img) =>{
			if(max < img.free_size){
				max  = img.free_size;
				file = img.path;
			}
		});

		return { size: max, file };
	}

	async all () {
		let obj = {};
		this.log("Getting all...");

		await this.each((img) => {
			Serializer.merge(obj, img.data);
		});

		this.log("Done!");

		return obj;
	}
}

module.exports = Database;