/*
 * 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.refactoring.java.plugins;

import com.sun.source.tree.*;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import java.util.List;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.modules.refactoring.java.api.JavaRefactoringUtils;
import org.netbeans.modules.refactoring.java.spi.RefactoringVisitor;
import org.netbeans.modules.refactoring.java.spi.ToPhaseException;
import org.openide.util.Exceptions;

/**
 * @author Jan Becicka, Bharath Ravi Kumar
 */
class VarUsageVisitor extends RefactoringVisitor {

    private final TypeElement superTypeElement;
    private final TypeElement subTypeElement;
    private boolean isReplCandidate = true;

    VarUsageVisitor(TypeElement subTypeElement, WorkingCopy workingCopy, 
            TypeElement superTypeElem) {
        try {
            setWorkingCopy(workingCopy);
        } catch (ToPhaseException ex) {
            Exceptions.printStackTrace(ex);
        }
        this.superTypeElement = superTypeElem;
        this.subTypeElement = subTypeElement;
    }

    @Override
    public Tree visitReturn(ReturnTree node, Element p) {
        ExpressionTree expression = node.getExpression();
        checkReturnType(expression, p);
        return super.visitReturn(node, p);
    }

    private void checkReturnType(ExpressionTree expression, Element p) {
        if(expression == null || p == null) { return; }
        if(expression.getKind() == Tree.Kind.IDENTIFIER) {
            IdentifierTree ident = (IdentifierTree) expression;
            checkReturnType(ident, p);
        } else if(expression.getKind() == Tree.Kind.CONDITIONAL_EXPRESSION) {
            ConditionalExpressionTree cet = (ConditionalExpressionTree) expression;
            checkReturnType(cet.getFalseExpression(), p);
            checkReturnType(cet.getTrueExpression(), p);
        }
    }

    private void checkReturnType(IdentifierTree ident, Element p) {
        Element el = asElement(ident);
        if(el.equals(p)) {
            MethodTree method = (MethodTree) JavaPluginUtils.findMethod(getCurrentPath()).getLeaf();
            TypeMirror returnType = workingCopy.getTrees().getTypeMirror(workingCopy.getTrees().getPath(workingCopy.getCompilationUnit(), method.getReturnType()));
            if(!workingCopy.getTypes().isSubtype(superTypeElement.asType(), returnType)) {
                isReplCandidate = false;
            }
        }
    }

    @Override
    public Tree visitMemberSelect(MemberSelectTree memSelectTree, Element refVarElem) {

        Element methodElement = this.asElement(memSelectTree);
        Element varElement = asElement(memSelectTree.getExpression());
        if (!refVarElem.equals(varElement)) {
            return super.visitMemberSelect(memSelectTree, refVarElem);
        }

        boolean isAssgCmptble = isMemberAvailable(subTypeElement, methodElement, 
                superTypeElement);
        if (!isAssgCmptble) {
            isReplCandidate = false;
        }
        return super.visitMemberSelect(memSelectTree, refVarElem);
    }

    @Override
    public Tree visitMethodInvocation(MethodInvocationTree node, Element p) {
        List<? extends ExpressionTree> arguments = node.getArguments();
        for (int i = 0; i < arguments.size(); i++) {
            ExpressionTree argument = arguments.get(i);
            Element argElement = asElement(argument); // TODO: Slow and misses ternary expressions
            if(p.equals(argElement)) {
                Element element = asElement(node);
                if (element.getKind() == ElementKind.METHOD) {
                    ExecutableElement method = (ExecutableElement) element;
                    VariableElement parameter = method.getParameters().get(i);
                    Types types = workingCopy.getTypes();
                    TypeMirror parameterType = parameter.asType();
                    if(parameterType.getKind().equals(TypeKind.TYPEVAR)) {
                        TypeVariable typeVariable = (TypeVariable) parameterType;
                        TypeMirror upperBound = typeVariable.getUpperBound();
                        TypeMirror lowerBound = typeVariable.getLowerBound();
                        if(upperBound != null && !types.isSubtype(superTypeElement.asType(), upperBound)) {
                            isReplCandidate = false;
                        }
                        if(lowerBound != null && !types.isSubtype(lowerBound, superTypeElement.asType())) {
                            isReplCandidate = false;
                        }
                    } else if(!types.isAssignable(superTypeElement.asType(), parameterType)) {
                        isReplCandidate = false;
                    }
                }
            }
        }
        return super.visitMethodInvocation(node, p);
    }

    @Override
    public Tree visitAssignment(AssignmentTree assgnTree, Element refVarElem) {

        ExpressionTree exprnTree = assgnTree.getExpression();
        Element exprElement = asElement(exprnTree);
        if (!refVarElem.equals(exprElement)) {
            return super.visitAssignment(assgnTree, refVarElem);
        }
        ExpressionTree varExprTree = assgnTree.getVariable();

        VariableElement varElement = (VariableElement) asElement(varExprTree);
        isReplCandidate = isReplacableAssgnmt(varElement) && isReplCandidate;
        return super.visitAssignment(assgnTree, refVarElem);
    }

