import _difference from 'lodash/difference';
import _get from 'lodash/get';
import _uniq from 'lodash/uniq';
import _reduce from 'lodash/reduce';
import _find from 'lodash/find';
import _keys from 'lodash/keys';

export default class Formula {
	constructor(formula, cursor) {
		this.cursor = cursor;
		this.dependencies = formula._dependencies; // eslint-disable-line no-underscore-dangle
		/* eslint-disable no-new-func */
		this.operation = new Function(
			...['contours', 'anchors', 'parentAnchors', 'Utils']
				.concat(formula._parameters || []) // eslint-disable-line no-underscore-dangle
				.concat(
					(typeof formula._operation === 'string' // eslint-disable-line no-underscore-dangle
					&& formula._operation.indexOf('return ') === -1 // eslint-disable-line no-underscore-dangle
						? 'return '
						: '')
						// The operation might be wrapped in a function (e.g. multi-
						// line code for debugging purpose). In this case, return
						// must be explicit
						+ formula._operation
							.toString() // eslint-disable-line no-underscore-dangle
							// [\s\S] need to be used instead of . because
							// javascript doesn't have a dotall flag (s)
							.replace(/^function\s*\(\)\s*\{([\s\S]*?)\}$/, '$1')
							.trim(),
					/* +
				// add sourceURL pragma to help debugging
				// TODO: restore sourceURL pragma if it proves necessary
				'\n\n//# sourceURL=' + path */
				),
		);
		/* eslint-enable no-new-func */
		this.parameters = formula._parameters; // eslint-disable-line no-underscore-dangle
		this.analyzing = false;
	}

	analyzeDependency(glyph, graph = []) {
		graph.push(this.cursor);
		if (this.analyzing) {
			throw new Error(`There is a circular dependency for glyph ${
				glyph.name.value
			} following the subsequent graph:
${graph.join(' => ')}
`);
		}

		this.analyzing = true;
		this.dependencies.forEach((dependency) => {
			try {
				if (dependency.indexOf('parentAnchors') === -1) {
					const formula = glyph.getFromXPath(dependency);

					formula.analyzeDependency(glyph, graph);
				}
			}
			catch (e) {
				throw new Error(`There was an error while checking glyph ${
					glyph.name.value
				} dependencies for cursor: ${dependency}.
					${e.message}`);
			}
			graph.pop();
		});
		this.analyzing = false;
	}

	getResult(parameters, contours, anchors, parentAnchors, utils) {
		/* #if dev */
		const missingParam = _difference(this.parameters, _keys(parameters));

		if (missingParam.length > 0) {
			console.error(`parameters are missing: ${missingParam}`); // eslint-disable-line no-console
		}
		/* #end */
		const args = [contours, anchors, parentAnchors, utils];

		for (let i = 0; i < this.parameters.length; i++) {
			const name = this.parameters[i];

			args.push(parameters[name]);
		}

		const result = this.operation.apply(this, args);

		if (typeof result === 'number' && isNaN(result)) {
			/* eslint-disable no-console */
			console.error(`Operation returned NaN
operation is:
${this.operation.toString()}
parameters value are:
${this.parameters.map(name => `${name}: ${parameters[name]}`)}
cursor used are:
${this.dependencies.map(name => `${name}: ${_get(contours, name)}`)}`);
			/* eslint-enable no-console */
		}

		return result;
	}

	solveOperationOrder(glyph, operationOrder) {
		const result = [];
		const operationsToSolve = _difference(
			_uniq(this.dependencies),
			operationOrder,
		);

		if (operationsToSolve.length > 0) {
			result.push(
				..._reduce(
					operationsToSolve,
					(acc, xpath) => {
						const expandedIndex = xpath.indexOf('expandedTo');
						const processedOps = [...operationOrder, ...result, ...acc];

						// We don't have to compute dependcy on parentAnchors they are not
						// our responsability and should be provided by parent
						if (xpath.indexOf('parentAnchors') !== -1) {
							return acc;
						}

						if (xpath.match(/handle(Out|In)/)) {
							const contourPath = xpath
								.split('.')
								.slice(0, 2)
								.join('.');
							const contour = glyph.getFromXPath(contourPath);

							acc.push(
								...contour.solveOperationOrder(glyph, [...processedOps]),
							);
						}
						else if (expandedIndex !== -1) {
							/* eslint-disable no-negated-condition, max-depth */
							const base = xpath.substr(0, expandedIndex - 1);
							const node = glyph.getFromXPath(`${base}`);

							if (node.expandedTo) {
								if (process.env.TESTING_FONT === 'yes') {
									if (!glyph.getFromXPath(xpath)) {
										console.log(`${glyph.name.value} on cursor ${xpath}`); // eslint-disable-line no-console
									}
								}
								acc.push(
									...glyph
										.getFromXPath(xpath)
										.solveOperationOrder(glyph, [...processedOps]),
								);
							}
							else {
								if (process.env.TESTING_FONT === 'yes') {
									if (
										!glyph.getFromXPath(`${base}.expand.width`)
										|| !glyph.getFromXPath(`${base}.expand.distr`)
										|| !glyph.getFromXPath(`${base}.expand.angle`)
										|| !glyph.getFromXPath(`${base}.x`)
										|| !glyph.getFromXPath(`${base}.y`)
									) {
										console.log(`${glyph.name.value} on cursor ${base}`); // eslint-disable-line no-console
									}
								}
								const expandResult = glyph
									.getFromXPath(`${base}.expand.width`)
									.solveOperationOrder(glyph, processedOps);

								expandResult.push(
									...glyph
										.getFromXPath(`${base}.expand.distr`)
										.solveOperationOrder(glyph, [
											...processedOps,
											...expandResult,
										]),
								);
								expandResult.push(
									...glyph
										.getFromXPath(`${base}.expand.angle`)
										.solveOperationOrder(glyph, [
											...processedOps,
											...expandResult,
										]),
								);
								expandResult.push(
									...glyph
										.getFromXPath(`${base}.x`)
										.solveOperationOrder(glyph, [
											...processedOps,
											...expandResult,
										]),
								);
								expandResult.push(
									...glyph
										.getFromXPath(`${base}.y`)
										.solveOperationOrder(glyph, [
											...processedOps,
											...expandResult,
										]),
								);

								const opToAdd = {
									action: 'expand',
									cursor: base,
								};

								if (!_find([...processedOps, ...expandResult], opToAdd)) {
									expandResult.push(opToAdd);
								}

								acc.push(...expandResult);
							}
						}
						else {
							/* eslint-disable no-negated-condition, max-depth */
							acc.push(
								...glyph
									.getFromXPath(xpath)
									.solveOperationOrder(glyph, processedOps),
							);
						}

						return acc;
					},
					[],
				),
			);
		}

		if ([...operationOrder, ...result].indexOf(this.cursor) === -1) {
			result.push(this.cursor);
		}
		return result;
	}
}
