/*
 * Copyright 2001-2013 the original author or authors.
 *
 * 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 org.jetbrains.java.generate.psi;

import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PropertyUtil;
import com.intellij.psi.util.PropertyUtilBase;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import static com.intellij.psi.CommonClassNames.*;

/**
 * Basic PSI Adapter with common function that works in all supported versions of IDEA.
 */
public final class PsiAdapter {

    private PsiAdapter() {}

    /**
     * Returns true if a field is constant.
     * <p/>
     * This is identified as the name of the field is only in uppercase and it has
     * a {@code static} modifier.
     *
     * @param field field to check if it's a constant
     * @return true if constant.
     */
    public static boolean isConstantField(PsiField field) {
        PsiModifierList list = field.getModifierList();
        if (list == null) {
            return false;
        }

        // modifier must be static
        if (!list.hasModifierProperty(PsiModifier.STATIC)) {
            return false;
        }

        // name must NOT have any lowercase character
        return !StringUtil.hasLowerCaseChar(field.getName());
    }

    /**
     * Finds an existing method with the given name.
     * If there isn't a method with the name, null is returned.
     *
     * @param clazz the class
     * @param name  name of method to find
     * @return the found method, null if none exist
     */
    public static @Nullable PsiMethod findMethodByName(PsiClass clazz, String name) {
        PsiMethod[] methods = clazz.getMethods();

        // use reverse to find from bottom as the duplicate conflict resolution policy requires this
        for (int i = methods.length - 1; i >= 0; i--) {
            PsiMethod method = methods[i];
            if (name.equals(method.getName())) {
                return method;
            }
        }
        return null;
    }

    /**
     * Returns true if the given field a primitive array type (e.g., int[], long[], float[]).
     *
     * @param type type.
     * @return true if field is a primitive array type.
     */
    public static boolean isPrimitiveArrayType(PsiType type) {
        return type instanceof PsiArrayType && isPrimitiveType(((PsiArrayType) type).getComponentType());
    }

    /**
     * Is the type an Object array type (etc. String[], Object[])?
     *
     * @param type type.
     * @return true if it's an Object array type.
     */
    public static boolean isObjectArrayType(PsiType type) {
        return type instanceof PsiArrayType && !isPrimitiveType(((PsiArrayType) type).getComponentType());
    }

    /**
     * Is the type a String array type (etc. String[])?
     *
     * @param type type.
     * @return true if it's a String array type.
     */
    public static boolean isStringArrayType(PsiType type) {
        if (isPrimitiveType(type))
            return false;

        return type.getCanonicalText().indexOf("String[]") > 0;
    }

    /**
     * Is the given field a {@link java.util.Collection} type?
     *
     * @param factory element factory.
     * @param type    type.
     * @return true if it's a Collection type.
     */
    public static boolean isCollectionType(PsiElementFactory factory, PsiType type) {
        return isTypeOf(factory, type, "java.util.Collection");
    }

    /**
     * Is the given field a {@link java.util.Map} type?
     *
     * @param factory element factory.
     * @param type    type.
     * @return true if it's a Map type.
     */
    public static boolean isMapType(PsiElementFactory factory, PsiType type) {
        return isTypeOf(factory, type, JAVA_UTIL_MAP);
    }

    /**
     * Is the given field a {@link java.util.Set} type?
     *
     * @param factory element factory.
     * @param type    type.
     * @return true if it's a Map type.
     */
    public static boolean isSetType(PsiElementFactory factory, PsiType type) {
        return isTypeOf(factory, type, JAVA_UTIL_SET);
    }

    /**
     * Is the given field a {@link java.util.List} type?
     *
     * @param factory element factory.
     * @param type    type.
     * @return true if it's a Map type.
     */
    public static boolean isListType(PsiElementFactory factory, PsiType type) {
        return isTypeOf(factory, type, JAVA_UTIL_LIST);
    }

    /**
     * Is the given field a {@link String} type?
     *
     * @param factory element factory.
     * @param type    type.
     * @return true if it's a String type.
     */
    public static boolean isStringType(PsiElementFactory factory, PsiType type) {
        return isTypeOf(factory, type, JAVA_LANG_STRING);
    }

    /**
     * Is the given field assignable from {@link Object}?
     *
     * @param factory element factory.
     * @param type    type.
     * @return true if it's an Object type.
     */
    public static boolean isObjectType(PsiElementFactory factory, PsiType type) {
        return isTypeOf(factory, type, JAVA_LANG_OBJECT);
    }

