/**
 * 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.cxf.testutil.common;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.StringUtils;

public class ServerLauncher {
    public static final long DEFAULT_TIMEOUT = TimeUnit.MINUTES.toMillis(1L);

    protected static final String SERVER_FAILED =
        "server startup failed (not a log message)";

    private static final boolean DEFAULT_IN_PROCESS = false;

    private static final Logger LOG = LogUtils.getLogger(ServerLauncher.class);

    private static final boolean DEBUG = false;

    private static final String JAVA_EXE =
        System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";

    boolean serverPassed;
    final String className;

    private boolean inProcess = DEFAULT_IN_PROCESS;
    private AbstractTestServerBase inProcessServer;

    private Process process;
    private boolean serverIsReady;
    private boolean serverIsStopped;
    private boolean serverLaunchFailed;
    private Map<String, String> properties;
    private String[] serverArgs;

    private final Mutex mutex = new Mutex();

    public ServerLauncher(String theClassName) {
        this(theClassName, DEFAULT_IN_PROCESS);
    }
    public ServerLauncher(AbstractTestServerBase b) {
        inProcess = true;
        inProcessServer = b;
        className = null;
    }
    public ServerLauncher(String theClassName, boolean inprocess) {
        inProcess = inprocess;
        className = theClassName;
    }
    public ServerLauncher(String theClassName, Map<String, String> p, String[] args) {
        this(theClassName, p, args, false);
    }
    public ServerLauncher(String theClassName, Map<String, String> p, String[] args, boolean inprocess) {
        className = theClassName;
        properties = p;
        serverArgs = args;
        inProcess = inprocess;
    }

    private boolean waitForServerToStop() {
        synchronized (mutex) {
            TimeoutCounter tc = new TimeoutCounter(DEFAULT_TIMEOUT);
            while (!serverIsStopped) {
                try {
                    mutex.wait(1000L);
                    if (tc.isTimeoutExpired()) {
                        System.out.println("destroying server process");
                        process.destroy();
                        break;
                    }
                } catch (InterruptedException ex) {
                    //ex.printStackTrace();
                }
            }
            if (!inProcess) {
                //wait for process to end...
                tc = new TimeoutCounter(DEFAULT_TIMEOUT);
                while (!tc.isTimeoutExpired()) {
                    try {
                        process.exitValue();
                        break;
                    } catch (IllegalThreadStateException ex) {
                        //ignore, process hasn't ended
                        try {
                            mutex.wait(1000L);
                        } catch (InterruptedException ex1) {
                            //ignore
                        }
                    }
                }
                if (tc.isTimeoutExpired()) {
                    process.destroy();
                }
            }
        }
        return serverIsStopped;
    }

    public void signalStop() throws IOException {
        if (process != null) {
            process.getOutputStream().write('q');
            process.getOutputStream().write('\n');
            process.getOutputStream().flush();
        }
    }
    public boolean stopServer() throws IOException {
        if (inProcess) {
            try {
                return inProcessServer.stopInProcess();
            } catch (Exception ex) {
                ex.printStackTrace();
                throw new IOException(ex.getMessage());
            }
        }
        if (process != null) {
            if (!serverIsStopped) {
                try {
                    signalStop();
                } catch (IOException ex) {
                    //ignore
                }
            }
            waitForServerToStop();
            process.destroy();
        }
        return serverPassed;
    }

    public boolean launchServer() throws IOException {

        serverIsReady = false;
        serverLaunchFailed = false;

        if (inProcess) {
            Class<?> cls;
            Map<String, String> old = new HashMap<>();
            try {
                if (null != properties) {
                    for (Map.Entry<String, String> entry : properties.entrySet()) {
                        old.put(entry.getKey(), System.getProperty(entry.getKey()));
                        if (entry.getValue() == null) {
                            System.clearProperty(entry.getKey());
                        } else {
                            System.setProperty(entry.getKey(), entry.getValue());
                        }
                    }
                }
                if (inProcessServer == null) {
                    cls = Class.forName(className);
                    Class<? extends AbstractTestServerBase> svcls =
                        cls.asSubclass(AbstractTestServerBase.class);
                    if (null == serverArgs) {
                        inProcessServer = svcls.newInstance();
                    } else {
                        Constructor<? extends AbstractTestServerBase> ctor
                            = svcls.getConstructor(serverArgs.getClass());
                        inProcessServer = ctor.newInstance(new Object[] {serverArgs});
                    }
                }
                inProcessServer.startInProcess();
                serverIsReady = true;
            } catch (Throwable ex) {
                ex.printStackTrace();
                serverLaunchFailed = true;
            } finally {
                for (Map.Entry<String, String> entry : old.entrySet()) {
                    if (entry.getValue() == null) {
                        System.clearProperty(entry.getKey());
                    } else {
                        System.setProperty(entry.getKey(), entry.getValue());
                    }
                }

            }
        } else {
            List<String> cmd = getCommand();

            LOG.fine("CMD: " + cmd);
            if (DEBUG) {
                System.err.print("CMD: " + cmd);
            }


            ProcessBuilder pb = new ProcessBuilder(cmd);
            pb.redirectErrorStream(true);
            process = pb.start();

            OutputMonitorThread out = new OutputMonitorThread(process.getInputStream());
            out.start();

            synchronized (mutex) {
                TimeoutCounter tc = new TimeoutCounter(DEFAULT_TIMEOUT);
                while (!(serverIsReady || serverLaunchFailed)) {
                    try {
                        mutex.wait(1000L);
                        if (tc.isTimeoutExpired()) {
                            break;
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            if (serverLaunchFailed || !serverIsReady) {
                System.err.println(out.getServerOutput());
            }

        }
        return serverIsReady && !serverLaunchFailed;
    }

    public int waitForServer() {
        int ret = -1;
        try {
            process.waitFor();
            ret = process.exitValue();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return ret;
    }

    private class OutputMonitorThread extends Thread {
        InputStream in;
        StringBuilder serverOutputAll = new StringBuilder();

        OutputMonitorThread(InputStream i) {
            in = i;
        }
        public String getServerOutput() {
            return serverOutputAll.toString();
        }

        public void run() {
            String outputDir = System.getProperty("server.output.dir", "target/surefire-reports/");
            OutputStream os = null;
            try {
                Path logFile = Paths.get(outputDir + className + ".out");
                Files.createDirectories(logFile.getParent());
                os = Files.newOutputStream(logFile);
            } catch (IOException ex) {
                if (!ex.getMessage().contains("Stream closed")) {
                    ex.printStackTrace();
                }
            }

            try (PrintStream ps = new PrintStream(os)) {
                StringBuilder serverOutput = new StringBuilder();
                for (int ch = in.read(); ch != -1; ch = in.read()) {
                    serverOutput.append((char)ch);
                    if (ch == '\n') {
                        final String line = serverOutput.toString();
                        serverOutput.setLength(0);
                        serverOutputAll.append(line);
                        if (DEBUG) {
                            System.err.print(line);
                        }
                        if (line.contains("server ready")) {
                            notifyServerIsReady();
                        } else if (line.contains("server passed")) {
                            serverPassed = true;
                        } else if (line.contains("server stopped")) {
                            notifyServerIsStopped();
                        } else if (line.contains(SERVER_FAILED)) {
                            notifyServerFailed();
                        }
                        ps.print(line);
                        if (serverOutputAll.length() > 64000) {
                            serverOutputAll.delete(0, 10000);
                        }
                    }
                }
            } catch (IOException ex) {
                if (!ex.getMessage().contains("Stream closed")) {
                    ex.printStackTrace();
                }
            }
        }
    }

    void notifyServerIsReady() {
        synchronized (mutex) {
            serverIsReady = true;
            mutex.notifyAll();
        }
    }

    void notifyServerIsStopped() {
        synchronized (mutex) {
            LOG.info("notify server stopped");
            serverIsStopped = true;
            mutex.notifyAll();
        }
    }

    void notifyServerFailed() {
        synchronized (mutex) {
            serverIsStopped = true;
            serverLaunchFailed = true;
            mutex.notifyAll();
        }
    }

    private List<String> getCommand() {

        List<String> cmd = new ArrayList<>();
        cmd.add(JAVA_EXE);

        if (null != properties) {
            for (Map.Entry<String, String> entry : properties.entrySet()) {
                cmd.add("-D" + entry.getKey() + "=" + entry.getValue());
            }
        }
        // expose only running server ports
        String simpleName = className.substring(className.lastIndexOf('.') + 1);
        int idx = simpleName.indexOf('$');
        if (-1 != idx) {
            simpleName = simpleName.substring(0,  idx);
        }
        for (Map.Entry<Object, Object> entry : TestUtil.getAllPorts().entrySet()) {
            final String key = entry.getKey().toString();
            if (key.contains(simpleName)) {
                cmd.add("-D" + key + "=" + entry.getValue());
            }
        }
        String vmargs = System.getProperty("server.launcher.vmargs");
        if (StringUtils.isEmpty(vmargs)) {
            cmd.add("-ea");
        } else {
            vmargs = vmargs.trim();
            idx = vmargs.indexOf(' ');
            while (idx != -1) {
                cmd.add(vmargs.substring(0, idx));
                vmargs = vmargs.substring(idx + 1);
                idx = vmargs.indexOf(' ');
            }
            cmd.add(vmargs);
        }

        String portClose = System.getProperty("org.apache.cxf.transports.http_jetty.DontClosePort");
        if (portClose != null) {
            cmd.add("-Dorg.apache.cxf.transports.http_jetty.DontClosePort=" + portClose);
        }
        String loggingPropertiesFile = System.getProperty("java.util.logging.config.file");
        if (null != loggingPropertiesFile) {
            cmd.add("-Djava.util.logging.config.file=" + loggingPropertiesFile);
        }

        cmd.add("-classpath");
        StringBuilder classpath = new StringBuilder(System.getProperty("java.class.path"));
        if (classpath.indexOf("/.compatibility/") != -1) {
            classpath.append(':');
            //on OSX, the compatibility lib brclasspath.indexOf("/.compatibility/")
            idx = classpath.indexOf("/.compatibility/");
            int idx1 = classpath.lastIndexOf(":", idx);
            int idx2 = classpath.indexOf(":", idx);
            classpath.replace(idx1, idx2, ":");
        }

        cmd.add(classpath.toString());


        // If the client set the transformer factory property,
        // we want the server to also set that property.
        String transformerProperty = System.getProperty("javax.xml.transform.TransformerFactory");
        if (null != transformerProperty) {
            cmd.add("-Djavax.xml.transform.TransformerFactory=" + transformerProperty);
        }
        String validationMode = System.getProperty("spring.validation.mode");
        if (null != validationMode) {
            cmd.add("-Dspring.validation.mode=" + validationMode);
        }
        String derbyHome = System.getProperty("derby.system.home");
        if (null != derbyHome) {
            cmd.add("-Dderby.system.home=" + derbyHome);
        }
        String tmp = System.getProperty("java.io.tmpdir");
        if (null != tmp) {
            cmd.add("-Djava.io.tmpdir=" + tmp);
        }

        cmd.add(className);

        if (null != serverArgs) {
            for (String s : serverArgs) {
                cmd.add(s);
            }
        }

        return cmd;
    }

    static class Mutex {
        // empty
    }

    static class TimeoutCounter {
        private final long expectedEndTime;

        TimeoutCounter(long theExpectedTimeout) {
            expectedEndTime = System.currentTimeMillis() + theExpectedTimeout;
        }

        public boolean isTimeoutExpired() {
            return System.currentTimeMillis() > expectedEndTime;
        }
    }
}
