/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * 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 com.android.tools.lint.detector.api;

import static com.android.tools.lint.client.api.JavaParser.TYPE_BOOLEAN;
import static com.android.tools.lint.client.api.JavaParser.TYPE_CHAR;
import static com.android.tools.lint.client.api.JavaParser.TYPE_DOUBLE;
import static com.android.tools.lint.client.api.JavaParser.TYPE_FLOAT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_INT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_LONG;
import static com.android.tools.lint.client.api.JavaParser.TYPE_STRING;
import static com.android.tools.lint.detector.api.JavaContext.getParentOfType;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.DefaultTypeDescriptor;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedField;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.client.api.JavaParser.ResolvedVariable;
import com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionStatement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.PsiTreeUtil;

import java.util.ListIterator;

import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.BooleanLiteral;
import lombok.ast.Cast;
import lombok.ast.CharLiteral;
import lombok.ast.Expression;
import lombok.ast.ExpressionStatement;
import lombok.ast.FloatingPointLiteral;
import lombok.ast.InlineIfExpression;
import lombok.ast.IntegralLiteral;
import lombok.ast.Literal;
import lombok.ast.Node;
import lombok.ast.NullLiteral;
import lombok.ast.Statement;
import lombok.ast.StringLiteral;
import lombok.ast.UnaryExpression;
import lombok.ast.VariableDeclaration;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;

/**
 * Evaluates the types of nodes. This goes deeper than
 * {@link JavaContext#getType(Node)} in that it analyzes the
 * flow and for example figures out that if you ask for the type of {@code var}
 * in this code snippet:
 * <pre>
 *     Object o = new StringBuilder();
 *     Object var = o;
 * </pre>
 * it will return "java.lang.StringBuilder".
 * <p>
 * <b>NOTE:</b> This type evaluator does not (yet) compute the correct
 * types when involving implicit type conversions, so be careful
 * if using this for primitives; e.g. for "int * long" it might return
 * the type "int".
 */
public class TypeEvaluator {
    private final JavaContext mContext;

    /**
     * Creates a new constant evaluator
     *
     * @param context the context to use to resolve field references, if any
     */
    public TypeEvaluator(@Nullable JavaContext context) {
        mContext = context;
    }


