/*
 * Copyright 2003-2013 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.plugins;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.module.SRepository;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * A parent class for ProjectPluginManager and ApplicationPluginManager.
 * Contains a {@link jetbrains.mps.plugins.PluginReloadingListener},
 * which is triggered by a {@link PluginLoaderRegistry} component. It causes plugins to load [unload].
 * Note that a subclass chooses by himself when it is time to attach [detach] the listener ({@link #register()}.
 *
 * @param <T> -- is a class type of plugin.
 *           @see jetbrains.mps.plugins.applicationplugins.BaseApplicationPlugin
 *           @see jetbrains.mps.plugins.projectplugins.BaseProjectPlugin
 */
public abstract class BasePluginManager<T> implements PluginLoader {
  private static final Logger LOG = LogManager.getLogger(BasePluginManager.class);

  protected final Object myPluginsLock = new Object(); //guarding my fields
  protected final SRepository myRepository;
  private final PluginLoaderRegistry myPluginLoaderRegistry;

  private List<T> mySortedPlugins = new ArrayList<T>(); // does not contain nulls
  private final Map<PluginContributor, T> myContributorToPlugin = new LinkedHashMap<>(); // NOTE ALLOWED NULL VALUES

  public BasePluginManager(@NotNull SRepository repository, PluginLoaderRegistry pluginLoaderRegistry) {
    myRepository = repository;
    myPluginLoaderRegistry = pluginLoaderRegistry;
  }

  protected abstract T createPlugin(PluginContributor contributor);

  protected abstract void afterPluginsCreated(List<T> plugins);

  protected abstract void beforePluginsDisposed(List<T> plugins);

  protected abstract void disposePlugin(T plugin);

  protected final void register() {
    myPluginLoaderRegistry.register(this);
  }

  protected final void unregister() {
    myPluginLoaderRegistry.unregister(this);
  }

  @Override
  public void loadPlugins(final List<PluginContributor> contributors) {
    int size = contributors.size();
    LOG.debug("Loading plugins from " + size + " contributors [" + toString() + "]");
    final Map<PluginContributor, T> plugins = createPlugins(contributors);
    synchronized (myPluginsLock) {
      plugins.entrySet().forEach(entry -> {
        PluginContributor contributor = entry.getKey();
        @Nullable T plugin = entry.getValue();
        if (myContributorToPlugin.containsKey(contributor)) { // not in one step because nulls are allowed
          LOG.error("", new IllegalArgumentException(this + ": contributor " + contributor + " is already registered"));
        }
        myContributorToPlugin.put(contributor, plugin);
      });
      List<T> notNullPlugins = plugins.values().stream().filter(Objects::nonNull).collect(Collectors.toList());
      mySortedPlugins.addAll(notNullPlugins);
      afterPluginsCreated(new ArrayList<>(notNullPlugins));
    }
  }

  @Override
  public void unloadPlugins(final List<PluginContributor> contributors) {
    int size = contributors.size();
    LOG.debug("Unloading MPS plugins from " + size + " contributors [" + toString() + "]");
    final List<T> plugins;
    synchronized (myPluginsLock) {
      plugins = calcPluginsToUnload(contributors);
      mySortedPlugins.removeAll(plugins);
    }

    beforePluginsDisposed(plugins);
    disposePlugins(plugins);
  }

  @NotNull
  private List<T> calcPluginsToUnload(List<PluginContributor> contributors) {
    final List<T> plugins = new ArrayList<>();

    for (PluginContributor contributor : contributors) {
      if (!myContributorToPlugin.containsKey(contributor)) { // not in one step because nulls are allowed
        LOG.error("", new IllegalArgumentException(this + ": contributor " + contributor + " was not registered"));
      } else {
        @Nullable T plugin = myContributorToPlugin.remove(contributor);
        LOG.trace(this + ": unloading plugin " + plugin + " from the contributor " + contributor);

        if (plugin != null) {
          plugins.add(plugin);
        }
      }
    }
    return plugins;
  }

  private Map<PluginContributor, T> createPlugins(List<PluginContributor> contributors) {
    Map<PluginContributor, T> plugins = new LinkedHashMap<>();

    for (PluginContributor contributor : contributors) {
      T plugin = createPluginChecked(contributor);
      plugins.put(contributor, plugin);
    }
    return plugins;
  }

  @Nullable
  private T createPluginChecked(PluginContributor contributor) {
    T plugin = null;
    try {
      plugin = createPlugin(contributor);
      LOG.trace(this + ": creating plugin " + plugin + " from the contributor " + contributor);
    } catch (LinkageError le) {
      LOG.error(this + ": contributor " + contributor + " threw a linkage error during plugin creation ", le);
    } catch (VirtualMachineError virtualMachineError) {
      throw virtualMachineError;
    } catch (Throwable t) {
      LOG.error(this + ": contributor " + contributor + " threw an exception during plugin creation " + t.getMessage(), t);
    }
    return plugin;
  }

  private void disposePlugins(final List<T> plugins) {
    for (T plugin : plugins) {
      try {
        disposePlugin(plugin);
      } catch (LinkageError le) {
        LOG.error(this + ": plugin " + plugin + " threw a linkage error during disposing", le);
      } catch (VirtualMachineError virtualMachineError) {
        throw virtualMachineError;
      } catch (Throwable t) {
        LOG.error(this + ": plugin " + plugin + " threw an exception during disposing " + t.getMessage(), t);
      }
    }
  }

  public List<T> getPlugins() {
    synchronized (myPluginsLock) {
      return new ArrayList<T>(mySortedPlugins);
    }
  }
}