    @Override
    public Tree visitVariable(VariableTree varTree, Element refVarElem) {
        ExpressionTree initTree = varTree.getInitializer();
        if (null == initTree) {
            return super.visitVariable(varTree, refVarElem);
        }
        Element exprElement = asElement(initTree);
        if (!refVarElem.equals(exprElement)) {
            return super.visitVariable(varTree, refVarElem);
        }
        VariableElement varElement = (VariableElement) asElement(varTree);
        isReplCandidate = isReplacableAssgnmt(varElement) && isReplCandidate;
        return super.visitVariable(varTree, refVarElem);
    }

    @Override
    public Tree visitMethod(MethodTree node, Element p) {
        List<? extends VariableTree> parameters = node.getParameters();
        for (VariableTree variableTree : parameters) {
            Element var = workingCopy.getTrees().getElement(new TreePath(getCurrentPath(), variableTree));
            ExecutableElement element = (ExecutableElement) workingCopy.getTrees().getElement(getCurrentPath());
            if(p.equals(var) && element != null) {
                // if var is != null then the enclosing method and class must exist as well.
                TypeElement classElement = (TypeElement) workingCopy.getTrees().getElement(JavaRefactoringUtils.findEnclosingClass(workingCopy, getCurrentPath(), true, true, true, true, false));
                List<ExecutableElement> methods = ElementFilter.methodsIn(workingCopy.getElements().getAllMembers(classElement));
                String methodName = node.getName().toString();
                for (ExecutableElement method : methods) {
                        if(methodName.equals(method.getSimpleName().toString())
                        && node.getParameters().size() == method.getParameters().size()) {
                    boolean sameParameters = true;
                    for (int j = 0; j < node.getParameters().size(); j++) {
                        TypeMirror exType = ((VariableElement)method.getParameters().get(j)).asType();
                        TypeMirror type;
                        VariableElement vari = (VariableElement)element.getParameters().get(j);
                        if(p.equals(vari)) {
                            type = superTypeElement.asType();
                        } else {
                            type = vari.asType();
                        }
                        if(!workingCopy.getTypes().isSameType(exType, type)) {
                            sameParameters = false;
                        }
                    }
                    if(sameParameters) {
                        isReplCandidate = false;
                    }
                    }
                }
            }
        }
        return super.visitMethod(node, p);
    }

    private boolean isMemberAvailable(TypeElement subTypeElement, Element methodElement, 
            TypeElement superTypeElement) {
        ElementKind memberKind = methodElement.getKind();
        if(ElementKind.METHOD.equals(memberKind)){
            return isMethodAvailable(subTypeElement, (ExecutableElement)methodElement, 
                    superTypeElement);
        }else{
            return isHidingMember(subTypeElement, methodElement, superTypeElement);
        }
    }

    private boolean isMethodAvailable(TypeElement subTypeElement, 
            ExecutableElement execElem, TypeElement superTypeElement) {
        Elements elements = workingCopy.getElements();
        List<? extends Element> memberElements = elements.getAllMembers(superTypeElement);
        for (Element elem : memberElements) {
            if(ElementKind.METHOD.equals(elem.getKind())){
                if(execElem.getModifiers().contains(Modifier.STATIC) && elements.hides(execElem, elem)){
                    return true;
                }else{
                    if(execElem.equals(elem) || elements.overrides(execElem, (ExecutableElement)elem, 
                            subTypeElement)){
                        return true;
                    }
                }
            }
        }

        return false;
    }

    private boolean isHidingMember(TypeElement subTypeElement, Element variableElement, 
            TypeElement superTypeElement) {
        //TODO: We do not handle nested types yet (includes enums)
        Elements elements = workingCopy.getElements();
        List<? extends Element> memberElements = elements.getAllMembers(superTypeElement);
        for (Element elem : memberElements) {
            if(variableElement.equals(elem) || elements.hides(variableElement, elem)){
                return true;
            }
        }
        return false;
    }

    private boolean isReplacableAssgnmt(VariableElement varElement) {
        if (isDeclaredType(varElement.asType())) {
            DeclaredType declType = (DeclaredType) varElement.asType();
            TypeElement varType = (TypeElement) declType.asElement();
            if (isAssignable(superTypeElement, varType)) {
                return true;
            }
        }
        return false;
    }

    boolean isReplaceCandidate() {
        return isReplCandidate;
    }

    private boolean isAssignable(TypeElement typeFrom, TypeElement typeTo) {
        Types types = workingCopy.getTypes();
        return types.isAssignable(typeFrom.asType(), typeTo.asType());
    }

    private Element asElement(Tree tree) {
        Trees treeUtil = workingCopy.getTrees();
        TreePath treePath = treeUtil.getPath(workingCopy.getCompilationUnit(), tree);
        Element element = treeUtil.getElement(treePath);
        return element;
    }

    private boolean isDeclaredType(TypeMirror type) {
        return TypeKind.DECLARED.equals(type.getKind());
    }

}
