/*
 * 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.stubsHierarchy.impl;

import com.intellij.psi.impl.java.stubs.hierarchy.IndexTree;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class StubResolver {
  private static final Import[] DEFAULT_JAVA_IMPORTS = {importPackage("java.lang")};
  private static final Import[] DEFAULT_GROOVY_IMPORTS = {
    importPackage("java.lang"),
    importPackage("java.util"),
    importPackage("java.io"),
    importPackage("java.net"),
    importPackage("groovy.lang"),
    importPackage("groovy.util"),
    new Import(NameEnvironment.fromString("java.math"), NameEnvironment.hashIdentifier("BigInteger"), false),
    new Import(NameEnvironment.fromString("java.math"), NameEnvironment.hashIdentifier("BigDecimal"), false),
  };
  private final Symbols mySymbols;
  private final StubHierarchyConnector myConnector;

  public StubResolver(Symbols symbols, StubHierarchyConnector connector) {
    this.mySymbols = symbols;
    myConnector = connector;
  }

  private static Import importPackage(String qname) {
    return new Import(NameEnvironment.fromString(qname), 0, false);
  }

  private static Import[] getDefaultImports(byte type) {
    if (type == IndexTree.JAVA) return DEFAULT_JAVA_IMPORTS;
    if (type == IndexTree.GROOVY) return DEFAULT_GROOVY_IMPORTS;
    return Imports.EMPTY_ARRAY;
  }

  // resolve class `sym` extends/implements `baseId`
  Set<Symbol> resolveBase(Symbol.ClassSymbol sym, @ShortName int[] qname) throws IncompleteHierarchyException {
    Set<Symbol> result = resolveUnqualified(sym, qname[0], qname.length > 1);
    for (int i = 1; i < qname.length; i++) {
      result = processQualifier(result, qname[i], i != qname.length - 1);
    }
    if (result.isEmpty()) {
      throw IncompleteHierarchyException.INSTANCE;
    }
    return result;
  }

  Set<Symbol> resolveUnqualified(Symbol.ClassSymbol sym, @ShortName final int shortName, boolean processPackages) throws IncompleteHierarchyException {
    Set<Symbol> symbols = findIdent(sym.myOwner, sym.myUnitInfo, shortName, processPackages);
    if (symbols.isEmpty()) {
      throw IncompleteHierarchyException.INSTANCE;
    }
    return symbols;
  }

  private Set<Symbol> processQualifier(Set<Symbol> contextResults, @ShortName final int shortName, final boolean processPackages) throws IncompleteHierarchyException {
    Set<Symbol> result = new HashSet<>();
    for (Symbol symbol : contextResults) {
      selectSym(symbol, shortName, processPackages, result);
    }
    return result;
  }

  @NotNull
  private Set<Symbol> findIdent(Symbol startScope, UnitInfo info, @ShortName int name, boolean processPackages) throws IncompleteHierarchyException {
    Set<Symbol> result = new HashSet<>();
    findType(startScope, name, result);
    findGlobalType(info, name, result);

    if (processPackages) {
      ContainerUtil.addIfNotNull(result, mySymbols.getPackage(name));
    }
    return result;
  }

  private void findType(Symbol startScope, int name, Set<Symbol> symbols) throws IncompleteHierarchyException {
    // looking up
    for (Symbol s = startScope; s != null; s = s.myOwner)
      findMemberType(s, name, symbols);
    // type from current package
    findIdentInPackage(startScope.pkg(), name, false, symbols);
  }

  // resolving `receiver.name`
  private void selectSym(Symbol receiver, @ShortName int name, boolean processPackages, Set<Symbol> symbols) throws IncompleteHierarchyException {
    if (receiver.isPackage())
      findIdentInPackage((Symbol.PackageSymbol)receiver, name, processPackages, symbols);
    else
      findMemberType(receiver, name, symbols);
  }

  private void findIdentInPackage(Symbol.PackageSymbol pck, @ShortName int name, boolean processPackages, Set<Symbol> symbols) {
    @QNameHash int fullname = NameEnvironment.qualifiedName(pck.myQualifiedName, name);
    if (processPackages) {
      ContainerUtil.addIfNotNull(symbols, mySymbols.getPackage(fullname));
    }
    Collections.addAll(symbols, findGlobalType(fullname));
  }

  private void findMemberType(Symbol s, @ShortName int name, Set<Symbol> symbols) throws IncompleteHierarchyException {
    if (s.isClass()) {
      processInheritedMembers((Symbol.ClassSymbol)s, name, symbols, null);
    } else {
      processMembers(s.getMembers(), name, symbols);
    }
  }

  private void processInheritedMembers(Symbol.ClassSymbol s,
                                       @ShortName int name,
                                       Set<Symbol> symbols,
                                       @Nullable Set<Symbol> processed) throws IncompleteHierarchyException {
    processMembers(s.getMembers(), name, symbols);

    @CompactArray(Symbol.ClassSymbol.class) Object supers = s.getSuperClasses(myConnector);
    if (supers == null) return;

    if (processed == null) processed = new HashSet<>();
    if (!processed.add(s)) return;

    if (supers instanceof Symbol.ClassSymbol) {
      processInheritedMembers((Symbol.ClassSymbol)supers, name, symbols, processed);
    } else if (supers instanceof Symbol.ClassSymbol[]) {
      for (Symbol.ClassSymbol st : (Symbol.ClassSymbol[])supers) {
        processInheritedMembers(st, name, symbols, processed);
      }
    }
  }

  public Symbol.ClassSymbol[] findGlobalType(@QNameHash int nameId) {
    return mySymbols.getClassSymbols(nameId);
  }

  private void findGlobalType(UnitInfo info, @ShortName int name, Set<Symbol> symbols) throws IncompleteHierarchyException {
    for (Import anImport : getDefaultImports(info.type))
      handleImport(anImport, name, symbols);
    for (Import anImport : info.imports)
      handleImport(anImport, name, symbols);
  }

  public void handleImport(Import anImport, @ShortName int name, Set<Symbol> symbols) throws IncompleteHierarchyException {
    if (anImport.isOnDemand()) {
      if (anImport.isStatic) {
        for (Symbol.ClassSymbol p : findGlobalType(anImport.qualifier))
          importNamedStatic(p, name, symbols);
      }
      else {
        importAll(anImport.qualifier, name, symbols);
      }
    }
    else {
      @ShortName int importedName = anImport.getAlias() != 0 ? anImport.getAlias() : anImport.importedName;
      if (name != importedName) return;

      if (anImport.isStatic) {
          for (Symbol.ClassSymbol s : findGlobalType(anImport.qualifier))
            importNamedStatic(s, anImport.importedName, symbols);
      }
      else {
        Collections.addAll(symbols, findGlobalType(NameEnvironment.qualifiedName(anImport.qualifier, anImport.importedName)));
      }
    }
  }

  // handling of `import prefix.*`
  private void importAll(@QNameHash int prefix, @ShortName int suffix, final Set<Symbol> symbols) {
    Collections.addAll(symbols, findGlobalType(NameEnvironment.qualifiedName(prefix, suffix)));
  }

  // handling of import static `tsym.name` as
  private void importNamedStatic(final Symbol.ClassSymbol tsym, @ShortName final int name, final Set<Symbol> symbols) throws IncompleteHierarchyException {
    processInheritedMembers(tsym, name, symbols, null);
  }

  private static void processMembers(Symbol.ClassSymbol[] members, @ShortName int name, Set<Symbol> symbols) {
    int index = getIndex(name, members);
    if (index < 0) return;

    // elem
    symbols.add(members[index]);
    // on the left
    int i = index - 1;
    while (i >= 0 && members[i].myShortName == name) {
      symbols.add(members[i]);
      i--;
    }
    // on the right
    i = index + 1;
    while (i < members.length && members[i].myShortName == name) {
      symbols.add(members[i]);
      i++;
    }
  }

  private static int getIndex(int key, Symbol.ClassSymbol[] a) {
    return ObjectUtils.binarySearch(0, a.length, mid-> Integer.compare(a[mid].myShortName, key));
  }
}
