/*******************************************************************************
 * Copyright (c) 2018-2019 Aston University.
 * 
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License, v. 2.0 are satisfied: GNU General Public License, version 3.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-3.0
 *
 * Contributors:
 *     Antonio Garcia-Dominguez - initial API and implementation
 ******************************************************************************/
package org.eclipse.hawk.timeaware.queries;


import org.eclipse.epsilon.eol.execute.operations.EolOperationFactory;
import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphNode;
import org.eclipse.hawk.timeaware.queries.operations.patterns.AlwaysReducer;
import org.eclipse.hawk.timeaware.queries.operations.patterns.BoundedVersionQuantifierOperation;
import org.eclipse.hawk.timeaware.queries.operations.patterns.EventuallyAtLeastReducer;
import org.eclipse.hawk.timeaware.queries.operations.patterns.EventuallyAtMostReducer;
import org.eclipse.hawk.timeaware.queries.operations.patterns.EventuallyReducer;
import org.eclipse.hawk.timeaware.queries.operations.patterns.NeverReducer;
import org.eclipse.hawk.timeaware.queries.operations.patterns.VersionQuantifierOperation;
import org.eclipse.hawk.timeaware.queries.operations.scopes.EndingTimeAwareNodeWrapper;
import org.eclipse.hawk.timeaware.queries.operations.scopes.StartingTimeAwareNodeWrapper;
import org.eclipse.hawk.timeaware.queries.operations.scopes.annotations.AfterAnnotatedOperation;
import org.eclipse.hawk.timeaware.queries.operations.scopes.annotations.BeforeAnnotatedOperation;
import org.eclipse.hawk.timeaware.queries.operations.scopes.annotations.SinceAnnotatedOperation;
import org.eclipse.hawk.timeaware.queries.operations.scopes.annotations.UntilAnnotatedOperation;
import org.eclipse.hawk.timeaware.queries.operations.scopes.annotations.WhenAnnotatedOperation;
import org.eclipse.hawk.timeaware.queries.operations.scopes.predicates.VersionRangeOperation;
import org.eclipse.hawk.timeaware.queries.operations.scopes.predicates.WhenOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Extended version of the EOL operation factory, adding a new set of first-order
 * operations over the versions of types and nodes.
 */
public class TimeAwareEOLOperationFactory extends EolOperationFactory {

	private static final Logger LOGGER = LoggerFactory.getLogger(TimeAwareEOLOperationFactory.class);

	private final TimeAwareEOLQueryEngine containerModel;

	public TimeAwareEOLOperationFactory(TimeAwareEOLQueryEngine model) {
		this.containerModel = model;

		operationCache.put("always",
			new VersionQuantifierOperation(this::getContainerModel, new AlwaysReducer()));
		operationCache.put("never",
			new VersionQuantifierOperation(this::getContainerModel, new NeverReducer()));
		operationCache.put("eventually",
			new VersionQuantifierOperation(this::getContainerModel, new EventuallyReducer()));
		operationCache.put("eventuallyAtMost",
			new BoundedVersionQuantifierOperation(this::getContainerModel, (count -> new EventuallyAtMostReducer(count))));
		operationCache.put("eventuallyAtLeast",
			new BoundedVersionQuantifierOperation(this::getContainerModel, (count -> new EventuallyAtLeastReducer(count))));

		/*
		 * First-order operation that makes the target time-aware node travel to the
		 * first timepoint since the current timepoint where a particular predicate was
		 * true, if it exists. The returned time-aware node will only report the
		 * versions since that moment: any past history will be ignored.
		 *
		 * This means that if you want to search through the entire history of a node,
		 * you will have to use <code>node.earliest.since(...)</code>. This is done to
		 * make it easier to restrict the scope of the search in long histories.
		 *
		 * If no such timepoint exists, the operation will return an undefined value,
		 * which can be checked against with <code>.isDefined()</code>.
		 */
		operationCache.put("since",
			new VersionRangeOperation(this::getContainerModel,
				(original, match) -> new StartingTimeAwareNodeWrapper(match)));

		/*
		 * Variant of .since which does not include the matching version, implementing
		 * an exclusive starting range.
		 */
		operationCache.put("after",
			new VersionRangeOperation(this::getContainerModel,
				(original, version) -> {
					try {
						ITimeAwareGraphNode nextVersion = version.getNext();
						if (nextVersion != null) {
							return new StartingTimeAwareNodeWrapper(nextVersion);
						}
					} catch (Exception e) {
						LOGGER.error(e.getMessage(), e);
					}

					return null;
				}
		));

		/*
		 * First-order operation that returns a version of the current node which
		 * will only report versions up to and including the first timepoint for
		 * which the predicate is true. This implements a closed ending range.
		 *
		 * If no such timepoint exists, the operation will return an undefined value,
		 * which can be checked against with <code>.isDefined()</code>.
		 */
		operationCache.put("until",
			new VersionRangeOperation(this::getContainerModel,
				(original, version) -> new EndingTimeAwareNodeWrapper(original, version.getTime())
		));

		/*
		 * First-order operation that returns a version of the current node which
		 * will only report versions before (excluding) the first timepoint for
		 * which the predicate is true. This implements an open ending range.
		 *
		 * If no such timepoint exists, the operation will return an undefined value,
		 * which can be checked against with <code>isDefined()</code>.
		 */
		operationCache.put("before",
			new VersionRangeOperation(this::getContainerModel, (original, version) -> {
				try {
					final long prevInstant = version.getPreviousInstant();
					if (prevInstant != ITimeAwareGraphNode.NO_SUCH_INSTANT) {
						return new EndingTimeAwareNodeWrapper(original, prevInstant);
					}
				} catch (Exception e) {
					LOGGER.error("Could not retrieve previous instant for before", e);
				}

				return null;
		}));

		operationCache.put("when", new WhenOperation(this::getContainerModel));

		/*
		 * Versions of these operations which use Boolean derived attributes in a
		 * time-aware node index.
		 */
		operationCache.put("whenAnnotated", new WhenAnnotatedOperation(this::getContainerModel));
		operationCache.put("sinceAnnotated", new SinceAnnotatedOperation(this::getContainerModel));
		operationCache.put("afterAnnotated", new AfterAnnotatedOperation(this::getContainerModel));
		operationCache.put("untilAnnotated", new UntilAnnotatedOperation(this::getContainerModel));
		operationCache.put("beforeAnnotated", new BeforeAnnotatedOperation(this::getContainerModel));
	}

	public TimeAwareEOLQueryEngine getContainerModel() {
		return containerModel;
	}

}
