/*
 * 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 com.imaginea.kodebeagle.impl.visitor;

import com.google.common.collect.Sets;
import com.imaginea.kodebeagle.base.object.WindowObjects;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiCatchSection;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiIdentifier;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiNewExpression;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiReturnStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeElement;
import com.intellij.psi.impl.source.tree.JavaElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.siyeh.ig.psiutils.ClassUtils;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class PsiJavaElementVisitor extends JavaRecursiveElementVisitor {
    private Map<String, Set<String>> importVsMethods = new HashMap<>();
    private final Set<String> emptySet = Sets.newHashSet();
    private int startOffset;
    private int endOffset;

    public final Map<String, Set<String>> getImportVsMethods() {
        return importVsMethods;
    }

    public PsiJavaElementVisitor(final int start, final int end) {
        super();
        startOffset = start;
        endOffset = end;
    }

    private void addInMap(final String qualifiedName, final Set<String> methodNames) {
        if (importVsMethods.containsKey(qualifiedName)) {
            Set<String> methods = importVsMethods.get(qualifiedName);
            methods.addAll(methodNames);
            importVsMethods.put(qualifiedName, methods);
        } else {
            importVsMethods.put(qualifiedName, methodNames);
        }
    }

    @Override
    public final void visitElement(final PsiElement element) {
        super.visitElement(element);
        if (startOffset <= element.getTextOffset() && element.getTextOffset() <= endOffset) {
            if (element.getNode().getElementType().equals(JavaElementType.FIELD)) {
                visitPsiFields((PsiField) element);
            } else if (element.getNode().getElementType().
                    equals(JavaElementType.DECLARATION_STATEMENT)) {
                visitPsiDeclarationStatement((PsiDeclarationStatement) element);
            } else if (element.getNode().getElementType().equals(JavaElementType.CATCH_SECTION)) {
                visitPsiCatchSection((PsiCatchSection) element);
            } else if (element.getNode().getElementType().
                    equals(JavaElementType.RETURN_STATEMENT)) {
                visitPsiReturnStatement((PsiReturnStatement) element);
            } else {
                visitExpression(element);
            }
        }
    }

    private void visitExpression(final PsiElement element) {
        if (element.getNode().getElementType().equals(JavaElementType.NEW_EXPRESSION)) {
            visitPsiNewExpression((PsiNewExpression) element);
        } else if (element.getNode().getElementType().
                equals(JavaElementType.METHOD_CALL_EXPRESSION)) {
            visitPsiMethodCallExpression((PsiMethodCallExpression) element);
        } else if (element.getNode().getElementType().
                equals(JavaElementType.ASSIGNMENT_EXPRESSION)) {
            visitPsiAssignmentExpression((PsiAssignmentExpression) element);
        } else if (element.getNode().getElementType().
                equals(JavaElementType.REFERENCE_EXPRESSION)) {
            visitPsiReferenceExpression((PsiReferenceExpression) element);
        }
    }

    private void visitPsiReferenceExpression(final PsiReferenceExpression element) {
        PsiExpression psiExpression = element.getQualifierExpression();
        if (psiExpression != null) {
            PsiType psiType = psiExpression.getType();
            if (psiType != null) {
                String qualifiedName = removeSpecialSymbols(psiType.getCanonicalText());
                addInMap(qualifiedName, emptySet);
            }
        }
    }

    private void visitPsiAssignmentExpression(final PsiAssignmentExpression
                                                      assignmentExpression) {
        PsiType lExpressionType = assignmentExpression.getLExpression().getType();
        if (lExpressionType != null && !ClassUtils.isPrimitive(lExpressionType)) {
            String type = removeSpecialSymbols(lExpressionType.getCanonicalText());
            addInMap(type, emptySet);
        }
        PsiExpression rExpression = assignmentExpression.getRExpression();
        if (rExpression != null) {
            PsiType rExpressionType = rExpression.getType();
            if (rExpressionType != null && !ClassUtils.isPrimitive(rExpressionType)) {
                String type = removeSpecialSymbols(rExpressionType.getCanonicalText());
                addInMap(type, emptySet);
            }
        }
    }

    private void visitPsiDeclarationStatement(final PsiDeclarationStatement
                                                      declarationStatement) {
        Collection<PsiTypeElement> typeElements =
                PsiTreeUtil.findChildrenOfType(declarationStatement, PsiTypeElement.class);
        for (PsiTypeElement element : typeElements) {
            String type = removeSpecialSymbols(element.getType().getCanonicalText());
            addInMap(type, emptySet);
        }
    }

    private void visitPsiNewExpression(final PsiNewExpression element) {
        if (element.getType() != null) {
            PsiType psiType = element.getType();
            if (psiType != null && !ClassUtils.isPrimitive(psiType)) {
                String type = removeSpecialSymbols(psiType.getCanonicalText());
                addInMap(type, emptySet);
            }
        }
    }

    private void visitPsiFields(final PsiField psiField) {
        if (!ClassUtils.isPrimitive(psiField.getType())) {
            String type = removeSpecialSymbols(psiField.getType().getCanonicalText());
            if (psiField.getInitializer() != null) {
                PsiExpression psiExpression = psiField.getInitializer();
                if (psiExpression != null) {
                    PsiType psiType = psiExpression.getType();
                    if (psiType != null && !ClassUtils.isPrimitive(psiType)) {
                        String psiFieldInitializer =
                                removeSpecialSymbols(psiType.getCanonicalText());
                        addInMap(psiFieldInitializer, emptySet);
                    }
                }
            }
            addInMap(type, emptySet);
        }
    }

    private Set<String> getMethods(final PsiReferenceExpression methodExpr) {
        Set<String> methods = new HashSet<>();
        if (WindowObjects.getInstance().isIncludeMethods()) {
            PsiIdentifier[] identifiers =
                    PsiTreeUtil.getChildrenOfType(methodExpr, PsiIdentifier.class);
            if (identifiers != null) {
                for (PsiIdentifier identifier : identifiers) {
                    methods.add(identifier.getText());
                }
            }
        }
        return methods;
    }

    private void visitPsiMethodCallExpression(final PsiMethodCallExpression element) {
        PsiReferenceExpression methodExpr = element.getMethodExpression();
        if (methodExpr.getQualifierExpression() != null) {
            PsiExpression psiExpression = methodExpr.getQualifierExpression();
            if (psiExpression != null) {
                PsiType psiType = psiExpression.getType();
                if (psiType != null && !ClassUtils.isPrimitive(psiExpression.getType())) {
                    String type = removeSpecialSymbols(psiType.getCanonicalText());
                    addInMap(type, getMethods(methodExpr));
                } else if (psiExpression.getReference() != null
                        && !ClassUtils.isPrimitive(psiExpression.getType())) {
                    PsiReference psiReference = psiExpression.getReference();
                    if (psiReference != null) {
                        String type = removeSpecialSymbols(psiReference.getCanonicalText());
                        addInMap(type, getMethods(methodExpr));
                    }
                }
            }
        }
    }

    private void visitPsiCatchSection(final PsiCatchSection element) {
        PsiType catchType = element.getCatchType();
        if (catchType != null) {
            String qualifiedName = removeSpecialSymbols(catchType.getCanonicalText());
            addInMap(qualifiedName, emptySet);
        }
    }

    private void visitPsiReturnStatement(final PsiReturnStatement element) {
        PsiExpression returnValue = element.getReturnValue();
        if (returnValue != null) {
            PsiType returnType = returnValue.getType();
            if (returnType != null) {
                String qualifiedName = removeSpecialSymbols(returnType.getCanonicalText());
                addInMap(qualifiedName, emptySet);
            }
        }
    }

    private String removeSpecialSymbols(final String pType) {
        String type = pType;
        if (type != null && type.contains("<")) {
            type = type.substring(0, type.indexOf("<"));
        } else if (type != null && type.contains("[")) {
            type = type.substring(0, type.indexOf("["));
        }
        return type;
    }
}