/*
 * 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.module.ReloadableModule;
import jetbrains.mps.module.ReloadableModuleBase;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.module.ModelAccess;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.util.ProgressMonitor;
import org.jetbrains.mps.openapi.util.SubProgressKind;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Broadcasting class loading load/unload events.
 * Guarantees that the listeners are invoked in write action
 */
public class ClassLoadingBroadCaster {
  private static final Logger LOG = LogManager.getLogger(ClassLoadingBroadCaster.class);
  private final LinkedHashSet<ReloadableModule> myLoadedModules = new LinkedHashSet<ReloadableModule>();
  private final ModelAccess myModelAccess;

  // reload handlers
  private final List<MPSClassesListener> myClassesHandlers = new CopyOnWriteArrayList<MPSClassesListener>();
  private final List<ModuleReloadListener> myReloadListeners = new CopyOnWriteArrayList<ModuleReloadListener>();
  private final List<DeployListener> myDeployListeners = new CopyOnWriteArrayList<>();

  public ClassLoadingBroadCaster(ModelAccess modelAccess) {
    myModelAccess = modelAccess;
  }

  public void addClassesHandler(MPSClassesListener handler) {
    myClassesHandlers.add(handler);
  }

  public void removeClassesHandler(MPSClassesListener handler) {
    myClassesHandlers.remove(handler);
  }

  public void addReloadListener(ModuleReloadListener listener) {
    myReloadListeners.add(listener);
  }

  public void removeReloadListener(ModuleReloadListener listener) {
    myReloadListeners.remove(listener);
  }

  public Set<ReloadableModule> onUnload(Collection<? extends SModuleReference> refsToUnload, @NotNull ProgressMonitor monitor) {
    if (refsToUnload.isEmpty()) return Collections.emptySet();

    myModelAccess.checkWriteAccess();
    final Set<ReloadableModule> modulesToUnload = new LinkedHashSet<>();
    for (ReloadableModule loadedModule : myLoadedModules) {
      SModuleReference mRef = loadedModule.getModuleReference();
      if (refsToUnload.contains(mRef)) {
        modulesToUnload.add(loadedModule);
      }
    }
    if (modulesToUnload.size() < refsToUnload.size()) {
      LOG.error("", new IllegalArgumentException("Broken contract : some of the passed module references have not been loaded"));
    }

    myLoadedModules.removeAll(modulesToUnload);

    try {
      monitor.start("Broadcasting Events", myClassesHandlers.size() + myDeployListeners.size());
      for (MPSClassesListener listener : myClassesHandlers) {
        try {
          listener.onUnloaded(modulesToUnload, monitor.subTask(1));
        } catch (VirtualMachineError e) {
          throw e;
        } catch (Throwable e) {
          LOG.error("Caught exception from the listener " + listener + ". Will continue.", e);
        }
      }
      for (DeployListener listener : myDeployListeners) {
        try {
          listener.onUnloaded(modulesToUnload, monitor.subTask(1));
        } catch (VirtualMachineError e) {
          throw e;
        } catch (Throwable e) {
          LOG.error("Caught exception from the listener " + listener + ". Will continue.", e);
        }
      }
    } finally {
      monitor.done();
    }

    final Set<ReloadableModule> resultingUnload = new LinkedHashSet<ReloadableModule>();
    for (ReloadableModule module : modulesToUnload) resultingUnload.add(module);
    return resultingUnload;
  }

  public void onLoad(Set<ReloadableModule> toLoad, @NotNull ProgressMonitor monitor) {
    if (toLoad.isEmpty()) return;

    myModelAccess.checkWriteAccess();
    final Set<ReloadableModuleBase> modulesToLoad = new LinkedHashSet<>(toLoad.size());
    for (ReloadableModule module : toLoad) {
      modulesToLoad.add((ReloadableModuleBase) module);
    }
    myLoadedModules.addAll(modulesToLoad);

    try {
      monitor.start("Broadcasting Events", myClassesHandlers.size() + myDeployListeners.size());
      for (MPSClassesListener listener : myClassesHandlers) {
        try {
          listener.onLoaded(toLoad, monitor.subTask(1));
        } catch (VirtualMachineError e) {
          throw e;
        } catch (Throwable e) {
          LOG.error("Caught exception from the listener " + listener + ". Will continue.", e);
        }
      }
      for (DeployListener listener : myDeployListeners) {
        try {
          listener.onLoaded(toLoad, monitor.subTask(1));
        } catch (VirtualMachineError e) {
          throw e;
        } catch (Throwable e) {
          LOG.error("Caught exception from the listener " + listener + ". Will continue.", e);
        }
      }
    } finally {
      monitor.done();
    }
  }

  public void onReload(Collection<ReloadableModule> reloadedModules) {
    if (reloadedModules.isEmpty()) return;

    myModelAccess.checkWriteAccess();
    final Set<ReloadableModule> modulesToReload = new LinkedHashSet<ReloadableModule>(reloadedModules.size());
    for (ReloadableModule module : reloadedModules) modulesToReload.add(module);

    for (ModuleReloadListener listener : myReloadListeners) {
      listener.modulesReloaded(modulesToReload);
    }
  }

  public void addListener(@NotNull DeployListener listener) {
    myDeployListeners.add(listener);
  }

  public void removeListener(@NotNull DeployListener listener) {
    myDeployListeners.remove(listener);
  }
}
