/*
 * #%L
 * Eureka Protempa ETL
 * %%
 * Copyright (C) 2012 - 2013 Emory University
 * %%
 * This program is dual licensed under the Apache 2 and GPLv3 licenses.
 * 
 * Apache License, Version 2.0:
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * GNU General Public License version 3:
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */
package edu.emory.cci.aiw.cvrg.eureka.etl.ksb;

import edu.emory.cci.aiw.cvrg.eureka.etl.config.EtlProperties;
import edu.emory.cci.aiw.cvrg.eureka.etl.config.EurekaProtempaConfigurations;
import org.protempa.KnowledgeSource;
import org.protempa.KnowledgeSourceReadException;
import org.protempa.PropositionDefinition;
import org.protempa.SourceFactory;
import org.protempa.backend.BackendInitializationException;
import org.protempa.backend.BackendNewInstanceException;
import org.protempa.backend.BackendProviderSpecLoaderException;
import org.protempa.backend.Configurations;
import org.protempa.backend.ConfigurationsLoadException;
import org.protempa.backend.ConfigurationsNotFoundException;
import org.protempa.backend.InvalidConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;
import org.apache.commons.collections4.map.ReferenceMap;
import org.protempa.SourceCloseException;

public class PropositionDefinitionFinder implements AutoCloseable {

	private static final Logger LOGGER = LoggerFactory.getLogger(PropositionDefinitionFinder.class);
	private static final Map<String, List<PropositionDefinition>> parentsCache = new ReferenceMap<>();
	private final KnowledgeSource knowledgeSource;
	private final EtlProperties etlProperties;
	private final Set<String> defaultProps;

	public PropositionDefinitionFinder(String configId,
			EtlProperties etlProperties) throws PropositionFinderException {
		this.etlProperties = etlProperties;
		try {
			this.validateConfigDir(etlProperties.getSourceConfigDirectory());
			LOGGER.debug("Creating new configurations, source factory, and knowledge source");
			Configurations configurations
					= new EurekaProtempaConfigurations(etlProperties);
			SourceFactory sf = new SourceFactory(configurations, configId);
			this.knowledgeSource = sf.newKnowledgeSourceInstance();
			this.defaultProps = new HashSet<>(this.etlProperties.getDefaultSystemPropositions());
			LOGGER.debug("Done: configurations, source factory, and knowledge source created");
		} catch (IOException | ConfigurationsNotFoundException | BackendInitializationException | BackendNewInstanceException | ConfigurationsLoadException | InvalidConfigurationException | BackendProviderSpecLoaderException ex) {
			throw new PropositionFinderException(ex);
		}
	}

	public List<PropositionDefinition> findAll(Collection<String> propIds) throws PropositionFinderException {
		try {
			return this.knowledgeSource.readPropositionDefinitions(propIds.toArray(new String[propIds.size()]));
		} catch (KnowledgeSourceReadException e) {
			throw new PropositionFinderException(e);
		}
	}

	public PropositionDefinition find(String inKey) throws
			PropositionFinderException {
		PropositionDefinition definition = null;
		try {
			definition = this.knowledgeSource.readPropositionDefinition(inKey);
		} catch (KnowledgeSourceReadException e) {
			throw new PropositionFinderException(e);
		}
		return definition;
	}

	public List<String> searchPropositions(String inSearchKey) throws PropositionFinderException {
		LinkedHashSet<String> nodesToLoad = new LinkedHashSet<>();
		try {
			List<PropositionDefinition> searchResults = knowledgeSource.getMatchingPropositionDefinitions(inSearchKey);
			for (PropositionDefinition pf : searchResults) {
				if (nodesToLoad.size() > etlProperties.getSearchLimit()) {
					break;
				} else {
					if (pf != null) {
						readParentsForSearchResult(pf, nodesToLoad);
					}
				}
			}
		} catch (KnowledgeSourceReadException e) {
			throw new PropositionFinderException(e);
		}
		return new ArrayList<>(nodesToLoad);
	}


	public List<PropositionDefinition> getPropositionsBySearchKey(String inSearchKey) throws PropositionFinderException {
		List<PropositionDefinition> nodesToLoad = new ArrayList<PropositionDefinition>();
		try {
			nodesToLoad = knowledgeSource.getMatchingPropositionDefinitions(inSearchKey);

		} catch (KnowledgeSourceReadException e) {
			throw new PropositionFinderException(e);
		}
		return nodesToLoad;
	}

	@Override
	public void close() throws PropositionFinderException {
		try {
			this.knowledgeSource.close();
		} catch (SourceCloseException ex) {
			throw new PropositionFinderException(ex);
		}
	}

	private void readParentsForSearchResult(PropositionDefinition pf, LinkedHashSet<String> nodesToLoad) throws PropositionFinderException {
		try {
			Queue<PropositionDefinition> toProcessQueue = new LinkedList<>();
			Stack<String> processedStack = new Stack<>();
			toProcessQueue.add(pf);
			while (!toProcessQueue.isEmpty()) {
				PropositionDefinition currentPropDef = toProcessQueue.remove();
				List<PropositionDefinition> parents;
				synchronized (parentsCache) {
					parents = parentsCache.get(currentPropDef.getId());
					if (parents == null) {
						parents = knowledgeSource.readParents(currentPropDef);
						parentsCache.put(currentPropDef.getId(), parents);
					}
				}
				for (PropositionDefinition parent : parents) {
					toProcessQueue.add(parent);
					processedStack.add(parent.getId());
				}
			}
			getNodesToLoad(processedStack, nodesToLoad);
		} catch (KnowledgeSourceReadException e) {
			throw new PropositionFinderException(e);
		}
	}

	private void getNodesToLoad(Stack<String> processedStack, LinkedHashSet<String> nodesToLoad) {
		while (!processedStack.empty()) {
			String node = processedStack.pop();
			if (!nodesToLoad.contains(node)) {
				if (defaultProps.contains(node)) {
					nodesToLoad.add(node);
				} else {
					List<PropositionDefinition> parents;
					synchronized (parentsCache) {
						parents = parentsCache.get(node);
					}
					if (parents != null) {
						for (PropositionDefinition parent : parents) {
							if (nodesToLoad.contains(parent.getId())) {
								nodesToLoad.add(node);
								break;
							}
						}
					}
				}
			}
		}
	}

	private void validateConfigDir(File inFile) {
		if (LOGGER.isErrorEnabled()) {
			if (!inFile.exists()) {
				LOGGER.error(
						"Configuration directory " + inFile.getAbsolutePath()
						+ " does not exist. Proposition finding will not work"
						+ " without it. Please create it and try again.");
			} else if (!inFile.isDirectory()) {
				LOGGER.error(
						"Path " + inFile.getAbsolutePath() + " is not a "
						+ "directory. Proposition finding requires it to be a"
						+ " directory.");
			}
		}
	}

}
