/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.apache.activemq.artemis.util;

import javax.jms.Connection;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.jms.ActiveMQJMSClient;
import org.apache.activemq.artemis.jms.client.ActiveMQConnection;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;

/**
 * A tool to let clients start, stop and kill Artemis servers
 */
public class ServerUtil {

   public static Process startServer(String artemisInstance, String serverName) throws Exception {
      return startServer(artemisInstance, serverName, 0, 0);
   }

   /**
    * Start the server.
    *
    * @param serverName it will be used on logs
    * @param id         it will be used to add on the port
    */
   public static Process startServer(String artemisInstance, String serverName, int id, int timeout) throws Exception {
      return startServer(artemisInstance, serverName, id, timeout, null);
   }

   public static Process startServer(String artemisInstance, String serverName, int id, int timeout, File brokerProperties) throws Exception {
      final Process process = internalStartServer(artemisInstance, serverName, brokerProperties);

      // wait for start
      if (timeout > 0) {
         waitForServerToStart(id, timeout);
      }

      return process;
   }

   public static Process startServer(String artemisInstance, String serverName, String uri, int timeout) throws Exception {
      return  startServer(artemisInstance, serverName, uri, timeout, null);
   }

   public static Process startServer(String artemisInstance, String serverName, String uri, int timeout, File propertiesFile) throws Exception {
      final Process process = internalStartServer(artemisInstance, serverName, propertiesFile);

      // wait for start
      if (timeout != 0) {
         waitForServerToStart(uri, timeout);
      }

      return process;
   }

   private static Process internalStartServer(String artemisInstance,
                                              String serverName) throws IOException, ClassNotFoundException {
      return internalStartServer(artemisInstance, serverName, null);
   }
   private static Process internalStartServer(String artemisInstance,
                                              String serverName,
                                              File propertiesFile) throws IOException, ClassNotFoundException {

      if (propertiesFile != null) {
         return execute(artemisInstance, serverName, "run", "--properties", propertiesFile.getAbsolutePath());
      } else {
         return execute(artemisInstance, serverName, "run");
      }
   }

   public static Process execute(String artemisInstance, String jobName, String...args) throws IOException, ClassNotFoundException {
      try {
         boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().trim().startsWith("win");

         List<String> command = new ArrayList<>();

         ProcessBuilder builder = null;
         if (IS_WINDOWS) {
            command.add("cmd");
            command.add("/c");
            command.add("artemis.cmd");
         } else {
            command.add("./artemis");
         }

         for (String arg: args) {
            command.add(arg);
         }

         builder = new ProcessBuilder(command);

         builder.directory(new File(artemisInstance + "/bin"));

         final Process process = builder.start();
         Runtime.getRuntime().addShutdownHook(new Thread(() -> process.destroy()));

         ProcessLogger outputLogger = new ProcessLogger(true, process.getInputStream(), jobName, false);
         outputLogger.start();

         // Adding a reader to System.err, so the VM won't hang on a System.err.println
         ProcessLogger errorLogger = new ProcessLogger(true, process.getErrorStream(), jobName, true);
         errorLogger.start();
         return process;
      } catch (IOException e) {
         throw new IOException("Cannot start server at " + artemisInstance, e);
      }
   }

   public static boolean waitForServerToStart(int id, int timeout) throws InterruptedException {
      return waitForServerToStart(id, null, null, timeout);
   }

   public static boolean waitForServerToStart(int id, String username, String password, int timeout) throws InterruptedException {
      return waitForServerToStartOnPort(61616 + id, username, password, timeout);
   }

   public static boolean waitForServerToStartOnPort(int port, String username, String password, int timeout) throws InterruptedException {
      return waitForServerToStart("tcp://localhost:" + port, username, password, timeout);
   }

   public static boolean waitForServerToStart(String uri, long timeout) throws InterruptedException {
      return waitForServerToStart(uri, null, null, timeout);
   }

   public static boolean waitForServerToStart(String uri, String username, String password, long timeout) throws InterruptedException {
      long realTimeout = System.currentTimeMillis() + timeout;
      while (System.currentTimeMillis() < realTimeout) {
         try (ActiveMQConnectionFactory cf = ActiveMQJMSClient.createConnectionFactory(uri, null)) {
            cf.createConnection(username, password).close();
            System.out.println("server " + uri + " started");
         } catch (Exception e) {
            System.out.println("awaiting server " + uri + " start at ");
            Thread.sleep(500);
            continue;
         }
         return true;
      }

      return false;
   }

   public static void killServer(final Process server) throws Exception {
      killServer(server, false);
   }

   public static void killServer(final Process server, boolean forcibly) throws Exception {
      if (server != null) {
         System.out.println("**********************************");
         System.out.println("Killing server " + server);
         System.out.println("**********************************");
         if (forcibly) {
            server.destroyForcibly();
         } else {
            server.destroy();
         }
         server.waitFor();
         if (!forcibly) {
            Thread.sleep(1000);
         }
      }
   }

   public static int getServer(Connection connection) {
      ClientSession session = ((ActiveMQConnection) connection).getInitialSession();
      TransportConfiguration transportConfiguration = session.getSessionFactory().getConnectorConfiguration();
      String port = (String) transportConfiguration.getParams().get("port");
      return Integer.parseInt(port) - 61616;
   }

   public static Connection getServerConnection(int server, Connection... connections) {
      for (Connection connection : connections) {
         ClientSession session = ((ActiveMQConnection) connection).getInitialSession();
         TransportConfiguration transportConfiguration = session.getSessionFactory().getConnectorConfiguration();
         String port = (String) transportConfiguration.getParams().get("port");
         if (Integer.parseInt(port) == server + 61616) {
            return connection;
         }
      }
      return null;
   }

   /**
    * Redirect the input stream to a logger (as debug logs)
    */
   static class ProcessLogger extends Thread {

      private final InputStream is;

      private final String logName;

      private final boolean print;

      private final boolean sendToErr;

      ProcessLogger(final boolean print,
                    final InputStream is,
                    final String logName,
                    final boolean sendToErr) throws ClassNotFoundException {
         this.is = is;
         this.print = print;
         this.logName = logName;
         this.sendToErr = sendToErr;
         setDaemon(false);
      }

      @Override
      public void run() {
         try {
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            String line;
            while ((line = br.readLine()) != null) {
               if (print) {
                  if (sendToErr) {
                     System.err.println(logName + "-err:" + line);
                  } else {
                     System.out.println(logName + "-out:" + line);
                  }
               }
            }
         } catch (IOException e) {
            // ok, stream closed
         }
      }
   }
}
