/*
 * Copyright (c) 2011-2017 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
 * which is available at https://www.apache.org/licenses/LICENSE-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
 */

package io.vertx.core;

import io.vertx.core.impl.Args;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.core.metrics.MetricsOptions;
import io.vertx.core.spi.VertxMetricsFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.*;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

/**
 * A {@code main()} class that can be used to create Vert.x instance and deploy a verticle, or run a bare Vert.x instance.
 * <p>
 * This class is used by the {@code vertx} command line utility to deploy verticles from the command line.
 * <p>
 * E.g.
 * <p>
 * {@code vertx run myverticle.js}
 * <p>
 * It can also be used as the main class of an executable jar so you can run verticles directly with:
 * <p>
 * {@code java -jar myapp.jar}
 *
 * @author <a href="http://tfox.org">Tim Fox</a>
 * @deprecated Use {@link Launcher} instead
 */
@Deprecated
public class Starter {

  public static final String VERTX_OPTIONS_PROP_PREFIX = "vertx.options.";
  public static final String DEPLOYMENT_OPTIONS_PROP_PREFIX = "vertx.deployment.options.";
  public static final String METRICS_OPTIONS_PROP_PREFIX = "vertx.metrics.options.";

  private static final String PATH_SEP = System.getProperty("path.separator");
  private static final Logger log = LoggerFactory.getLogger(Starter.class);
  public static List<String> PROCESS_ARGS;

  public static void main(String[] sargs) {
    Args args = new Args(sargs);

    String extraCP = args.map.get("-cp");
    if (extraCP != null) {
      // If an extra CP is specified (e.g. to provide cp to a jar or cluster.xml) we must create a new classloader
      // and run the starter using that so it's visible to the rest of the code
      String[] parts = extraCP.split(PATH_SEP);
      URL[] urls = new URL[parts.length];
      for (int p = 0; p < parts.length; p++) {
        String part = parts[p];
        File file = new File(part);
        try {
          URL url = file.toURI().toURL();
          urls[p] = url;
        } catch (MalformedURLException e) {
          throw new IllegalStateException(e);
        }
      }
      ClassLoader icl = new URLClassLoader(urls, Starter.class.getClassLoader());
      ClassLoader oldTCCL = Thread.currentThread().getContextClassLoader();
      Thread.currentThread().setContextClassLoader(icl);
      try {
        Class<?> clazz = icl.loadClass(Starter.class.getName());
        Object instance = clazz.newInstance();
        Method run = clazz.getMethod("run", Args.class, String[].class);
        run.invoke(instance, args, sargs);
      } catch (Exception e) {
        throw new IllegalStateException(e);
      } finally {
        Thread.currentThread().setContextClassLoader(oldTCCL);
      }
    } else {
      // No extra CP, just invoke directly
      new Starter().run(args, sargs);
    }
  }

  public static void runCommandLine(String commandLine) {
    new Starter().run(commandLine);
  }

  protected Vertx vertx;
  protected VertxOptions options;
  protected DeploymentOptions deploymentOptions;

  protected void run(String commandLine) {
    String[] sargs = commandLine.split(" ");
    Args args = new Args(sargs);
    run(args, sargs);
  }

  protected void run(String[] sargs) {
    run(new Args(sargs), sargs);
  }

  // Note! Must be public so can be called by reflection
  public void run(Args args, String[] sargs) {

    PROCESS_ARGS = Collections.unmodifiableList(Arrays.asList(sargs));

    String main = readMainVerticleFromManifest();
    if (main != null) {
      runVerticle(main, args);
    } else {
      if (sargs.length > 0) {
        String first = sargs[0];
        if (first.equals("-version")) {
          log.info(getVersion());
          return;
        } else if (first.equals("run")) {
          if (sargs.length < 2) {
            displaySyntax();
            return;
          } else {
            main = sargs[1];
            runVerticle(main, args);
            return;
          }
        } else if (first.equals("-ha")) {
          // Create a bare instance
          runBare(args);
          return;
        }
      }
      displaySyntax();
    }
  }

  /**
   * Hook for sub classes of {@link Starter} before the vertx instance is started.
   */
  protected void beforeStartingVertx(VertxOptions options) {

  }

  /**
   * Hook for sub classes of {@link Starter} after the vertx instance is started.
   */
  protected void afterStartingVertx() {

  }