    /**
     * Is the given field a {@link java.util.Date} type?
     *
     * @param factory element factory.
     * @param type    type.
     * @return true if it's a Date type.
     */
    public static boolean isDateType(PsiElementFactory factory, PsiType type) {
        return isTypeOf(factory, type, "java.util.Date");
    }

    /**
     * Is the given field a {@link java.util.Calendar} type?
     *
     * @param factory element factory.
     * @param type    type.
     * @return true if it's a Calendar type.
     */
    public static boolean isCalendarType(PsiElementFactory factory, PsiType type) {
        return isTypeOf(factory, type, "java.util.Calendar");
    }

    /**
     * Is the given type a {@link Boolean} or a primitive boolean?
     *
     * @param factory unused.
     * @param type    type.
     * @return true if it's a Boolean or boolean type.
     */
    public static boolean isBooleanType(PsiElementFactory factory, PsiType type) {
      return PsiTypes.booleanType().equals(type) || type.equalsToText("java.lang.Boolean");
    }

    /**
     * Is the given field a numeric type (assignable from java.lang.Numeric or a primitive type of byte, short, int, long, float, double type)?
     *
     * @param factory element factory.
     * @param type    type.
     * @return true if it's a numeric type.
     */
    public static boolean isNumericType(PsiElementFactory factory, PsiType type) {
        if (isPrimitiveType(type)) {
            // test for simple type of numeric
            String s = type.getCanonicalText();
            return "byte".equals(s) || "double".equals(s) || "float".equals(s) || "int".equals(s) || "long".equals(s) || "short".equals(s);
        } else {
            // test for Object type of numeric
            return isTypeOf(factory, type, "java.lang.Number");
        }
    }

    /**
     * Does the javafile have the import statement?
     *
     * @param javaFile        javafile.
     * @param importStatement import statement to test existing for.
     * @return true if the javafile has the import statement.
     */
    public static boolean hasImportStatement(PsiJavaFile javaFile, String importStatement) {
        PsiImportList importList = javaFile.getImportList();
        if (importList == null) {
            return false;
        }

        if (importStatement.endsWith(".*")) {
            return importList.findOnDemandImportStatement(fixImportStatement(importStatement)) != null;
        } else {
            return importList.findSingleClassImportStatement(importStatement) != null;
        }
    }

    /**
     * Adds an import statement to the javafile and optimizes the imports afterwards.
     *
     *
     * @param javaFile                javafile.
     * @param importStatementOnDemand name of import statement, must be with a wildcard (etc. java.util.*).
     * @throws IncorrectOperationException
     *          is thrown if there is an error creating the import statement.
     */
    public static void addImportStatement(PsiJavaFile javaFile, String importStatementOnDemand) {
        PsiElementFactory factory = JavaPsiFacade.getElementFactory(javaFile.getProject());
        PsiImportStatement is = factory.createImportStatementOnDemand(fixImportStatement(importStatementOnDemand));

        // add the import to the file, and optimize the imports
        PsiImportList importList = javaFile.getImportList();
        if (importList != null) {
            importList.add(is);
        }

        JavaCodeStyleManager.getInstance(javaFile.getProject()).optimizeImports(javaFile);
    }

    /**
     * Fixes the import statement to be returned as packagename only (without .* or any Classname).
     * <p/>
     * <br/>Example: java.util will be returned as java.util
     * <br/>Example: java.util.* will be returned as java.util
     * <br/>Example: java.text.SimpleDateFormat will be returned as java.text
     *
     * @param importStatementOnDemand import statement
     * @return import statement only with packagename
     */
    private static String fixImportStatement(String importStatementOnDemand) {
        if (importStatementOnDemand.endsWith(".*")) {
            return importStatementOnDemand.substring(0, importStatementOnDemand.length() - 2);
        } else {
            boolean hasClassname = StringUtil.hasUpperCaseChar(importStatementOnDemand);

            if (hasClassname) {
                // extract packagename part
                int pos = importStatementOnDemand.lastIndexOf(".");
                return importStatementOnDemand.substring(0, pos);
            } else {
                // it is a pure packagename
                return importStatementOnDemand;
            }
        }
    }

    /**
     * Gets the fields fully qualified classname (etc java.lang.String, java.util.ArrayList)
     *
     * @param type the type.
     * @return the fully qualified classname, null if the field is a primitive.
     * @see #getTypeClassName(PsiType) for the non qualified version.
     */
    public static @Nullable String getTypeQualifiedClassName(PsiType type) {
        if (isPrimitiveType(type)) {
            return null;
        }

        // avoid [] if the type is an array
        String name = type.getCanonicalText();
        if (name.endsWith("[]")) {
            return name.substring(0, name.length() - 2);
        }

        return name;
    }

