/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * 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.intellij.psi;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

/**
 * @author ven
 */
public class GenericsUtil {

  private static final Logger LOG = Logger.getInstance("#" + GenericsUtil.class.getName());

  private GenericsUtil() {}

  public static PsiType getGreatestLowerBound(@Nullable PsiType type1, @Nullable PsiType type2) {
    if (type1 == null || type2 == null) return null;
    return PsiIntersectionType.createIntersection(type1, type2);
  }

  @Nullable
  public static PsiType getLeastUpperBound(PsiType type1, PsiType type2, PsiManager manager) {
    if (TypeConversionUtil.isPrimitiveAndNotNull(type1) || TypeConversionUtil.isPrimitiveAndNotNull(type2)) return null;
    if (TypeConversionUtil.isNullType(type1)) return type2;
    if (TypeConversionUtil.isNullType(type2)) return type1;
    if (Comparing.equal(type1, type2)) return type1;
    return getLeastUpperBound(type1, type2, new LinkedHashSet<Couple<PsiType>>(), manager);
  }

  @NotNull
  private static PsiType getLeastUpperBound(PsiType type1, PsiType type2, Set<Couple<PsiType>> compared, PsiManager manager) {
    if (type1 instanceof PsiCapturedWildcardType) {
      return getLeastUpperBound(((PsiCapturedWildcardType)type1).getUpperBound(), type2, compared, manager);
    }
    if (type2 instanceof PsiCapturedWildcardType) {
      return getLeastUpperBound(type1, ((PsiCapturedWildcardType)type2).getUpperBound(), compared, manager);
    }

    if (type1 instanceof PsiWildcardType) {
      return getLeastUpperBound(((PsiWildcardType)type1).getExtendsBound(), type2, compared, manager);
    }
    if (type2 instanceof PsiWildcardType) {
      return getLeastUpperBound(type1, ((PsiWildcardType)type2).getExtendsBound(), compared, manager);
    }

    if (type1 instanceof PsiArrayType && type2 instanceof PsiArrayType) {
      final PsiType componentType1 = ((PsiArrayType)type1).getComponentType();
      final PsiType componentType2 = ((PsiArrayType)type2).getComponentType();
      final PsiType componentType = getLeastUpperBound(componentType1, componentType2, compared, manager);
      if (componentType1 instanceof PsiPrimitiveType && 
          componentType2 instanceof PsiPrimitiveType && 
          componentType.equalsToText(CommonClassNames.JAVA_LANG_OBJECT)) {
        final PsiElementFactory factory = JavaPsiFacade.getInstance(manager.getProject()).getElementFactory();
        final GlobalSearchScope resolveScope = GlobalSearchScope.allScope(manager.getProject());
        final PsiClassType cloneable = factory.createTypeByFQClassName(CommonClassNames.JAVA_LANG_CLONEABLE, resolveScope);
        final PsiClassType serializable = factory.createTypeByFQClassName(CommonClassNames.JAVA_IO_SERIALIZABLE, resolveScope);
        return PsiIntersectionType.createIntersection(componentType, cloneable, serializable);
      }
      return componentType.createArrayType();
    }
    if (type1 instanceof PsiIntersectionType) {
      Set<PsiType> newConjuncts = new LinkedHashSet<PsiType>();
      final PsiType[] conjuncts = ((PsiIntersectionType)type1).getConjuncts();
      for (PsiType type : conjuncts) {
        newConjuncts.add(getLeastUpperBound(type, type2, compared, manager));
      }
      return PsiIntersectionType.createIntersection(newConjuncts.toArray(PsiType.createArray(newConjuncts.size())));
    }
    if (type2 instanceof PsiIntersectionType) {
      return getLeastUpperBound(type2, type1, compared, manager);
    }
    if (type1 instanceof PsiClassType && type2 instanceof PsiClassType) {
      PsiClassType.ClassResolveResult classResolveResult1 = ((PsiClassType)type1).resolveGenerics();
      PsiClassType.ClassResolveResult classResolveResult2 = ((PsiClassType)type2).resolveGenerics();
      PsiClass aClass = classResolveResult1.getElement();
      PsiClass bClass = classResolveResult2.getElement();
      if (aClass == null || bClass == null) {
        return PsiType.getJavaLangObject(manager, GlobalSearchScope.allScope(manager.getProject()));
      }

      PsiClass[] supers = getLeastUpperClasses(aClass, bClass);
      if (supers.length == 0) {
        return PsiType.getJavaLangObject(manager, aClass.getResolveScope());
      }

      final PsiElementFactory elementFactory = JavaPsiFacade.getInstance(manager.getProject()).getElementFactory();
      PsiClassType[] conjuncts = new PsiClassType[supers.length];
      for (int i = 0; i < supers.length; i++) {
        PsiClass aSuper = supers[i];
        PsiSubstitutor subst1 = TypeConversionUtil.getSuperClassSubstitutor(aSuper, aClass, classResolveResult1.getSubstitutor());
        PsiSubstitutor subst2 = TypeConversionUtil.getSuperClassSubstitutor(aSuper, bClass, classResolveResult2.getSubstitutor());
        PsiSubstitutor substitutor = PsiSubstitutor.EMPTY;

        final Couple<PsiType> types = Couple.<PsiType>of(elementFactory.createType(aSuper, subst1), elementFactory.createType(aSuper, subst2));

        for (PsiTypeParameter parameter : PsiUtil.typeParametersIterable(aSuper)) {
          PsiType mapping1 = subst1.substitute(parameter);
          PsiType mapping2 = subst2.substitute(parameter);

          if (mapping1 != null && mapping2 != null) {
            if (compared.contains(types)) {
              substitutor = substitutor.put(parameter, PsiWildcardType.createUnbounded(manager));
            }
            else {
              compared.add(types);
              try {
                substitutor = substitutor.put(parameter, getLeastContainingTypeArgument(mapping1, mapping2, compared, manager));
              }
              finally {
                compared.remove(types);
              }
            }
          }
          else {
            substitutor = substitutor.put(parameter, null);
          }
        }

        conjuncts[i] = elementFactory.createType(aSuper, substitutor);
      }

      return PsiIntersectionType.createIntersection(conjuncts);
    }
    if (type2 instanceof PsiArrayType && !(type1 instanceof PsiArrayType)) {
      return getLeastUpperBound(type2, type1, compared, manager);
    }
    if (type1 instanceof PsiArrayType) {
      PsiElementFactory factory = JavaPsiFacade.getInstance(manager.getProject()).getElementFactory();
      GlobalSearchScope all = GlobalSearchScope.allScope(manager.getProject());
      PsiClassType serializable = factory.createTypeByFQClassName(CommonClassNames.JAVA_IO_SERIALIZABLE, all);
      PsiClassType cloneable = factory.createTypeByFQClassName(CommonClassNames.JAVA_LANG_CLONEABLE, all);
      PsiType arraySupers = PsiIntersectionType.createIntersection(serializable, cloneable);
      return getLeastUpperBound(arraySupers, type2, compared, manager);
    }

    return PsiType.getJavaLangObject(manager, GlobalSearchScope.allScope(manager.getProject()));
  }

