/**
 * Parent
 *
 * Represents defined keys mapped to other Nodes which themselves could be
 * Parents
 *
 * @author Chris Nasr
 * @copyright OuroborosCoding
 * @created 2019-03-01
 */

// Import modules
import BaseNode from './BaseNode';
import Child from './Child';

// Import helpers
import is from './is';
import types from './types';

// Parent class
export default class Parent extends BaseNode {

	/**
	 * Constructor
	 *
	 * Initialises the instance
	 *
	 * @name Parent
	 * @access public
	 * @param Object details			Node structure
	 * @returns Parent
	 */
	constructor(details) {

		// If the details are not an Object
		if(!is.object(details)) {
			throw 'details must be an Object';
		}

		// Call the parent constructor
		super(details, 'Parent');

		// Init the nodes and requires dicts
		this._nodes = {};
		this._requires = {};

		// Go through the keys in the details
		for(var k in details) {

			// If key is standard
			if(types.standard.test(k)) {

				// Add the child
				this._nodes[k] = Child.create(details[k]);
			}
		}

		// If there's a require hash available
		if('__require__' in details) {
			this.requires(details['__require__']);
		}
	}

	/**
	 * Iterator
	 *
	 * Allows the instance to be used as an iterator
	 *
	 * @name [Symbol.iterator]
	 * @return Object
	 */
	[Symbol.iterator]() {

		// Init the variables that have the keys and current position
		var iPos = 0;
		var aKeys = Object.keys(this._nodes);

		return {
			"next": function() {
				return iPos < aKeys.length ?
						{"value": aKeys[iPos++], "done": false} :
						{"done": true};
			}
		};
	}

	/**
	 * Clean
	 *
	 * Goes through each of the values in the Object, cleans it, stores it,
	 * and returns a new Object
	 *
	 * @name clean
	 * @access public
	 * @param Object value				The value to validate
	 * @return Object
	 */
	clean(value) {

		// If the value is null and it's optional, return as is
		if(value == null && this._optional) {
			return null;
		}

		// If the value is not an Object
		if(!is.object(value)) {
			throw 'value must be an Object';
		}

		// Init the return value
		var oRet = {};

		// Go through each value and clean it using the associated node
		for(var k in value) {
			try {
				oRet[k] = this._nodes[k].clean(value[k]);
			} catch(err) {
				throw String(k) + ' is not a valid node in the parent';
			}
		}

		// Return the cleaned values
		return oRet;
	}

	/**
	 * Contains
	 *
	 * Returns whether a specific key exists in the parent
	 *
	 * @name contains
	 * @access public
	 * @param str key					The key to check the existence of
	 * @return bool
	 */
	contains(key) {
		return key in this._nodes;
	}

	/**
	 * Get
	 *
	 * Returns the node associated with the given key
	 *
	 * @name get
	 * @access public
	 * @param String key				The key associated with the node to get
	 * @param mixed def					If set, returned when a node is not found
	 * @return BaseNode|OptionsNode
	 */
	get(key, def) {
		if(key in this._nodes) {
			return this._nodes[key];
		} else {
			if(typeof def === 'undefined') {
				throw 'No such node in the Parent';
			} else {
				return def;
			}
		}
	}

	/**
	 * Keys
	 *
	 * Returns a list of the node names in the parent
	 *
	 * @name keys
	 * @access public
	 * @return Array
	 */
	keys() {
		return Object.keys(this._nodes);
	}

	/**
	 * Requires
	 *
	 * Sets or gets the require rules used to validate the Parent
	 *
	 * @name requires
	 * @access public
	 * @param object require			An Object expressing requirements of fields
	 * @return Object | void
	 */
	requires(require) {

		// If require is not defined, this is a getter
		if(typeof require == 'undefined') {
			return this._requires;
		}

		// If it's not a valid Object
		if(!is.object(require)) {
			throw '__require__ must be a valid Object';
		}

		// Go through each key and make sure it goes with a field
		for(var k in require) {

			// If the field doesn't exist
			if(!(k in this._nodes)) {
				throw '__require__[' + String(k) + '] does not exist in the Parent';
			}

			// If the value is a string
			if(is.string(require[k])) {
				require[k] = [require[k]];
			}

			// Else if it's not an array type
			else if(!is.array(require[k])) {
				throw '__require__[' + String(k) + '] must be a single string or an Array';
			}

			// Make sure each required field also exists
			for(var s in require[k]) {
				if(!(s in this._nodes)) {
					throw '__require__[' + String(k) + ']:' + String(require[k])
				}
			}

			// If it's all good
			this._requires[k] = require[k];
		}
	}

	/**
	 * To Object
	 *
	 * Returns the Node as an Object in the same format as is used in
	 * constructing it
	 *
	 * @name toObject
	 * @access public
	 * @return Object
	 */
	toObject() {

		// Get the parents Object as the starting point of our return
		var oRet = super.toObject();

		// Go through each field and add it to the return
		for(var k in this._nodes) {
			oRet[k] = this._nodes[k].toObject();
		}

		// Return
		return oRet;
	}

	/**
	 * Valid
	 *
	 * Checks if the passed dict has the right values for this Parent
	 *
	 * @name valid
	 * @param Object value				The value to validate
	 * @return bool
	 */
	valid(value, level) {

		// Reset validation failures
		this.validation_failures = [];

		// If the value is null and it's optional, we're good
		if(value == null && this._optional) {
			return true;
		}

		// If level is not passed
		if(typeof level == 'undefined') {
			level = [];
		}

		// If the value isn't an Object
		if(!is.object(value)) {
			this.validation_failures.push([level.join('.'), String(value)]);
			return false;
		}

		// Init the return, assume valid
		var bRet = true;

		// Go through each node in the instance
		for(var k in this._nodes) {

			// Add the field to the level
			var lLevel = level.slice(0);
			lLevel.push(k);

			// If we are missing a node
			if(!(k in value)) {

				// If the value is not optional
				if(!this._nodes[k].optional()) {
					this.validation_failures.push([lLevel.join('.'), 'missing']);
					bRet = false;
				}

				// Continue to next node
				continue;
			}

			// If the element isn't valid, return false
			if(!this._nodes[k].valid(value[k], lLevel)) {
				this.validation_failures = this.validation_failures.concat(this._nodes[k].validation_failures.splice(0));
				bRet = false;
				continue;
			}

			// If the element requires others
			if(k in this._requires) {

				// Go through each required field
				for(var f in this._requires[k]) {

					// If the field doesn't exist in the value
					if(!(f in value) || ['0000-00-00','',null].indexOf(value[f]) != -1) {
						this.validation_failures.push([lLevel.join('.'), 'requires \'' + String(f) + '\' to also be set']);
						bRet = false;
					}
				}
			}
		}

		// Return whatever the result was
		return bRet;
	}
}

// Register with Child
Child.register('parent', Parent);
