Source: scripts/storage/jsonStorage.js

const Data = require(__dirname + '/../handle/data.js');
const Storage = require(__dirname + '/storage.js');

const {appendFileSync, existsSync, readFileSync, writeFileSync, unlinkSync, rmdirSync, renameSync} = require('fs');
const {getCurrentTimestamp, timestampToFilename} = require(__dirname + '/../utils/dateHandler.js');
const {createPath, getSettingsFilePath, getStoragePath, listJsonFiles, sep} = require(__dirname + '/paths.js');

/**
 * Class for loading and storing data on the user's computer.
 */
class JsonStorage extends Storage {
	constructor() {
		super();

		this.data = new Data(this);

		this.defPref = Object.assign(this.defPref, {
			'fullscreen': false,
			'path': getStoragePath() + sep() + 'data',
			'windowSize': '1920x1080'
		});

		let currFileDir = this.getDataPath();
		if (!existsSync(currFileDir)) {
			createPath(currFileDir);
		}

		let currFile = currFileDir + timestampToFilename(getCurrentTimestamp());
		if (!existsSync(currFile)) {
			appendFileSync(currFile, JSON.stringify([], null, 4));
		}

		this.writeMainStorage('currentDate', getCurrentTimestamp());
	}

	/**
	 * Reads a field in the settings.json file.
	 * 
	 * @param {string} pref The name of the field we want to access.
	 * @return {string} The corresponding value of the field.
	 */
	readPreference(pref) {
		let storagePath = getStoragePath();
		if (!existsSync(storagePath)) { // Create storage directory if it is missing.
			createPath(storagePath);
		}

		let settingsPath = getSettingsFilePath();
		if (!existsSync(settingsPath)) { // Create settings.json if it is missing.
			appendFileSync(settingsPath, JSON.stringify(this.defPref, null, 4));
		}
		
		return JSON.parse(readFileSync(settingsPath))[pref];
	}

	/**
	 * Saves a value in the settings.json file.
	 * 
	 * @param {string} name The name of the field we want to access.
	 * @param {any} value The value we want to set for the corresponding field.
	 */
	storePreference(name, value) {
		let storagePath = getStoragePath();
		if (!existsSync(storagePath)) { // Create storage directory if it is missing.
			createPath(storagePath);
		}

		let settingsPath = getSettingsFilePath();
		if (!existsSync(settingsPath)) { // Create settings.json if it is missing.
			appendFileSync(settingsPath, JSON.stringify(this.defPref, null, 4));
		}

		let settingsObj = JSON.parse(readFileSync(settingsPath));
		settingsObj[name] = value;
		
		writeFileSync(settingsPath, JSON.stringify(settingsObj, null, 4));
	}

	/**
	 * Reads a specified field in the mainStorage.json file.
	 * 
	 * @param {string} field The field we want to read.
	 * @return {string} The corresponding value for the field.
	 */
	readMainStorage(field) {
		let storagePath = this.getDataPath();
		if (!existsSync(storagePath)) { // Create storage directory if it is missing.
			createPath(storagePath);
		}

		let mainStoragePath = storagePath + sep() + 'mainstorage.json';
		if (!existsSync(mainStoragePath)) { // Create mainstorage.json if it is missing.
			appendFileSync(mainStoragePath, JSON.stringify(this.defStor, null, 4));
		}

		return JSON.parse(readFileSync(mainStoragePath))[field];
	}

	/**
	 * Writes to mainStorage.json and sets a new value for the specified field.
	 * 
	 * @param {string} field The field which value we want to set.
	 * @param {any} value The new value for the specified field.
	 */
	writeMainStorage(field, value) {
		let storagePath = this.getDataPath();
		if (!existsSync(storagePath)) { // Create storage directory if it is missing.
			createPath(storagePath);
		}

		let mainStoragePath = storagePath + sep() + 'mainstorage.json';
		if (!existsSync(mainStoragePath)) { // Create mainstorage.json if it is missing.
			appendFileSync(mainStoragePath, JSON.stringify(this.defStor, null, 4));
		}

		let mainStorageObj = JSON.parse(readFileSync(mainStoragePath));
		mainStorageObj[field] = value;
		writeFileSync(mainStoragePath, JSON.stringify(mainStorageObj, null, 4));
	}

	/**
	 * Returns the content of an arbitrary json file.
	 * 
	 * @param {string} file The full path to the json file which we want to read.
	 * @return {array} The contents of the file (as an array of objects). If the file does not
	 * exist, an empty array will be returned.
	 */
	readJsonFile(file) {
		return existsSync(file) ? JSON.parse(readFileSync(file)) : [];
	}

	/**
	 * Returns all json files which have data in it, sorted by their name (date).
	 * 
	 * @return {array} Array of the file names of all json files with data in it (with .json ending!).
	 */
	getJsonFiles() {
		return listJsonFiles(this.getDataPath())
			.filter(e => e !== 'mainstorage.json').sort((a, b) => {
				return a.split('.').reverse().join('.') < b.split('.').reverse().join('.') ? -1 : 1;
			});
	}

