/*
 * Copyright 2000-2013 JetBrains s.r.o.
 * Copyright 2014-2014 AS3Boyan
 * Copyright 2014-2014 Elias Ku
 *
 * 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.plugins.haxe.lang.psi.impl;

import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.LogLevel;
import com.intellij.openapi.util.Pair;
import com.intellij.plugins.haxe.lang.lexer.HaxeTokenTypes;
import com.intellij.plugins.haxe.lang.psi.*;
import com.intellij.plugins.haxe.util.UsefulPsiTreeUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.PsiSuperMethodImplUtil;
import com.intellij.psi.impl.source.tree.java.PsiTypeParameterImpl;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import lombok.CustomLog;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

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

/**
 * Created by ebishton on 10/22/14.
 */
@CustomLog
public class HaxeTypeListPartPsiMixinImpl extends HaxePsiCompositeElementImpl implements HaxeTypeListPartPsiMixin {

  // XXX: TypeListPart might be better just being a private entry in the BNF.  I'm not sure.
  //
  // The thing I most dislike about subclassing PsiTypeParameter is that it implements
  // the PsiClass interface.  One of the (three possible)child elements that this class
  // holds is HaxeAnonymousType, which itself derives from HaxeClass (thus, PsiClass).
  // So, I'd prefer that HaxeTypePsiMixin implement the class interface.  Then we
  // would have better balance between the child implementations (and less special code)
  // and we can cleanly ignore this class in the hierarchy.
  //
  // The third child type that we can hold is HaxeFunctionType, which is simply a
  // construct for holding (effectively) a typedef, described via the arrow notation:
  //   type -> type -> return_type
  // This type doesn't really support a class, just gives a function pointer.  So, now
  // we've got a mostly empty stub class as well...

  static { log.setLevel(LogLevel.DEBUG); }


  // The child type that implements an interface for one of these three parameter types.
  // We can't assign this type in the constructor because the children aren't known
  // (to us) at construction time.  We'll check it later when one of the interfaces
  // are hit.
  PsiClass myChildClass = null;

  HaxeTypeListPartPsiMixinImpl(ASTNode node) {
    super(node);
  }

  @NotNull
  private PsiClass getDelegate() {
    // We're going to try to cache our child.  If the code changes, then this reference
    // may refer to the wrong class.  In that case, it would be  better to do the
    // lookup (resolveHaxeClass()), which has a caching algorithm of its own and
    // deals with naming changes accordingly.

    if (null == myChildClass) {
      PsiElement child = getFirstChild();
      PsiElement target = null;
      ASTNode node = child.getNode();
      if ( null != node ) {

        IElementType type = node.getElementType();
        if (HaxeTokenTypes.TYPE_OR_ANONYMOUS.equals(type)) {

          target = child.getFirstChild();
          IElementType targetType = target.getNode().getElementType();

          if (HaxeTokenTypes.TYPE.equals(targetType)) {
            myChildClass = new HaxeClassDelegateForTypeChild((HaxeType)target);
          } else if (HaxeTokenTypes.ANONYMOUS_TYPE.equals(targetType)){
            myChildClass = (HaxeAnonymousType) target;
          } else {
            log.debug("Target: " + target.toString());
            log.assertTrue(false, "Unexpected token type for child of TYPE_OR_ANONYMOUS");
            myChildClass = AbstractHaxePsiClass.createEmptyFacade(getProject());
          }
        } else if (HaxeTokenTypes.FUNCTION_TYPE.equals(type)) {
          // FIXME: Temporary Hack to get around an unexpected PSI state #627.
          try {
            myChildClass = (HaxeAnonymousType) child;
          }
          catch(ClassCastException e) {
            log.warn("Unexpected PSI state.  See issue #627");
            myChildClass = AbstractHaxePsiClass.createEmptyFacade(getProject());
          }
        } else {
          myChildClass = AbstractHaxePsiClass.createEmptyFacade(getProject());
        }
      } else {
        // No child node?  How can this be?
        log.assertTrue(false, "No child node found for TYPE_LIST_PART");
        myChildClass = AbstractHaxePsiClass.createEmptyFacade(getProject());
      }
    }
    return myChildClass;

  }


  //
  // PsiTypeParameter overrides
  //

  @Override
  public PsiTypeParameterListOwner getOwner() {
    final PsiElement parent = getParent();
    if (parent == null) throw new PsiInvalidElementAccessException(this);
    return PsiTreeUtil.getParentOfType(this, PsiTypeParameterListOwner.class);
  }

