'use strict';
var utils = require('./utils');
var History = require('./History');
var Transaction = require('./Transaction');

class Fluxerino {
	constructor(actionsContext) {
		this.notifySubs = this.notifySubs.bind(this);

		this.actionsContext = actionsContext;
		this.history = new History(this);

		this.stores = [];
		this.handlerRegistry = {};
		this.subscribers = [];

		this.state = {};
	}

	mergeContext(context) {
		this.actionsContext = Object.assign({}, this.actionsContext, context);
	}

	push(action, params, callback) {
		const transaction = new Transaction(action);

		this.history.appendTransaction(transaction);
		return transaction.execute(params, this.actionsContext, this.onTransactionProgress.bind(this), callback);
	}

	onTransactionProgress() {
		this.dispatchChanges(this.history.rollForward());
	}

	dispatch(state, actionType, payload) {
		if (!this.handlerRegistry[actionType]) {
			this.handlerRegistry[actionType] = utils.getHandlers(this.stores, actionType);
		}

		return this.applyHandlers(this.handlerRegistry[actionType], payload, state);
	}

	applyHandlers(handlers, payload, state) {
		let newState = Object.assign({}, state);

		for (let i = 0; i < handlers.length; i++) {
			let key = handlers[i].key;
			let handler = handlers[i].handler;

			const result = handler.call(handler, payload, state[key]);

			if (result !== undefined) {
				newState[key] = result;
			}
		}

		return newState;
	}

	getSlice(slice) {
		if (Array.isArray(slice)) {
			const partial = Object.create(null);

			slice.forEach(function (key) {
				partial[key] = this.state[key];
			}, this);

			return partial;
		}
		else {
			return this.state[slice];
		}
	}

	addStore(key, store) {
		this.stores.push([key, store]);

		this.handlerRegistry = {};
	}

	/**
	 * Subscribes to state changes
	 * @param  {function} handler Change handler
	 *
	 * @param  {array} handler Change subscriptions
	 * @return {string}           Sub key
	 */
	subscribe(handler, slices) {
		let key = utils.generateKey();

		while (!isSubKeyUniue(key, this.subscribers)) {
			key = key.concat('-' + utils.generateKey());
		}

		if (Array.isArray(handler)) {
			handler.forEach((subscription) => {
				this.subscribers.push({
					handler: subscription.handler,
					slices: subscription.slices,
					key: key
				});
			}, this);
		}
		else {
			this.subscribers.push({
				handler: handler,
				slices: slices,
				key: key
			});
		}

		return key;
	}

	/**
	 * Cancels subscription
	 * @param  {string} subKey Subkey identifying the subscriber
	 */
	unsubscribe(subKey) {
		this.subscribers = this.subscribers.filter((sub) => {
			return sub.key !== subKey;
		});
	}

	/**
	 * Calculate changes and dispatch changes to all subscribers
	 * @param  {Object} newState [description]
	 */
	dispatchChanges(newState) {
		const changes = utils.findChanges(this.state, newState);
		if (changes.length === 0) {
			return;
		}

		this.state = newState;
		global.setTimeout(this.notifySubs, 0, changes);
	}

	/**
	 * Notify all subscribers sending them current state and a list of changed keys
	 * @param  {Array} changes List of changed keys
	 */
	notifySubs(changes) {
		this.subscribers.forEach((sub) => {
			if (sub.slices) {
				const intersection = utils.getIntersection(sub.slices, changes);

				if (intersection.length > 0) {
					sub.handler(intersection, this.state);
				}
			}
			else {
				sub.handler(changes, this.state);
			}
		});
	}
}

function isSubKeyUniue(key, subscribers) {
	return !subscribers.some((sub) => {
		return sub.key === key;
	});
}

module.exports = Fluxerino;
