package util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.ENamedElement;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EcoreFactory;

/**
 * Specific helper that is used from templates that need
 * database stuff. 
 *  
 @author patrik.nordwall
 */
public class DatabaseGenerationHelper extends EcoreGenerationHelper {
    
    private static final String ID_COLUMN_NAME = "id";
    private Map<String, String> databaseTypeNameMap = new HashMap<String, String>();
    
    public DatabaseGenerationHelper() {
        initDatabaseTypeMap();
    }
    
    public List<EClass> getClassesInCreateOrder(EPackage ePackage) {
        List<EClass> packageClasses = getClasses(ePackage);
        List<EClass> databaseClasses = new ArrayList<EClass>();
        
        for (EClass eClass : packageClasses) {
            addClassRecursive(eClass, databaseClasses);
        }
        
        return databaseClasses;
    }
    
    private void addClassRecursive(EClass eClass, List<EClass> databaseClasses) {
        // skip repository classes
        if (eClass.getName().endsWith("Repository")) {
            return;
        }
        if (databaseClasses.contains(eClass)) {
            // already added
            return;
        }
        // we must have the referenced super class first, due to foreign key
        if (!getExtends(eClass).isEmpty()) {
            EClass superClass = getExtends(eClass).get(0);
            addClassRecursive(superClass, databaseClasses);
        }
        
        // we must have the referenced classes first, due to foreign keys
        for (EReference ref : getAllOneReferences(eClass)) {
            EClass refClass = ref.getEReferenceType();
            if (!databaseClasses.contains(refClass)) {
                addClassRecursive(refClass, databaseClasses);
            }
        }
        
        if (!databaseClasses.contains(eClass)) {
            databaseClasses.add(eClass);
        }
    }
    
    private void initDatabaseTypeMap() {
        databaseTypeNameMap.put("EBoolean""CHAR(1)");
        databaseTypeNameMap.put("EInt""INTEGER");
        
        databaseTypeNameMap.put("EDate""DATE");
        databaseTypeNameMap.put("EBigDecimal""DOUBLE");
//      length of VARCHAR can be specified with 'database_type' annotation
        databaseTypeNameMap.put("EString""VARCHAR(100)");
        
        // TODO more
    }
    
    public String getDatabaseType(EAttribute eAttribute) {
        return getDatabaseType(eAttribute, false);
    }
    
