
/**
 * The main base class. Manages the signal system. Objects based on this class
 * can be instantiated declaratively using XML.
 */
module.exports = class VnObject {
	/**
	 * Tag to be used when the class instance is defined via XML. All classes 
	 * must define this attribute, even if it is not used.
	 */
	static Tag = 'vn-object';

	/**
	 * Class public properties.
	 */
	static Properties = {};

	/*
	 * Reference count.
	 */
	_refCount = 1;

	/*
	 * Signal handlers data.
	 */
	_thisArg = null;

	/**
	 * Initializes the object and sets all properties passed to the class
	 * constructor.
	 * 
	 * @param {Object} props The properties passed to the contructor
	 */
	constructor(props) {
		this.setProperties(props);
	}

	initialize(props) {
		this.setProperties(props);
	}
	
	/**
	 * Sets a group of object properties.
	 * 
	 * @param {Object} props Properties
	 */
	setProperties(props) {
		for (var prop in props)
			this[prop] = props[prop];
	}
	
	/**
	 * Increases the object reference count.
	 */
	ref() {
		this._refCount++;
		return this;
	}
	
	/**
	 * Decreases the object reference count.
	 */
	unref() {
		this._refCount--;
		
		if (this._refCount === 0)
			this._destroy();
	}

	/**
	 * Called from @Vn.Builder when it finds a custom tag as a child of the
	 * element.
	 * 
	 * @param {Vn.Scope} scope The scope instance
	 * @param {Node} node The custom tag child nodes
	 */
	loadXml() {}

	/**
	 * Called from @Vn.Builder when it finds a a child tag that isn't
	 * associated to any property.
	 * 
	 * @param {Object} child The child object instance
	 */
	appendChild() {}

	/**
	 * Conects a signal with a function.
	 *
	 * @param {string} id The signal identifier
	 * @param {function} callback The callback
	 * @param {Object} instance The instance
	 */
	on(id, callback, instance) {
		if (!(callback instanceof Function)) {
			console.warn('Vn.Object: Invalid callback for signal \'%s\'', id);
			return;
		}
		
		this._signalInit();
		var callbacks = this._thisArg.signals[id];

		if (!callbacks)
			callbacks = this._thisArg.signals[id] = [];
			
		callbacks.push({
			blocked: false
			,callback: callback
			,instance: instance
		});
	}

	/**
	 * Locks/Unlocks a signal emission to the specified object.
	 *
	 * @param {string} id The signal identifier
	 * @param {function} callback The callback
	 * @param {boolean} block %true for lock the signal, %false for unlock
	 */
	blockSignal(id, callback, block, instance) {
		if (!this._thisArg)
			return;

		var callbacks = this._thisArg.signals[id];
		
		if (!callbacks)
			return;
			
		for (var i = 0; i < callbacks.length; i++)
		if (callbacks[i].callback == callback
		&& callbacks[i].instance == instance)
			callbacks[i].blocked = block;
	}
	
	/**
	 * Emits a signal in the object.
	 *
	 * @param {string} id The signal identifier
	 */
	emit(id) {
		if (!this._thisArg)
			return;

		var callbacks = this._thisArg.signals[id];
		
		if (!callbacks)
			return;

		var callbackArgs = [];
		callbackArgs.push(this);
	
		for (var i = 1; i < arguments.length; i++)
			callbackArgs.push(arguments[i]);

		for (var i = 0; i < callbacks.length; i++)
		if (!callbacks[i].blocked)
			callbacks[i].callback.apply(callbacks[i].instance, callbackArgs);
	}
	
	/**
	 * Disconnects a signal from current object.
	 *
	 * @param {string} id The signal identifier
	 * @param {function} callback The connected callback
	 * @param {Object} instance The instance
	 */
	disconnect(id, callback, instance) {
		if (!this._thisArg)
			return;

		var callbacks = this._thisArg.signals[id];
		
		if (callbacks)
		for (var i = callbacks.length; i--;)
		if (callbacks[i].callback === callback
		&& callbacks[i].instance === instance)
			callbacks.splice(i, 1);
	}
	
	/**
	 * Disconnects all signals for the given instance.
	 *
	 * @param {Object} instance The instance
	 */
	disconnectByInstance(instance) {
		if (!this._thisArg)
			return;
	
		var signals = this._thisArg.signals;

		for (var signalId in signals) {
			var callbacks = signals[signalId];

			if (callbacks)
			for (var i = callbacks.length; i--;)
			if (callbacks[i].instance === instance)
				callbacks.splice(i, 1);
		}
	}

	/**
	 * Destroys the object, this method should only be called before losing
	 * the last reference to the object. It can be overwritten by child classes
	 * but should always call the parent method.
	 */
	_destroy() {
		if (!this._thisArg)
			return;
	
		var links = this._thisArg.links;
	
		for (var key in links)
			this._unlink(links[key]);
		
		this._thisArg = null;
	}
	
	/**
	 * Links the object with another object.
	 * 
	 * @param {Object} prop The linked property
	 * @param {Object} handlers The object events to listen with
	 */
	link(prop, handlers) {
		this._signalInit();
		var links = this._thisArg.links;
	
		for (var key in prop) {
			var newObject = prop[key];
			var oldObject = this[key];
		
			if (oldObject)
				this._unlink(oldObject);

			this[key] = newObject;

			if (newObject) {
				links[key] = newObject.ref();

				for (var signal in handlers)
					newObject.on(signal, handlers[signal], this);
			} else if (oldObject)
				links[key] = undefined;
		}
	}

	_unlink(object) {
		if (!object) return;
		object.disconnectByInstance(this);
		object.unref();
	}
	
	_signalInit() {
		if (!this._thisArg)
			this._thisArg = {
				signals: {},
				links: {}
			};
	}
}
