// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you 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 org.openqa.grid.selenium;

import com.google.common.collect.ImmutableMap;

import com.beust.jcommander.JCommander;

import org.openqa.grid.common.GridRole;
import org.openqa.grid.common.RegistrationRequest;
import org.openqa.grid.internal.utils.SelfRegisteringRemote;
import org.openqa.grid.internal.utils.configuration.CoreRunnerConfiguration;
import org.openqa.grid.internal.utils.configuration.GridHubConfiguration;
import org.openqa.grid.internal.utils.configuration.GridNodeConfiguration;
import org.openqa.grid.internal.utils.configuration.StandaloneConfiguration;
import org.openqa.grid.shared.CliUtils;
import org.openqa.grid.web.Hub;
import org.openqa.selenium.internal.BuildInfo;
import org.openqa.selenium.remote.server.SeleniumServer;
import org.openqa.selenium.remote.server.log.LoggingOptions;
import org.openqa.selenium.remote.server.log.TerseFormatter;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.function.Supplier;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class GridLauncherV3 {

  private static final Logger log = Logger.getLogger(GridLauncherV3.class.getName());
  private static final String CORE_RUNNER_CLASS =
    "org.openqa.selenium.server.htmlrunner.HTMLLauncher";

  private static abstract class GridItemLauncher {
    protected StandaloneConfiguration configuration;
    protected boolean helpRequested;
    abstract void setConfiguration(String[] args);
    abstract void launch() throws Exception;
    void printUsage() {
      new JCommander(configuration).usage();
    }
  }

  private static ImmutableMap<String, Supplier<GridItemLauncher>> LAUNCHERS = buildLaunchers();

  public static void main(String[] args) throws Exception {
    GridItemLauncher launcher = buildLauncher(args);
    if (launcher == null) {
      return;
    }

    if (launcher.helpRequested) {
      launcher.printUsage();
      return;
    }

    configureLogging(launcher.configuration);

    BuildInfo buildInfo = new BuildInfo();
    log.info(String.format(
      "Selenium build info: version: '%s', revision: '%s'",
      buildInfo.getReleaseLabel(),
      buildInfo.getBuildRevision()));
    try {
      launcher.launch();
    } catch (Exception e) {
      launcher.printUsage();
      e.printStackTrace();
    }
  }

  /**
   * From the {@code args}, builds a new {@link GridItemLauncher} and populates it properly.
   *
   * @return null if no role is found, or a properly populated {@link GridItemLauncher}.
   */
  private static GridItemLauncher buildLauncher(String[] args) {
    String role = "standalone";

    for (int i = 0; i < args.length; i++) {
      if (args[i].equals("-htmlSuite")) {
        Supplier<GridItemLauncher> launcherSupplier = LAUNCHERS.get("corerunner");
        if (launcherSupplier == null) {
          System.err.println(
            "Unable to find the HTML runner. This is normally because you have not downloaded " +
            "or made available the 'selenium-leg-rc' jar on the CLASSPATH. Your test will " +
            "not be run.");
          return null;
        }
        GridItemLauncher launcher = launcherSupplier.get();
        launcher.setConfiguration(args);
        return launcher;
      }
      if (args[i].startsWith("-role=")) {
        role = args[i].substring("-role=".length());
      } else if (args[i].equals("-role")) {
        i++;  // Increment, because we're going to need this.
        if (i < args.length) {
          role = args[i];
        } else {
          role = null;  // Will cause us to print the usage information.
        }
      }
    }

    GridRole gridRole = GridRole.get(role);
    if (gridRole == null) {
      printInfoAboutRoles(role);
      return null;
    }

    Supplier<GridItemLauncher> supplier = LAUNCHERS.get(gridRole.toString());
    if (supplier == null) {
      System.err.println("Unknown role: " + gridRole);
      return null;
    }
    GridItemLauncher toReturn = supplier.get();
    toReturn.setConfiguration(args);
    return toReturn;
  }

  private static void printInfoAboutRoles(String roleCommandLineArg) {
    if (roleCommandLineArg != null) {
      CliUtils.printWrappedLine(
        "",
        "Error: the role '" + roleCommandLineArg + "' does not match a recognized server role: node/hub/standalone\n");
    } else {
      CliUtils.printWrappedLine(
        "",
        "Error: -role option needs to be followed by the value that defines role of this component in the grid\n");
    }
    System.out.println(
      "Selenium server can run in one of the following roles:\n" +
      "  hub         as a hub of a Selenium grid\n" +
      "  node        as a node of a Selenium grid\n" +
      "  standalone  as a standalone server not being a part of a grid\n" +
      "\n" +
      "If -role option is omitted the server runs standalone\n");
    CliUtils.printWrappedLine(
      "",
      "To get help on the options available for a specific role run the server"
      + " with -help option and the corresponding -role option value");
  }

  private static void configureLogging(StandaloneConfiguration configuration) {
    Level logLevel =
        configuration.debug
        ? Level.FINE
        : LoggingOptions.getDefaultLogLevel();
    if (logLevel == null) {
      logLevel = Level.INFO;
    }
    Logger.getLogger("").setLevel(logLevel);
    Logger.getLogger("org.openqa.jetty").setLevel(Level.WARNING);

    String logFilename =
        configuration.log != null
        ? configuration.log
        : LoggingOptions.getDefaultLogOutFile();
    if (logFilename != null) {
      for (Handler handler : Logger.getLogger("").getHandlers()) {
        if (handler instanceof ConsoleHandler) {
          Logger.getLogger("").removeHandler(handler);
        }
      }
      try {
        Handler logFile = new FileHandler(new File(logFilename).getAbsolutePath(), true);
        logFile.setFormatter(new TerseFormatter(true));
        logFile.setLevel(logLevel);
        Logger.getLogger("").addHandler(logFile);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    } else {
      for (Handler handler : Logger.getLogger("").getHandlers()) {
        if (handler instanceof ConsoleHandler) {
          handler.setLevel(logLevel);
          handler.setFormatter(new TerseFormatter(configuration.logLongForm));
        }
      }
    }
  }

  private static ImmutableMap<String, Supplier<GridItemLauncher>> buildLaunchers() {
    ImmutableMap.Builder<String, Supplier<GridItemLauncher>> launchers =
      ImmutableMap.<String, Supplier<GridItemLauncher>>builder()
        .put(GridRole.NOT_GRID.toString(), () -> new GridItemLauncher() {
          public void setConfiguration(String[] args) {
            configuration = new StandaloneConfiguration();
            new JCommander(configuration, args);
            helpRequested = configuration.help;
          }

          public void launch() throws Exception {
            log.info("Launching a standalone Selenium Server");
            SeleniumServer server = new SeleniumServer(configuration);
            server.boot();
            log.info("Selenium Server is up and running");
          }
        })
        .put(GridRole.HUB.toString(), () -> new GridItemLauncher() {
          public void setConfiguration(String[] args) {
            GridHubConfiguration pending = new GridHubConfiguration();
            new JCommander(pending, args);
            configuration = pending;
            //re-parse the args using any -hubConfig specified to init
            if (pending.hubConfig != null) {
              configuration = GridHubConfiguration.loadFromJSON(pending.hubConfig);
              new JCommander(configuration, args); //args take precedence
            }
            helpRequested = configuration.help;
          }

          public void launch() throws Exception {
            log.info("Launching Selenium Grid hub");
            Hub h = new Hub((GridHubConfiguration) configuration);
            h.start();
            log.info("Nodes should register to " + h.getRegistrationURL());
            log.info("Selenium Grid hub is up and running");
          }
        })
        .put(GridRole.NODE.toString(), () -> new GridItemLauncher() {
          public void setConfiguration(String[] args) {
            GridNodeConfiguration pending = new GridNodeConfiguration();
            new JCommander(pending, args);
            configuration = pending;
            //re-parse the args using any -nodeConfig specified to init
            if (pending.nodeConfigFile != null) {
              configuration = GridNodeConfiguration.loadFromJSON(pending.nodeConfigFile);
              new JCommander(configuration, args); //args take precedence
            }
            helpRequested = configuration.help;
            if (configuration.port == null) {
              configuration.port = 5555;
            }
          }

          public void launch() throws Exception {
            log.info("Launching a Selenium Grid node");
            RegistrationRequest
              c =
              RegistrationRequest.build((GridNodeConfiguration) configuration);
            SelfRegisteringRemote remote = new SelfRegisteringRemote(c);
            remote.setRemoteServer(new SeleniumServer(configuration));
            remote.startRemoteServer();
            log.info("Selenium Grid node is up and ready to register to the hub");
            remote.startRegistrationProcess();
          }
        });

    try {
      Class.forName(CORE_RUNNER_CLASS, false, GridLauncherV3.class.getClassLoader());

      launchers.put("corerunner", () -> new GridItemLauncher() {
        @Override
        void setConfiguration(String[] args) {
          configuration = new CoreRunnerConfiguration();
          new JCommander(configuration, args);
          helpRequested = configuration.help;
        }

        @Override
        void launch() throws Exception {
          Class<?> coreRunnerClass = Class.forName(CORE_RUNNER_CLASS);
          Object coreRunner = coreRunnerClass.newInstance();
          Method mainInt = coreRunnerClass.getMethod("mainInt", String[].class);

          CoreRunnerConfiguration runnerConfig = (CoreRunnerConfiguration) this.configuration;
          String[] args = new String[] {
            /* Results file */ runnerConfig.htmlSuite.get(3),
            /* suite */ runnerConfig.htmlSuite.get(2),
            /* start url */ runnerConfig.htmlSuite.get(1),
            /* multi window */ "true",
            /* browser string */ runnerConfig.htmlSuite.get(0),
          };
          Integer result = (Integer) mainInt.invoke(coreRunner, (Object) args);
          System.exit(result);
        }
      });
    } catch (ReflectiveOperationException e) {
      // Do nothing. It's fine.
    }

    return launchers.build();
  }
}
