/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.netbeans.modules.j2ee.jpa.refactoring;


import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import org.netbeans.api.java.source.CancellableTask;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.TreePathHandle;
import org.netbeans.modules.j2ee.persistence.api.metadata.orm.EntityMappingsMetadata;
import org.netbeans.modules.j2ee.metadata.model.api.MetadataModel;
import org.netbeans.modules.j2ee.metadata.model.api.MetadataModelAction;
import org.netbeans.modules.j2ee.persistence.api.metadata.orm.Entity;
import org.netbeans.modules.j2ee.persistence.api.metadata.orm.OneToMany;
import org.netbeans.modules.j2ee.persistence.api.metadata.orm.OneToOne;
import org.openide.util.Parameters;

/**
 * This class resolves non-type safe associations between entities. Entities
 * might have non-type safe associations via annotations that
 * can have attributes such as "mappedBy" that specifies a field in another
 * entity.
 *
 * @author Erno Mononen
 */
public class EntityAssociationResolver {
    
    static final String ONE_TO_ONE = "javax.persistence.OneToOne"; //NO18N
    static final String ONE_TO_MANY = "javax.persistence.OneToMany"; //NO18N
    static final String MANY_TO_ONE = "javax.persistence.ManyToOne"; //NO18N
    static final String MANY_TO_MANY = "javax.persistence.ManyToMany"; //NO18N
    
    // supported annotations
    private static final List<String> ANNOTATIONS = Arrays.asList(
            new String[]{ONE_TO_ONE, ONE_TO_MANY, MANY_TO_ONE, MANY_TO_MANY});
    
    
    static final String MAPPED_BY = "mappedBy"; //NO18N
    static final String TARGET_ENTITY = "targetEntity"; //NO18N
    
    private final MetadataModel<EntityMappingsMetadata> entityMappingsModel;
    /**
     * The property being refactored.
     */
    private final TreePathHandle refactoringSource;
    
    /**
     * Constructs a new EntityAssociationResolver.
     *
     * @param refactoringSource the property being refactored.
     * @param entityMappingsModel
     *
     */
    public EntityAssociationResolver(TreePathHandle refactoringSource, MetadataModel<EntityMappingsMetadata> entityMappingsModel) {
        Parameters.notNull("entityMappingsModel", entityMappingsModel); //NO18N
        Parameters.notNull("refactoringSource", refactoringSource); //NO18N
        this.entityMappingsModel = entityMappingsModel;
        this.refactoringSource = refactoringSource;
    }
    
    
    /**
     * Resolves the references to the property being refactored.
     *
     * @return the references or an empty list if there were none.
     */
    public List<EntityAnnotationReference> resolveReferences() throws IOException{

        final List<EntityAnnotationReference> result = new ArrayList<EntityAnnotationReference>();
            final List<Reference> references = getReferringProperties();

        entityMappingsModel.runReadAction(new MetadataModelAction<EntityMappingsMetadata, Void>(){

            public Void run(EntityMappingsMetadata metadata) throws Exception {
                
                for (Reference reference : references){
                        Entity entity = getByClass(metadata.getRoot().getEntity(), reference.getClassName());
                    if (entity == null){
                            continue;
                        }
                    result.addAll(getOneToX(entity, reference));
                    }
                return null;
                }
            
            });
        return result;
        }
    
    private List<EntityAnnotationReference> getOneToX(Entity entity, Reference reference) throws IOException{
        
        List<EntityAnnotationReference> result = new ArrayList<EntityAnnotationReference>();
        boolean fieldAccess = Entity.FIELD_ACCESS.equals(entity.getAccess());
        
        TreePathHandle handle = RefactoringUtil.getTreePathHandle(reference.getPropertyName(), reference.getClassName(), refactoringSource.getFileObject());
        for (OneToMany oneToMany : entity.getAttributes().getOneToMany()){
            if (isMatching(oneToMany.getName(), fieldAccess, reference)){
                result.add(new EntityAnnotationReference(reference.getClassName(),
                        ONE_TO_MANY, MAPPED_BY, reference.getSourceProperty(), handle));
            }
        }
        
        for (OneToOne oneToOne : entity.getAttributes().getOneToOne()){
            if (isMatching(oneToOne.getName(), fieldAccess, reference)){
                result.add(new EntityAnnotationReference(reference.getClassName(),
                        ONE_TO_ONE, MAPPED_BY, reference.getSourceProperty(), handle));
            }
        }
        return result;
    }
    
    private boolean isMatching(String propertyName, boolean fieldAccess, Reference reference){
        if (fieldAccess && reference.isField()){
            return propertyName.equals(reference.getPropertyName());
        }
        if (!fieldAccess && !reference.isField()){
            return propertyName.equals(RefactoringUtil.getPropertyName(reference.getPropertyName()));
        }
        return false;
    }
    
