/*
 * Copyright 2003-2022 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 jetbrains.mps.project.dependency;

import jetbrains.mps.logging.Logger;
import jetbrains.mps.smodel.LanguageModuleScanner;
import jetbrains.mps.smodel.language.LanguageRegistry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.module.SDependency;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;

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

/**
 * This class helps extracting all dependencies of a given type for a given set of modules.
 * E.g. we can give it a set of modules and ask, which modules are needed to compile the given set:
 * new GlobalModuleDependenciesManager(startSet).getModules(Deptype.COMPILE)
 * Note that if we have M modules and N dependencies, and want to know something about a set of S modules,
 * this will work O(M+N) in the worst case, regardless of S
 */
public class GlobalModuleDependenciesManager {
  final static Logger LOG = Logger.getLogger(GlobalModuleDependenciesManager.class);

  private final Set<SModule> myModules;
  private final UsedModulesCollector myUsedModulesCollector;

  public GlobalModuleDependenciesManager(Collection<? extends SModule> modules, @NotNull ErrorHandler handler) {
    myModules = new HashSet<>(modules);
    if (modules.isEmpty()) {
      // XXX with no modules, any operation over the class would be no-op
      myUsedModulesCollector = null;
    } else {
      final SRepository repo = modules.stream().findAny().orElseThrow().getRepository();
      if (repo == null) {
        myUsedModulesCollector = new UsedModulesCollector(null, handler);
      }else {
        myUsedModulesCollector = new UsedModulesCollector(new LanguageModuleScanner(LanguageRegistry.getInstance(repo), repo), handler);
      }
    }
  }

  public GlobalModuleDependenciesManager(Collection<? extends SModule> modules) {
    this(modules, DEFAULT_HANDLER);
  }

  public GlobalModuleDependenciesManager(@NotNull SModule module, ErrorHandler handler) {
    this(Collections.singletonList(module), handler);
  }

  public GlobalModuleDependenciesManager(@NotNull SModule module) {
    this(module, DEFAULT_HANDLER);
  }

  /**
   * Return all modules of a given dependency type in scope of given
   * <p/>
   * RUNTIMES:
   * If we need runtimes, this only adds additional edges to our graph. M -uses> L -runtime> R is equivalent
   * to M -non-reexp> R in this case
   * <p/>
   * REEXPORT:
   * If we need dependencies with respect to reexport flag, we should first collect all neighbours of the
   * given nodes in graph, and then, considering the graph with "reexport" edges only, collect all nodes
   * accessible from (start set+neighbours) in this graph
   * If we don't respect reexport flag, we should collect all accessible nodes from the given set in a
   * dependencies graph. The "neighbours scheme" works in this case, too.
   *
   * @param depType determines the type of dependencies we want to get
   * @return all modules in scope of given
   */
  @NotNull
  public Collection<SModule> getModules(Deptype depType) {
    Set<SModule> neighbours = collectNeighbours(depType);

    Set<SModule> result = new HashSet<>();
    for (SModule neighbour : neighbours) {
      collect(neighbour, result, depType);
    }

    return result;
  }

  private Set<SModule> collectNeighbours(Deptype depType) {
    HashSet<SModule> result = new HashSet<>();
    for (SModule module : myModules) {
      result.addAll(myUsedModulesCollector.directlyUsedModules(module, true, depType.runtimes));
    }
    result.addAll(myModules);
    return result;
  }

  private void collect(SModule current, Set<SModule> result, Deptype depType) {
    if (!result.contains(current)) {
      result.add(current);
      for (SModule m : myUsedModulesCollector.directlyUsedModules(current, depType.reexportAll, depType.runtimes)) {
        collect(m, result, depType);
      }
    }
  }

  public enum Deptype {
    /**
     * All modules visible from given modules
     * This includes modules from dependencies, transitive, respecting reexports
     * Including initial modules
     */
    VISIBLE(false, false),

    /**
     * All modules required for compilation of given modules
     * This includes visible modules and used language runtimes, respecting reexports
     * Including languages with runtime stub paths
     * Including initial modules
     */
    COMPILE(true, false),

    /**
     * All modules required for execution of given modules
     * This includes transitive closure of visible modules, with no respect for reexports,
     * and runtimes of used languages, not respecting reexports
     * Including languages with runtime stub paths
     * Including initial modules
     */
    EXECUTE(true, true);

    public boolean runtimes;

    public boolean reexportAll;

    Deptype(boolean runtimes, boolean reexportAll) {
      this.runtimes = runtimes;
      this.reexportAll = reexportAll;
    }
  }

  public interface ErrorHandler {
    void depCannotBeResolved(@NotNull SModule module, @NotNull SDependency unresolvableDep);

    void langSourceModuleCannotBeResolved(@NotNull SLanguage languageWithoutSource);

    // FIXME shall we drop this method, it's not invoked from anywhere
    void runtimeDependencyCannotBeFound(@NotNull SLanguage usedLang, @NotNull SModuleReference runtimeRef);

    void runtimeDependencyCannotBeFound(@NotNull SModuleReference runtimeRef);
  }

  public final static ErrorHandler DEFAULT_HANDLER = new PostingWarningsErrorHandler();
}
