/*
 * Copyright 2013 The Sculptor Project Team, including the original 
 * author or authors.
 * 
 * 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.
 */

package org.sculptor.generator.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.inject.Inject;

import sculptormetamodel.Application;
import sculptormetamodel.Attribute;
import sculptormetamodel.BasicType;
import sculptormetamodel.DomainObject;
import sculptormetamodel.Entity;
import sculptormetamodel.Enum;
import sculptormetamodel.EnumConstructorParameter;
import sculptormetamodel.EnumValue;
import sculptormetamodel.Inheritance;
import sculptormetamodel.Module;
import sculptormetamodel.NamedElement;
import sculptormetamodel.Reference;
import sculptormetamodel.SculptormetamodelFactory;
import sculptormetamodel.impl.SculptormetamodelFactoryImpl;

public class DbHelperBase {

	private static final String ID_ATTRIBUTE_NAME = "id";

	@Inject private HelperBase helperBase;
	@Inject private PropertiesBase propBase;
	@Inject private SingularPluralConverter singularPluralConverter;

	public List<DomainObject> getDomainObjectsInCreateOrder(Application application, Boolean ascending) {
		List<DomainObject> all = getAllDomainObjects(application);
		List<DomainObject> orderedClasses = new ArrayList<DomainObject>();
		Set<DomainObject> handledClasses = new HashSet<DomainObject>();

		for (DomainObject domainObject : all) {
			addClassRecursive(domainObject, orderedClasses, handledClasses);
		}

		if (!ascending) {
			Collections.reverse(orderedClasses);
		}

		return orderedClasses;
	}

	/**
	 * @return all persistent DomainObjects
	 */
	public List<DomainObject> getAllDomainObjects(Application application) {
		List<DomainObject> all = new ArrayList<DomainObject>();
		List<Module> modules = HelperBase.sortByName(application.getModules());
		for (Module m : modules) {
			if (m.isExternal()) {
				continue;
			}
			List<DomainObject> domainObjects = HelperBase.sortByName(m.getDomainObjects());
			for (DomainObject d : domainObjects) {
				if (helperBase.isPersistent(d) && includeInDdl(d)) {
					all.add(d);
				}
			}
		}
		return all;
	}

	private boolean includeInDdl(DomainObject domainObj) {
		return !helperBase.hasHintImpl(domainObj.getHint(), "skipddl");
	}

	private void addClassRecursive(DomainObject domainObject, List<DomainObject> orderedClasses,
			Set<DomainObject> handledClasses) {
		if (handledClasses.contains(domainObject)) {
			// already added
			return;
		}
		if (domainObject instanceof BasicType) {
			// no tables for BasicType
			return;
		}

		if (!helperBase.isPersistent(domainObject) || !includeInDdl(domainObject)) {
			// no tables for non persistent ValueObject
			return;
		}

		if (domainObject.getModule().isExternal()) {
			return;
		}

		// add current class, will break recursion if referred again
		handledClasses.add(domainObject);

		// we must have the referenced super class first, due to foreign key
		if (domainObject.getExtends() != null) {
			addClassRecursive(domainObject.getExtends(), orderedClasses, handledClasses);
		}

		// we must have the referenced classes first, due to foreign keys
		for (Reference ref : getAllOneReferences(domainObject)) {
			addClassRecursive(ref.getTo(), orderedClasses, handledClasses);
		}

		if (!orderedClasses.contains(domainObject)) {
			orderedClasses.add(domainObject);
		}

	}

	public String getDiscriminatorColumnDatabaseType(Inheritance inheritance) {
		// create an Attribute so that we can reuse existing logic for
		// databaseType
		SculptormetamodelFactory factory = SculptormetamodelFactoryImpl.eINSTANCE;
		Attribute attr = factory.createAttribute();
		if (inheritance.getDiscriminatorColumnName() == null) {
			attr.setName(propBase.getProperty("db.discriminatorColumnName"));
		} else {
			attr.setName(inheritance.getDiscriminatorColumnName());
		}
		attr.setType("discriminatorType." + inheritance.getDiscriminatorType().getLiteral());
		if (inheritance.getDiscriminatorColumnLength() != null) {
			attr.setLength(inheritance.getDiscriminatorColumnLength());
		}

		return getDatabaseType(attr);
	}