  @Override
  public int getIndex() {
    int ret = 0;
    PsiElement element = getPrevSibling();
    while (element != null) {
      if (element instanceof PsiTypeParameter) {
        ret++;
      }
      element = element.getPrevSibling();
    }
    return ret;
  }


  //
  // PsiClass overrides
  //

  @Nullable
  @Override
  public String getQualifiedName() {
    return getDelegate().getQualifiedName();
  }

  @Override
  public boolean isInterface() {
    return getDelegate().isInterface();
  }

  @Override
  public boolean isAnnotationType() {
    return getDelegate().isAnnotationType();
  }

  @Override
  public boolean isEnum() {
    return getDelegate().isEnum();
  }

  @Override
  @NotNull
  public PsiField[] getFields() {
    return getDelegate().getFields();
  }

  @Override
  @NotNull
  public PsiMethod[] getMethods() {
    return getDelegate().getMethods();
  }

  @Override
  public PsiMethod findMethodBySignature(PsiMethod patternMethod, boolean checkBases) {
    return getDelegate().findMethodBySignature(patternMethod, checkBases);
  }

  @Override
  @NotNull
  public PsiMethod[] findMethodsBySignature(PsiMethod patternMethod, boolean checkBases) {
    return getDelegate().findMethodsBySignature(patternMethod, checkBases);
  }

  @Override
  public PsiField findFieldByName(String name, boolean checkBases) {
    return getDelegate().findFieldByName(name, checkBases);
  }

  @Override
  @NotNull
  public PsiMethod[] findMethodsByName(String name, boolean checkBases) {
    return getDelegate().findMethodsByName(name, checkBases);
  }

  @Override
  @NotNull
  public List<Pair<PsiMethod, PsiSubstitutor>> findMethodsAndTheirSubstitutorsByName(String name, boolean checkBases) {
    return getDelegate().findMethodsAndTheirSubstitutorsByName(name, checkBases);
  }

  @Override
  @NotNull
  public List<Pair<PsiMethod, PsiSubstitutor>> getAllMethodsAndTheirSubstitutors() {
    return getDelegate().getAllMethodsAndTheirSubstitutors();
  }

  @Override
  public PsiClass findInnerClassByName(String name, boolean checkBases) {
    return getDelegate().findInnerClassByName(name, checkBases);
  }

  @Override
  public PsiTypeParameterList getTypeParameterList() {
    return getDelegate().getTypeParameterList();
  }

  @Override
  public boolean hasTypeParameters() {
    return getDelegate().hasTypeParameters();
  }

  @Override
  public PsiElement getScope() {
    return getDelegate().getScope();
  }

  @Override
  public boolean isInheritorDeep(PsiClass baseClass, PsiClass classToByPass) {
    return getDelegate().isInheritorDeep(baseClass, classToByPass);
  }

  @Override
  public boolean isInheritor(@NotNull PsiClass baseClass, boolean checkDeep) {
    return getDelegate().isInheritor(baseClass, checkDeep);
  }

  @Override
  @Nullable
  public PsiIdentifier getNameIdentifier() {
    return getDelegate().getNameIdentifier();
  }

  @Override
  public PsiElement setName(@NotNull String name) throws IncorrectOperationException {
    return getDelegate().setName(name);
  }

  @Override
  @NotNull
  public PsiMethod[] getConstructors() {
    return getDelegate().getConstructors();
  }

  @Override
  public PsiDocComment getDocComment() {
    return getDelegate().getDocComment();
  }

  @Override
  public boolean isDeprecated() {
    return getDelegate().isDeprecated();
  }

  @Override
  @NotNull
  public PsiReferenceList getExtendsList() {
    return getDelegate().getExtendsList();
  }

  @Override
  public PsiReferenceList getImplementsList() {
    return getDelegate().getImplementsList();
  }

  @Override
  @NotNull
  public PsiClassType[] getExtendsListTypes() {
    return getDelegate().getExtendsListTypes();
  }

  @Override
  @NotNull
  public PsiClassType[] getImplementsListTypes() {
    return getDelegate().getImplementsListTypes();
  }

  @Override
  @NotNull
  public PsiClass[] getInnerClasses() {
    return getDelegate().getInnerClasses();
  }

  @Override
  @NotNull
  public PsiField[] getAllFields() {
    return getDelegate().getAllFields();
  }

  @Override
  @NotNull
  public PsiMethod[] getAllMethods() {
    return getDelegate().getAllMethods();
  }

  @Override
  @NotNull
  public PsiClass[] getAllInnerClasses() {
    return getDelegate().getAllInnerClasses();
  }