    public String getDatabaseType(EAttribute eAttribute, boolean ignorePrimaryKey) {
        String databaseTypeAnnotation = getAnnotation(eAttribute, "database_type");
        String databaseType;
        if (databaseTypeAnnotation == null) {
            String typeName = eAttribute.getEAttributeType().getName();
            databaseType = databaseTypeNameMap.get(typeName);
        else {
            databaseType = databaseTypeAnnotation;
        }
        String nullableAnnotation = getAnnotation(eAttribute, "nullable");
        if (nullableAnnotation == null || nullableAnnotation.equalsIgnoreCase("false")) {
            databaseType += " NOT NULL";
        }
        
        if (!ignorePrimaryKey) {
            if (eAttribute.getName().equalsIgnoreCase(ID_COLUMN_NAME)) {
                if (databaseType.startsWith("INTEGER")) {
                    databaseType += " AUTO_INCREMENT";
                }
                databaseType += " PRIMARY KEY";
            }
        }
        
        return databaseType;
    }
    
    public String getDatabaseName(ENamedElement element) {
        String name = element.getName();
        // TODO here we could replace camel notation with underscore
        return name.toUpperCase();
    }
    
    public String getForeignKeyName(EReference ref) {
        EClass referencedClass = ref.getEReferenceType();
        return getForeignKeyName(referencedClass);
    }
    
    public String getForeignKeyName(EClass referencedClass) {
        if (getIdAttribute(referencedClass== null) {
            throw new IllegalArgumentException("Referenced class " + referencedClass.getName() +
                    " doesn't contain 'id' attribute");
        }
        String name = referencedClass.getName() "_" + ID_COLUMN_NAME;
        return name.toUpperCase();
    }
    
    public EAttribute getIdAttribute(EClass eClass) {
        return getAttributeWithName(ID_COLUMN_NAME, eClass);
    }
    
    private EAttribute getAttributeWithName(String name, EClass eClass) {
        for (EAttribute attribute : getAttributes(eClass)) {
            if (attribute.getName().equals(name)) {
                return attribute;
            }
        }
        // not found
        return null;
    }
    
    public String getForeignKeyType(EReference ref) {
        EClass referencedClass = ref.getEReferenceType();
        return getForeignKeyType(referencedClass);
    }
    
    public String getForeignKeyType(EClass referencedClass) {
        EAttribute idAttribute = getAttributeWithName(ID_COLUMN_NAME, referencedClass)
        checkIdAttribute(referencedClass, idAttribute);
        String type = getDatabaseType(idAttribute, true);
        return type;
    }
    
    public String getForeignKeyConstraint(EReference ref) {
        EClass referencedClass = ref.getEReferenceType();
        return getForeignKeyConstraint(referencedClass);
    }
    
    public String getForeignKeyConstraint(EClass referencedClass) {
        String constraint = "CONSTRAINT FOREIGN KEY (" 
            getForeignKeyName(referencedClass") REFERENCES " +
            getDatabaseName(referencedClass"(ID)";
        return constraint;
    }

    private void checkIdAttribute(EClass referencedClass, EAttribute idAttribute) {
        if (idAttribute == null) {
            throw new IllegalArgumentException("Referenced class " + referencedClass.getName() +
                    " doesn't contain 'id' attribute");
        }
    }
    
    public List<EClass> resolveManyToManyRelations(EPackage ePackage) {
        // first, find all many-to-many references
        Set<EReference> manyToManyReferences = new HashSet<EReference>();
        for (EClass eClass : getClasses(ePackage)) {
            for (EReference ref : getAllManyReferences(eClass)) {
                EReference opposite = ref.getEOpposite();
                if (isManyToMany(ref&& !manyToManyReferences.contains(opposite)) {
                    manyToManyReferences.add(ref);
                }
            }
        }
        // then, create an EClass for each many-to-many reference
        List<EClass> manyToManyClasses = new ArrayList<EClass>();
        for (EReference ref : manyToManyReferences) {
            EReference opposite = ref.getEOpposite();
            EcoreFactory ecoreFactory = EcoreFactory.eINSTANCE;
            EClass eClass = ecoreFactory.createEClass();
            String name = getManyToManyJoinTableName(ref);
            eClass.setName(name.toUpperCase());
            
            EReference ref1 = ecoreFactory.createEReference();
            ref1.setEType(ref.getEReferenceType());
            ref1.setUpperBound(1);
            eClass.getEStructuralFeatures().add(ref1);
            
            EReference ref2 = ecoreFactory.createEReference();
            ref2.setEType(opposite.getEReferenceType());
            ref2.setUpperBound(1);
            eClass.getEStructuralFeatures().add(ref2);
            
            manyToManyClasses.add(eClass);
        }
        
        return manyToManyClasses;
        
    }

    public String getManyToManyJoinTableName(EReference ref) {
        EReference opposite = ref.getEOpposite();
        String name1 = getDatabaseName(ref.getEReferenceType());
        String name2 = getDatabaseName(opposite.getEReferenceType());
        // order them in some well defined way, like alphabetic order
        if (name1.compareTo(name20) {
            return name1 + "_" + name2;
        else {
            return name2 + "_" + name1;
        }
    }
    
    /**
     * Inverse attribute for many-to-many associations.
     */
    public boolean isInverse(EReference ref) {
        return getManyToManyJoinTableName(ref).startsWith(
                getDatabaseName(ref.getEReferenceType()) "_");
    }
    
    public boolean isManyToMany(EReference ref) {
        EReference opposite = ref.getEOpposite();
        return ((opposite != null&& isManyReference(opposite));
    }
    
}