	public String getDatabaseType(Attribute attribute) {
		String databaseTypeProperty = attribute.getDatabaseType();
		String databaseType;
		if (databaseTypeProperty == null) {
			databaseType = getDefaultDbType(attribute.getType());
		} else {
			databaseType = databaseTypeProperty;
		}

		String length = getDatabaseLength(attribute);
		if (length != null && databaseType.indexOf('(') == -1) {
			databaseType += ("(" + length + ")");
		}

		return databaseType;
	}

	public String getEnumDatabaseType(Reference reference) {
		Attribute enumAttribute = getEnumAttribute((Enum) reference.getTo());
		if (helperBase.hasHintImpl(reference.getHint(), "databaseLength"))
			enumAttribute.setLength(helperBase.getHintImpl(reference.getHint(), "databaseLength"));
		return getDatabaseType(enumAttribute);
	}

	public String getEnumDatabaseType(Enum _enum) {
		return getDatabaseType(getEnumAttribute(_enum));
	}

	public String getEnumType(Enum _enum) {
		return getEnumAttribute(_enum).getType();
	}

	public String getEnumDatabaseLength(Enum _enum) {
		return getEnumAttribute(_enum).getLength();
	}

	private Attribute getEnumKeyAttribute(Enum _enum) {
		List<Attribute> enumAttributes = _enum.getAttributes();
		for (Attribute attribute : enumAttributes) {
			if (attribute.isNaturalKey()) {
				return attribute;
			}
		}

		return createDefaultEnumAttribute();
	}

	private Attribute createDefaultEnumAttribute() {
		SculptormetamodelFactory factory = SculptormetamodelFactoryImpl.eINSTANCE;
		Attribute attr = factory.createAttribute();
		attr.setType("String");
		return attr;
	}

	private boolean hasNaturalKeyAttribute(Enum _enum) {
		return (getEnumKeyAttribute(_enum).isNaturalKey());
	}

	private Attribute getEnumAttribute(Enum _enum) {
		Attribute attribute = getEnumKeyAttribute(_enum);
		if (_enum.isOrdinal()) {
			attribute.setType("int");
			attribute.setLength(null);
		} else {
			if (helperBase.hasHintImpl(_enum.getHint(), "databaseLength")) {
				attribute.setType("String");
				attribute.setLength(helperBase.getHintImpl(_enum.getHint(), "databaseLength"));
			} else {
				attribute.setType(attribute.getType() != null ? attribute.getType() : "String");
				attribute.setLength(calcEnumDatabaseLength(_enum));
			}
		}
		return attribute;
	}

	private String calcEnumDatabaseLength(Enum _enum) {
		int maxLength = 0;
		if (hasNaturalKeyAttribute(_enum)) {
			int attributePosition = 0;
			for (Attribute attribute : (List<Attribute>) _enum.getAttributes()) {
				if (attribute.isNaturalKey()) {
					for (EnumValue enumValue : (List<EnumValue>) _enum.getValues()) {
						EnumConstructorParameter enumParam = (EnumConstructorParameter) enumValue.getParameters().get(
								attributePosition);
						maxLength = calcMaxLength(enumParam.getValue(), maxLength);
					}
					break;
				}
				attributePosition++;
			}
		}
		if (maxLength == 0) {
			for (EnumValue value : (List<EnumValue>) _enum.getValues()) {
				maxLength = calcMaxLength(value.getName(), maxLength);
			}
		}
		return "" + maxLength;
	}

	private int calcMaxLength(String value, int maxLength) {
		int length = value.length();
		if (value.startsWith("\"")) {
			length = length - 2;
		}
		return (maxLength < length) ? length : maxLength;
	}

