import defaults from './core.defaults';
import registry from './core.registry';
import {mergeIf} from '../helpers/helpers.core';

/**
 * @typedef { import("./core.controller").default } Chart
 * @typedef { import("../platform/platform.base").IEvent } IEvent
 * @typedef { import("../plugins/plugin.tooltip").default } Tooltip
 */

export default class PluginService {
	/**
	 * Calls enabled plugins for `chart` on the specified hook and with the given args.
	 * This method immediately returns as soon as a plugin explicitly returns false. The
	 * returned value can be used, for instance, to interrupt the current action.
	 * @param {Chart} chart - The chart instance for which plugins should be called.
	 * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate').
	 * @param {Array} [args] - Extra arguments to apply to the hook call.
	 * @returns {boolean} false if any of the plugins return false, else returns true.
	 */
	notify(chart, hook, args) {
		const descriptors = this._descriptors(chart);

		for (let i = 0; i < descriptors.length; ++i) {
			const descriptor = descriptors[i];
			const plugin = descriptor.plugin;
			const method = plugin[hook];
			if (typeof method === 'function') {
				const params = [chart].concat(args || []);
				params.push(descriptor.options);
				if (method.apply(plugin, params) === false) {
					return false;
				}
			}
		}

		return true;
	}

	invalidate() {
		this._cache = undefined;
	}

	/**
	 * @param {Chart} chart
	 * @private
	 */
	_descriptors(chart) {
		if (this._cache) {
			return this._cache;
		}

		const config = (chart && chart.config) || {};
		const options = (config.options && config.options.plugins) || {};
		const plugins = allPlugins(config);
		const descriptors = createDescriptors(plugins, options);

		this._cache = descriptors;

		return descriptors;
	}
}

function allPlugins(config) {
	const plugins = [];
	const keys = Object.keys(registry.plugins.items);
	for (let i = 0; i < keys.length; i++) {
		plugins.push(registry.getPlugin(keys[i]));
	}

	const local = config.plugins || [];
	for (let i = 0; i < local.length; i++) {
		const plugin = local[i];

		if (plugins.indexOf(plugin) === -1) {
			plugins.push(plugin);
		}
	}

	return plugins;
}

function createDescriptors(plugins, options) {
	const result = [];

	for (let i = 0; i < plugins.length; i++) {
		const plugin = plugins[i];
		const id = plugin.id;

		let opts = options[id];
		if (opts === false) {
			continue;
		}
		if (opts === true) {
			opts = {};
		}
		result.push({
			plugin,
			options: mergeIf({}, [opts, defaults.plugins[id]])
		});
	}

	return result;
}

/**
 * Plugin extension hooks.
 * @interface IPlugin
 * @typedef {object} IPlugin
 * @since 2.1.0
 */
/**
 * @method IPlugin#beforeInit
 * @desc Called before initializing `chart`.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#afterInit
 * @desc Called after `chart` has been initialized and before the first update.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#beforeUpdate
 * @desc Called before updating `chart`. If any plugin returns `false`, the update
 * is cancelled (and thus subsequent render(s)) until another `update` is triggered.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 * @returns {boolean} `false` to cancel the chart update.
 */
/**
 * @method IPlugin#afterUpdate
 * @desc Called after `chart` has been updated and before rendering. Note that this
 * hook will not be called if the chart update has been previously cancelled.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#reset
 * @desc Called during chart reset
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 * @since version 3.0.0
 */
/**
 * @method IPlugin#beforeDatasetsUpdate
 * @desc Called before updating the `chart` datasets. If any plugin returns `false`,
 * the datasets update is cancelled until another `update` is triggered.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 * @returns {boolean} false to cancel the datasets update.
 * @since version 2.1.5
*/
/**
 * @method IPlugin#afterDatasetsUpdate
 * @desc Called after the `chart` datasets have been updated. Note that this hook
 * will not be called if the datasets update has been previously cancelled.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 * @since version 2.1.5
 */
/**
 * @method IPlugin#beforeDatasetUpdate
 * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin
 * returns `false`, the datasets update is cancelled until another `update` is triggered.
 * @param {Chart} chart - The chart instance.
 * @param {object} args - The call arguments.
 * @param {number} args.index - The dataset index.
 * @param {object} args.meta - The dataset metadata.
 * @param {object} options - The plugin options.
 * @returns {boolean} `false` to cancel the chart datasets drawing.
 */
