content: /*
   * SoulFire
   * Copyright (C) 2024  AlexProgrammerDE
   *
   * This program is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with this program.  If not, see <https://www.gnu.org/licenses/>.
   */
  package com.soulfiremc.bootstrap;
  
  import com.soulfiremc.server.api.InternalPluginClass;
  import com.soulfiremc.server.util.PortHelper;
  import com.soulfiremc.server.util.SFPathConstants;
  import io.github.classgraph.ClassGraph;
  import io.github.classgraph.ClassInfo;
  import lombok.SneakyThrows;
  import lombok.extern.slf4j.Slf4j;
  import org.apache.logging.log4j.LogManager;
  
  import java.io.IOException;
  import java.nio.file.Files;
  import java.time.Instant;
  import java.util.Scanner;
  
  /**
   * This class prepares the earliest work possible, such as loading mixins and setting up logging.
   */
  @Slf4j
  public abstract class SoulFireAbstractBootstrap {
    public static final Instant START_TIME = Instant.now();
  
    protected SoulFireAbstractBootstrap() {}
  
    public static int getRPCPort(int defaultPort) {
      return Integer.getInteger("sf.grpc.port", defaultPort);
    }
  
    public static int getRandomRPCPort() {
      var port = getRPCPort(0);
  
      return port == 0 ? PortHelper.getRandomAvailablePort() : port;
    }
  
    public static String getRPCHost(String defaultHost) {
      return System.getProperty("sf.grpc.host", defaultHost);
    }
  
    private void initPlugins() {
      try (var scanResult =
             new ClassGraph()
               .verbose()
               .enableAllInfo()
               .acceptPackages("com.soulfiremc.server.plugins")
               .scan()) {
        scanResult.getClassesWithAnnotation(InternalPluginClass.class)
          .stream()
          .sorted((a, b) -> {
            var aOrder = (int) a.getAnnotationInfo(InternalPluginClass.class).getParameterValues().getValue("order");
            var bOrder = (int) b.getAnnotationInfo(InternalPluginClass.class).getParameterValues().getValue("order");
            return Integer.compare(aOrder, bOrder);
          })
          .forEach(this::processRouteClass);
      } catch (Throwable t) {
        log.error("Failed to initialize plugins", t);
        throw new RuntimeException("Failed to initialize plugins", t);
      }
    }
  
    private void processRouteClass(ClassInfo classInfo) {
      try {
        var clazz = Class.forName(classInfo.getName());
        clazz.getConstructor().newInstance();
      } catch (Throwable t) {
        log.error("Failed to load plugin class {}", classInfo.getName(), t);
      }
    }
  
    @SneakyThrows
    protected void internalBootstrap(String[] args) {
      try {
        injectFileProperties();
  
        initPlugins();
  
        injectMixinsAndRun(args);
      } catch (Throwable t) {
        // Ensure logs are written out on fatal errors
        log.error("Failed to start server", t);
        LogManager.shutdown();
        System.exit(1);
      }
    }
  
    private void injectFileProperties() {
      var optionsFile = SFPathConstants.BASE_DIR.resolve("soulfire.properties");
      if (!Files.exists(optionsFile)) {
        return;
      }
  
      log.info("Loading options from {}", optionsFile);
      try (var scanner = new Scanner(optionsFile)) {
        var lineNumber = 0;
        while (scanner.hasNextLine()) {
          lineNumber++;
          var line = scanner.nextLine();
          if (line.startsWith("#") || line.isBlank()) {
            continue;
          }
  
          var parts = line.split("=", 2);
          if (parts.length != 2 || parts[0].isBlank() || parts[1].isBlank()) {
            log.warn("Invalid line in options file at line {}: {}", lineNumber, line);
            continue;
          }
  
          var parsedKey = parts[0].strip();
          var parsedValue = parts[1].strip();
          if (!parsedKey.startsWith("sf.")) {
            log.warn("Invalid key in options file at line {}: {}", lineNumber, parsedKey);
            continue;
          }
  
          System.setProperty(parsedKey, parsedValue);
        }
      } catch (IOException e) {
        log.error("Failed to read options file", e);
      }
    }
  
    public void injectMixinsAndRun(String[] args) {
      this.postMixinMain(args);
    }
  
    protected abstract void postMixinMain(String[] args);
  }
  