  private static PsiType getLeastContainingTypeArgument(PsiType type1,
                                                        PsiType type2,
                                                        Set<Couple<PsiType>> compared,
                                                        PsiManager manager) {
    if (type1 instanceof PsiWildcardType) {
      PsiWildcardType wild1 = (PsiWildcardType)type1;
      final PsiType bound1 = wild1.getBound();
      if (bound1 == null) return type1;
      if (type2 instanceof PsiWildcardType) {
        PsiWildcardType wild2 = (PsiWildcardType)type2;
        final PsiType bound2 = wild2.getBound();
        if (bound2 == null) return type2;
        if (wild1.isExtends() == wild2.isExtends()) {
          return wild1.isExtends() ?
                 PsiWildcardType.createExtends(manager, getLeastUpperBound(bound1, bound2, compared, manager)) :
                 PsiWildcardType.createSuper(manager, getGreatestLowerBound(bound1, bound2));
        }
        else {
          return bound1.equals(bound2) ? bound1 : PsiWildcardType.createUnbounded(manager);
        }
      }
      else {
        return wild1.isExtends() ? PsiWildcardType.createExtends(manager, getLeastUpperBound(bound1, type2, compared, manager)) :
               wild1.isSuper() ? PsiWildcardType.createSuper(manager, getGreatestLowerBound(bound1, type2)) :
               wild1;
      }
    }
    else if (type2 instanceof PsiWildcardType) {
      return getLeastContainingTypeArgument(type2, type1, compared, manager);
    }
    //Done with wildcards

    if (type1.equals(type2)) return type1;
    return PsiWildcardType.createExtends(manager, getLeastUpperBound(type1, type2, compared, manager));
  }

