var Assert = require('assert');

/**
* A hook function indicating a change in the validation state of a
* ValidatorSelector object.
*
* @callback ValidatorSelector~state_change_hook
*
* @param {ValidatorSelector} The ValidatorSelector object calling the hook.
*/

/**
* A selector class for selecting input elements to validate.
*
* @memberof module:libresignage/ui/validator
*/
class ValidatorSelector {
	/**
	* Construct a ValidatorSelector object.
	*
	* The 'style' HTML DOM element should be a DIV element that contains
	* the input element 'validate'. The 'style' DIV can also contain
	* another DIV with the class 'invalid-feedback'. The invalid input
	* message is written into this DIV.
	*
	* 'style' and 'validators' can be left null if they aren't needed.
	*
	* @param {object}               validate   The HTML DOM element to validate
	* @param {object|null}          style      The HTML DOM element to style.
	* @param {BaseValidator[]|null} validators An array of validator objects.
	*
	* @param {ValidatorSelector~state_change_hook[]} hooks An array of hooks.
	*
	* @throws {AssetError} If no input element is selected. (validate == null)
	*/
	constructor(validate, style, validators, hooks) {
		Assert.ok(this.validate != null, 'No input element selected.');

		this.validators = [];
		this.hooks      = [];
		this.enabled    = true;
		this.valid      = null;
		this.element    = validate;
		this.style      = style;

		this.element.addEventListener('input', () => this.validate());
		if (validators) {
			for (let v of validators) {
				this.add(v);
			}
		}
		if (hooks) {
			for (let c of hooks) {
				this.add_hook(c);
			}
		}
		this.validate();
	}

	/**
	* Add a validator.
	*
	* @param {BaseValidator[]} The validator to add.
	*
	* @throws {AssertionError} If validator == null.
	* @throws {AssertionError} If validator is not an object.
	*/
	add(validator) {
		Assert.ok(validator != null, "Invalid null validator.");
		Assert.ok(validator === Object(validator), "Invalid validator.")
		this.validators.push(validator);
	}

	/**
	* Add a hook function.
	*
	* @param {ValidatorSelector~state_change_hook} The hook to add.
	* @throws {AssertionError} If hook == null.
	*/
	add_hook(hook) {
		Assert.ok(hook != null, "Invalid null hook.");
		this.hooks.push(hook);
	}

	/**
	* Style input elements.
	*
	* If nostyle == true, this function clears any input styling.
	*
	* @param {boolean} valid   The validation state.
	* @param {string}  message The invalid input message.
	* @param {boolean} nostyle Disable styling.
	*/
	set_style(valid, message, nostyle) {
		let divs = [];
		let inputs = [];

		if (nostyle) {
			valid = true;
			message = "";
		}

		if (this.style) {
			// Add invalid input message to DOM.
			divs = this.style.getElementsByClassName('invalid-feedback');
			for (let d of divs) { d.innerHTML = message; }

			// Add or remove the is-invalid class from input elements.
			inputs = this.style.getElementsByTagName('input');
			if (!valid) {
				for (let i of inputs) { i.classList.add('is-invalid'); }
			} else {
				for (let i of inputs) { i.classList.remove('is-invalid'); }
			}
		}
	}

	/**
	* Set the validation state.
	*
	* @param {boolean} valid The validation state.
	*/
	set_state(valid) {
		if (this.valid != valid) {
			this.valid = valid;
			for (let hook of this.hooks) { hook(this); }
		}
	}

	/**
	* Get the validaton state of a ValidatorSelector.
	*
	* @return {boolean} True = valid, false = invalid.
	*/
	get_state() { return this.valid; }

	/**
	* Validate a ValidatorSelector.
	*/
	validate() {
		if (this.enabled) {
			for (let v of this.validators) {
				if (!v.validate(this.element)) {
					this.set_state(false);
					this.set_style(false, v.get_msg(), v.nostyle);
					return;
				}
			}
		}
		this.set_state(true);
		this.set_style(true, false, "");
	}

	/**
	* Enable/Disable a ValidatorSelector.
	*
	* @param {boolean} state True = enabled, false = disabled.
	*/
	enable(state) {
		this.enabled = state;
		this.validate();
	}
}
module.exports = ValidatorSelector;