	public String getDatabaseTypeNullability(Attribute attribute) {
		if (!attribute.isNullable() || attribute.isNaturalKey()) {
			return " NOT NULL";
		}
		return "";
	}

	public String getDatabaseTypeNullability(Reference reference) {
		if (!reference.isNullable() || reference.isNaturalKey()) {
			return " NOT NULL";
		}
		return "";
	}

	public String getDatabaseLength(Attribute attribute) {
		if (attribute.getLength() == null) {
			return getDefaultDbLength(attribute.getType());
		} else {
			return attribute.getLength();
		}
	}

	private String getDefaultDbType(String javaType) {
		return propBase.getDbType(javaType);
	}

	private String getDefaultDbLength(String javaType) {
		return propBase.getDbLength(javaType);
	}

	public String getDatabaseNameBase(NamedElement element) {
		String name = element.getName();
		name = convertDatabaseName(name);
		return name;
	}

	/** 
	 * Moved to DbHelper
	 * @deprecated
	 */
	@Deprecated 
	private String convertDatabaseName(String name) {
		if (propBase.getBooleanProperty("db.useUnderscoreNaming")) {
			name = CamelCaseConverter.camelCaseToUnderscore(name);
		}
		name = truncateLongDatabaseName(name);
		name = name.toUpperCase();
		return name;
	}

	public String truncateLongDatabaseName(String name) {
		int max = propBase.getMaxDbName();
		return truncateLongDatabaseName(name, max);
	}

	public String truncateLongDatabaseName(String name, Integer max) {
		if (name.length() <= max) {
			return name; // no problem
		} else if (propBase.getBooleanProperty("db.errorWhenTooLongName")) {
			throw new RuntimeException("Generation aborted due to too long database name: " + name);
		} else {
			String hash = String.valueOf(Math.abs(name.hashCode()));
			hash = "0" + hash; // make sure that it is at least 2 chars
			hash = hash.substring(hash.length() - 2); // use 2 last characters
			String truncated = name.substring(0, max - hash.length()) + hash;
			return truncated;
		}
	}

	public String getDefaultForeignKeyNameBase(Reference ref) {
		String name;
		if (ref.isMany()) {
			name = singularPluralConverter.toSingular(ref.getName());
		} else {
			name = ref.getName();
		}
		DomainObject to = ref.getTo();
		name += idSuffix(name, to);
		return convertDatabaseName(name);
	}

	public String getDefaultOppositeForeignKeyName(Reference ref) {
		if (ref.getOpposite() == null) {
			return getForeignKeyNameForUnidirectionalToManyWithJoinTable(ref);
		} else {
			return getDefaultForeignKeyNameBase(ref.getOpposite());
		}
	}

	private String getForeignKeyNameForUnidirectionalToManyWithJoinTable(Reference ref) {
		if (ref.getDatabaseJoinColumn() != null) {
			return ref.getDatabaseJoinColumn();
		}

		DomainObject from = ref.getFrom();
		String name = from.getDatabaseTable();
		if (name == null) {
			name = from.getName();
		}
		name += idSuffix(name, from);
		return convertDatabaseName(name);
	}

	
	/** 
	 * Moved to DbHelper
	 * @deprecated
	 */
	@Deprecated 
	private String idSuffix(String name, DomainObject to) {
		if (useIdSuffixInForeignKey()) {
			Attribute idAttribute = getIdAttribute(to);
			if (idAttribute != null) {
				String idName = idAttribute.getDatabaseColumn().toUpperCase();
				String convertedName = convertDatabaseName(name);
				if (idName.equals(convertedName) && idName.startsWith(to.getDatabaseTable())) {
					idName = idName.substring(to.getDatabaseTable().length());
				} else if (idName.startsWith(convertedName)) {
					idName = idName.substring(convertedName.length());
				}
				if (idName.startsWith("_")) {
					return idName;
				} else {
					return ("_" + idName);
				}
			}
		}
		return "";
	}

