/*
 * Copyright 2003-2015 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.smodel.Language;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
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 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 = LogManager.getLogger(GlobalModuleDependenciesManager.class);

  private final Set<SModule> myModules;
  @NotNull private final ErrorHandler myHandler;
  private final UsedModulesCollector myUsedModulesCollector = new UsedModulesCollector();

  public GlobalModuleDependenciesManager(Collection<? extends SModule> modules, @NotNull ErrorHandler handler) {
    myModules = new HashSet<>(modules);
    myHandler = 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 languages used by the given modules
   * @deprecated Use {@link org.jetbrains.mps.openapi.module.SModule#getUsedLanguages()} directly.
   */
  @Deprecated
  @ToRemove(version = 3.3)
  public Collection<Language> getUsedLanguages() {
    Set<Language> result = new HashSet<Language>();
    for (SModule module : myModules) {
      result.addAll(directlyUsedLanguages(module));
    }
    return result;
  }

  /**
   * Return only modules with 'reexport' mark in the dependents subtree
   * @deprecated one usage does not justify method's existence
   */
  @Deprecated
  @ToRemove(version = 3.4)
  public Collection<SModule> getOnlyReexportModules() {
    Set<SModule> result = new HashSet<>();
    for (SModule module : myModules) {
      collect(module, result, Deptype.VISIBLE);
    }
    return result;
  }

  /**
   * 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, myHandler, 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, myHandler, depType.reexportAll, depType.runtimes)) {
        collect(m, result, depType);
      }
    }
  }

  private static Collection<Language> directlyUsedLanguages(@NotNull SModule module) {
    Set<Language> result = new HashSet<>();
    for (SLanguage language : module.getUsedLanguages()) {
      final SModule sourceModule = language.getSourceModule();
      // respect sourceModule may be null
      if (sourceModule instanceof Language) {
        result.add((Language) sourceModule);
      }
    }
    return result;
  }

  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 SDependency unresolvableDep);

    void langSourceModuleCannotBeResolved(@NotNull SLanguage languageWithoutSource);

    void runtimeDependencyCannotBeFound(@NotNull SLanguage usedLang, @NotNull SModuleReference runtimeRef);

    void runtimeDependencyCannotBeFound(@NotNull SModuleReference runtimeRef);
  }

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