/*******************************************************************************
 * Copyright (c) 2015-2018 The University of York, 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.graph;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.NoSuchElementException;

import org.eclipse.hawk.core.graph.IGraphDatabase;
import org.eclipse.hawk.core.graph.IGraphTransaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Read-only abstraction of the known metadata about a slot (an attribute or a
 * reference) in the graph populated by this updater.
 */
public class Slot {

	public static final String ATTR_TYPE_TIMEANNOTATION = "TimelineAnnotation";

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

	private final TypeNode typeNode;
	private final String propertyName, propertyType;
	private final PropertyCategory propertyCategory;
	private final boolean isMany, isOrdered, isUnique, isIndexed;

	// Only valid if this is derived
	private final String derivationLanguage, derivationLogic;

	public enum PropertyCategory {
		ATTRIBUTE {
			@Override
			public String getLetter() {
				return "a";
			}
		}, REFERENCE {
			@Override
			public String getLetter() {
				return "r";
			}
		}, MIXED {
			@Override
			public String getLetter() {
				return "m";
			}
		}, DERIVED {
			@Override
			public String getLetter() {
				return "d";
			}
		};

		public abstract String getLetter();

		public static PropertyCategory fromLetter(String letter) {
			for (PropertyCategory pc : PropertyCategory.values()) {
				if (pc.getLetter().equals(letter)) {
					return pc;
				}
			}
			throw new NoSuchElementException(letter);
		}
	}

	public static class MetadataBuilder {
		private String propertyType, derivationLanguage, derivationLogic;

		private PropertyCategory pCategory = PropertyCategory.ATTRIBUTE;
		private boolean isMany, isOrdered, isUnique, isIndexed;

		public MetadataBuilder attribute() { pCategory = PropertyCategory.ATTRIBUTE; return this; }
		public MetadataBuilder reference() { pCategory = PropertyCategory.REFERENCE; return this; }
		public MetadataBuilder mixed()     { pCategory = PropertyCategory.MIXED; return this; }
		public MetadataBuilder derived()   { pCategory = PropertyCategory.DERIVED; return this; }

		public MetadataBuilder type(String s) { propertyType = s; return this; }
		public MetadataBuilder derivationLanguage(String s) { derivationLanguage = s; return this; }
		public MetadataBuilder derivationLogic(String s)    { derivationLogic = s;    return this; }

		public MetadataBuilder many(boolean b)    { isMany = b;    return this; }
		public MetadataBuilder ordered(boolean b) { isOrdered = b; return this; }
		public MetadataBuilder unique(boolean b)  { isUnique = b;  return this; }
		public MetadataBuilder indexed(boolean b) { isIndexed = b; return this; }

		public String[] build() {
			String[] metadata = pCategory == PropertyCategory.DERIVED ? new String[7] : new String[6];

			metadata[0] = pCategory.getLetter();
			metadata[1] = boolString(isMany);
			metadata[2] = boolString(isOrdered);
			metadata[3] = boolString(isUnique);
			metadata[4] = propertyType;

			if (pCategory == PropertyCategory.DERIVED) {
				metadata[5] = derivationLanguage;
				metadata[6] = derivationLogic;
			} else {
				metadata[5] = boolString(pCategory == PropertyCategory.ATTRIBUTE && isIndexed);
			}

			return metadata;
		}
	}

	private static String boolString(boolean b) {
		return b ? "t" : "f";
	}

	private static boolean stringBool(String s) {
		return "t".equals(s);
	}

	public Slot(TypeNode typeNode, String propertyName, String[] propertyMetadata) {
		this.typeNode = typeNode;
		this.propertyName = propertyName;

		this.propertyCategory = PropertyCategory.fromLetter(propertyMetadata[0]);
		this.isMany = stringBool(propertyMetadata[1]);
		this.isOrdered = stringBool(propertyMetadata[2]);
		this.isUnique = stringBool(propertyMetadata[3]);
		this.propertyType = propertyMetadata[4];
		this.isIndexed = propertyCategory == PropertyCategory.ATTRIBUTE && stringBool(propertyMetadata[5]);

		if (propertyCategory == PropertyCategory.DERIVED) {
			this.derivationLanguage = propertyMetadata[5];
			this.derivationLogic = propertyMetadata[6];
		} else {
			this.derivationLanguage = null;
			this.derivationLogic = null;
		}
	}

	public Slot(TypeNode typeNode, String propertyName) {
		this(typeNode, propertyName, (String[]) typeNode.getNode().getProperty(propertyName));
	}

	/**
	 * Returns the collection container that should be used for the value of
	 * this slot.
	 */
	protected Collection<Object> getCollection() {
		if (isOrdered && isUnique) {
			return new LinkedHashSet<Object>(); // ordered set
		} else if (isOrdered) {
			return new ArrayList<Object>(); // sequence
		} else if (isUnique) {
			return new HashSet<Object>(); // set
		} else {
			return new ArrayList<Object>(); // bag
		}
	}

	public TypeNode getTypeNode() {
		return typeNode;
	}

	public String getName() {
		return propertyName;
	}

	public boolean isAttribute() {
		return propertyCategory == PropertyCategory.ATTRIBUTE;
	}

	public boolean isReference() {
		return propertyCategory == PropertyCategory.REFERENCE;
	}

	public boolean isMixed() {
		return propertyCategory == PropertyCategory.MIXED;
	}

	public boolean isDerived() {
		return propertyCategory == PropertyCategory.DERIVED;
	}

	public boolean isMany() {
		return isMany;
	}

	public boolean isOrdered() {
		return isOrdered;
	}

	public boolean isUnique() {
		return isUnique;
	}

	public boolean isIndexed() {
		return isIndexed;
	}

	public String getType() {
		return propertyType;
	}

	/**
	 * Returns the full name of the node index that tracks this slot in all instances
	 * of this type, if any.
	 */
	public String getNodeIndexName() {
		if (!isDerived() && !isIndexed()) {
			return null;
		}
		
		String result = null;
		final IGraphDatabase graph = typeNode.getNode().getGraph();
		try (IGraphTransaction ignored = graph.beginTransaction()) {
			final String indexname = String.format("%s##%s##%s", typeNode.getMetamodelURI(), typeNode.getTypeName(), this.propertyName);
			if (graph.nodeIndexExists(indexname)) {
				result = indexname;
			}
			ignored.success();
		} catch (Exception e) {
			LOGGER.warn("Error while locating the index for this derived property", e);
		}

		return result;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((propertyName == null) ? 0 : propertyName.hashCode());
		result = prime * result
				+ ((typeNode == null) ? 0 : typeNode.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Slot other = (Slot) obj;
		if (propertyName == null) {
			if (other.propertyName != null)
				return false;
		} else if (!propertyName.equals(other.propertyName))
			return false;
		if (typeNode == null) {
			if (other.typeNode != null)
				return false;
		} else if (!typeNode.equals(other.typeNode))
			return false;
		return true;
	}

	public String getDerivationLanguage() {
		return derivationLanguage;
	}

	public String getDerivationLogic() {
		return derivationLogic;
	}

	@Override
	public String toString() {
		return "Slot [typeNode=" + typeNode + ", propertyName=" + propertyName + ", propertyType=" + propertyType
				+ ", propertyCategory=" + propertyCategory + ", isMany=" + isMany + ", isOrdered=" + isOrdered
				+ ", isUnique=" + isUnique + ", isIndexed=" + isIndexed + ", derivationLanguage=" + derivationLanguage
				+ ", derivationLogic=" + derivationLogic + "]";
	}
}