  @NotNull
  public static PsiClass[] getLeastUpperClasses(PsiClass aClass, PsiClass bClass) {
    if (InheritanceUtil.isInheritorOrSelf(aClass, bClass, true)) return new PsiClass[]{bClass};
    Set<PsiClass> supers = new LinkedHashSet<PsiClass>();
    Set<PsiClass> visited = new HashSet<PsiClass>();
    getLeastUpperClassesInner(aClass, bClass, supers, visited);
    return supers.toArray(new PsiClass[supers.size()]);
  }

  private static void getLeastUpperClassesInner(PsiClass aClass, PsiClass bClass, Set<PsiClass> supers, Set<PsiClass> visited) {
    if (bClass.isInheritor(aClass, true)) {
      addSuper(supers, aClass);
    }
    else {
      final PsiClass[] aSupers = aClass.getSupers();
      for (PsiClass aSuper : aSupers) {
        if (visited.add(aSuper)) {
          getLeastUpperClassesInner(aSuper, bClass, supers, visited);
        }
      }
    }
  }

  private static void addSuper(final Set<PsiClass> supers, final PsiClass classToAdd) {
    for (Iterator<PsiClass> iterator = supers.iterator(); iterator.hasNext();) {
      PsiClass superClass = iterator.next();
      if (InheritanceUtil.isInheritorOrSelf(superClass, classToAdd, true)) return;
      if (classToAdd.isInheritor(superClass, true)) iterator.remove();
    }
    
    supers.add(classToAdd);
  }

  public static boolean isTypeArgumentsApplicable(final PsiTypeParameter[] typeParams,
                                                  final PsiSubstitutor substitutor,
                                                  final PsiElement context) {
    return isTypeArgumentsApplicable(typeParams, substitutor, context, true);
  }

  public static boolean isTypeArgumentsApplicable(final PsiTypeParameter[] typeParams,
                                                  final PsiSubstitutor substitutor,
                                                  final PsiElement context,
                                                  final boolean allowUncheckedConversion) {
    return findTypeParameterWithBoundError(typeParams, substitutor, context, allowUncheckedConversion) == null;
  }

  public static Pair<PsiTypeParameter, PsiType> findTypeParameterWithBoundError(final PsiTypeParameter[] typeParams,
                                                                                final PsiSubstitutor substitutor,
                                                                                final PsiElement context,
                                                                                final boolean allowUncheckedConversion) {
    for (PsiTypeParameter typeParameter : typeParams) {
      PsiType boundError = findTypeParameterBoundError(typeParameter, typeParameter.getExtendsListTypes(),
                                                       substitutor, context, allowUncheckedConversion);
      if (boundError != null) {
        return Pair.create(typeParameter, boundError);
      }
    }
    return null;
  }

  public static PsiType findTypeParameterBoundError(PsiTypeParameter typeParameter,
                                                    PsiType[] extendsTypes,
                                                    PsiSubstitutor substitutor,
                                                    PsiElement context,
                                                    boolean allowUncheckedConversion) {
    PsiType substituted = substitutor.substitute(typeParameter);
    if (substituted == null) return null;
    if (context != null) {
      substituted = PsiUtil.captureToplevelWildcards(substituted, context);
    }

    if (substituted instanceof PsiWildcardType) {
      if (((PsiWildcardType)substituted).isSuper()) {
        return null;
      }
    }

    for (PsiType type : extendsTypes) {
      PsiType extendsType = substitutor.substitute(type);
      if (extendsType != null && 
          !TypeConversionUtil.isAssignable(extendsType, substituted, allowUncheckedConversion)) {
        return extendsType;
      }
    }
    return null;
  }