    /**
     * Returns the inferred type of the given node
     * @deprecated Use {@link #evaluate(PsiElement)} instead
     */
    @Deprecated
    @Nullable
    public TypeDescriptor evaluate(@NonNull Node node) {
        ResolvedNode resolved = null;
        if (mContext != null) {
            resolved = mContext.resolve(node);
        }
        if (resolved instanceof ResolvedMethod) {
            TypeDescriptor type;
            ResolvedMethod method = (ResolvedMethod) resolved;
            if (method.isConstructor()) {
                ResolvedClass containingClass = method.getContainingClass();
                type = containingClass.getType();
            } else {
                type = method.getReturnType();
            }
            return type;
        }
        if (resolved instanceof ResolvedField) {
            ResolvedField field = (ResolvedField) resolved;
            Node astNode = field.findAstNode();
            if (astNode instanceof VariableDeclaration) {
                VariableDeclaration declaration = (VariableDeclaration)astNode;
                VariableDefinition definition = declaration.astDefinition();
                if (definition != null) {
                    VariableDefinitionEntry first = definition.astVariables().first();
                    if (first != null) {
                        Expression initializer = first.astInitializer();
                        if (initializer != null) {
                            TypeDescriptor type = evaluate(initializer);
                            if (type != null) {
                                return type;
                            }
                        }
                    }
                }
            }
            return field.getType();
        }

        if (node instanceof VariableReference) {
            Statement statement = getParentOfType(node, Statement.class, false);
            if (statement != null) {
                ListIterator<Node> iterator = statement.getParent().getChildren().listIterator();
                while (iterator.hasNext()) {
                    if (iterator.next() == statement) {
                        if (iterator.hasPrevious()) { // should always be true
                            iterator.previous();
                        }
                        break;
                    }
                }

                String targetName = ((VariableReference) node).astIdentifier().astValue();
                while (iterator.hasPrevious()) {
                    Node previous = iterator.previous();
                    if (previous instanceof VariableDeclaration) {
                        VariableDeclaration declaration = (VariableDeclaration) previous;
                        VariableDefinition definition = declaration.astDefinition();
                        for (VariableDefinitionEntry entry : definition.astVariables()) {
                            if (entry.astInitializer() != null && entry.astName().astValue()
                                    .equals(targetName)) {
                                return evaluate(entry.astInitializer());
                            }
                        }
                    } else if (previous instanceof ExpressionStatement) {
                        ExpressionStatement expressionStatement = (ExpressionStatement) previous;
                        Expression expression = expressionStatement.astExpression();
                        if (expression instanceof BinaryExpression &&
                                ((BinaryExpression) expression).astOperator()
                                        == BinaryOperator.ASSIGN) {
                            BinaryExpression binaryExpression = (BinaryExpression) expression;
                            if (targetName.equals(binaryExpression.astLeft().toString())) {
                                return evaluate(binaryExpression.astRight());
                            }
                        }
                    }
                }
            }
        } else if (node instanceof Cast) {
            Cast cast = (Cast) node;
            if (mContext != null) {
                ResolvedNode typeReference = mContext.resolve(cast.astTypeReference());
                if (typeReference instanceof ResolvedClass) {
                    return ((ResolvedClass) typeReference).getType();
                }
            }
            TypeDescriptor viewType = evaluate(cast.astOperand());
            if (viewType != null) {
                return viewType;
            }
        } else if (node instanceof Literal) {
            if (node instanceof NullLiteral) {
                return null;
            } else if (node instanceof BooleanLiteral) {
                return new DefaultTypeDescriptor(TYPE_BOOLEAN);
            } else if (node instanceof StringLiteral) {
                return new DefaultTypeDescriptor(TYPE_STRING);
            } else if (node instanceof CharLiteral) {
                return new DefaultTypeDescriptor(TYPE_CHAR);
            } else if (node instanceof IntegralLiteral) {
                IntegralLiteral literal = (IntegralLiteral) node;
                // Don't combine to ?: since that will promote astIntValue to a long
                if (literal.astMarkedAsLong()) {
                    return new DefaultTypeDescriptor(TYPE_LONG);
                } else {
                    return new DefaultTypeDescriptor(TYPE_INT);
                }
            } else if (node instanceof FloatingPointLiteral) {
                FloatingPointLiteral literal = (FloatingPointLiteral) node;
                // Don't combine to ?: since that will promote astFloatValue to a double
                if (literal.astMarkedAsFloat()) {
                    return new DefaultTypeDescriptor(TYPE_FLOAT);
                } else {
                    return new DefaultTypeDescriptor(TYPE_DOUBLE);
                }
            }
        } else if (node instanceof UnaryExpression) {
            return evaluate(((UnaryExpression) node).astOperand());
        } else if (node instanceof InlineIfExpression) {
            InlineIfExpression expression = (InlineIfExpression) node;
            if (expression.astIfTrue() != null) {
                return evaluate(expression.astIfTrue());
            } else if (expression.astIfFalse() != null) {
                return evaluate(expression.astIfFalse());
            }
        } else if (node instanceof BinaryExpression) {
            BinaryExpression expression = (BinaryExpression) node;
            BinaryOperator operator = expression.astOperator();
            switch (operator) {
                case LOGICAL_OR:
                case LOGICAL_AND:
                case EQUALS:
                case NOT_EQUALS:
                case GREATER:
                case GREATER_OR_EQUAL:
                case LESS:
                case LESS_OR_EQUAL:
                    return new DefaultTypeDescriptor(TYPE_BOOLEAN);
            }

            TypeDescriptor type = evaluate(expression.astLeft());
            if (type != null) {
                return type;
            }
            return evaluate(expression.astRight());
        }

        if (resolved instanceof ResolvedVariable) {
            ResolvedVariable variable = (ResolvedVariable) resolved;
            return variable.getType();
        }

        return null;
    }