/**
 * @method IPlugin#afterDatasetUpdate
 * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note
 * that this hook will not be called if the datasets update has been previously cancelled.
 * @param {Chart} chart - The chart instance.
 * @param {object} args - The call arguments.
 * @param {number} args.index - The dataset index.
 * @param {object} args.meta - The dataset metadata.
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#beforeLayout
 * @desc Called before laying out `chart`. If any plugin returns `false`,
 * the layout update is cancelled until another `update` is triggered.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 * @returns {boolean} `false` to cancel the chart layout.
 */
/**
 * @method IPlugin#afterLayout
 * @desc Called after the `chart` has been laid out. Note that this hook will not
 * be called if the layout update has been previously cancelled.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#beforeRender
 * @desc Called before rendering `chart`. If any plugin returns `false`,
 * the rendering is cancelled until another `render` is triggered.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 * @returns {boolean} `false` to cancel the chart rendering.
 */
/**
 * @method IPlugin#afterRender
 * @desc Called after the `chart` has been fully rendered (and animation completed). Note
 * that this hook will not be called if the rendering has been previously cancelled.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#beforeDraw
 * @desc Called before drawing `chart` at every animation frame. If any plugin returns `false`,
 * the frame drawing is cancelled untilanother `render` is triggered.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 * @returns {boolean} `false` to cancel the chart drawing.
 */
/**
 * @method IPlugin#afterDraw
 * @desc Called after the `chart` has been drawn. Note that this hook will not be called
 * if the drawing has been previously cancelled.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#beforeDatasetsDraw
 * @desc Called before drawing the `chart` datasets. If any plugin returns `false`,
 * the datasets drawing is cancelled until another `render` is triggered.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 * @returns {boolean} `false` to cancel the chart datasets drawing.
 */
/**
 * @method IPlugin#afterDatasetsDraw
 * @desc Called after the `chart` datasets have been drawn. Note that this hook
 * will not be called if the datasets drawing has been previously cancelled.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#beforeDatasetDraw
 * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets
 * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing
 * is cancelled until another `render` is triggered.
 * @param {Chart} chart - The chart instance.
 * @param {object} args - The call arguments.
 * @param {number} args.index - The dataset index.
 * @param {object} args.meta - The dataset metadata.
 * @param {object} options - The plugin options.
 * @returns {boolean} `false` to cancel the chart datasets drawing.
 */
/**
 * @method IPlugin#afterDatasetDraw
 * @desc Called after the `chart` datasets at the given `args.index` have been drawn
 * (datasets are drawn in the reverse order). Note that this hook will not be called
 * if the datasets drawing has been previously cancelled.
 * @param {Chart} chart - The chart instance.
 * @param {object} args - The call arguments.
 * @param {number} args.index - The dataset index.
 * @param {object} args.meta - The dataset metadata.
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#beforeTooltipDraw
 * @desc Called before drawing the `tooltip`. If any plugin returns `false`,
 * the tooltip drawing is cancelled until another `render` is triggered.
 * @param {Chart} chart - The chart instance.
 * @param {object} args - The call arguments.
 * @param {Tooltip} args.tooltip - The tooltip.
 * @param {object} options - The plugin options.
 * @returns {boolean} `false` to cancel the chart tooltip drawing.
 */
/**
 * @method IPlugin#afterTooltipDraw
 * @desc Called after drawing the `tooltip`. Note that this hook will not
 * be called if the tooltip drawing has been previously cancelled.
 * @param {Chart} chart - The chart instance.
 * @param {object} args - The call arguments.
 * @param {Tooltip} args.tooltip - The tooltip.
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#beforeEvent
 * @desc Called before processing the specified `event`. If any plugin returns `false`,
 * the event will be discarded.
 * @param {Chart} chart - The chart instance.
 * @param {IEvent} event - The event object.
 * @param {object} options - The plugin options.
 * @param {boolean} replay - True if this event is replayed from `Chart.update`
 */
/**
 * @method IPlugin#afterEvent
 * @desc Called after the `event` has been consumed. Note that this hook
 * will not be called if the `event` has been previously discarded.
 * @param {Chart} chart - The chart instance.
 * @param {IEvent} event - The event object.
 * @param {object} options - The plugin options.
 * @param {boolean} replay - True if this event is replayed from `Chart.update`
 */
/**
 * @method IPlugin#resize
 * @desc Called after the chart as been resized.
 * @param {Chart} chart - The chart instance.
 * @param {number} size - The new canvas display size (eq. canvas.style width & height).
 * @param {object} options - The plugin options.
 */
/**
 * @method IPlugin#destroy
 * @desc Called after the chart as been destroyed.
 * @param {Chart} chart - The chart instance.
 * @param {object} options - The plugin options.
 */
