// This should be the new way to implement mecanisms
// In a specific file
// TODO import them automatically
// TODO convert the legacy functions to new files
import barème from 'Engine/mecanisms/barème'
import barèmeContinu from 'Engine/mecanisms/barème-continu'
import barèmeLinéaire from 'Engine/mecanisms/barème-linéaire'
import operation from 'Engine/mecanisms/operation'
import variations from 'Engine/mecanisms/variations'
import { Grammar, Parser } from 'nearley'
import {
	add,
	cond,
	divide,
	equals,
	fromPairs,
	gt,
	gte,
	is,
	keys,
	lt,
	lte,
	multiply,
	propOr,
	subtract,
	T,
	without
} from 'ramda'
import React from 'react'
import grammar from './grammar.ne'
import {
	mecanismAllOf,
	mecanismComplement,
	mecanismError,
	mecanismInversion,
	mecanismMax,
	mecanismMin,
	mecanismNumericalSwitch,
	mecanismOneOf,
	mecanismOnePossibility,
	mecanismProduct,
	mecanismReduction,
	mecanismSum,
	mecanismSynchronisation
} from './mecanisms'
import { parseReferenceTransforms } from './parseReference'
import { formatValue } from 'Engine/format'

export let parse = (rules, rule, parsedRules) => rawNode => {
	let onNodeType = cond([
		[is(String), parseString(rules, rule, parsedRules)],
		[is(Number), parseNumber],
		[is(Object), parseObject(rules, rule, parsedRules)],
		[T, parseOther]
	])

	let defaultEvaluate = (cache, situationGate, parsedRules, node) => node
	let parsedNode = onNodeType(rawNode)

	return parsedNode.evaluate
		? parsedNode
		: { ...parsedNode, evaluate: defaultEvaluate }
}

const compiledGrammar = Grammar.fromCompiled(grammar)

export let parseString = (rules, rule, parsedRules) => rawNode => {
	/* Strings correspond to infix expressions.
	 * Indeed, a subset of expressions like simple arithmetic operations `3 + (quantity * 2)` or like `salary [month]` are more explicit that their prefixed counterparts.
	 * This function makes them prefixed operations. */
	let [parseResult] = new Parser(compiledGrammar).feed(rawNode).results
	return parseObject(rules, rule, parsedRules)(parseResult)
}

export let parseNumber = rawNode => ({
	text: '' + rawNode,
	category: 'number',
	nodeValue: rawNode,
	type: 'numeric',
	jsx: <span className="number">{rawNode}</span>
})

export let parseOther = rawNode => {
	throw new Error(
		'Cette donnée : ' + rawNode + ' doit être un Number, String ou Object'
	)
}

export let parseObject = (rules, rule, parsedRules) => rawNode => {
	/* TODO instead of describing mecanisms in knownMecanisms.yaml, externalize the mecanisms themselves in an individual file and describe it
	let mecanisms = intersection(keys(rawNode), keys(knownMecanisms))

	if (mecanisms.length != 1) {
	}
	*/

	let attributes = keys(rawNode),
		descriptiveAttributes = ['description', 'note', 'référence'],
		relevantAttributes = without(descriptiveAttributes, attributes)
	if (relevantAttributes.length !== 1)
		throw new Error(`OUPS : On ne devrait reconnaître que un et un seul mécanisme dans cet objet (au-delà des attributs descriptifs tels que "description", "commentaire", etc.)
			Objet YAML : ${JSON.stringify(rawNode)}
			Cette liste doit avoir un et un seul élément.
			Si vous venez tout juste d'ajouter un nouveau mécanisme, vérifier qu'il est bien intégré dans le dispatch de parse.js
		`)
	let k = relevantAttributes[0],
		v = rawNode[k]

	let knownOperations = {
			'*': [multiply, '×'],
			'/': [divide, '∕'],
			'+': [add],
			'-': [subtract, '−'],
			'<': [lt],
			'<=': [lte, '≤'],
			'>': [gt],
			'>=': [gte, '≥'],
			'=': [equals],
			'!=': [(a, b) => !equals(a, b), '≠']
		},
		operationDispatch = fromPairs(
			Object.entries(knownOperations).map(([k, [f, symbol]]) => [
				k,
				operation(k, f, symbol)
			])
		)

	let dispatch = {
			'une de ces conditions': mecanismOneOf,
			'toutes ces conditions': mecanismAllOf,
			'aiguillage numérique': mecanismNumericalSwitch,
			somme: mecanismSum,
			multiplication: mecanismProduct,
			barème,
			'barème linéaire': barèmeLinéaire,
			'barème continu': barèmeContinu,
			'le maximum de': mecanismMax,
			'le minimum de': mecanismMin,
			complément: mecanismComplement,
			'une possibilité': mecanismOnePossibility(rule.dottedName),
			'inversion numérique': mecanismInversion(rule.dottedName),
			allègement: mecanismReduction,
			variations,
			synchronisation: mecanismSynchronisation,
			...operationDispatch,
			filter: () =>
				parseReferenceTransforms(rules, rule, parsedRules)({
					filter: v.filter,
					variable: v.explanation
				}),
			variable: () =>
				parseReferenceTransforms(rules, rule, parsedRules)({ variable: v }),
			temporalTransform: () =>
				parseReferenceTransforms(rules, rule, parsedRules)({
					variable: v.explanation,
					temporalTransform: v.temporalTransform
				}),
			constant: () => ({
				type: v.type,
				nodeValue: v.nodeValue,
				unit: v.unit,
				// eslint-disable-next-line
				jsx: () => (
					<span className={v.type}>
						{formatValue({
							unit: v.unit,
							value: v.nodeValue,
							// We want to display constants with full precision,
							// espacilly for percentages like APEC 0,036 %
							maximumFractionDigits: 5
						})}
					</span>
				)
			})
		},
		action = propOr(mecanismError, k, dispatch)

	return action(parse(rules, rule, parsedRules), k, v)
}