"use strict";

const syncpipe = require("syncpipe");
const splitFilter = require("split-filter");
const asExpression = require("as-expression");

const NoChange = require("./util/no-change");
const deriveNode = require("../derive-node");
const operations = require("../operations");
const typeOf = require("../type-of");
const unreachable = require("../unreachable");
const concat = require("../concat");
const merge = require("../merge");

const uniqueByPredicate = require("../unique-by-predicate");

// FIXME: Support for remote field names

function uniqueFields(fields) {
	return uniqueByPredicate(fields, (field) => field.name);
}

/*
valid fields when collapsing:
- fields that appear in the collapseBy field list, within or without a hierarchical wrapper
- any field that is wrapped in an aggregrate function of some sort
*/

module.exports = {
	name: "set-collapse-by-fields",
	category: [ "normalization" ],
	visitors: {
		collapseBy: (_node, { setState }) => {
			setState("isCollapsing", true);
			return NoChange;
		},
		field: (node, { setState }) => {
			setState("fieldSeen", node);
			return NoChange;
		},
		// FIXME: Think of a generic way to express "only match fields under this specific child property" (+ direct descendants also?) -- maybe (node, property, parentNode) signature?
		collapseByFields: (node, { registerStateHandler, setState, defer }) => {
			let fields = [];

			registerStateHandler("fieldSeen", (node) => {
				fields.push(node);
			});

			return defer(() => {
				setState("setCollapsedFields", fields);
				return NoChange;
			});
		},
		addFields: (node, { setState }) => {
			setState("setAddFields", node.fields);
			return NoChange;
		},
		onlyFields: (node, { setState }) => {
			setState("setOnlyFields", node.fields);
			return NoChange;
		},
		aggregrateFunction: (node, { registerStateHandler }) => {
			// FIXME: Also report isCollapsing here, due to aggregrate function use, but make sure that the error describes this as the (possible) cause
			return NoChange;
		},
		compute: (node, { setState }) => {
			setState("computeSeen", node);
			return NoChange;
		},
		select: (node, { registerStateHandler, defer }) => {
			let isCollapsing;
			let onlyFields = [];
			let addFields = [];
			let computes = [];
			let collapsedFields;

			registerStateHandler("isCollapsing", (value) => {
				isCollapsing = isCollapsing || value;
			});

			registerStateHandler("setCollapsedFields", (fields) => {
				if (collapsedFields == null) {
					collapsedFields = fields;
				} else {
					throw new Error(`You can currently only specify a single 'collapseBy' clause. Please file an issue if you have a reason to need more than one!`);
				}
			});

			registerStateHandler("setOnlyFields", (fields) => {
				onlyFields = onlyFields.concat(fields);
			});

			registerStateHandler("setAddFields", (fields) => {
				addFields = addFields.concat(fields);
			});

			registerStateHandler("computeSeen", (node) => {
				computes.push(node);
			});

			return defer(() => {
				if (isCollapsing) {
					if (addFields.length > 0) {
						let extraFieldNames = addFields.map((field) => field.name);

						throw new Error(`You tried to add extra fields (${extraFieldNames.join(", ")}) in your query, but this is not possible when using collapseBy. See [FIXME: link] for more information, and how to solve this.`);
					} else if (onlyFields.length > 0) {
						// NOTE: This can happen either because the user specified an onlyFields clause, *or* because a previous run of this optimizer did so!
						let uniqueSelectedFields = uniqueFields(onlyFields);
						let collapsedFieldNames = collapsedFields.map((field) => field.name);

						let invalidFieldSelection = uniqueSelectedFields.filter((node) => {
							let isAggregrateComputation = typeOf(node) === "alias" && typeOf(node.expression) === "aggregrateFunction";
							let isCollapsedField = typeOf(node) === "field" && collapsedFieldNames.includes(node.name);

							let isValid = isAggregrateComputation || isCollapsedField;

							return !isValid;
						});

						// FIXME: We can probably optimize this by marking the optimizer-created onlyFields as inherently-valid, via some sort of node metadata mechanism

						if (invalidFieldSelection.length > 0) {
							let invalidFieldNames = invalidFieldSelection.map((item) => {
								let fieldType = typeOf(item);

								if (fieldType === "field") {
									return item.name;
								} else if (fieldType === "alias") {
									// FIXME: Show alias target instead of field name here?
									return item.field.name;
								} else {
									return unreachable(`Encountered '${fieldType}' node in invalid fields`);
								}
							});

							throw new Error(`You tried to include one or more field in your query (${invalidFieldNames.join(", ")}), that are not used in a collapseBy clause or aggregrate function. See [FIXME: link] for more information.`);
						} else {
							return NoChange;
						}
					} else {
						// TODO: Move compute -> alias/withResult conversion out into its own optimizer eventually, as it's a separate responsibility
						let [ withResultComputes, aliasComputes ] = splitFilter(computes, (node) => typeof node.expression === "function");

						let computeAliases = aliasComputes.map((node) => {
							return operations.alias(node.field, node.expression);
						});

						let withResultsOperations = asExpression(() => {
							if (withResultComputes.length > 0) {
								return operations.withResult((item) => {
									// FIXME: Remote fields support
									let computedFields = syncpipe(withResultComputes, [
										(_) => _.map((compute) => {
											let fieldName = compute.field.name;
											return [ fieldName, compute.expression(item[fieldName]) ];
										}),
										(_) => Object.fromEntries(_)
									]);

									return merge(item, computedFields);
								});
							} else {
								return [];
							}
						});

						return deriveNode(node, {
							clauses: node.clauses.concat([
								operations.onlyFields(concat([
									collapsedFields,
									computeAliases
								])),
								withResultsOperations
							])
						});
					}
				}
			});
		},
	}
};

// FIXME: a ConsumeNode marker, like RemoveNode but it does not invalidate that node's state... may need to actually make it a reference, so that a parent node can decide whether to consume that node. Basically passing a "consume this node" function as a state value, that correctly internally triggers the optimizer infrastructure to change the tree as a result.
// FIXME: Consume the compute nodes, and have an optimizer that removes empty computeMultiple nodes