  /**
   * Hook for sub classes of {@link Starter} before the verticle is deployed.
   */
  protected void beforeDeployingVerticle(DeploymentOptions deploymentOptions) {

  }

  /**
   * A deployment failure has been encountered. You can override this method to customize the behavior.
   * By default it closes the `vertx` instance.
   */
  protected void handleDeployFailed() {
    // Default behaviour is to close Vert.x if the deploy failed
    vertx.close();
  }


  private Vertx startVertx(boolean clustered, boolean ha, Args args) {
    MetricsOptions metricsOptions;
    ServiceLoader<VertxMetricsFactory> factories = ServiceLoader.load(VertxMetricsFactory.class);
    if (factories.iterator().hasNext()) {
      VertxMetricsFactory factory = factories.iterator().next();
      metricsOptions = factory.newOptions();
    } else {
      metricsOptions = new MetricsOptions();
    }
    configureFromSystemProperties(metricsOptions, METRICS_OPTIONS_PROP_PREFIX);
    options = new VertxOptions().setMetricsOptions(metricsOptions);
    configureFromSystemProperties(options, VERTX_OPTIONS_PROP_PREFIX);
    if (clustered) {
      log.info("Starting clustering...");
      int clusterPort = args.getInt("-cluster-port");
      if (clusterPort == -1) {
        // Default to zero - this means choose an ephemeral port
        clusterPort = 0;
      }
      String clusterHost = args.map.get("-cluster-host");
      if (clusterHost == null) {
        clusterHost = getDefaultAddress();
        if (clusterHost == null) {
          log.error("Unable to find a default network interface for clustering. Please specify one using -cluster-host");
          return null;
        } else {
          log.info("No cluster-host specified so using address " + clusterHost);
        }
      }
      CountDownLatch latch = new CountDownLatch(1);
      AtomicReference<AsyncResult<Vertx>> result = new AtomicReference<>();

      options.getEventBusOptions().setClustered(true).setHost(clusterHost).setPort(clusterPort);
      if (ha) {
        String haGroup = args.map.get("-hagroup");
        int quorumSize = args.getInt("-quorum");
        options.setHAEnabled(true);
        if (haGroup != null) {
          options.setHAGroup(haGroup);
        }
        if (quorumSize != -1) {
          options.setQuorumSize(quorumSize);
        }
      }
      beforeStartingVertx(options);
      Vertx.clusteredVertx(options, ar -> {
        result.set(ar);
        latch.countDown();
      });
      try {
        if (!latch.await(2, TimeUnit.MINUTES)) {
          log.error("Timed out in starting clustered Vert.x");
          return null;
        }
      } catch (InterruptedException e) {
        log.error("Thread interrupted in startup");
        return null;
      }
      if (result.get().failed()) {
        log.error("Failed to form cluster");
        result.get().cause().printStackTrace();
        return null;
      }
      vertx = result.get().result();
    } else {
      beforeStartingVertx(options);
      vertx = Vertx.vertx(options);
    }
    addShutdownHook();
    afterStartingVertx();
    return vertx;
  }

  private void runBare(Args args) {
    // ha is necessarily true here,
    // so clustered is
    Vertx vertx = startVertx(true, true, args);
    if (vertx == null) {
      // Throwable should have been logged at this point
      return;
    }

    // As we do not deploy a verticle, other options are irrelevant (instances, worker, conf)
  }