  public static boolean isFromExternalTypeLanguage(@NotNull PsiType type) {
    String internalCanonicalText = type.getInternalCanonicalText();
    return internalCanonicalText != null && internalCanonicalText.equals(type.getCanonicalText());
  }

  @Contract("null -> null")
  public static PsiType getVariableTypeByExpressionType(@Nullable PsiType type) {
    return getVariableTypeByExpressionType(type, true);
  }

  @Contract("null, _ -> null")
  public static PsiType getVariableTypeByExpressionType(@Nullable PsiType type, final boolean openCaptured) {
    if (type == null) return null;
    if (type instanceof PsiCapturedWildcardType) {
      type = ((PsiCapturedWildcardType)type).getWildcard();
    }
    PsiType transformed = type.accept(new PsiTypeVisitor<PsiType>() {
      @Override
      public PsiType visitArrayType(PsiArrayType arrayType) {
        PsiType componentType = arrayType.getComponentType();
        PsiType type = componentType.accept(this);
        if (type == componentType) return arrayType;
        if (type instanceof PsiWildcardType) {
          type = ((PsiWildcardType)type).getBound();
        }
        return type != null ? type.createArrayType() : arrayType;
      }

      @Override
      public PsiType visitType(PsiType type) {
        return type;
      }

      @Override
      public PsiType visitWildcardType(final PsiWildcardType wildcardType) {
        final PsiType bound = wildcardType.getBound();
        PsiManager manager = wildcardType.getManager();
        if (bound != null) {

          if (wildcardType.isSuper() && bound instanceof PsiIntersectionType) {
            return PsiWildcardType.createUnbounded(manager);
          }

          final PsiType acceptedBound = bound.accept(this);
          if (acceptedBound instanceof PsiWildcardType) {
            if (((PsiWildcardType)acceptedBound).isExtends() != wildcardType.isExtends()) return PsiWildcardType.createUnbounded(manager);
            return acceptedBound;
          }
          if (wildcardType.isExtends() && acceptedBound.equalsToText(CommonClassNames.JAVA_LANG_OBJECT)) return PsiWildcardType.createUnbounded(manager);
          if (acceptedBound.equals(bound)) return wildcardType;
          return wildcardType.isExtends()
                 ? PsiWildcardType.createExtends(manager, acceptedBound)
                 : PsiWildcardType.createSuper(manager, acceptedBound);
        }
        return wildcardType;
      }

      @Override
      public PsiType visitCapturedWildcardType(PsiCapturedWildcardType capturedWildcardType) {
        return openCaptured ? capturedWildcardType.getWildcard().accept(this) : capturedWildcardType;
      }

      @Override
      public PsiType visitClassType(PsiClassType classType) {
        PsiClassType.ClassResolveResult resolveResult = classType.resolveGenerics();
        PsiClass aClass = resolveResult.getElement();
        if (aClass == null) return classType;
        boolean toExtend = false;
        PsiSubstitutor substitutor = PsiSubstitutor.EMPTY;
        for (PsiTypeParameter typeParameter : PsiUtil.typeParametersIterable(aClass)) {
          PsiType typeArgument = resolveResult.getSubstitutor().substitute(typeParameter);
          if (typeArgument instanceof PsiCapturedWildcardType) toExtend = true;
          if (typeArgument instanceof PsiWildcardType && ((PsiWildcardType)typeArgument).getBound() instanceof PsiIntersectionType) {
            toExtend = true;
          }
          PsiType toPut;
          if (typeArgument == null) {
            toPut = null;
          }
          else {
            final PsiType accepted = typeArgument.accept(this);
            if (typeArgument instanceof PsiIntersectionType && !(accepted instanceof PsiWildcardType)) {
              toPut = PsiWildcardType.createExtends(typeParameter.getManager(), accepted);
            }
            else {
              toPut = accepted;
            }
          }
          LOG.assertTrue(toPut == null || toPut.isValid(), toPut);
          substitutor = substitutor.put(typeParameter, toPut);
        }
        final PsiAnnotation[] applicableAnnotations = classType.getApplicableAnnotations();
        if (substitutor == PsiSubstitutor.EMPTY && !toExtend && applicableAnnotations.length == 0 && !(aClass instanceof PsiTypeParameter)) return classType;
        PsiManager manager = aClass.getManager();
        PsiType result = JavaPsiFacade.getInstance(manager.getProject()).getElementFactory()
          .createType(aClass, substitutor, PsiUtil.getLanguageLevel(aClass), applicableAnnotations);
        if (toExtend) result = PsiWildcardType.createExtends(manager, result);
        return result;
      }
    });

    PsiType componentType = transformed != null ? transformed.getDeepComponentType() : null;
    if (componentType instanceof PsiWildcardType) {
      componentType = ((PsiWildcardType)componentType).getExtendsBound();
      int dims = transformed.getArrayDimensions();
      for (int i = 0; i < dims; i++) componentType = componentType.createArrayType();
      return componentType;
    }

    return transformed;
  }