    /**
     * Gets the fields classname (etc. String, ArrayList)
     *
     * @param type the type.
     * @return the classname, null if the field is a primitive.
     * @see #getTypeQualifiedClassName(PsiType) for the qualified version.
     */
    public static @Nullable String getTypeClassName(PsiType type) {
        String name = getTypeQualifiedClassName(type);

        // return null if it was a primitive type
        if (name == null) {
            return null;
        }

        return StringUtil.getShortName(name);
    }

    /**
     * Finds the public static void main(String[] args) method.
     *
     * @param clazz the class.
     * @return the method if it exists, null if not.
     */
    public static @Nullable PsiMethod findPublicStaticVoidMainMethod(PsiClass clazz) {
        PsiMethod[] methods = clazz.findMethodsByName("main", false);

        // is it public static void main(String[] args)
        for (PsiMethod method : methods) {
            // must be public
            if (!method.hasModifierProperty(PsiModifier.PUBLIC)) {
                continue;
            }

            // must be static
            if (!method.hasModifierProperty(PsiModifier.STATIC)) {
                continue;
            }

            // must have void as return type
            PsiType returnType = method.getReturnType();
            if (!PsiTypes.voidType().equals(returnType)) {
                continue;
            }

            // must have one parameter
            PsiParameter[] parameters = method.getParameterList().getParameters();
            if (parameters.length != 1) {
                continue;
            }

            // parameter must be string array
            if (!isStringArrayType(parameters[0].getType())) {
                continue;
            }

            // public static void main(String[] args) method found
            return method;
        }

        // main not found
        return null;
    }

    /**
     * Add or replaces the javadoc comment to the given method.
     *
     * @param method           the method the javadoc should be added/set to.
     * @param javadoc          the javadoc comment.
     * @param replace          true if any existing javadoc should be replaced. false will not replace any existing javadoc and thus leave the javadoc untouched.
     * @return the added/replace javadoc comment, null if the was an existing javadoc and it should <b>not</b> be replaced.
     * @throws IncorrectOperationException is thrown if error adding/replacing the javadoc comment.
     */
    public static @Nullable PsiComment addOrReplaceJavadoc(PsiMethod method, String javadoc, boolean replace) {
        final Project project = method.getProject();
        PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
        PsiComment comment = factory.createCommentFromText(javadoc, null);

        // does a method already exists?
        PsiDocComment doc = method.getDocComment();
        if (doc != null) {
            if (replace) {
                // javadoc already exists, so replace
                doc.replace(comment);
                final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
                codeStyleManager.reformat(method); // to reformat javadoc
                return comment;
            } else {
                // do not replace existing javadoc
                return null;
            }
        } else {
            // add new javadoc
            method.addBefore(comment, method.getFirstChild());
            final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
            codeStyleManager.reformat(method); // to reformat javadoc
            return comment;
        }
    }

    /**
     * Is the given type a "void" type.
     *
     * @param type the type.
     * @return true if a void type, false if not.
     */
    public static boolean isTypeOfVoid(PsiType type) {
        return type != null && type.equalsToText("void");
    }

    /**
     * Is the method a getter method?
     * <p/>
     * The name of the method must start with {@code get} or {@code is}.
     * And if the method is a {@code isXXX} then the method must return a java.lang.Boolean or boolean.
     *
     *
     * @param method  the method
     * @return true if a getter method, false if not.
     */
    public static boolean isGetterMethod(PsiMethod method) {
        return PropertyUtil.isSimpleGetter(method);
    }

    /**
     * Gets the field name of the getter method.
     * <p/>
     * The method must be a getter method for a field.
     * Returns null if this method is not a getter.
     * <p/>
     * The fieldname is the part of the name that is after the {@code get} or {@code is} part
     * of the name.
     * <p/>
     * Example: methodName=getName will return fieldname=name
     *
     *
     * @param method  the method
     * @return the field name if this is a getter method.
     * @see #isGetterMethod(PsiMethod) for the getter check
     */
    public static @Nullable String getGetterFieldName(PsiMethod method) {
        // must be a getter
        if (!isGetterMethod(method)) {
            return null;
        }
        return PropertyUtilBase.getPropertyNameByGetter(method);
    }