  @Override
  @NotNull
  public PsiClassInitializer[] getInitializers() {
    return getDelegate().getInitializers();
  }

  @Override
  @NotNull
  public PsiTypeParameter[] getTypeParameters() {
    return getDelegate().getTypeParameters();
  }

  @Override
  public PsiClass getSuperClass() {
    return getDelegate().getSuperClass();
  }

  @Override
  public PsiClass[] getInterfaces() {
    return getDelegate().getInterfaces();
  }

  @Override
  @NotNull
  public PsiClass[] getSupers() {
    return getDelegate().getSupers();
  }

  @Override
  @NotNull
  public PsiClassType[] getSuperTypes() {
    return getDelegate().getSuperTypes();
  }

  @Override
  public PsiClass getContainingClass() {
    return getDelegate().getContainingClass();
  }

  @Override
  @NotNull
  public Collection<HierarchicalMethodSignature> getVisibleSignatures() {
    return getDelegate().getVisibleSignatures();
  }

  @Override
  public HaxeModifierList getModifierList() {
    return (HaxeModifierList) getDelegate().getModifierList();
  }

  @Override
  public boolean hasModifierProperty(@NotNull String name) {
    return getDelegate().hasModifierProperty(name);
  }

  @Override
  public PsiJavaToken getLBrace() {
    // Casting this is only doable because HaxeAstFactory
    // creates PsiJavaTokens.
    return (PsiJavaToken)getDelegate().getLBrace();
  }

  @Override
  public PsiJavaToken getRBrace() {
    return (PsiJavaToken) getDelegate().getRBrace();
  }

  //
  // PsiAnnotationOwner
  //

  @Override
  @NotNull
  public PsiAnnotation[] getAnnotations() {
    // Type parameters don't get modifiers.
    return PsiAnnotation.EMPTY_ARRAY;
  }

  @Override
  public PsiAnnotation findAnnotation(@NotNull @NonNls String qualifiedName) {
    // Type parameters don't get modifiers.
    return null;
  }

  @Override
  @NotNull
  public PsiAnnotation addAnnotation(@NotNull @NonNls String qualifiedName) {
    throw new IncorrectOperationException();
  }

  @Override
  @NotNull
  public PsiAnnotation[] getApplicableAnnotations() {
    return getAnnotations();
  }



  // //////////////////////////////////////////////////////////////////////////////////
  // //////////////////////////////////////////////////////////////////////////////////
  //
  // HaxeTypeListPartForTypeChild
  //
  // //////////////////////////////////////////////////////////////////////////////////
  // //////////////////////////////////////////////////////////////////////////////////

  /**
   * Deals with type references that appear as children to HaxeTypeOrAnonymous
   * child elements.
   */
  private static class HaxeClassDelegateForTypeChild extends PsiTypeParameterImpl {

    private HaxeType myChildType = null;

    public HaxeClassDelegateForTypeChild(@NotNull HaxeType childType) {
      super(childType.getNode());
      myChildType = childType;
    }

    @NotNull
    protected HaxeReference getDelegate() {
      return myChildType.getReferenceExpression();
    }

    @Nullable
    protected HaxeClass getResolvedDelegate() {
      PsiElement elem = getDelegate().resolve();
      return elem instanceof HaxeClass? (HaxeClass) elem : null;
    }

    //
    // PsiTypeParameter overrides
    //

    // PsiTypeParameter extends PsiClass instead of a reference.  It just stubs
    // out most of the functionality.

    @Override
    @Nullable
    public PsiMethod findMethodBySignature(PsiMethod patternMethod, boolean checkBases) {
      // XXX: If it turns out that we need this method, we need to find the
      //      actual class implementation (getDelegate().resolveHaxeClass())
      //      and call that.
      log.warn("Unexpected call to findMethodBySignature()");
      return null;
    }

    @Override
    @NotNull
    public PsiMethod[] findMethodsBySignature(PsiMethod patternMethod, boolean checkBases) {
      log.warn("Unexpected call to findMethodsBySignature()");
      return PsiMethod.EMPTY_ARRAY;
    }

    @Override
    public PsiField findFieldByName(String name, boolean checkBases) {
      log.warn("Unexpected call to findFieldByName()");
      return null;
    }

    @Override
    @NotNull
    public PsiMethod[] findMethodsByName(String name, boolean checkBases) {
      log.warn("Unexpected call to findMethodsByName()");
      return PsiMethod.EMPTY_ARRAY;
    }