  public static PsiSubstitutor substituteByParameterName(final PsiClass psiClass, final PsiSubstitutor parentSubstitutor) {

    final Map<PsiTypeParameter, PsiType> substitutionMap = parentSubstitutor.getSubstitutionMap();
    final List<PsiType> result = new ArrayList<PsiType>(substitutionMap.size());
    for (PsiTypeParameter typeParameter : psiClass.getTypeParameters()) {
      final String name = typeParameter.getName();
      final PsiTypeParameter key = ContainerUtil.find(substitutionMap.keySet(), new Condition<PsiTypeParameter>() {
        @Override
        public boolean value(final PsiTypeParameter psiTypeParameter) {
          return name.equals(psiTypeParameter.getName());
        }
      });
      if (key != null) {
        result.add(substitutionMap.get(key));
      }
    }
    return PsiSubstitutor.EMPTY.putAll(psiClass, result.toArray(PsiType.createArray(result.size())));
  }

  public static PsiType eliminateWildcards(PsiType type) {
    return eliminateWildcards(type, true);
  }

  public static PsiType eliminateWildcards(PsiType type, final boolean eliminateInTypeArguments) {
    return eliminateWildcards(type, eliminateInTypeArguments, !eliminateInTypeArguments);
  }

  public static PsiType eliminateWildcards(PsiType type,
                                           final boolean eliminateInTypeArguments,
                                           boolean eliminateCapturedWildcards) {
    if (eliminateInTypeArguments && type instanceof PsiClassType) {
      PsiClassType classType = (PsiClassType)type;
      JavaResolveResult resolveResult = classType.resolveGenerics();
      PsiClass aClass = (PsiClass)resolveResult.getElement();
      if (aClass != null) {
        PsiManager manager = aClass.getManager();
        PsiTypeParameter[] typeParams = aClass.getTypeParameters();
        Map<PsiTypeParameter, PsiType> map = new HashMap<PsiTypeParameter, PsiType>();
        for (PsiTypeParameter typeParam : typeParams) {
          PsiType substituted = resolveResult.getSubstitutor().substitute(typeParam);
          if (substituted instanceof PsiWildcardType) {
            substituted = ((PsiWildcardType)substituted).getBound();
            if (substituted instanceof PsiCapturedWildcardType) {
              substituted = ((PsiCapturedWildcardType)substituted).getWildcard().getBound();
            }
            if (substituted == null) substituted = TypeConversionUtil.typeParameterErasure(typeParam);
          }
          map.put(typeParam, substituted);
        }

        PsiElementFactory factory = JavaPsiFacade.getInstance(manager.getProject()).getElementFactory();
        PsiSubstitutor substitutor = factory.createSubstitutor(map);
        type = factory.createType(aClass, substitutor);
      }
    }
    else if (type instanceof PsiArrayType) {
      return eliminateWildcards(((PsiArrayType)type).getComponentType(), false).createArrayType();
    }
    else if (type instanceof PsiWildcardType) {
      final PsiType bound = ((PsiWildcardType)type).getBound();
      return eliminateWildcards(bound != null ? bound : ((PsiWildcardType)type).getExtendsBound(), false);//object
    }
    else if (type instanceof PsiCapturedWildcardType && eliminateCapturedWildcards) {
      return eliminateWildcards(((PsiCapturedWildcardType)type).getUpperBound(), false);
    }
    return type;
  }

