/*
 * Copyright 2000-2015 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.patterns;

import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * @author peter
 */
public class PsiJavaElementPattern<T extends PsiElement,Self extends PsiJavaElementPattern<T,Self>> extends PsiElementPattern<T,Self> {
  @NonNls private static final String VALUE = "value";

  public PsiJavaElementPattern(final Class<T> aClass) {
    super(aClass);
  }

  public PsiJavaElementPattern(@NotNull final InitialPatternCondition<T> condition) {
    super(condition);
  }

  public Self annotationParam(@NonNls final String annotationQualifiedName, @NonNls final String parameterName) {
    return annotationParam(StandardPatterns.string().equalTo(annotationQualifiedName), parameterName);
  }

  public Self annotationParam(@NonNls final String annotationQualifiedName) {
    return annotationParam(annotationQualifiedName, VALUE);
  }

  public Self annotationParam(final ElementPattern<String> annotationQualifiedName, @NonNls final String parameterName) {
    return withParent(
      PsiJavaPatterns.psiNameValuePair().withName(parameterName).withParent(
        PlatformPatterns.psiElement(PsiAnnotationParameterList.class).withParent(
          PsiJavaPatterns.psiAnnotation().qName(annotationQualifiedName))));
  }
  public Self annotationParam(@NotNull ElementPattern<PsiAnnotation> annotation) {
    return withParent(
      PsiJavaPatterns.psiNameValuePair().withParent(
        PlatformPatterns.psiElement(PsiAnnotationParameterList.class).withParent(
          annotation)));
  }

  public Self annotationParam(String parameterName, @NotNull ElementPattern<PsiAnnotation> annotation) {
    return withParent(
      PsiJavaPatterns.psiNameValuePair().withName(parameterName).withParent(
        PlatformPatterns.psiElement(PsiAnnotationParameterList.class).withParent(annotation)));
  }

  public Self insideAnnotationParam(final ElementPattern<String> annotationQualifiedName, @NonNls final String parameterName) {
    return withAncestor(3,   // can be array initializer
                        PsiJavaPatterns.psiNameValuePair().withName(parameterName).withParent(
        PlatformPatterns.psiElement(PsiAnnotationParameterList.class).withParent(
          PsiJavaPatterns.psiAnnotation().qName(annotationQualifiedName))));
  }

  public Self insideAnnotationParam(final ElementPattern<String> annotationQualifiedName) {
    return insideAnnotationParam(annotationQualifiedName, VALUE);
  }

  public Self insideAnnotationParam(@NotNull String annotationQualifiedName) {
    return insideAnnotationParam(StandardPatterns.string().equalTo(annotationQualifiedName));
  }

  public Self nameIdentifierOf(final Class<? extends PsiMember> aClass) {
    return nameIdentifierOf(StandardPatterns.instanceOf(aClass));
  }

  public Self nameIdentifierOf(final ElementPattern<? extends PsiMember> pattern) {
    return with(new PatternCondition<T>("nameIdentifierOf") {
      @Override
      public boolean accepts(@NotNull final T t, final ProcessingContext context) {
        if (!(t instanceof PsiIdentifier)) return false;

        final PsiElement parent = t.getParent();
        if (parent instanceof PsiClass && t != ((PsiClass) parent).getNameIdentifier()) return false;
        if (parent instanceof PsiMethod && t != ((PsiMethod) parent).getNameIdentifier()) return false;
        if (parent instanceof PsiVariable && t != ((PsiVariable) parent).getNameIdentifier()) return false;

        return pattern.accepts(parent, context);
      }
    });
  }

  public Self methodCallParameter(final int index, final ElementPattern<? extends PsiMethod> methodPattern) {
    //noinspection unchecked
    final PsiNamePatternCondition nameCondition = ContainerUtil.findInstance(methodPattern.getCondition().getConditions(), PsiNamePatternCondition.class);

    return with(new PatternCondition<T>("methodCallParameter") {
      @Override
      public boolean accepts(@NotNull final T literal, final ProcessingContext context) {
        final PsiElement parent = literal.getParent();
        if (parent instanceof PsiExpressionList) {
          final PsiExpressionList psiExpressionList = (PsiExpressionList)parent;
          final PsiExpression[] psiExpressions = psiExpressionList.getExpressions();
          if (!(psiExpressions.length > index && psiExpressions[index] == literal)) return false;

          return checkCall(context, psiExpressionList, methodPattern, nameCondition);
        }
        return false;
      }
    });
  }

  public Self methodCallParameter(@NotNull final ElementPattern<? extends PsiMethod> methodPattern) {
    //noinspection unchecked
    final PsiNamePatternCondition nameCondition = ContainerUtil.findInstance(methodPattern.getCondition().getConditions(), PsiNamePatternCondition.class);

    return with(new PatternCondition<T>("methodCallParameter") {
      @Override
      public boolean accepts(@NotNull final T literal, final ProcessingContext context) {
        final PsiElement parent = literal.getParent();
        return parent instanceof PsiExpressionList && checkCall(context, (PsiExpressionList)parent, methodPattern, nameCondition);
      }
    });
  }

  private static boolean checkCall(ProcessingContext context,
                                   PsiExpressionList psiExpressionList,
                                   ElementPattern<? extends PsiMethod> methodPattern,
                                   @Nullable PsiNamePatternCondition nameCondition) {
    final PsiElement element = psiExpressionList.getParent();
    if (element instanceof PsiMethodCallExpression) {
      PsiReferenceExpression methodExpression = ((PsiMethodCallExpression)element).getMethodExpression();
      if (nameCondition != null && !nameCondition.getNamePattern().accepts(methodExpression.getReferenceName())) {
        return false;
      }

      for (JavaResolveResult result : methodExpression.multiResolve(false)) {
        if (methodPattern.accepts(result.getElement(), context)) {
          return true;
        }
      }
    }
    return false;
  }

  public Self constructorParameter(final int index, final String... fqns) {
    return with(new PatternCondition<T>("constructorParameter") {
      @Override
      public boolean accepts(@NotNull final T literal, final ProcessingContext context) {
        final PsiElement parent = literal.getParent();
        if (parent instanceof PsiExpressionList) {
          final PsiExpressionList psiExpressionList = (PsiExpressionList)parent;
          final PsiExpression[] psiExpressions = psiExpressionList.getExpressions();
          if (!(psiExpressions.length > index && psiExpressions[index] == literal)) return false;

          final PsiElement element = psiExpressionList.getParent();
          if (element instanceof PsiNewExpression) {
            PsiJavaCodeReferenceElement reference = ((PsiNewExpression)element).getClassOrAnonymousClassReference();
            if (reference != null) {
              String qualifiedName = reference.getQualifiedName();
              for (String fqn : fqns) {
                if( fqn.equals(qualifiedName)) return true;
              }
            }
          }
        }
        return false;
      }
    });
  }

  public static class Capture<T extends PsiElement> extends PsiJavaElementPattern<T, Capture<T>> {
    public Capture(final Class<T> aClass) {
      super(aClass);
    }

    public Capture(@NotNull final InitialPatternCondition<T> condition) {
      super(condition);
    }
  }



}