    /**
     * Returns true if the field is enum (JDK1.5).
     *
     * @param field   field to check if it's a enum
     * @return true if enum.
     */
    public static boolean isEnumField(PsiField field) {
      final PsiClass aClass = PsiUtil.resolveClassInClassTypeOnly(field.getType());
      return (aClass != null) && aClass.isEnum();
    }

    /**
     * Is the class an exception - extends Throwable (will check super).
     *
     * @param clazz class to check.
     * @return true if class is an exception.
     */
    public static boolean isExceptionClass(PsiClass clazz) {
      return InheritanceUtil.isInheritor(clazz, JAVA_LANG_THROWABLE);
    }

    /**
     * Finds the public boolean equals(Object o) method.
     *
     * @param clazz the class.
     * @return the method if it exists, null if not.
     */
    public static @Nullable PsiMethod findEqualsMethod(PsiClass clazz) {
        PsiMethod[] methods = clazz.findMethodsByName("equals", false);

        // is it public boolean equals(Object o)
        for (PsiMethod method : methods) {
            // must be public
            if (!method.hasModifierProperty(PsiModifier.PUBLIC)) {
                continue;
            }

            // must not be static
            if (method.hasModifierProperty(PsiModifier.STATIC)) {
                continue;
            }

            // must have boolean as return type
            PsiType returnType = method.getReturnType();
            if (!PsiTypes.booleanType().equals(returnType)) {
                continue;
            }

            // must have one parameter
            PsiParameter[] parameters = method.getParameterList().getParameters();
            if (parameters.length != 1) {
                continue;
            }

            // parameter must be Object
            if (!(parameters[0].getType().getCanonicalText().equals(JAVA_LANG_OBJECT))) {
                continue;
            }

            // equals method found
            return method;
        }

        // equals not found
        return null;
    }

    /**
     * Finds the public int hashCode() method.
     *
     * @param clazz the class.
     * @return the method if it exists, null if not.
     */
    public static @Nullable PsiMethod findHashCodeMethod(PsiClass clazz) {
        PsiMethod[] methods = clazz.findMethodsByName("hashCode", false);

        // is it public int hashCode()
        for (PsiMethod method : methods) {
            // must be public
            if (!method.hasModifierProperty(PsiModifier.PUBLIC)) {
                continue;
            }

            // must not be static
            if (method.hasModifierProperty(PsiModifier.STATIC)) {
                continue;
            }

            // must have int as return type
            PsiType returnType = method.getReturnType();
            if (!PsiTypes.intType().equals(returnType)) {
                continue;
            }

            // must not have a parameter
            PsiParameterList parameters = method.getParameterList();
            if (!parameters.isEmpty()) {
                continue;
            }

            // hashCode method found
            return method;
        }

        // hashCode not found
        return null;
    }

    /**
     * Check if the given type against a FQ classname (assignable).
     *
     * @param factory         IDEA factory
     * @param type            the type
     * @param typeFQClassName the FQ classname to test against.
     * @return true if the given type is assignable of FQ classname.
     */
    private static boolean isTypeOf(PsiElementFactory factory, PsiType type, String typeFQClassName) {
        // fix for IDEA where fields can have 'void' type and generate NPE.
        if (isTypeOfVoid(type)) {
            return false;
        }

        if (isPrimitiveType(type)) {
            return false;
        }

        GlobalSearchScope scope = type.getResolveScope();
        if (scope == null) {
            return false;
        }
        PsiType typeTarget = factory.createTypeByFQClassName(typeFQClassName, scope);
        return typeTarget.isAssignableFrom(type);
    }

    /**
     * Gets the names the given class implements (not FQ names).
     *
     * @param clazz the class
     * @return the names.
     */
    public static String @NotNull [] getImplementsClassnames(PsiClass clazz) {
        PsiClass[] interfaces = clazz.getInterfaces();

        if (interfaces.length == 0) {
          return ArrayUtilRt.EMPTY_STRING_ARRAY;
        }

        String[] names = new String[interfaces.length];
        for (int i = 0; i < interfaces.length; i++) {
            PsiClass anInterface = interfaces[i];
            names[i] = anInterface.getName();
        }

        return names;
    }

    /**
     * Is the given type a primitive?
     *
     * @param type the type.
     * @return true if primitive, false if not.
     */
    public static boolean isPrimitiveType(PsiType type) {
        return type instanceof PsiPrimitiveType;
    }

  public static boolean isNestedArray(PsiType aType) {
    if (!(aType instanceof PsiArrayType)) return false;
    final PsiType componentType = ((PsiArrayType)aType).getComponentType();
    return componentType instanceof PsiArrayType;
  }
}
