content: package org.allaymc.server.plugin;
  
  import lombok.extern.slf4j.Slf4j;
  import org.allaymc.api.eventbus.event.plugin.PluginDisableEvent;
  import org.allaymc.api.eventbus.event.plugin.PluginEnableEvent;
  import org.allaymc.api.message.I18n;
  import org.allaymc.api.message.TrKeys;
  import org.allaymc.api.plugin.*;
  import org.allaymc.server.datastruct.dag.DAGCycleException;
  import org.allaymc.server.datastruct.dag.DirectedAcyclicGraph;
  import org.allaymc.server.datastruct.dag.HashDirectedAcyclicGraph;
  import org.allaymc.server.plugin.jar.JarPluginLoader;
  import org.allaymc.server.utils.GitProperties;
  import org.semver4j.Semver;
  import org.semver4j.range.RangeListFactory;
  
  import java.nio.file.Path;
  import java.util.*;
  import java.util.stream.Collectors;
  
  /**
   * @author daoge_cmd
   */
  @Slf4j
  public class AllayPluginManager implements PluginManager {
  
      protected Set<PluginSource> sources;
      protected Set<PluginSource> customSources;
      protected Set<PluginLoader.Factory> loaderFactories;
      protected Set<PluginLoader.Factory> customLoaderFactories;
  
      protected Map<String, PluginContainer> plugins;
      protected Map<String, PluginContainer> enabledPlugins;
      protected List<String> sortedPluginList;
  
      protected State state;
  
      public AllayPluginManager() {
          this.sources = new HashSet<>();
          this.customSources = new HashSet<>();
          this.loaderFactories = new HashSet<>();
          this.customLoaderFactories = new HashSet<>();
          this.plugins = new HashMap<>();
          this.enabledPlugins = new HashMap<>();
          this.state = State.NEW;
  
          registerSource(new DefaultPluginSource());
          registerLoaderFactory(new JarPluginLoader.Factory());
      }
  
      protected void registerSource(PluginSource pluginSource) {
          this.state.ensureNotBiggerThan(State.LOADED);
          this.sources.add(pluginSource);
      }
  
      @Override
      public void registerCustomSource(PluginSource customPluginSource) {
          this.state.ensureNotBiggerThan(State.LOADED);
          this.customSources.add(customPluginSource);
      }
  
      protected void registerLoaderFactory(PluginLoader.Factory loaderFactory) {
          this.state.ensureNotBiggerThan(State.LOADED);
          this.loaderFactories.add(loaderFactory);
      }
  
      @Override
      public void registerCustomLoaderFactory(PluginLoader.Factory customLoaderFactory) {
          this.state.ensureNotBiggerThan(State.LOADED);
          this.customLoaderFactories.add(customLoaderFactory);
      }
  
      public void loadPlugins() {
          this.state.ensureEquals(State.NEW);
  
          // 1. Load possible plugin foundPaths from plugin sources
          var dag = new HashDirectedAcyclicGraph<String>();
          var foundPaths = findPluginPaths(this.sources);
          var foundDescriptors = new HashMap<String, PluginDescriptor>();
          var foundLoaders = new HashMap<String, PluginLoader>();
          // 2. Find and use plugin loader to load plugin descriptor for each plugin path
          findLoadersAndLoadDescriptors(foundPaths, foundDescriptors, foundLoaders, this.loaderFactories);
          // 3. Check for circular dependencies
          calculateLoadingOrder(dag, foundDescriptors);
          // 4. Load plugins (parent -> child)
          onLoad(foundDescriptors, foundLoaders);
  
          // 5. Load plugins through custom sources and loaders
          // Plugins may add custom sources and loaders during
          // their onLoad() method
          loadPluginsCustom(dag, foundPaths);
  
          this.state = State.LOADED;
      }
  
      /**
       * Find plugin paths from the given plugin sources.
       *
       * @param sources the plugin sources to find plugin paths from
       * @return a set of plugin paths found from the given plugin sources
       */
      protected Set<Path> findPluginPaths(Set<PluginSource> sources) {
          return sources.stream().flatMap(source -> source.find().stream()).collect(Collectors.toSet());
      }
  
      /**
       * Try to find plugin loader for each plugin path. If found, try to load descriptor through the found plugin loader.
       *
       * @param foundPaths       the plugin paths to find loaders and load descriptors from
       * @param foundDescriptors the map that to store the found plugin descriptors
       * @param foundLoaders     the map that to store the found plugin loaders
       * @param loaderFactories  the plugin loader factories to find loader from
       */
      protected void findLoadersAndLoadDescriptors(
              Set<Path> foundPaths, Map<String, PluginDescriptor> foundDescriptors,
              Map<String, PluginLoader> foundLoaders, Set<PluginLoader.Factory> loaderFactories
      ) {
          for (var path : foundPaths) {
              var loader = findLoaderIn(loaderFactories, path);
              if (loader == null) {
                  continue;
              }
  
              PluginDescriptor descriptor;
              try {
                  descriptor = loader.loadDescriptor();
              } catch (Throwable t) {
                  log.error(I18n.get().tr(TrKeys.ALLAY_PLUGIN_DESCRIPTOR_ERROR, path.getFileName(), t.getMessage() != null ? t.getMessage() : ""), t);
                  continue;
              }
  
              var name = descriptor.getName();
              if (foundDescriptors.containsKey(name)) {
                  log.error(I18n.get().tr(TrKeys.ALLAY_PLUGIN_DUPLICATE, name));
                  continue;
              }
  
              if (!descriptor.getAPIVersion().isBlank()) {
                  // Replace the possible dev suffix in the api version string to make it strictly meet the semver rules
                  var apiVersion = Semver.coerce(GitProperties.getBuildApiVersion().replace("-dev", ""));
                  var apiVersionRange = RangeListFactory.create(descriptor.getAPIVersion());
                  // noinspection all
                  if (!apiVersion.satisfies(apiVersionRange)) {
                      log.error(I18n.get().tr(TrKeys.ALLAY_PLUGIN_API_VERSION_MISMATCH, apiVersion, name, apiVersionRange));
                      continue;
                  }
              }
  
              foundDescriptors.put(name, descriptor);
              foundLoaders.put(name, loader);
          }
      }
  
      /**
       * Add given descriptors to the dag and calculate the loading order. Circular dependencies
       * will also be checked. By the end, {@link #sortedPluginList} will be updated.
       *
       * @param dag         the directed acyclic graph to store the plugin dependencies
       * @param descriptors the plugin descriptors that should be added to the directed acyclic graph
       */
      protected void calculateLoadingOrder(DirectedAcyclicGraph<String> dag, Map<String, ? extends PluginDescriptor> descriptors) {
          // Add all plugin names to dag firstly
          dag.addAll(descriptors.keySet());
          for (var descriptor : descriptors.values()) {
              for (var dependency : descriptor.getDependencies()) {
                  var name = dependency.name();
                  // Add dependency plugin to DAG
                  dag.add(name);
                  try {
                      dag.setBefore(name, descriptor.getName());
                  } catch (DAGCycleException e) {
                      log.error(I18n.get().tr(TrKeys.ALLAY_PLUGIN_DEPENDENCY_CIRCULAR, descriptor.getName(), e.getMessage() != null ? e.getMessage() : ""));
                      dag.remove(descriptor.getName());
                  }
              }
          }
  
          this.sortedPluginList = dag.getSortedList();
      }
  
      /**
       * Load plugins based on the given descriptors and loaders.
       * <p>
       * The loading order is based on {@link #sortedPluginList}, so before calling this method, make sure to call
       * {@link #calculateLoadingOrder(DirectedAcyclicGraph, Map)}
       *
       * @param descriptors  the descriptors of the plugins that need to be loaded
       * @param foundLoaders the loaders of the plugins that need to be loaded
       */
      protected void onLoad(Map<String, PluginDescriptor> descriptors, Map<String, PluginLoader> foundLoaders) {
          var iterator = sortedPluginList.iterator();
          start:
          while (iterator.hasNext()) {
              var str = iterator.next();
              var descriptor = descriptors.get(str);
              if (descriptor == null) {
                  continue;
              }
  
              var loader = foundLoaders.get(str);
              for (var dependency : descriptor.getDependencies()) {
                  var dependencyContainer = plugins.get(dependency.name());
  
                  if (dependencyContainer == null) {
                      if (dependency.optional()) {
                          // This dependency is optional, so we can still load this plugin
                          continue;
                      }
  
                      log.error(I18n.get().tr(TrKeys.ALLAY_PLUGIN_DEPENDENCY_MISSING, descriptor.getName(), dependency.name()));
                      iterator.remove();
                      continue start;
                  }
  
                  if (isUnexpectedDependencyVersion(dependencyContainer.descriptor(), dependency)) {
                      // Mismatched dependency version, skip this plugin
                      log.error(I18n.get().tr(
                              TrKeys.ALLAY_PLUGIN_DEPENDENCY_VERSION_MISMATCH, descriptor.getName(), dependency.name(),
                              dependency.version(), dependencyContainer.descriptor().getVersion()
                      ));
                      iterator.remove();
                      continue start;
                  }
              }
  
              log.info(I18n.get().tr(TrKeys.ALLAY_PLUGIN_LOADING, descriptor.getName()));
              PluginContainer pluginContainer;
              try {
                  pluginContainer = loader.loadPlugin();
                  pluginContainer.plugin().onLoad();
              } catch (Throwable t) {
                  log.error(I18n.get().tr(TrKeys.ALLAY_PLUGIN_LOAD_ERROR, descriptor.getName(), t.getMessage() != null ? t.getMessage() : ""), t);
                  continue;
              }
  
              plugins.put(descriptor.getName(), pluginContainer);
          }
      }
  
      protected void loadPluginsCustom(DirectedAcyclicGraph<String> dag, Set<Path> existingPaths) {
          var paths = findPluginPaths(this.customSources);
          paths.addAll(existingPaths);
          var foundDescriptors = new HashMap<String, PluginDescriptor>();
          var foundLoaders = new HashMap<String, PluginLoader>();
          // findLoadersAndLoadDescriptors will only find loader in this.customLoaderFactories,
          // so already loaded plugin won't be loaded twice
          findLoadersAndLoadDescriptors(paths, foundDescriptors, foundLoaders, this.customLoaderFactories);
          calculateLoadingOrder(dag, foundDescriptors);
          onLoad(foundDescriptors, foundLoaders);
      }
  
      protected boolean isUnexpectedDependencyVersion(PluginDescriptor dependency, PluginDependency requirement) {
          var requireVersion = requirement.version();
          if (requireVersion == null) {
              return false;
          }
  
          var dependencyVersion = dependency.getVersion();
          var dependencySemver = Semver.coerce(dependencyVersion);
          var versionRanges = RangeListFactory.create(requireVersion);
          // noinspection all
          return !dependencySemver.satisfies(versionRanges);
      }
  
      public void enablePlugins() {
          this.state.ensureEquals(State.LOADED);
  
          for (var pluginName : sortedPluginList) {
              var pluginContainer = getPlugin(pluginName);
              if (pluginContainer == null) {
                  // Plugin failed to be loaded
                  continue;
              }
  
              log.info(I18n.get().tr(TrKeys.ALLAY_PLUGIN_ENABLING, pluginContainer.descriptor().getName()));
              try {
                  var plugin = pluginContainer.plugin();
                  new PluginEnableEvent(plugin).call();
                  plugin.onEnable();
              } catch (Throwable t) {
                  log.error(I18n.get().tr(TrKeys.ALLAY_PLUGIN_ENABLE_ERROR, pluginContainer.descriptor().getName(), t.getMessage() != null ? t.getMessage() : ""), t);
                  continue;
              }
  
              enabledPlugins.put(pluginContainer.descriptor().getName(), pluginContainer);
          }
  
          this.state = State.ENABLED;
      }
  
      public void disablePlugins() {
          this.state.ensureEquals(State.ENABLED);
  
          for (var pluginName : sortedPluginList.reversed()) {
              if (!isPluginEnabled(pluginName)) continue;
  
              var pluginContainer = getPlugin(pluginName);
              log.info(I18n.get().tr(TrKeys.ALLAY_PLUGIN_DISABLING, pluginContainer.descriptor().getName()));
              try {
                  var plugin = pluginContainer.plugin();
                  new PluginDisableEvent(plugin).call();
                  plugin.onDisable();
              } catch (Throwable t) {
                  log.error(I18n.get().tr(TrKeys.ALLAY_PLUGIN_DISABLE_ERROR, pluginContainer.descriptor().getName()), t);
              }
          }
  
          this.state = State.DISABLED;
      }
  
      @Override
      public Map<String, PluginContainer> getPlugins() {
          return Collections.unmodifiableMap(plugins);
      }
  
      @Override
      public PluginContainer getPlugin(String name) {
          return plugins.get(name);
      }
  
      @Override
      public PluginContainer getEnabledPlugin(String name) {
          return enabledPlugins.get(name);
      }
  
      @Override
      public Map<String, PluginContainer> getEnabledPlugins() {
          return Collections.unmodifiableMap(enabledPlugins);
      }
  
      @Override
      public boolean isPluginEnabled(String name) {
          return enabledPlugins.containsKey(name);
      }
  
      protected PluginLoader findLoaderIn(Set<PluginLoader.Factory> loaderFactories, Path pluginPath) {
          return loaderFactories.stream()
                  .filter(loaderFactory -> loaderFactory.canLoad(pluginPath))
                  .findFirst()
                  .map(loaderFactory -> loaderFactory.create(pluginPath))
                  .orElse(null);
      }
  
      protected enum State {
          /**
           * The manager is just created.
           */
          NEW,
          /**
           * Plugins have been loaded but not enabled.
           */
          LOADED,
          /**
           * Loaded plugins have been enabled if they can be enabled.
           */
          ENABLED,
          /**
           * Enabled plugins have been disabled.
           */
          DISABLED;
  
          /**
           * Ensure the state is not bigger than the given state.
           *
           * @param state the state to compare with
           * @throws IllegalStateException if the state is bigger than the given state
           */
          public void ensureNotBiggerThan(State state) {
              if (this.ordinal() > state.ordinal()) {
                  throw new IllegalStateException();
              }
          }
  
          /**
           * Ensure the state is equals to the given state.
           *
           * @param state the state to compare with
           * @throws IllegalStateException if the state is not equals to the given state
           */
          public void ensureEquals(State state) {
              if (state != this) {
                  throw new IllegalStateException();
              }
          }
      }
  }
  