    /**
     * Returns the inferred type of the given node
     */
    @Nullable
    public PsiType evaluate(@Nullable PsiElement node) {
        if (node == null) {
            return null;
        }

        PsiElement resolved = null;
        if (node instanceof PsiReference) {
            resolved = ((PsiReference) node).resolve();
        }
        if (resolved instanceof PsiMethod) {
            PsiMethod method = (PsiMethod) resolved;
            if (method.isConstructor()) {
                PsiClass containingClass = method.getContainingClass();
                if (containingClass != null && mContext != null) {
                    return mContext.getEvaluator().getClassType(containingClass);
                }
            } else {
                return method.getReturnType();
            }
        }

        if (resolved instanceof PsiField) {
            PsiField field = (PsiField) resolved;
            if (field.getInitializer() != null) {
                PsiType type = evaluate(field.getInitializer());
                if (type != null) {
                    return type;
                }
            }
            return field.getType();
        } else if (resolved instanceof PsiLocalVariable) {
            PsiLocalVariable variable = (PsiLocalVariable) resolved;
            PsiStatement statement = PsiTreeUtil.getParentOfType(node, PsiStatement.class,
                    false);
            if (statement != null) {
                PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
                        PsiStatement.class);
                String targetName = variable.getName();
                if (targetName == null) {
                    return null;
                }
                while (prev != null) {
                    if (prev instanceof PsiDeclarationStatement) {
                        for (PsiElement element : ((PsiDeclarationStatement)prev).getDeclaredElements()) {
                            if (variable.equals(element)) {
                                return evaluate(variable.getInitializer());
                            }
                        }
                    } else if (prev instanceof PsiExpressionStatement) {
                        PsiExpression expression = ((PsiExpressionStatement)prev).getExpression();
                        if (expression instanceof PsiAssignmentExpression) {
                            PsiAssignmentExpression assign = (PsiAssignmentExpression) expression;
                            PsiExpression lhs = assign.getLExpression();
                            if (lhs instanceof PsiReferenceExpression) {
                                PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
                                if (targetName.equals(reference.getReferenceName()) &&
                                        reference.getQualifier() == null) {
                                    return evaluate(assign.getRExpression());
                                }
                            }
                        }
                    }
                    prev = PsiTreeUtil.getPrevSiblingOfType(prev,
                            PsiStatement.class);
                }
            }

            return variable.getType();
        } else if (node instanceof PsiExpression) {
            PsiExpression expression = (PsiExpression) node;
            return expression.getType();
        }

        return null;
    }

    /**
     * Evaluates the given node and returns the likely type of the instance. Convenience
     * wrapper which creates a new {@linkplain TypeEvaluator}, evaluates the node and returns
     * the result.
     *
     * @param context the context to use to resolve field references, if any
     * @param node    the node to compute the type for
     * @return the corresponding type descriptor, if found
     * @deprecated Use {@link #evaluate(JavaContext, PsiElement)} instead
     */
    @Deprecated
    @Nullable
    public static TypeDescriptor evaluate(@NonNull JavaContext context, @NonNull Node node) {
        return new TypeEvaluator(context).evaluate(node);
    }

    /**
     * Evaluates the given node and returns the likely type of the instance. Convenience
     * wrapper which creates a new {@linkplain TypeEvaluator}, evaluates the node and returns
     * the result.
     *
     * @param context the context to use to resolve field references, if any
     * @param node    the node to compute the type for
     * @return the corresponding type descriptor, if found
     */
    @Nullable
    public static PsiType evaluate(@NonNull JavaContext context, @NonNull PsiElement node) {
        return new TypeEvaluator(context).evaluate(node);
    }
}
