package util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.eclipse.emf.codegen.util.ImportManager;
import org.eclipse.emf.ecore.EAnnotation;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EModelElement;
import org.eclipse.emf.ecore.ENamedElement;
import org.eclipse.emf.ecore.EOperation;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EParameter;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.ETypedElement;

/**
 * This helper class is used from JET templates to simplify access
 * to an Ecore model.
 
 @author patrik.nordwall
 *
 */
public class EcoreGenerationHelper {
    
    private Map<String, String> typeNameMap = new HashMap<String, String>();
    private Map<String, String> primitive2ObjectTypeNameMap = new HashMap<String, String>();
    
    private ImportManager importManager = new ImportManager("");
    private boolean autoImport = true;

    public EcoreGenerationHelper() {
        initTypeMap();
    }
    
    /**
     * Mapping between primitive Ecore types and 
     * Java types are defined here. 
     */
    private void initTypeMap() {
        typeNameMap.put("ecore.EBoolean""boolean");
        typeNameMap.put("ecore.EInt""int");
        
        typeNameMap.put("ecore.EDate""java.util.Date");
        typeNameMap.put("ecore.EBigDecimal""java.math.BigDecimal");
        typeNameMap.put("ecore.EString""String");
        // TODO more
        
        typeNameMap.put("Map""java.util.Map");
        typeNameMap.put("List""java.util.List");
        typeNameMap.put("Set""java.util.Set");
        
        primitive2ObjectTypeNameMap.put("int""Integer");
        primitive2ObjectTypeNameMap.put("long""Long");
        primitive2ObjectTypeNameMap.put("double""Double");
    }
    
    /**
     * This map contains the mapping between Ecore types
     * and Java types. Additional mapping can be added
     * to the map.
     */
    public Map<String, String> getTypeNameMap() {
        return typeNameMap;
    }

    
    public ImportManager getImportManager() {
        return importManager;
    }
    
    public ImportManager makeImportManager(EPackage ePackage) {
        return makeImportManager(getQualifiedName(ePackage));
    }

    public ImportManager makeImportManager(String packageName) {
        importManager = new ImportManager(packageName);
        return importManager;
    }
    
    /**
     * When auto import is on then imports are
     * automatically added to the ImportManager
     * when some methods are used, such as
     {@link #getTypeName(ETypedElement)}.
     * By default auto import is on.
     */
    public boolean isAutoImport() {
        return autoImport;
    }

    /**
     * Switch off auto import.
     @see #isAutoImport()
     */
    public void setAutoImport(boolean autoImport) {
        this.autoImport = autoImport;
    }
    
    /**
     * Add a specific import manually.
     @param name fully qualified class name
     */
    public void addImport(String name) {
        if (name.equals("void")) {
            return;
        }
        name = removeArrayBrackets(name);
        
        getImportManager().addImport(name);
    }
    
