/*
 * Copyright 2000-2009 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.openapi.paths;

import com.intellij.codeInsight.daemon.EmptyResolveMessageProvider;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.LocalQuickFixProvider;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceOwner;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.PsiFileReference;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @author Dmitry Avdeev
 */
public class PsiDynaReference<T extends PsiElement> extends PsiReferenceBase<T>
  implements FileReferenceOwner, PsiPolyVariantReference, LocalQuickFixProvider, EmptyResolveMessageProvider {

  private final List<PsiReference> myReferences = new ArrayList<>();
  private int myChosenOne = -1;
  private ResolveResult[] myCachedResult;

  public PsiDynaReference(final T psiElement) {
    super(psiElement, true);
  }

  public void addReferences(Collection<PsiReference> references) {
    myReferences.addAll(references);
    for (PsiReference reference : references) {
      if (!reference.isSoft()) mySoft = false;
    }
  }

  public List<PsiReference> getReferences() {
    return myReferences;
  }

  public void addReference(PsiReference reference) {
    myReferences.add(reference);
    if (!reference.isSoft()) mySoft = false;
  }

  @Override
  public TextRange getRangeInElement() {

    PsiReference resolved = null;
    PsiReference reference = myReferences.get(0);

    if (reference.resolve() != null) {
      resolved = reference;
    }

    final TextRange range = reference.getRangeInElement();
    int start = range.getStartOffset();
    int end = range.getEndOffset();
    for (int i = 1; i < myReferences.size(); i++) {
      reference = myReferences.get(i);
      final TextRange textRange = getRange(reference);
      start = Math.min(start, textRange.getStartOffset());
      if (resolved == null) {
        end = Math.max(end, textRange.getEndOffset());
      }
    }
    return new TextRange(start, end);
  }

  private TextRange getRange(PsiReference reference) {
    TextRange rangeInElement = reference.getRangeInElement();
    PsiElement element = reference.getElement();
    while(element != myElement) {
      rangeInElement = rangeInElement.shiftRight(element.getStartOffsetInParent());
      element = element.getParent();
      if (element instanceof PsiFile) break;
    }
    return rangeInElement;
  }

  @Override
  public PsiElement resolve(){
    final ResolveResult[] resolveResults = multiResolve(false);
    return resolveResults.length == 1 ? resolveResults[0].getElement() : null;
  }

  @Override
  @NotNull
  public String getCanonicalText(){
    final PsiReference reference = chooseReference();
    return reference == null ? myReferences.get(0).getCanonicalText() : reference.getCanonicalText();
  }

  @Override
  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException{
    final PsiReference reference = chooseReference();
    if (reference != null) {
      return reference.handleElementRename(newElementName);
    }
    return myElement;
  }

  @Override
  public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
    for (PsiReference reference : myReferences) {
      if (reference instanceof FileReference) {
        return reference.bindToElement(element);
      }
    }
    return myElement;
  }

  @Override
  public boolean isReferenceTo(PsiElement element){
    for (PsiReference reference : myReferences) {
      if (reference.isReferenceTo(element)) return true;
    }
    return false;
  }


  @Override
  @NotNull
  public Object[] getVariants() {
    return ArrayUtil.EMPTY_OBJECT_ARRAY;
  }

  @Override
  @NotNull
  public ResolveResult[] multiResolve(final boolean incompleteCode) {
    if (myCachedResult == null) {
      myCachedResult = innerResolve(incompleteCode);
    }
    return myCachedResult;
  }

  protected ResolveResult[] innerResolve(final boolean incompleteCode) {
    List<ResolveResult> result = new ArrayList<>();
    for (PsiReference reference : myReferences) {
      if (reference instanceof PsiPolyVariantReference) {
        for (ResolveResult rr: ((PsiPolyVariantReference)reference).multiResolve(incompleteCode)) {
          if (rr.isValidResult()) {
            result.add(rr);
          }
        }
      }
      else {
        final PsiElement resolved = reference.resolve();
        if (resolved != null) {
          result.add(new PsiElementResolveResult(resolved));
        }
      }
    }

    return result.toArray(new ResolveResult[result.size()]);
  }

  @Nullable
  private PsiReference chooseReference(){
    if(myChosenOne != -1){
      return myReferences.get(myChosenOne);
    }
    boolean flag = false;
    for(int i = 0; i < myReferences.size(); i++){
      final PsiReference reference = myReferences.get(i);
      if(reference.isSoft() && flag) continue;
      if(!reference.isSoft() && !flag){
        myChosenOne = i;
        flag = true;
        continue;
      }
      if(reference.resolve() != null){
        myChosenOne = i;
      }
    }
    return myChosenOne >= 0 ? myReferences.get(myChosenOne) : null;
  }

  @NotNull
  @Override
  @SuppressWarnings({"UnresolvedPropertyKey"})
  public String getUnresolvedMessagePattern() {
    final PsiReference reference = chooseReference();

    return reference instanceof EmptyResolveMessageProvider ?
           ((EmptyResolveMessageProvider)reference).getUnresolvedMessagePattern() :
            PsiBundle.message("cannot.resolve.symbol");
  }

  @Override
  public LocalQuickFix[] getQuickFixes() {
    final ArrayList<LocalQuickFix> list = new ArrayList<>();
    for (Object ref: myReferences) {
      if (ref instanceof LocalQuickFixProvider) {
        ContainerUtil.addAll(list, ((LocalQuickFixProvider)ref).getQuickFixes());
      }
    }
    return list.toArray(new LocalQuickFix[list.size()]);
  }

  public String toString() {
    //noinspection HardCodedStringLiteral
    return "PsiDynaReference containing " + myReferences.toString();
  }

  @Override
  public PsiFileReference getLastFileReference() {
    for (PsiReference reference : myReferences) {
      if (reference instanceof FileReferenceOwner) {
        return ((FileReferenceOwner)reference).getLastFileReference();
      }
    }
    return null;
  }
}
