/*
 * Copyright 2000-2016 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.impl.source;

import com.intellij.lang.LighterAST;
import com.intellij.lang.LighterASTNode;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.impl.source.tree.ElementType;
import com.intellij.psi.impl.source.tree.LightTreeUtil;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.List;

import static com.intellij.psi.impl.source.tree.JavaElementType.*;

/**
 * Resolves unambiguous Java identifiers inside a file (if it can), using {@link LighterAST}. Can be used during indexing.
 */
public class FileLocalResolver {
  private final LighterAST myTree;

  public FileLocalResolver(@NotNull LighterAST tree) {
    myTree = tree;
  }

  @NotNull
  public LighterAST getLightTree() {
    return myTree;
  }

  /**
   * @param ref reference node
   * @return a resolve result corresponding to a local variable, parameter or field that the given reference resolves to.
   */
  @NotNull
  public LightResolveResult resolveLocally(@NotNull LighterASTNode ref) {
    final String refName = JavaLightTreeUtil.getNameIdentifierText(myTree, ref);
    if (refName == null) return LightResolveResult.UNKNOWN;
    if (!canResolveToLocalVariable(myTree, ref)) return LightResolveResult.NON_LOCAL;

    boolean passedClass = false;
    LighterASTNode lastParent = ref;
    while (true) {
      ProgressManager.checkCanceled();
      LighterASTNode scope = myTree.getParent(lastParent);
      if (scope == null) return LightResolveResult.NON_LOCAL;

      for (LighterASTNode var : getDeclarations(scope, lastParent)) {
        if (refName.equals(JavaLightTreeUtil.getNameIdentifierText(myTree, var))) {
          if (passedClass) return var.getTokenType() == FIELD ? LightResolveResult.NON_LOCAL : LightResolveResult.UNKNOWN;
          return LightResolveResult.resolved(var);
        }
      }

      lastParent = scope;
      passedClass |= scope.getTokenType() == CLASS || scope.getTokenType() == ANONYMOUS_CLASS;
    }
  }

  private boolean canResolveToLocalVariable(@NotNull LighterAST tree, @NotNull LighterASTNode ref) {
    LighterASTNode parent = tree.getParent(ref);
    return parent != null && parent.getTokenType() != METHOD_CALL_EXPRESSION && !hasQualifier(ref);
  }

  private boolean hasQualifier(@NotNull LighterASTNode ref) {
    return LightTreeUtil.firstChildOfType(myTree, ref, ElementType.EXPRESSION_BIT_SET) != null;
  }

  @NotNull
  private Iterable<LighterASTNode> getDeclarations(LighterASTNode scope, @Nullable LighterASTNode lastParent) {
    IElementType type = scope.getTokenType();
    if (type == CODE_BLOCK) {
      return walkChildrenScopes(before(LightTreeUtil.getChildrenOfType(myTree, scope, DECLARATION_STATEMENT), lastParent));
    }
    if (type == DECLARATION_STATEMENT) {
      return before(LightTreeUtil.getChildrenOfType(myTree, scope, LOCAL_VARIABLE), lastParent);
    }
    if (type == FOR_STATEMENT) {
      return walkChildrenScopes(before(LightTreeUtil.getChildrenOfType(myTree, scope, ElementType.JAVA_STATEMENT_BIT_SET), lastParent));
    }
    if (type == FOREACH_STATEMENT || type == CATCH_SECTION) {
      return JBIterable.of(LightTreeUtil.firstChildOfType(myTree, scope, PARAMETER));
    }
    if (type == TRY_STATEMENT) {
      return walkChildrenScopes(before(LightTreeUtil.getChildrenOfType(myTree, scope, RESOURCE_LIST), lastParent));
    }
    if (type == RESOURCE_LIST) {
      return before(LightTreeUtil.getChildrenOfType(myTree, scope, RESOURCE_VARIABLE), lastParent);
    }
    if (type == CLASS) {
      return LightTreeUtil.getChildrenOfType(myTree, scope, FIELD);
    }
    if (type == LAMBDA_EXPRESSION || type == METHOD) {
      LighterASTNode paramList = LightTreeUtil.firstChildOfType(myTree, scope, PARAMETER_LIST);
      return paramList == null ? ContainerUtil.emptyList() : LightTreeUtil.getChildrenOfType(myTree, paramList, PARAMETER);
    }
    return Collections.emptyList();
  }

  @NotNull
  private JBIterable<LighterASTNode> walkChildrenScopes(JBIterable<LighterASTNode> children) {
    return children.flatMap(child -> getDeclarations(child, null));
  }

  @NotNull
  private static JBIterable<LighterASTNode> before(List<LighterASTNode> children, @Nullable final LighterASTNode lastParent) {
    return JBIterable.from(children).filter(node -> lastParent == null || node.getStartOffset() < lastParent.getStartOffset());
  }

  /**
   * Determine the type name of the given variable. Can be used to later iterate over all classes with this name for lightweight checks
   * before loading AST and fully resolving the type.
   * @param var Variable node
   * @return the short name of the class corresponding to the type of the variable, or null if the variable is not of class type or
   * the type is generic
   */
  @Nullable
  public String getShortClassTypeName(@NotNull LighterASTNode var) {
    LighterASTNode typeRef = LightTreeUtil.firstChildOfType(myTree, LightTreeUtil.firstChildOfType(myTree, var, TYPE), JAVA_CODE_REFERENCE);
    String refName = JavaLightTreeUtil.getNameIdentifierText(myTree, typeRef);
    if (refName == null) return null;

    if (LightTreeUtil.firstChildOfType(myTree, typeRef, JAVA_CODE_REFERENCE) != null) return refName;
    if (isTypeParameter(refName, var)) return null;
    return refName;
  }

  private boolean isTypeParameter(String name, LighterASTNode place) {
    LighterASTNode scope = place;
    while (scope != null) {
      if (scope.getTokenType() == CLASS || scope.getTokenType() == METHOD) {
        if (hasOwnTypeParameter(name, scope)) {
          return true;
        }
        if (isStatic(scope)) {
          break;
        }
      }
      scope = myTree.getParent(scope);
    }
    return false;
  }

  private boolean hasOwnTypeParameter(String name, LighterASTNode member) {
    LighterASTNode typeParams = LightTreeUtil.firstChildOfType(myTree, member, TYPE_PARAMETER_LIST);
    if (typeParams != null) {
      for (LighterASTNode param : LightTreeUtil.getChildrenOfType(myTree, typeParams, TYPE_PARAMETER)) {
        if (name.equals(JavaLightTreeUtil.getNameIdentifierText(myTree, param))) {
          return true;
        }
      }
    }
    return false;
  }

  private boolean isStatic(LighterASTNode scope) {
    LighterASTNode modList = LightTreeUtil.firstChildOfType(myTree, scope, MODIFIER_LIST);
    return modList != null && LightTreeUtil.firstChildOfType(myTree, modList, JavaTokenType.STATIC_KEYWORD) != null;
  }

  public static class LightResolveResult {
    /** We know nothing about the resolve result */
    public static final LightResolveResult UNKNOWN = new LightResolveResult();

    /** The result can't be determined, but it's definitely not a local variable/parameter */
    public static final LightResolveResult NON_LOCAL = new LightResolveResult();

    @NotNull
    static LightResolveResult resolved(@NotNull final LighterASTNode target) {
      return new LightResolveResult() {
        @Nullable
        @Override
        public LighterASTNode getTarget() {
          return target;
        }
      };
    }

    @Nullable
    public LighterASTNode getTarget() {
      return null;
    }
  }
}