    private String removeArrayBrackets(String name) {
        if (name.endsWith("[]")) {
            return name.substring(0, name.length() 2);
        else {
            return name;
        }
    }

    /**
     * Convenience method to get the name of an Ecore element.
     */
    public String getName(ENamedElement namedElement) {
        if (autoImport && (namedElement instanceof EClassifier)) {
            addImport(getQualifiedName((EClassifiernamedElement));
        }
        return namedElement.getName();
    }

    /**
     * Get the full name, including package name, of
     * an element.
     */
    public String getQualifiedName(EClassifier element) {
        String qualifiedName = "";
        if (element.getEPackage() != null) {
            qualifiedName += getQualifiedName(element.getEPackage());
        }
        qualifiedName += "." + element.getName();
        return qualifiedName;
    }

    /**
     * The full package name, including parent packages.
     */
    public String getQualifiedName(EPackage pack) {
        String qualifiedName = pack.getName();
        EPackage parentPackage = pack.getESuperPackage();
        while (parentPackage != null) {
            qualifiedName = parentPackage.getName() "." + qualifiedName;
            parentPackage = parentPackage.getESuperPackage();
        }
        return qualifiedName;
    }
    
    /**
     * Some of the CRUD operations can be mapped
     * by this method to support different names
     * in different classes. 
     */
    public String getMappedOperationName(EOperation op) {
        // a few naming mapping conventions
        String mappedOpName = getName(op);
        // forId is same as findById
        if (mappedOpName.equals("forId")) {
          mappedOpName = "findById";
        }
        // add is same as create
        if (mappedOpName.equals("add")) {
          mappedOpName = "create";
        }
        return mappedOpName;
    }
    
    /**
     * Get the throws clause of an operation by looking up the 'throws' annotation.
     * The exceptions can be comma separated. The exceptions are added
     * to the ImportManager if autoImport is on.
     */
    public String getThrows(EOperation op) {
        if (getAnnotation(op, "throws"== null) {
            return "";
        else {
            String s = " throws ";
            String throwsAnnotation = getAnnotation(op, "throws");
            StringTokenizer st = new StringTokenizer(throwsAnnotation, ",");
            while (st.hasMoreTokens()) {
                String qualifiedName = st.nextToken().trim();
                if (autoImport) {
                    addImport(qualifiedName);
                }
                String shortName = shortName(qualifiedName);
                s += shortName;
                if (st.hasMoreTokens()) {
                    s += ", ";
                }
            }
            return s;
        }
    }

    /**
     * Returns the primitive or class name for the given Type. Class names will
     * be added as imports to the ImportManager, and the imported
     * form will be returned.
     */
    public String getTypeName(ETypedElement element) {
        return getTypeName(element, true);
    }
    
    public String getTypeName(ETypedElement element, boolean convertArrayTypes) {
        String typeQualifiedName = getTypeQualifiedName(element);
        if (autoImport) {
            addImport(typeQualifiedName);
        }
        String shortName = shortName(typeQualifiedName);
        
        // remove array brackets
        if (shortName.endsWith("[]")) {
            shortName = removeArrayBrackets(shortName);
            if (convertArrayTypes) {
                // use typed List instead of arrays
                if (autoImport) {
                    addImport("java.util.List");
                }
                shortName = "List<" + shortName + ">";
            }
        }
        return shortName;
    }
    
    /**
     * Get the generic type declaration for generic 
     * access objects.
     */
    public String getGenericType(EOperation op) {
        String mappedOpName = getMappedOperationName(op);
        if (mappedOpName.equals("findById")) {
            EParameter idParam = getParameters(op).get(0);
            String idTypeName = getTypeName(idParam);
            // use object types
            if (primitive2ObjectTypeNameMap.containsKey(idTypeName)) {
                idTypeName = primitive2ObjectTypeNameMap.get(idTypeName);
            }
            return "<" + getTypeName(op", " + idTypeName + ">";
        else if (mappedOpName.equals("findAll"||
                   mappedOpName.equals("findByQuery"||
                   mappedOpName.equals("findByExample")) {
            return "<" + getTypeName(op, false">";
        else if (mappedOpName.equals("create"||
                   mappedOpName.equals("update"||
                   mappedOpName.equals("delete")) {
            EParameter firstParam = getParameters(op).get(0);
            return "<" + getTypeName(firstParam">";
        else {
            return "";
        }
    }

    /**
     * Collection type can be set, list, bag or map.
     * It corresponds to the Hibernate collection types.
     */
    public String getCollectionType(EReference ref) {
        return getAnnotation(ref, "collectionType""set").toLowerCase();
    }
    
    /**
     * Java interface for the collection type.
     @see #getCollectionType(EReference)
     */
    public String getCollectionInterfaceType(EReference ref) {
        String collectionType = getCollectionType(ref)
        if ("list".equals(collectionType)) {
            return "List";
        else if ("bag".equals(collectionType)) {
            return "List";
        else if ("map".equals(collectionType)) {
            return "Map";
        else {
            return "Set";
        }
    }
    
    /**
     * Java implementation class for the collection type.
     @see #getCollectionType(EReference)
     */
    public String getCollectionImplType(EReference ref) {
        String collectionType = getCollectionType(ref)
        if ("list".equals(collectionType)) {
            return "ArrayList";
        else if ("bag".equals(collectionType)) {
            return "ArrayList";
        else if ("map".equals(collectionType)) {
            return "HashMap";
        else {
            return "HashSet";
        }
    }
    
    
    
    private String shortName(String qualifiedName) {
        int i = qualifiedName.lastIndexOf('.');
        if (i == -1) {
            return qualifiedName;
        else {
            return qualifiedName.substring(i+1);
        }
    }
    
    /**
     * Get the qualified name of the type of an element, e.g. 
     * the return type of an operation.
     */
    public String getTypeQualifiedName(ETypedElement element) {
        if (element.getEType() == null) {
            return "void";
        }
        String mappedName = (StringtypeNameMap.get(getQualifiedName(element.getEType()));
        if (mappedName == null) {
            return getQualifiedName(element.getEType());
        else {
            return mappedName;
        }
    }

    /**
     * First character to upper case.
     */
    public String capName(String name) {
        if (name.length() == 0) {
            return name;
        else {
            return name.substring(01).toUpperCase() + name.substring(1);
        }
    }

    /**
     * First character to lower case. 
     */
    public String uncapName(String name) {
        if (name.length() == 0) {
            return name;
        else {
            return name.substring(01).toLowerCase() + name.substring(1);
        }
    }

    /**
     * Lower all except the last upper case character if there are
     * more than one upper case characters in the beginning.
     * e.g. XSDElementContent -> xsdElementContent
     * However if the whole string is uppercase, the whole string
     * is turned into lower case.
     * e.g. CPU -> cpu
     */  
    public String uncapPrefixedName(String name) {
        
        if (name.length() == 0) {
            return name;
        else {
            String lowerName = name.toLowerCase();
            int i;
            for (i = 0; i < name.length(); i++) {
                if (name.charAt(i== lowerName.charAt(i)) {
                    break;
                }
            }
            if (i > && i < name.length()) {
                --i;
            }
            return name.substring(0, i).toLowerCase() + name.substring(i);
        }
    }
    
    /**
     * Get-accessor method name of a property, according to
     * JavaBeans naming conventions.
     */
    public String getGetAccessor(EStructuralFeature feature) {
        String capName = capName(getName(feature));
        String result = isBooleanType(feature"is" + capName : "get"
                ("Class".equals(capName"Class_" : capName);
        return result;
    }
    
    public String getAccessorName(EStructuralFeature feature) {
        return capName(getName(feature));
    }
    
    /**
     * List of super types.
     * If eClass is an interface the list contains all 
     * interfaces that it extends. If eClass is a class
     * the list contains one (or zero) element with
     * the class it extends.
     */
    public List<EClass> getExtends(EClass eClass) {
        List<EClass> superTypes = eClass.getESuperTypes();
        if (eClass.isInterface()) {
            return getImplements(eClass);
        else {
            for (EClass t : superTypes) {
                if (!t.isInterface()) {
                    List<EClass> list = new ArrayList<EClass>();
                    list.add(t);
                    return list;
                }
            }
            // no class found
            return Collections.emptyList();
        }
    }
    
    /**
     * List of super types.
     * If eClass is an interface the list contains all 
     * interfaces that it extends. If eClass is a class
     * the list contains all interfaces it implements.
     
     */
    public List<EClass> getImplements(EClass eClass) {
        List<EClass> superTypes = eClass.getESuperTypes();
        List<EClass> interfaces = new ArrayList<EClass>();
        for (EClass t : superTypes) {
            if (t.isInterface()) {
                interfaces.add(t);
            }
        }
        return interfaces;
    }

    /**
     * The normal Java extends and implements String for the class.
     */
    public String getExtendsAndImplementsLitteral(EClass eClass) {
        List<EClass> interfaces = getImplements(eClass);
        if (eClass.isInterface()) {
            if (interfaces.isEmpty()) {
                return "";
            else {
                return "extends " + toCommaSeparatedString(interfaces);
            }
        else {
            StringBuffer sb = new StringBuffer();
            List<EClass> extendsClass = getExtends(eClass);
            if (!extendsClass.isEmpty()) {
                sb.append("extends ").append(getName(extendsClass.get(0)));
            }
            if (!interfaces.isEmpty()) {
                if (sb.length() != 0) {
                    sb.append(" ");
                }
                sb.append("implements " + toCommaSeparatedString(interfaces));
            }
            return sb.toString();
        }
    }

    private String toCommaSeparatedString(List<? extends ENamedElement> list) {
        StringBuffer sb = new StringBuffer();
        for (Iterator<? extends ENamedElement> iter = list.iterator(); iter.hasNext();) {
            sb.append(getName(iter.next()));
            if (iter.hasNext()) {
                sb.append(", ");
            }
        }
        return sb.toString();
    }

    
    /**
     @param eClass the super class
     @return subclasses to eClass in same package
     */
    public List<EClass> getSubClasses(EClass eClass) {
        List<EClass> subClasses = new ArrayList<EClass>();
        EPackage ePackage = eClass.getEPackage();
        for (EClass c : getClasses(ePackage)) {
            if (getExtends(c).contains(eClass)) {
                subClasses.add(c);
            }
        }
        return subClasses;
    }
    
    
    public boolean isBooleanType(ETypedElement element) {
        return getTypeName(element).equalsIgnoreCase("boolean")
    }

    public boolean isStringType(ETypedElement element) {
        return getTypeName(element).equals("String");
    }

    /**
     * The parameters of the operation as a comma separated String.
     * Each item includes the parameter type and name.
     */
    public String getParameterList(EOperation operation) {
        return getParameterList(operation, true);
    }

    public String getParameterList(EOperation operation, boolean formal) {
        StringBuffer result = new StringBuffer();
        Iterator<EParameter> iter = operation.getEParameters().iterator();
        while (iter.hasNext()) {
            EParameter parameter = iter.next();
            if (formal) {
                result.append(getTypeName(parameter));
                result.append(' ');
            }
            String paramName = parameter.getName();
            if (paramName != null && paramName.trim().length() == 0) {
                paramName = null;
            }
            result.append(paramName == null 
                "arg" + operation.getEParameters().indexOf(parameter)
                : paramName);
            if (iter.hasNext()) {
                result.append(", ");
            }
        }
        return result.toString();
    }

    /**
     * List of references with multiplicity > 1
     */
    public List<EReference> getAllManyReferences(EClass eClass) {
        List<EReference> allReferences = eClass.getEReferences();
        List<EReference> allManyReferences = new ArrayList<EReference>();
        for (EReference ref : allReferences) {
            if (isManyReference(ref)) {
                allManyReferences.add(ref);
            }
        }
        return allManyReferences;
    }
    
    /**
     * List of references with multiplicity = 1
     */
    public List<EReference> getAllOneReferences(EClass eClass) {
        List<EReference> allReferences = eClass.getEReferences();
        List<EReference> allOneReferences = new ArrayList<EReference>();
        for (EReference ref : allReferences) {
            if (isOneReference(ref)) {
                allOneReferences.add(ref);
            }
        }
        return allOneReferences;
    }
    
    /**
     * List of references with multiplicity > 1, which opposite
     * reference also has multiplicity > 1, i.e. many-to-many.
     */
    public List<EReference> getManyToManyReferences(EClass eClass) {
        List<EReference> many2ManyReferences = new ArrayList<EReference>();
        for (EReference ref : getAllManyReferences(eClass)) {
            if (isManyReference(ref.getEOpposite())) {
                many2ManyReferences.add(ref);
            }
        }
        return many2ManyReferences;
    }
    
    /**
     * List of references with multiplicity > 1, which opposite
     * reference has multiplicity = 1, i.e. many-to-one.
     */
    public List<EReference> getManyToOneReferences(EClass eClass) {
        List<EReference> many2OneReferences = new ArrayList<EReference>();
        for (EReference ref : getAllManyReferences(eClass)) {
            if (isOneReference(ref.getEOpposite())) {
                many2OneReferences.add(ref);
            }
        }
        return many2OneReferences;
    }

    /**
     @return true if multiplicity > 1 (or unbounded)
     */
    public boolean isManyReference(EReference ref) {
        return (ref.getUpperBound() == ETypedElement.UNBOUNDED_MULTIPLICITY|| (ref.getUpperBound() 1);
    }
    
    /**
     @return true if multiplicity = 1 (or unspecified)
     */
    private boolean isOneReference(EReference ref) {
        return (ref.getUpperBound() == ETypedElement.UNSPECIFIED_MULTIPLICITY|| (ref.getUpperBound() == 1);
    }

    /**
     * Convenience method to get the annotation associated with
     * an element.
     @param element the element to look at
     @param key key name of the annotation
     @param defaultValue if the annotation is not defined this value will be returned
     @return the value of the annotation, or defaultValue if it is not defined
     */
    public String getAnnotation(EModelElement element, String key, String defaultValue) {
        String value = getAnnotation(element, key);
        if (value == null) {
            return defaultValue;
        else {
            return value;
        }
    }

    public String getAnnotation(EModelElement element, String key) {
        List<EAnnotation> annotations = element.getEAnnotations();
        for (EAnnotation a : annotations) {
            String value = (Stringa.getDetails().get(key);
            if (value != null) {
                return value;
            }
        }
        // none found
        return null;
    }
    
    /**
     * Convenience method to get a EAttribute typed List of the attributes
     * of the class.
     */
    public List<EAttribute> getAttributes(EClass eClass) {
        List<EAttribute> attributes = eClass.getEAttributes();
        return attributes;
    }
    
    /**
     * Convenience method to get a EOperation typed List of the operations 
&