'use strict';

var fs = require('fs');
var path = require('path');
var extend = require('node.extend');
var assign = require('object.assign');
var is = require('is');
var promiseback = require('promiseback');
var Promise = promiseback.Deferred.Promise;
var Buffer = require('safer-buffer').Buffer;

var checkKey = function checkKey(key) {
	if ((typeof key !== 'string' || key.length === 0) && typeof key !== 'symbol') {
		throw new TypeError('key must be a Symbol, or a nonempty string');
	}
};

var JSONData = function JSONData(raw) {
	var hasTrailingNewline = (/\n\n$/).test(raw);
	var indentMatch = String(raw).match(/^[ \t]+/m);
	var indent = indentMatch ? indentMatch[0] : 2;

	this.format = {
		indent: indent,
		trailing: hasTrailingNewline
	};
	if (raw) {
		this.data = JSON.parse(raw);
	}
};

JSONData.prototype.get = function (key, callback) {
	var data = assign({}, this.data);
	if (is.fn(key)) {
		callback = key;
		key = null;
	}
	var value = key ? data[key] : data;
	if (is.hash(value)) {
		value = assign({}, value);
	}
	var deferred = promiseback(callback);
	deferred.resolve(value);
	return deferred.promise;
};

JSONData.prototype.set = function (obj) {
	if (!is.hash(obj)) {
		throw new TypeError('object must be a plain object');
	}
	extend(true, this.data, obj);
};

JSONData.prototype.remove = function (key, callback) {
	var data = this.data;
	var deletion = Promise.resolve().then(function () {
		checkKey(key);
		var status = delete data[key];
		if (!status) {
			return Promise.reject(new Error('deletion failed'));
		}
		return void 0;
	});
	return promiseback(deletion, callback);
};

JSONData.prototype.stringify = function stringify() {
	var endingNewlines = this.format.trailing ? '\n\n' : '\n';
	var indent = this.format.indent || 2;
	return Buffer.from(JSON.stringify(this.data, null, indent) + endingNewlines);
};

var JSONFile = function JSONFile(filename, raw) {
	JSONData.call(this, raw);
	this.filename = filename;
};
JSONFile.prototype = new JSONData();

JSONFile.prototype.save = function (callback) {
	var deferred = promiseback(callback);
	fs.writeFile(this.filename, this.stringify(), function (err, result) {
		if (err) {
			deferred.reject(err);
		} else {
			deferred.resolve(result);
		}
	});
	return deferred.promise;
};

JSONFile.prototype.saveSync = function () {
	fs.writeFileSync(this.filename, this.stringify());
};

var readJSON = function readJSON(filename) {
	var callback;
	if (arguments.length > 1) {
		callback = arguments[1];
		if (!is.fn(callback)) {
			throw new TypeError('callback must be a function if provided');
		}
	}
	var deferred = promiseback(callback);
	fs.readFile(path.resolve(filename), { encoding: 'utf8' }, function (err, raw) {
		var file;

		if (err) {
			deferred.reject(err);
		} else {
			try {
				file = new JSONFile(filename, raw);
				deferred.resolve(file);
			} catch (e) {
				deferred.reject(e);
			}
		}
	});
	return deferred.promise;
};

var readJSONSync = function readJSONSync(filename) {
	var raw = fs.readFileSync(filename, 'utf8');
	return new JSONFile(filename, raw);
};

readJSON.sync = readJSONSync;

readJSON.JSONFile = JSONFile;
readJSON.JSONData = JSONData;

module.exports = readJSON;