	/** 
	 * Use Properties.useIdSuffixInForeigKey() instead
	 * @deprecated
	 */
	@Deprecated 
	private boolean useIdSuffixInForeignKey() {
		return propBase.getBooleanProperty("db.useIdSuffixInForeigKey");
	}

	protected Attribute getIdAttribute(DomainObject domainObject) {
		Attribute idAttribute = getAttributeWithName(ID_ATTRIBUTE_NAME, domainObject);
		if ((idAttribute == null) && (domainObject.getExtends() != null)) {
			// look in extended DomainOject, recursive call
			idAttribute = getIdAttribute(domainObject.getExtends());
		}
		return idAttribute;
	}

	private Attribute getAttributeWithName(String name, DomainObject domainObject) {
		for (Object obj : domainObject.getAttributes()) {
			Attribute attribute = (Attribute) obj;
			if (attribute.getName().equals(name)) {
				return attribute;
			}
		}
		// not found
		return null;
	}

	public String getForeignKeyType(Reference ref) {
		DomainObject referencedClass = ref.getTo();
		return getForeignKeyType(referencedClass) + getDatabaseTypeNullability(ref);
	}

	public String getForeignKeyType(DomainObject referencedClass) {
		Attribute idAttribute = getIdAttribute(referencedClass);
		checkIdAttribute(referencedClass, idAttribute);
		String type = getDatabaseType(idAttribute);
		return type;
	}

	/** 
	 * Moved to DbHelper
	 * @deprecated
	 */
	@Deprecated 
	private void checkIdAttribute(DomainObject referencedClass, Attribute idAttribute) {
		if (idAttribute == null) {
			throw new IllegalArgumentException("Referenced class " + referencedClass.getName() + " doesn't contain 'id' attribute");
		}
	}

	public List<DomainObject> resolveManyToManyRelations(Application application, Boolean ascending) {
		// first, find all many-to-many references
		Set<Reference> manyToManyReferences = new HashSet<Reference>();
		List<DomainObject> domainObjects = getAllDomainObjects(application);
		for (DomainObject domainObject : domainObjects) {
			for (Reference ref : getAllManyReferences(domainObject)) {
				if (!helperBase.isPersistent(ref.getTo()) || !includeInDdl(ref.getTo())) {
					// skip this reference, since it refers to a non persistent
					// object
					continue;
				}
				if (ref.isTransient()) {
					// skip this reference, since it is transient
					continue;
				}
				Reference opposite = ref.getOpposite();
				// undirectional many references are designed in db as
				// many-to-many, except when inverse is defined to true
				if ((opposite == null && !ref.isInverse())
						|| (opposite != null && opposite.isMany() && !opposite.isTransient() && !manyToManyReferences
								.contains(opposite))) {
					manyToManyReferences.add(ref);
				}
			}
		}
		// then, create an fictive DomainObject for each many-to-many reference
		List<DomainObject> manyToManyClasses = new ArrayList<DomainObject>();
		for (Reference ref : manyToManyReferences) {
			DomainObject relObj = createFictiveManyToManyObject(ref);
			manyToManyClasses.add(relObj);
		}

		manyToManyClasses = HelperBase.sortByName(manyToManyClasses);

		if (!ascending) {
			Collections.reverse(manyToManyClasses);
		}
		return manyToManyClasses;

	}

