/*******************************************************************************
 * Copyright (c) 2011-2015 The University of York.
 * 
 * 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:
 *     Konstantinos Barmpis - initial API and implementation
 ******************************************************************************/
package org.eclipse.hawk.epsilon.emc;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.epsilon.common.parse.problem.ParseProblem;
import org.eclipse.epsilon.eol.EolModule;
import org.eclipse.epsilon.eol.execute.context.Variable;
import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.graph.IGraphNode;
import org.eclipse.hawk.epsilon.emc.EOLQueryEngine.GraphNodeWrapper;
import org.eclipse.hawk.epsilon.emc.pgetters.GraphPropertyGetter;
import org.eclipse.hawk.epsilon.emc.tracking.AccessListener;
import org.eclipse.hawk.graph.updater.DirtyDerivedFeaturesListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DeriveFeature {

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

	public Object deriveFeature(Map<String, EolModule> cachedModules, IModelIndexer indexer, IGraphNode n,
			EOLQueryEngine containerModel, String EOLScript) throws Exception {

		// remove prefix (_NYD)
		final String actualEOLScript = EOLScript.startsWith(DirtyDerivedFeaturesListener.NOT_YET_DERIVED_PREFIX)
				? EOLScript.substring(DirtyDerivedFeaturesListener.NOT_YET_DERIVED_PREFIX.length())
				: EOLScript;

		try {
			final EolModule currentModule = setupModule(cachedModules, indexer, n, containerModel, actualEOLScript);
			if (currentModule == null) {
				return "DERIVATION_PARSE_ERROR";
			}
			
			final GraphPropertyGetter pg = (GraphPropertyGetter) containerModel.getPropertyGetter();
			final AccessListener accessListener = pg.getAccessListener();
			accessListener.setSourceObject(n.getId() + "");
			return runModule(n, currentModule);
		} catch (Exception e) {
			LOGGER.error("ERROR IN DERIVING ATTRIBUTE, returning \"DERIVATION_OTHER_ERROR\" as value", e);
		}

		return "DERIVATION_OTHER_ERROR";
	}

	public Object deriveTimelineAnnotation(Map<String, EolModule> cachedModules, IModelIndexer indexer, IGraphNode n,
			EOLQueryEngine containerModel, String EOLScript) throws Exception {
		final GraphPropertyGetter pg = (GraphPropertyGetter) containerModel.getPropertyGetter();
		final boolean originalBroadcast = pg.getBroadcastStatus();
		pg.setBroadcastAccess(false);
		try {
			final EolModule currentModule = setupModule(cachedModules, indexer, n, containerModel, EOLScript);
			if (currentModule == null) {
				return "DERIVATION_PARSE_ERROR";
			}
			return runModule(n, currentModule);
		} catch (Exception e) {
			LOGGER.error("ERROR IN DERIVING ATTRIBUTE, returning \"DERIVATION_OTHER_ERROR\" as value", e);
		} finally {
			pg.setBroadcastAccess(originalBroadcast);
		}

		return "DERIVATION_OTHER_ERROR";
	}

	private Object runModule(IGraphNode n, EolModule currentModule) {
		try {
			final Object ret = currentModule.execute();
			return normalizeResult(ret);
		} catch (Exception e) {
			LOGGER.error("error in derive feature on {}, returning derivation execution error", n.getId());
			LOGGER.error(e.getMessage(), e);
			return "DERIVATION_EXECUTION_ERROR";
		}
	}


	private Object normalizeResult(Object ret) {
		if (!(ret instanceof Collection<?>)) {
			return AbstractHawkModel.toPrimitive(ret);
		} else {

			Collection<Object> collection = null;
			// check for uniqueness
			if (ret instanceof Set<?>)
				collection = new LinkedHashSet<>();
			else
				collection = new LinkedList<>();

			final Collection<?> srcCollection = (Collection<?>) ret;
			Class<?> elemClass = null;
			for (Object o : srcCollection) {
				Object converted = AbstractHawkModel.toPrimitive(o);
				if (converted != null) {
					collection.add(converted);
				}
				if (elemClass == null) {
					elemClass = converted.getClass();
				}
			}
			if (elemClass == null) {
				elemClass = String.class;
			}

			Object r = Array.newInstance(elemClass, collection.size());
			return collection.toArray((Object[]) r);
		}
	}


	private EolModule setupModule(Map<String, EolModule> cachedModules, IModelIndexer indexer, IGraphNode n,
			EOLQueryEngine containerModel, final String actualEOLScript) throws Exception {
		EolModule currentModule;
		if (!cachedModules.containsKey(actualEOLScript)) {
			// not already parsed
			LOGGER.debug("adding new module to cache, key: {}",
				actualEOLScript.length() > 100
					? actualEOLScript.substring(0, 100) + "\n[! long script, snipped !]"
					: actualEOLScript
			);

			currentModule = initModule(indexer, containerModel);
			currentModule.parse(actualEOLScript);
			List<ParseProblem> pps = currentModule.getParseProblems();
			for (ParseProblem p : pps) {
				LOGGER.error("Parsing problem: {}", p);
			}

			if (pps.size() > 0) {
				LOGGER.error("There were parse problems, returning \"DERIVATION_PARSE_ERROR\" as value\n");
				return null;
			} else {
				cachedModules.put(actualEOLScript, currentModule);
			}
		} else {
			// already parsed
			currentModule = cachedModules.get(actualEOLScript);
		}

		final IGraphNode modelElementNode = n.getIncoming().iterator().next().getStartNode();
		final GraphNodeWrapper meGW = containerModel.wrap(modelElementNode);
		currentModule.getContext().getFrameStack().put(Variable.createReadOnlyVariable("self", meGW));

		return currentModule;
	}

	private EolModule initModule(IModelIndexer m, EOLQueryEngine model) throws Exception {
		EolModule currentModule = new EolModule();
		model.load(m);
		currentModule.getContext().getModelRepository().addModel(model);
		return currentModule;
	}

}