  private void runVerticle(String main, Args args) {
    boolean ha = args.map.get("-ha") != null;
    boolean clustered = args.map.get("-cluster") != null || ha;

    Vertx vertx = startVertx(clustered, ha, args);
    if (vertx == null) {
      // Throwable should have been logged at this point
      return;
    }

    String sinstances = args.map.get("-instances");
    int instances;
    if (sinstances != null) {
      try {
        instances = Integer.parseInt(sinstances);

        if (instances != -1 && instances < 1) {
          log.error("Invalid number of instances");
          displaySyntax();
          return;
        }
      } catch (NumberFormatException e) {
        displaySyntax();
        return;
      }
    } else {
      instances = 1;
    }

    String confArg = args.map.get("-conf");
    JsonObject conf;

    if (confArg != null) {
      try (Scanner scanner = new Scanner(new File(confArg)).useDelimiter("\\A")){
        String sconf = scanner.next();
        try {
          conf = new JsonObject(sconf);
        } catch (DecodeException e) {
          log.error("Configuration file " + sconf + " does not contain a valid JSON object");
          return;
        }
      } catch (FileNotFoundException e) {
        try {
          conf = new JsonObject(confArg);
        } catch (DecodeException e2) {
          log.error("-conf option does not point to a file and is not valid JSON: " + confArg);
          return;
        }
      }
    } else {
      conf = null;
    }

    boolean worker = args.map.get("-worker") != null;
    String message = (worker) ? "deploying worker verticle" : "deploying verticle";
    deploymentOptions = new DeploymentOptions();
    configureFromSystemProperties(deploymentOptions, DEPLOYMENT_OPTIONS_PROP_PREFIX);

    deploymentOptions.setConfig(conf).setWorker(worker).setHa(ha).setInstances(instances);

    beforeDeployingVerticle(deploymentOptions);
    vertx.deployVerticle(main, deploymentOptions, createLoggingHandler(message, res -> {
      if (res.failed()) {
        // Failed to deploy
        handleDeployFailed();
      }
    }));
  }

  private <T> Handler<AsyncResult<T>> createLoggingHandler(final String message, final Handler<AsyncResult<T>> completionHandler) {
    return res -> {
      if (res.failed()) {
        Throwable cause = res.cause();
        if (cause instanceof VertxException) {
          VertxException ve = (VertxException)cause;
          log.error(ve.getMessage());
          if (ve.getCause() != null) {
            log.error(ve.getCause());
          }
        } else {
          log.error("Failed in " + message, cause);
        }
      } else {
        log.info("Succeeded in " + message);
      }
      if (completionHandler != null) {
        completionHandler.handle(res);
      }
    };
  }

  private void configureFromSystemProperties(Object options, String prefix) {
    Properties props = System.getProperties();
    Enumeration e = props.propertyNames();
    // Uhh, properties suck
    while (e.hasMoreElements()) {
      String propName = (String)e.nextElement();
      String propVal = props.getProperty(propName);
      if (propName.startsWith(prefix)) {
        String fieldName = propName.substring(prefix.length());
        Method setter = getSetter(fieldName, options.getClass());
        if (setter == null) {
          log.warn("No such property to configure on options: " + options.getClass().getName() + "." + fieldName);
          continue;
        }
        Class<?> argType = setter.getParameterTypes()[0];
        Object arg;
        try {
          if (argType.equals(String.class)) {
            arg = propVal;
          } else if (argType.equals(int.class)) {
            arg = Integer.valueOf(propVal);
          } else if (argType.equals(long.class)) {
            arg = Long.valueOf(propVal);
          } else if (argType.equals(boolean.class)) {
            arg = Boolean.valueOf(propVal);
          } else if (argType.isEnum()){
            arg = Enum.valueOf((Class<? extends Enum>)argType, propVal);
          } else {
            log.warn("Invalid type for setter: " + argType);
            continue;
          }
        } catch (IllegalArgumentException e2) {
          log.warn("Invalid argtype:" + argType + " on options: " + options.getClass().getName() + "." + fieldName);
          continue;
        }
        try {
          setter.invoke(options, arg);
        } catch (Exception ex) {
          throw new VertxException("Failed to invoke setter: " + setter, ex);
        }
      }
    }
  }

  private Method getSetter(String fieldName, Class<?> clazz) {
    Method[] meths = clazz.getDeclaredMethods();
    for (Method meth: meths) {
      if (("set" + fieldName).toLowerCase().equals(meth.getName().toLowerCase())) {
        return meth;
      }
    }
    return null;
  }

  private void addShutdownHook() {
    Runtime.getRuntime().addShutdownHook(new Thread() {
      public void run() {
        CountDownLatch latch = new CountDownLatch(1);
        vertx.close(ar -> {
          if (!ar.succeeded()) {
            log.error("Failure in stopping Vert.x", ar.cause());
          }
          latch.countDown();
        });
        try {
          if (!latch.await(2, TimeUnit.MINUTES)) {
            log.error("Timed out waiting to undeploy all");
          }
        } catch (InterruptedException e) {
          throw new IllegalStateException(e);
        }
      }
    });
  }