	public DomainObject createFictiveManyToManyObject(Reference ref) {
		DomainObject relObj;
		if (ref.getFrom() instanceof Entity || ref.getTo() instanceof Entity) {
			relObj = SculptormetamodelFactory.eINSTANCE.createEntity();
		} else {
			// many-to-many between two value objects is not likely (good
			// design) but whatever...
			relObj = SculptormetamodelFactory.eINSTANCE.createValueObject();
		}
		String name = getManyToManyJoinTableName(ref);
		relObj.setName(name.toUpperCase());
		relObj.setDatabaseTable(getDatabaseNameBase(relObj));
		relObj.setAbstract(true);

		Reference ref1 = SculptormetamodelFactory.eINSTANCE.createReference();
		ref1.setTo(ref.getTo());
		ref1.setName(singularPluralConverter.toSingular(ref.getName()));
		ref1.setDatabaseColumn(ref.getDatabaseColumn());
		ref1.setFrom(relObj);
		relObj.getReferences().add(ref1);

		Reference ref2 = SculptormetamodelFactory.eINSTANCE.createReference();
		ref2.setTo(ref.getFrom());
		if (ref.getOpposite() == null) {
			// use table name of from obj, it doesn't matter that we loose
			// upper/lower case
			ref2.setName(helperBase.toFirstLower(ref.getFrom().getName()));
			ref2.setDatabaseColumn(getForeignKeyNameForUnidirectionalToManyWithJoinTable(ref));
		} else {
			ref2.setName(singularPluralConverter.toSingular(ref.getOpposite().getName()));
			ref2.setDatabaseColumn(ref.getOpposite().getDatabaseColumn());
		}
		ref2.setFrom(relObj);
		relObj.getReferences().add(ref2);

		String tablespaceHint = helperBase.getHintImpl(ref.getFrom().getHint(), "tablespace");
		if (tablespaceHint != null) {
			helperBase.addHint(relObj, "tablespace=" + tablespaceHint);
		}

		return relObj;
	}

	public String getManyToManyJoinTableName(Reference ref) {
		if (ref.getDatabaseJoinTable() != null) {
			return ref.getDatabaseJoinTable();
		}
		if (ref.getOpposite() != null && ref.getOpposite().getDatabaseJoinTable() != null) {
			return ref.getOpposite().getDatabaseJoinTable();
		}

		String name1 = ref.getDatabaseColumn();
		name1 = removeIdSuffix(name1, ref.getTo());
		String name2;
		if (ref.getOpposite() == null) {
			name2 = (ref.getFrom().getDatabaseTable() != null) ? ref.getFrom().getDatabaseTable() : ref.getFrom().getName()
					.toUpperCase();
		} else {
			name2 = ref.getOpposite().getDatabaseColumn();
			name2 = removeIdSuffix(name2, ref.getOpposite().getTo());
		}

		return getJoinTableName(name1, name2, true);
	}

	public String getElementCollectionTableName(Attribute attribute) {
		String hintParam = "databaseJoinTableName";
		if (helperBase.hasHintImpl(attribute.getHint(), hintParam)) {
			return helperBase.getHintImpl(attribute.getHint(), hintParam);
		}

		String name1 = singularPluralConverter.toSingular(attribute.getDatabaseColumn().toLowerCase()).toUpperCase();
		String name2 = ((DomainObject) attribute.eContainer()).getDatabaseTable();

		return getJoinTableName(name1, name2, false);
	}

	public String getElementCollectionTableName(Reference reference) {
		if (reference.getDatabaseJoinTable() != null) {
			return reference.getDatabaseJoinTable();
		}

		String name1 = reference.getDatabaseColumn();
		name1 = removeIdSuffix(name1, reference.getTo());
		String name2;
		if (reference.getOpposite() == null) {
			name2 = (reference.getFrom().getDatabaseTable() != null) ? reference.getFrom().getDatabaseTable() : reference.getFrom()
					.getName().toUpperCase();
		} else {
			name2 = reference.getOpposite().getDatabaseColumn();
			name2 = removeIdSuffix(name2, reference.getOpposite().getTo());
		}

		return getJoinTableName(name1, name2, false);
	}