    private Entity getByClass(Entity[] entities, String clazz){
        for (Entity entity : entities){
            if (entity.getClass2().equals(clazz)){
                return entity;
            }
        }
        return null;
    }
    
    /**
     * Gets the references to the properties that might have an annotation containing a reference
     * to the property that is being refactored.
     *
     * @return a list of <code>Reference<code>s representing the referring properties.
     */
    List<Reference> getReferringProperties() throws IOException{
        
        final List<Reference> result = new ArrayList<Reference>();
        
        JavaSource source = JavaSource.forFileObject(refactoringSource.getFileObject());
        
        //TODO: should not be an anonymous class
        source.runUserActionTask(new CancellableTask<CompilationController>(){
            
            public void cancel() {
            }
            
            private List<IdentifierTree> getTypeArgs(ParameterizedTypeTree ptt){
                List<IdentifierTree> result = new ArrayList<IdentifierTree>();
                for (Tree typeArg : ptt.getTypeArguments()){
                    if (Tree.Kind.IDENTIFIER == typeArg.getKind()){
                        IdentifierTree it = (IdentifierTree) typeArg;
                        result.add(it);
                    }
                }
                return result;
            }
            
            private List<IdentifierTree> getIdentifier(Tree tree){
                if (Tree.Kind.PARAMETERIZED_TYPE == tree.getKind()){
                    return getTypeArgs((ParameterizedTypeTree) tree);
                }
                if (Tree.Kind.IDENTIFIER == tree.getKind()){
                    return Collections.<IdentifierTree>singletonList((IdentifierTree)tree);
                }
                return Collections.<IdentifierTree>emptyList();
                
            }
            
            public void run(CompilationController info) throws Exception {
                info.toPhase(JavaSource.Phase.RESOLVED);
                Element refactoringTargetProperty = refactoringSource.resolveElement(info);
                
                if(refactoringTargetProperty == null || refactoringTargetProperty.getEnclosingElement()==null) {
                    return;
                }
                
                String sourceClass = refactoringTargetProperty.getEnclosingElement().asType().toString();
                String propertyName = refactoringTargetProperty.getSimpleName().toString();
                String targetClass = refactoringTargetProperty.asType().toString();
                
                TypeElement te = info.getElements().getTypeElement(targetClass);
                if (te == null){
                    //TODO:
                    // seem to be possible, for methods type.toString returns "()f.q.N"
                    // either incorrect usage or a bug
                    return;
                }
                for (Element element : te.getEnclosedElements()){
                    Tree propertyTree = info.getTrees().getTree(element);
                    if (propertyTree == null){
                        continue;
                    }
                    List<IdentifierTree> identifiers = new ArrayList<IdentifierTree>();
                    boolean field = false;
                    if (element.getKind().equals(ElementKind.FIELD)){
                        field = true;
                        if (Tree.Kind.VARIABLE == propertyTree.getKind()){
                            VariableTree vt = (VariableTree) propertyTree;
                            identifiers = getIdentifier(vt.getType());
                        }
                    } else if (element.getKind().equals(ElementKind.METHOD)){
                        MethodTree mt = (MethodTree) propertyTree;
                        identifiers = getIdentifier(mt.getReturnType());
                    }
                    for (IdentifierTree it : identifiers){
                        TypeMirror type = info.getTreeUtilities().parseType(it.getName().toString(), te);
                        if (sourceClass.equals(type.toString())){
                            result.add(new Reference(element.getSimpleName().toString(),
                                    propertyName,
                                    targetClass,
                                    field));
                        }
                    }
                }
            }
        }, false);
        
        return result;
    }
    
    /**
     * Encapsulates information of a referring property.
     */
    static class Reference {
        
        /**
         * The FQN of the class to which the property being refactored points to.
         */
        private final String className;
        /**
         * The name of the property in the target class that possibly has a reference
         * to the property being refactored.
         */
        private final String propertyName;
        /**
         * The name of the property that is being refactored.
         */
        private final String sourceProperty;
        /**
         * Specifies whether the property to which the <code>propertyName</code> points
         * is a field or a method.
         */
        private final boolean field;
        
        public Reference(String propertyName, String sourceProperty, String clazz, boolean field){
            this.propertyName = propertyName;
            this.sourceProperty = sourceProperty;
            this.className = clazz;
            this.field = field;
        }
        
        /**
         * @see #propertyName
         */
        public String getPropertyName(){
            return propertyName;
        }
        
        /**
         * @see #className
         */
        public String getClassName(){
            return className;
        }
        
        /**
         * @see #sourceProperty
         */
        public String getSourceProperty() {
            return sourceProperty;
        }
        
        /**
         * @see #field
         */
        public boolean isField() {
            return field;
        }
        
        
    }
}