    @Override
    @NotNull
    public List<Pair<PsiMethod, PsiSubstitutor>> findMethodsAndTheirSubstitutorsByName(String name, boolean checkBases) {
      log.warn("Unexpected call to findMethodsAndTheirSubstitutorsByName()");
      return new ArrayList<Pair<PsiMethod,PsiSubstitutor>>();
    }

    @Override
    @NotNull
    public List<Pair<PsiMethod, PsiSubstitutor>> getAllMethodsAndTheirSubstitutors() {
      log.warn("Unexpected call to getAllMethodsAndTheirSubstitutors");
      return new ArrayList<Pair<PsiMethod,PsiSubstitutor>>();
    }

    @Override
    public PsiClass findInnerClassByName(String name, boolean checkBases) {
      return null; // No named inner classes in Haxe.
    }

    @Override
    public PsiElement getScope() {
      /**
       * Returns the PSI member in which the class has been declared (for example,
       * the method containing the anonymous inner class, or the file containing a regular
       * class, or the class owning a type parameter).
       */
      // As a TypeListPart, we are only children of extends, implements, and generic parameters.
      // In each of these cases, the class for which we are being declared (which may be
      // an anonymous type) is the scope.
      PsiElement parent = getParent();
      while (parent != null) {
        IElementType parentType = parent.getNode().getElementType();
        if (HaxeTokenTypes.ANONYMOUS_TYPE.equals(parentType)
        || (HaxeTokenTypes.CLASS_DECLARATION.equals(parentType))
        || parent instanceof HaxeFile )  // HaxeFile is a catchall..
        {
          break;
        }
        parent = parent.getParent();
      }

      return parent;
    }

    @NotNull
    @Override
    public PsiIdentifier getNameIdentifier() {
      // For a HaxeType, the identifier is two children below.  The first is
      // a reference.
      HaxeReferenceExpression ref = PsiTreeUtil.getRequiredChildOfType(this, HaxeReferenceExpression.class);
      if (ref.getFirstChild() instanceof HaxeReferenceExpression) {
        ref = UsefulPsiTreeUtil.getLastChild(ref, HaxeReferenceExpression.class, ref);
      }
      HaxeIdentifier id = PsiTreeUtil.getRequiredChildOfType(ref, HaxeIdentifier.class);
      return id;
    }

    @Override
    public boolean isInheritorDeep(PsiClass baseClass, PsiClass classToByPass) {
      HaxeClass resolved = getResolvedDelegate();
      return null != resolved && resolved.isInheritorDeep(baseClass, classToByPass);
    }

    @Override
    public boolean isInheritor(@NotNull PsiClass baseClass, boolean checkDeep) {
      HaxeClass resolved = getResolvedDelegate();
      return null != resolved && resolved.isInheritor(baseClass, checkDeep);
    }

    @Override
    @NotNull
    public PsiReferenceList getExtendsList() {
      // Haxe BNF doesn't allow for extends in this position.
      return new HaxeExtendsDeclarationImpl(new HaxeDummyASTNode("Empty Extends List", getProject()));
    }

    @Override
    public PsiReferenceList getImplementsList() {
      // Haxe BNF doesn't allow for implements in this position.
      return new HaxeImplementsDeclarationImpl(new HaxeDummyASTNode("Empty Implements List", getProject()));
    }

    @Override
    @NotNull
    public PsiClassType[] getExtendsListTypes() {
      return PsiClassType.EMPTY_ARRAY;
    }

    @Override
    @NotNull
    public PsiClassType[] getImplementsListTypes() {
      return PsiClassType.EMPTY_ARRAY;
    }

    @Override
    public PsiClass getSuperClass() {
      HaxeClass resolved = getResolvedDelegate();
      return null == resolved ? null : resolved.getSuperClass();
    }

    @Override
    public PsiClass[] getInterfaces() {
      HaxeClass resolved = getResolvedDelegate();
      return null == resolved ? PsiClass.EMPTY_ARRAY : resolved.getInterfaces();
    }

    @Override
    @NotNull
    public PsiClass[] getSupers() {
      HaxeClass resolved = getResolvedDelegate();
      return null == resolved ? PsiClass.EMPTY_ARRAY : resolved.getSupers();
    }

    @Override
    @NotNull
    public PsiClassType[] getSuperTypes() {
      HaxeClass resolved = getResolvedDelegate();
      return null == resolved ? PsiClassType.EMPTY_ARRAY : resolved.getSuperTypes();
    }

    @Override
    @NotNull
    public Collection<HierarchicalMethodSignature> getVisibleSignatures() {
      HaxeClass resolved = getResolvedDelegate();
      return PsiSuperMethodImplUtil.getVisibleSignatures(resolved);
    }

  }

}
