/*
 * Copyright 2003-2014 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.classloading;

import jetbrains.mps.classloading.GraphHolder.Graph;
import jetbrains.mps.module.ReloadableModule;
import jetbrains.mps.project.dependency.UsedModulesCollector;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.util.Condition;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class ModuleUpdater {
  private static final Logger LOG = LogManager.getLogger(ModuleUpdater.class);
  private static final Object LOCK = new Object();

  private volatile boolean myChangedFlag = false;
  private final Set<ReloadableModule> myModulesToAdd = new LinkedHashSet<ReloadableModule>();
  private final Set<ReloadableModule> myModulesToReload = new LinkedHashSet<ReloadableModule>();
  private final Set<SModuleReference> myModulesToRemove = new LinkedHashSet<SModuleReference>();
  private final Condition<ReloadableModule> myWatchableCondition;
  private final GraphHolder<SModuleReference> myDepGraph = new GraphHolder<SModuleReference>();
  private final ReferenceStorage<ReloadableModule> myRefStorage;
  private final SRepository myRepository;
  private final Map<ReloadableModule, List<SearchError>> myModulesWithAbsentDeps = new HashMap<>();

  public ModuleUpdater(SRepository repository, Condition<ReloadableModule> watchableCondition, ReferenceStorage<ReloadableModule> refStorage) {
    myRepository = repository;
    myWatchableCondition = watchableCondition;
    myRefStorage = refStorage;
  }

  public void updateModules(@NotNull Collection<? extends ReloadableModule> modules) {
    synchronized (LOCK) {
      myChangedFlag = true;
      for (ReloadableModule module : modules) {
        if (myWatchableCondition.met(module)) {
          myModulesToReload.add(module);
        }
        // need this call because we might get #addModules notification later than this one
        myRefStorage.moduleAdded(module);
      }
    }
  }

  public void addModules(@NotNull Collection<? extends ReloadableModule> modules) {
    synchronized (LOCK) {
      myChangedFlag = true;
      for (ReloadableModule module : modules) {
        if (myWatchableCondition.met(module)) {
          myModulesToAdd.add(module);
          myModulesToRemove.add(module.getModuleReference());
        }
        myRefStorage.moduleAdded(module);
      }
    }
  }

  public void removeModules(@NotNull Collection<? extends SModuleReference> mRefs) {
    synchronized (LOCK) {
      for (SModuleReference mRef : mRefs) {
        if (myRefStorage.moduleRemoved(mRef) != null) {
          // need to clean up myModulesToLoad and myModulesToReload
          removeMRefFromModules(mRef, myModulesToAdd);
          removeMRefFromModules(mRef, myModulesToReload);
          myModulesToRemove.add(mRef);
          myChangedFlag = true;
        }
      }
    }
  }

  public Collection<SModuleReference> getModules() {
    synchronized (LOCK) {
      return myDepGraph.getVertices();
    }
  }

  private void removeMRefFromModules(SModuleReference mRef, Collection<ReloadableModule> modules) {
    for (Iterator<ReloadableModule> iterator = modules.iterator(); iterator.hasNext();) {
      ReloadableModule module = iterator.next();
      SModuleReference ref = module.getModuleReference();
      if (mRef.equals(ref)) iterator.remove();
    }
  }

  /**
   * @return if graph did change (some edges or vertices added/removed)
   */
  public boolean refreshGraph() {
    myRepository.getModelAccess().checkReadAccess();
    synchronized (LOCK) {
      final long beginTime = System.nanoTime();
      LOG.debug(String.format("Refreshing classloading graph adding: %d, removing %d, updating %d", myModulesToAdd.size(),
          myModulesToRemove.size(), myModulesToReload.size()));
      try {
        myChangedFlag = false;
        UsedModulesCollector usedModulesCollector = new UsedModulesCollector();
        myDepGraph.checkGraphsCorrectness();
        int wasEdges = myDepGraph.getEdgesCount();
        int wasVertices = myDepGraph.getVerticesCount();

        myModulesWithAbsentDeps.clear();
        boolean updated = !myModulesToAdd.isEmpty() || !myModulesToRemove.isEmpty();
        updateRemoved(myModulesToRemove);
        updateAdded(myModulesToAdd, usedModulesCollector);
        updated |= updateReloaded(myModulesToReload, usedModulesCollector);
        myModulesToRemove.clear();
        myModulesToAdd.clear();
        myModulesToReload.clear();

        LOG.debug("Difference in the vertex count after validation " + (myDepGraph.getVerticesCount() - wasVertices));
        LOG.debug("Difference in the edge count after validation " + (myDepGraph.getEdgesCount() - wasEdges));
        return updated;
      } finally {
        LOG.info(String.format("Classloading refresh took %.3f s", (System.nanoTime() - beginTime) / 1e9));
      }
    }
  }

  public Map<ReloadableModule, List<SearchError>> getModulesWithAbsentDeps() {
    return Collections.unmodifiableMap(myModulesWithAbsentDeps);
  }

  private void updateRemoved(Set<? extends SModuleReference> modulesToRemove) {
    for (SModuleReference mRef : modulesToRemove) {
      if (!myDepGraph.contains(mRef)) continue;
      LOG.debug("Removing module " + mRef);
      myDepGraph.remove(mRef);
    }
  }

  private void updateAdded(final Set<? extends ReloadableModule> modulesToAdd, UsedModulesCollector usedModulesCollector) {
    updateAddedVertices(modulesToAdd);
    updateAllEdges(usedModulesCollector);
  }

  /**
   * @return true if actual update happened
   */
  private boolean updateReloaded(final Set<? extends ReloadableModule> modulesToReload, UsedModulesCollector usedModulesCollector) {
    if (modulesToReload.isEmpty()) {
      return false;
    }
    boolean updated = updateReloadedVertices(modulesToReload);
    updated |= updateReloadedEdges(modulesToReload, usedModulesCollector);
    return updated;
  }

  private void updateAddedVertices(Set<? extends ReloadableModule> modulesToAdd) {
    for (ReloadableModule module : modulesToAdd) {
      LOG.debug("Adding module " + module);
      assert myWatchableCondition.met(module);
      assert module.getRepository() != null;
      myDepGraph.add(module.getModuleReference());
    }
  }

  /**
   * Here we are updating references from all the existing modules
   * Also we are going through all the modules in the repository and checking that their dependencies do exist.
   * It checks every module in the current graph and tracks whether it has some unresolved dependencies.
   * If so it puts it to the map {@link #myModulesWithAbsentDeps}.
   */
  private void updateAllEdges(UsedModulesCollector usedModulesCollector) {
    myRepository.getModelAccess().checkReadAccess();
    Collection<? extends SModuleReference> allRefs = myDepGraph.getVertices();
    for (SModuleReference ref : allRefs) {
      ReloadableModule module = myRefStorage.resolveRef(ref);
      assert module != null;
      Collection<? extends ReloadableModule> deps;
      DepsWithErrors depsWithErrors = getDepsWithErrors(module, usedModulesCollector);
      deps = depsWithErrors.deps;
      if (!depsWithErrors.errors.isEmpty()) {
        myModulesWithAbsentDeps.put(module, depsWithErrors.errors);
        continue;
      }
      for (ReloadableModule dep : deps) {
        if (allRefs.contains(dep.getModuleReference())) {
          myDepGraph.addEdge(ref, dep.getModuleReference());
        } else {
//        valid if somebody calls reloadModule in moduleAdded() listener before us
          LOG.warn("The dependent module " + dep + " of the " + module + " is not registered");
        }
      }
    }
  }

  private boolean updateReloadedVertices(Set<? extends ReloadableModule> modulesToReload) {
    boolean updated = false;
    for (ReloadableModule module : modulesToReload) {
      LOG.debug("Reloading module " + module);
      assert myWatchableCondition.met(module);
      assert module.getRepository() != null;
      SModuleReference mRef = module.getModuleReference();
      if (!myDepGraph.contains(mRef)) {
        myDepGraph.add(mRef);
        updated = true;
      }
    }
    return updated;
  }

  /**
   * calculates difference in the outgoing edges for each given module
   */
  private boolean updateReloadedEdges(Set<? extends ReloadableModule> modulesToReload, UsedModulesCollector usedModulesCollector) {
    boolean updated = false;
    myRepository.getModelAccess().checkReadAccess();
    Collection<? extends SModuleReference> allRefs = myDepGraph.getVertices();
    for (ReloadableModule module : modulesToReload) {
      SModuleReference mRef = module.getModuleReference();
      Collection<? extends SModuleReference> currentDeps = new HashSet<SModuleReference>(myDepGraph.getOutgoingEdges(mRef));
      DepsWithErrors depsWithErrors = getDepsWithErrors(module, usedModulesCollector);
      if (!depsWithErrors.errors.isEmpty()) {
        assert myModulesWithAbsentDeps.containsKey(module);
        return true;
      }
      Collection<? extends ReloadableModule> newModuleDeps = depsWithErrors.deps;
      for (ReloadableModule moduleDep : newModuleDeps) {
        SModuleReference depRef = moduleDep.getModuleReference();
        if (!currentDeps.contains(depRef)) {
          if (allRefs.contains(depRef)) {
            myDepGraph.addEdge(mRef, depRef);
            updated = true;
          }
        } else {
          currentDeps.remove(depRef);
        }
      }
      for (SModuleReference curDep : currentDeps) {
        myDepGraph.removeEdge(mRef, curDep);
        updated = true;
      }
    }
    return updated;
  }

  @NotNull
  private DepsWithErrors getDepsWithErrors(@NotNull ReloadableModule module, UsedModulesCollector usedModulesCollector) {
    myRepository.getModelAccess().checkReadAccess();
    if (module.getRepository() == null) {
      return DepsWithErrors.EMPTY;
    }

    ErrorContainer errorContainer = new ErrorContainer();
    Collection<SModule> directlyUsedModules = usedModulesCollector.directlyUsedModules(module, errorContainer, true, true);
    Set<ReloadableModule> deps = new LinkedHashSet<>();
    for (SModule dep : directlyUsedModules) {
      if (dep instanceof ReloadableModule) {
        ReloadableModule reloadableModule = (ReloadableModule) dep;
        if (myWatchableCondition.met(reloadableModule)) {
          deps.add(reloadableModule);
        }
      }
    }
    List<SearchError> errors = new ArrayList<>(errorContainer.getErrors());
    return new DepsWithErrors(deps, errors);
  }

  public Collection<SModuleReference> getDeps(Iterable<? extends SModuleReference> mRefs) {
    synchronized (LOCK) {
      final Collection<SModuleReference> result = new ArrayList<SModuleReference>();
      Graph<SModuleReference> depGraph = myDepGraph.getGraph();
      depGraph.dfs(mRefs, result::add);
      return Collections.unmodifiableCollection(result);
    }
  }

  public Collection<SModuleReference> getBackDeps(Iterable<? extends SModuleReference> mRefs) {
    synchronized (LOCK) {
      final Collection<SModuleReference> result = new LinkedHashSet<SModuleReference>();
      Graph<SModuleReference> backDepGraph = myDepGraph.getConjugateGraph();
      backDepGraph.dfs(mRefs, result::add);
      return Collections.unmodifiableCollection(result);
    }
  }

  public boolean isDirty() {
    return myChangedFlag;
  }

  public boolean contains(SModuleReference mRef) {
    synchronized (LOCK) {
      return myDepGraph.contains(mRef);
    }
  }

  private final static class DepsWithErrors {
    public final Collection<ReloadableModule> deps;
    public final List<SearchError> errors;

    private DepsWithErrors(@NotNull Collection<ReloadableModule> deps, @NotNull List<SearchError> errors) {
      this.deps = deps;
      this.errors = errors;
    }

    public final static DepsWithErrors EMPTY = new DepsWithErrors(Collections.emptySet(), Collections.emptyList());
  }

  static class SearchError {
    private final String myMsg;

    SearchError(String msg) {
      myMsg = msg;
    }

    @NotNull
    public String getMsg() {
      return myMsg;
    }

    public static SearchError of(@NotNull String msg) {
      return new SearchError(msg);
    }

    @Override
    public String toString() {
      return "SearchError " + myMsg;
    }
  }
}
