/**
 * Copyright (C) 2010-2016 Structr GmbH
 *
 * This file is part of Structr <http://structr.org>.
 *
 * Structr 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.
 *
 * Structr 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 Structr.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.structr.core.graph;

import java.io.File;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.structr.api.DatabaseService;
import org.structr.api.index.Index;
import org.structr.api.graph.Node;
import org.structr.api.graph.Relationship;
import org.structr.api.Transaction;
import org.structr.api.config.Structr;
import org.structr.common.SecurityContext;
import org.structr.common.error.FrameworkException;
import org.structr.api.service.Command;
import org.structr.core.GraphObject;
import org.structr.core.Services;
import org.structr.api.service.SingletonService;
import org.structr.api.service.StructrServices;
import org.structr.core.app.StructrApp;


/**
 * The graph/node service main class.
 *
 *
 *
 */
public class NodeService implements SingletonService {

	private static final Logger logger = Logger.getLogger(NodeService.class.getName());

	//~--- fields ---------------------------------------------------------

	private DatabaseService graphDb                 = null;
	private Index<Node> fulltextIndex               = null;
	private Index<Node> keywordIndex                = null;
	private Index<Node> layerIndex                  = null;
	private Index<Relationship> relFulltextIndex    = null;
	private Index<Relationship> relKeywordIndex     = null;

	// indices
	private final Map<RelationshipIndex, Index<Relationship>> relIndices = new EnumMap<>(RelationshipIndex.class);
	private final Map<NodeIndex, Index<Node>> nodeIndices                = new EnumMap<>(NodeIndex.class);

	/** Dependent services */
	private String filesPath      = null;
	private boolean isInitialized = false;

	//~--- constant enums -------------------------------------------------

	/**
	 * The list of existing node indices.
	 */
	public static enum NodeIndex { keyword, fulltext, layer }

	/**
	 * The list of existing relationship indices.
	 */
	public static enum RelationshipIndex { rel_keyword, rel_fulltext }

	@Override
	public void injectArguments(Command command) {

		if (command != null) {

			command.setArgument("graphDb", graphDb);

			command.setArgument(NodeIndex.fulltext.name(), fulltextIndex);
			command.setArgument(NodeIndex.keyword.name(), keywordIndex);
			command.setArgument(NodeIndex.layer.name(), layerIndex);

			command.setArgument(RelationshipIndex.rel_fulltext.name(), relFulltextIndex);
			command.setArgument(RelationshipIndex.rel_keyword.name(), relKeywordIndex);

			command.setArgument("filesPath", filesPath);

			command.setArgument("indices", NodeIndex.values());
			command.setArgument("relationshipIndices", RelationshipIndex.values());



		}
	}

	@Override
	public void initialize(final StructrServices services, final Properties config) throws ClassNotFoundException, InstantiationException, IllegalAccessException {

		final String databaseDriver = config.getProperty(Structr.DATABASE_DRIVER, "org.structr.neo4j.Neo4jDatabaseService");
		graphDb = (DatabaseService)Class.forName(databaseDriver).newInstance();
		if (graphDb != null) {

			graphDb.initialize(config);

			filesPath = config.getProperty(Services.FILES_PATH);

			// check existence of files path
			File files = new File(filesPath);
			if (!files.exists()) {

				files.mkdir();
			}

			logger.log(Level.INFO, "Database ready.");

			// index creation transaction
			try ( final Transaction tx = graphDb.beginTx() ) {

				fulltextIndex = graphDb.nodeIndexer().fulltext();
				nodeIndices.put(NodeIndex.fulltext, fulltextIndex);

				keywordIndex = graphDb.nodeIndexer().exact();
				nodeIndices.put(NodeIndex.keyword, keywordIndex);

				layerIndex = graphDb.nodeIndexer().spatial();
				nodeIndices.put(NodeIndex.layer, layerIndex);

				relFulltextIndex = graphDb.relationshipIndexer().fulltext();
				relIndices.put(RelationshipIndex.rel_fulltext, relFulltextIndex);

				relKeywordIndex = graphDb.relationshipIndexer().exact();
				relIndices.put(RelationshipIndex.rel_keyword, relKeywordIndex);

				tx.success();

			} catch (Throwable t) {

				logger.log(Level.WARNING, "Error while initializing indexes.", t);
			}

			isInitialized = true;

		}
	}

	@Override
	public void initialized() {

		// check for empty database and seed file
		importSeedFile(StructrApp.getConfigurationValue(Services.BASE_PATH));
	}

	@Override
	public void shutdown() {

		if (isRunning()) {

			logger.log(Level.INFO, "Shutting down graph database service");
			graphDb.shutdown();

			graphDb       = null;
			isInitialized = false;

		}

	}

	@Override
	public String getName() {

		return NodeService.class.getSimpleName();

	}

	public DatabaseService getGraphDb() {
		return graphDb;
	}

	@Override
	public boolean isRunning() {

		return ((graphDb != null) && isInitialized);
	}

	@Override
	public boolean isVital() {
		return true;
	}

	public Collection<Index<Node>> getNodeIndices() {
		return nodeIndices.values();
	}

	public Collection<Index<Relationship>> getRelationshipIndices() {
		return relIndices.values();
	}

	public Index<Node> getNodeIndex(NodeIndex name) {
		return nodeIndices.get(name);
	}

	public Index<Relationship> getRelationshipIndex(RelationshipIndex name) {
		return relIndices.get(name);
	}

	private void importSeedFile(final String basePath) {

		final File seedFile = new File(StructrServices.trim(basePath) + "/" + Services.INITIAL_SEED_FILE);

		if (seedFile.exists()) {

			boolean hasApplicationNodes   = false;

			try (final Tx tx = StructrApp.getInstance().tx()) {

				final Iterator<Node> allNodes = graphDb.getAllNodes().iterator();
				final String idName           = GraphObject.id.dbName();

				while (allNodes.hasNext()) {

					if (allNodes.next().hasProperty(idName)) {

						hasApplicationNodes = true;
						break;
					}
				}

				tx.success();

			} catch (FrameworkException fex) { }

			if (!hasApplicationNodes) {

				logger.log(Level.INFO, "Found initial seed file and no application nodes, applying initial seed..");

				try {

					SyncCommand.importFromFile(graphDb, SecurityContext.getSuperUserInstance(), seedFile.getAbsoluteFile().getAbsolutePath(), false);

				} catch (FrameworkException fex) {

					logger.log(Level.WARNING, "Unable to import initial seed file.", fex);
				}
			}
		}
	}
}