	/**
	 * Returns the name of the current file (with .json ending!).
	 * 
	 * @return {string} The name of the current file (with .json ending!).
	 */
	getCurrentFilename() {
		return timestampToFilename(getCurrentTimestamp());
	}

	/**
	 * Returns the path to the folder containing the data.
	 * 
	 * @return {string} The path to the folder containing the data.
	 */
	getDataPath() {
		let activeUser = this.readPreference('activeUser');
		let suffix = activeUser !== undefined ? (activeUser + sep()) : '';

		return this.readPreference('path') + sep() + suffix;
	}

	/**
	 * Checks whether a given path exists.
	 * 
	 * @param {string} path The path to be checked.
	 * @return {bool} True if the path exists, else false.
	 */
	exists(path) {
		return existsSync(path);
	}

	/**
	 * Renames a given directory.
	 * 
	 * @param {string} oldPath The name of the directory to be renamed.
	 * @param {string} newPath The new name for the directory.
	 */
	renamePath(oldPath, newPath) {
		if (existsSync(oldPath)) {
			renameSync(oldPath, newPath);
		}
	}

	/**
	 * Deletes a given directory and all of its contents.
	 * 
	 * @param {string} path The path to the directory which should be deleted.
	 */
	deletePath(path) {
		if (existsSync(path)) {
			rmdirSync(path, {recursive: true});
		}
	}

	/**
	 * Filters data from a given file according to a given quest.
	 * 
	 * @param {string} file The file containing the data to be filtered.
	 * @param {object} quest The quest for filtering the data. quest contains a connector
	 * (or/and) and an array of parameters to filter objects. Example:
	 * quest = { connector: 'or', params: [['type', 'earning'], ['budget', 'checking account']] }
	 * @return {array} All the data which match the quest, in form of an array containing objects.
	 */
	getData(file, quest) {
		let dataPath = this.getDataPath() + file;

		if (!this.exists(dataPath)) {
			return [];
		}

		return this.data.getData(JSON.parse(readFileSync(dataPath)), quest);
	}

	/**
	 * Stores data in the appropriate data file. The file is determined by the date of the data.
	 * 
	 * @param {object} data The data we want to store.
	 */
	storeData(data) {
		let dataPath = this.getDataPath() + timestampToFilename(data.date);

		if (existsSync(dataPath)) {
			let content = JSON.parse(readFileSync(dataPath));
			content.push(data);
			writeFileSync(dataPath, JSON.stringify(this.data.sortData(content), null, 4));
		} else {
			appendFileSync(dataPath, JSON.stringify([data], null, 4));
		}
	}

	/**
	 * Replaces a specific file with new data.
	 * 
	 * @param {string} file The file to override.
	 * @param {array} data The data to write (in form of an array containing objects).
	 */
	replaceData(file, data) {
		let filePath = this.getDataPath() + file;
		
		if (existsSync(filePath)) {
			writeFileSync(filePath, JSON.stringify(data, null, 4));
		} else {
			appendFileSync(filePath, JSON.stringify(data, null, 4));
		}
	}

	/**
	 * Deletes a given entry in a given file.
	 * 
	 * @param {string} file The file which contains the data.
	 * @param {string} id The id (timestamp) of the data we want to delete.
	 */
	deleteData(file, id) {
		let dataPath = this.getDataPath() + file;

		if (!existsSync(dataPath)) {
			return;
		}

		let newContent = [];

		this.readJsonFile(dataPath).forEach(obj => {
			if (parseInt(id) !== obj.date) {
				newContent.push(obj);
			} else {
				this.removeStats(obj);
			}
		});

		writeFileSync(dataPath, JSON.stringify(newContent, null, 4));
	}

	/**
	 * After deleting an object, this function removes the object's influence on the statistics.
	 * 
	 * @param {object} obj The object which got deleted.
	 */
	removeStats(obj) {
		let budgets = this.readMainStorage('budgets');

		budgets.forEach((budget, index) => {
			if (budget[0] === obj.budget) {
				let factor = obj.type === 'earning' ? 1 : -1;
				budgets[index][1] = Math.round((budget[1] - (obj.amount * factor)) * 100) / 100;
			}
		});

		this.writeMainStorage('budgets', budgets);

		let allTime = this.readMainStorage(obj.type === 'earning' ? 'allTimeEarnings' : 'allTimeSpendings');

		allTime.forEach((trans, index) => {
			if (trans[0] === obj.budget) {
				allTime[index][1] = Math.round((trans[1] - obj.amount) * 100) / 100;
			}
		});

		this.writeMainStorage(obj.type === 'earning' ? 'allTimeEarnings' : 'allTimeSpendings', allTime);
	}

	/**
	 * Removes a given file.
	 * 
	 * @param {string} file The name of the file to remove.
	 */
	removeFile(file) {
		let dataPath = this.getDataPath() + file;

		if (existsSync(dataPath)) {
			unlinkSync(dataPath);
		}
	}
}

module.exports = JsonStorage;