  /*
  Get default interface to use since the user hasn't specified one
   */
  private String getDefaultAddress() {
    Enumeration<NetworkInterface> nets;
    try {
      nets = NetworkInterface.getNetworkInterfaces();
    } catch (SocketException e) {
      return null;
    }
    NetworkInterface netinf;
    while (nets.hasMoreElements()) {
      netinf = nets.nextElement();

      Enumeration<InetAddress> addresses = netinf.getInetAddresses();

      while (addresses.hasMoreElements()) {
        InetAddress address = addresses.nextElement();
        if (!address.isAnyLocalAddress() && !address.isMulticastAddress()
          && !(address instanceof Inet6Address)) {
          return address.getHostAddress();
        }
      }
    }
    return null;
  }

  public String getVersion() {
    try (InputStream is = getClass().getClassLoader().getResourceAsStream("META-INF/vertx/vertx-version.txt")) {
      if (is == null) {
        throw new IllegalStateException("Cannot find vertx-version.txt on classpath");
      }
      try (Scanner scanner = new Scanner(is, "UTF-8").useDelimiter("\\A")) {
        return scanner.hasNext() ? scanner.next() : "";
      }
    } catch (IOException e) {
      throw new IllegalStateException(e.getMessage());
    }
  }

  private String readMainVerticleFromManifest() {
    try {
      Enumeration<URL> resources = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF");
      while (resources.hasMoreElements()) {
        InputStream stream = null;
        try {
          stream = resources.nextElement().openStream();
          Manifest manifest = new Manifest(stream);
          Attributes attributes = manifest.getMainAttributes();
          String mainClass = attributes.getValue("Main-Class");
          if (Starter.class.getName().equals(mainClass)) {
            String theMainVerticle = attributes.getValue("Main-Verticle");
            if (theMainVerticle != null) {
              return theMainVerticle;
            }
          }
        } finally {
          closeQuietly(stream);
        }
      }
    } catch (IOException e) {
      throw new IllegalStateException(e.getMessage());
    }
    return null;
  }

  private void closeQuietly(InputStream stream) {
    if (stream != null) {
      try {
        stream.close();
      } catch (IOException e) {
        // Ignore it.
      }
    }
  }

  private void displaySyntax() {

    String usage =

        "    vertx run <main> [-options]                                                \n" +
        "        runs a verticle called <main> in its own instance of vert.x.         \n\n" +
        "    valid options are:                                                         \n" +
        "        -conf <config>         Specifies configuration that should be provided \n" +
        "                               to the verticle. <config> should reference      \n" +
        "                               either a text file containing a valid JSON      \n" +
        "                               object which represents the configuration OR    \n" +
        "                               be a JSON string.                               \n" +
        "        -instances <instances> specifies how many instances of the verticle    \n" +
        "                               will be deployed. Defaults to 1                 \n" +
        "        -worker                if specified then the verticle is a worker      \n" +
        "                               verticle.                                       \n" +
        "        -cp <classpath>        provide an extra classpath to be used for the   \n" +
        "                               verticle deployment.                            \n" +
        "        -cluster               if specified then the vert.x instance will form \n" +
        "                               a cluster with any other vert.x instances on    \n" +
        "                               the network.                                    \n" +
        "        -cluster-port          port to use for cluster communication.          \n" +
        "                               Default is 0 which means choose a spare         \n" +
        "                               random port.                                    \n" +
        "        -cluster-host          host to bind to for cluster communication.      \n" +
        "                               If this is not specified vert.x will attempt    \n" +
        "                               to choose one from the available interfaces.    \n" +
        "        -ha                    if specified the verticle will be deployed as a \n" +
        "                               high availability (HA) deployment.              \n" +
        "                               This means it can fail over to any other nodes  \n" +
        "                               in the cluster started with the same HA group   \n" +
        "        -quorum                used in conjunction with -ha this specifies the \n" +
        "                               minimum number of nodes in the cluster for any  \n" +
        "                               HA deploymentIDs to be active. Defaults to 0    \n" +
        "        -hagroup               used in conjunction with -ha this specifies the \n" +
        "                               HA group this node will join. There can be      \n" +
        "                               multiple HA groups in a cluster. Nodes will only\n" +
        "                               failover to other nodes in the same group.      \n" +
        "                               Defaults to __DEFAULT__                       \n\n" +

        "    vertx -version                                                             \n" +
        "        displays the version";

    log.info(usage);
  }
}