	private String getJoinTableName(String name1, String name2, boolean ordered) {
		int max = propBase.getMaxDbName();
		if ((name1.length() > (max - 6)) && (name2.length() > (max - 6))) {
			// both names are long, truncate both
			name1 = name1.substring(0, (max / 2));
			name2 = name2.substring(0, (max / 2));
		}

		if ((name1.length() + name2.length() + 1) > max) {
			// too long, truncate the longest name
			if (name1.length() > name2.length()) {
				name1 = name1.substring(0, (max - name2.length() - 1));
			} else {
				name2 = name2.substring(0, (max - name1.length() - 1));
			}
		}

		// order them in some well defined way, like alphabetic order
		if (ordered) {
			String name;
			if (name1.compareTo(name2) < 0) {
				name = name1 + "_" + name2;
			} else {
				name = name2 + "_" + name1;
			}
			return name;
		}

		return name2 + "_" + name1;
	}

	private String removeIdSuffix(String name, DomainObject to) {
		String idSuffix = idSuffix(name, to);
		if (idSuffix.equals("")) {
			return name;
		}
		if (name.endsWith(idSuffix)) {
			return name.substring(0, name.length() - idSuffix.length());
		}
		return name;
	}

	/**
	 * Inverse attribute for many-to-many associations.
	 */
	public boolean isRefInverse(Reference ref) {
		if (ref.isInverse()) {
			return true;
		}
		if (!ref.isInverse() && (ref.getOpposite() != null) && ref.getOpposite().isInverse()) {
			return false;
		}
		if (ref.getOpposite() == null) {
			return false;
		}
		// inverse not defined on any side, use this algorithm
		String name1 = ref.getTo().getName();
		String name2 = ref.getFrom().getName();

		// use a well defined algorithm, like alphabetic order
		// A -> B inverse=false
		// B -> A inverse= true
		return (name1.compareTo(name2) < 0);
	}

	/**
	 * List of references with multiplicity > 1
	 */
	private List<Reference> getAllManyReferences(DomainObject domainObject) {
		List<Reference> allReferences = domainObject.getReferences();
		List<Reference> allManyReferences = new ArrayList<Reference>();
		for (Reference ref : allReferences) {
			if (ref.isMany()) {
				allManyReferences.add(ref);
			}
		}
		return allManyReferences;
	}

	/**
	 * List of references with multiplicity = 1
	 */
	private List<Reference> getAllOneReferences(DomainObject domainObject) {
		List<Reference> allReferences = domainObject.getReferences();
		List<Reference> allOneReferences = new ArrayList<Reference>();
		for (Reference ref : allReferences) {
			if (!ref.isMany()) {
				allOneReferences.add(ref);
			}
		}
		return allOneReferences;
	}

	public String getDerivedCascade(Reference ref) {
		boolean inSameModule = ref.getFrom().getModule().equals(ref.getTo().getModule());
		boolean biDirectional = ref.getOpposite() != null;
		boolean aggregate = helperBase.isEntityOrPersistentValueObject(ref.getTo()) && !ref.getTo().isAggregateRoot();
		boolean oneToMany = biDirectional && ref.isMany() && !ref.getOpposite().isMany();
		boolean manyToMany = biDirectional && ref.isMany() && ref.getOpposite().isMany();
		boolean oneToOne = biDirectional && !ref.isMany() && !ref.getOpposite().isMany();

		if (aggregate) {
			if (oneToMany) {
				return propBase.getDefaultCascade("aggregate.oneToMany");
			} else {
				return propBase.getDefaultCascade("aggregate");
			}
		}

		if (!inSameModule) {
			return null;
		}

		if (manyToMany) {
			return isRefInverse(ref) ? null : propBase.getDefaultCascade("manyToMany");
		}

		if (oneToMany) {
			return propBase.getDefaultCascade("oneToMany");
		}

		if (oneToOne) {
			return propBase.getDefaultCascade("oneToOne");
		}

		if (!biDirectional && ref.isMany()) {
			return propBase.getDefaultCascade("toMany");
		}

		if (!biDirectional && !ref.isMany()) {
			return propBase.getDefaultCascade("toOne");
		}

		return null;
	}

}