  public static boolean checkNotInBounds(PsiType type, PsiType bound, PsiReferenceParameterList referenceParameterList) {
    //allow unchecked conversions in method calls but not in type declaration
    return checkNotInBounds(type, bound, PsiTreeUtil.getParentOfType(referenceParameterList, PsiCallExpression.class) != null);
  }

  public static boolean checkNotInBounds(PsiType type, PsiType bound, boolean uncheckedConversionByDefault) {
    if (type instanceof PsiClassType) {
      return checkNotAssignable(bound, type, uncheckedConversionByDefault);
    }
    if (type instanceof PsiWildcardType) {
      if (((PsiWildcardType)type).isExtends()) {
        return checkExtendsWildcardCaptureFailure((PsiWildcardType)type, bound);
      }
      else if (((PsiWildcardType)type).isSuper()) {
        final PsiType superBound = ((PsiWildcardType)type).getSuperBound();
        if (PsiUtil.resolveClassInType(superBound) instanceof PsiTypeParameter) return TypesDistinctProver.provablyDistinct(type, bound);
        return checkNotAssignable(bound, superBound, false);
      }
    }
    else if (type instanceof PsiArrayType) {
      return checkNotAssignable(bound, type, true);
    }
    else if (type instanceof PsiIntersectionType) {
      for (PsiType psiType : ((PsiIntersectionType)type).getConjuncts()) {
        if (!checkNotInBounds(psiType, bound, uncheckedConversionByDefault)) {
          return false; 
        }
      }
      return true;
    }
    return false;
  }

  //JLS 5.1.10
  private static boolean checkExtendsWildcardCaptureFailure(PsiWildcardType type, PsiType bound) {
    LOG.assertTrue(type.isExtends());
    final PsiType extendsBound = type.getExtendsBound();
    PsiType boundBound = bound;
    if (bound instanceof PsiWildcardType) {
      if (((PsiWildcardType)bound).isBounded()) {
        boundBound = ((PsiWildcardType)bound).isSuper()
                     ? ((PsiWildcardType)bound).getSuperBound()
                     : ((PsiWildcardType)bound).getExtendsBound();
      }
      else {
        return false;
      }
    }

    final PsiClass extendsBoundClass = PsiUtil.resolveClassInClassTypeOnly(extendsBound);
    final PsiClass boundBoundClass = PsiUtil.resolveClassInClassTypeOnly(boundBound);
    if (boundBoundClass != null && extendsBoundClass != null && !boundBoundClass.isInterface() && !extendsBoundClass.isInterface()) {
      return !InheritanceUtil.isInheritorOrSelf(boundBoundClass, extendsBoundClass, true) &&
             !InheritanceUtil.isInheritorOrSelf(extendsBoundClass, boundBoundClass, true);
    }

    return !TypeConversionUtil.areTypesConvertible(boundBound, extendsBound) &&
           !TypeConversionUtil.areTypesConvertible(extendsBound, boundBound);
  }

  private static boolean checkNotAssignable(final PsiType bound,
                                            final PsiType type,
                                            final boolean allowUncheckedConversion) {
    if (bound instanceof PsiWildcardType) {
      if (((PsiWildcardType)bound).isBounded()) {
        final PsiType boundBound = ((PsiWildcardType)bound).isExtends()
                                   ? ((PsiWildcardType)bound).getExtendsBound()
                                   : ((PsiWildcardType)bound).getSuperBound();
        return !TypeConversionUtil.isAssignable(boundBound, type, allowUncheckedConversion);
      }
      else {
        return true;
      }
    }
    else {
      return !TypeConversionUtil.isAssignable(bound, type, allowUncheckedConversion);
    }
  }
}
