/*
 * 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.karaf.kittests;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.felix.utils.properties.InterpolationHelper;
import org.apache.karaf.util.process.PumpStreamHandler;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;

public final class Helper {

    private Helper() {
    }

    public static class Instance {
        public static final String STOPPED = "Stopped";
        public static final String STARTING = "Starting";
        public static final String STARTED = "Started";
        public static final String ERROR = "Error";

        private static final String CONFIG_PROPERTIES_FILE_NAME = "config.properties";
        private static final String KARAF_SHUTDOWN_PORT = "karaf.shutdown.port";
        private static final String KARAF_SHUTDOWN_HOST = "karaf.shutdown.host";
        private static final String KARAF_SHUTDOWN_PORT_FILE = "karaf.shutdown.port.file";
        private static final String KARAF_SHUTDOWN_COMMAND = "karaf.shutdown.command";
        private static final String KARAF_SHUTDOWN_PID_FILE = "karaf.shutdown.pid.file";
        private static final String DEFAULT_SHUTDOWN_COMMAND = "SHUTDOWN";

        private String location;
        private Process process;

        public Instance(String location, Process process) {
            this.location = location;
            this.process = process;
        }

        public boolean exists() {
            return new File(location).isDirectory();
        }

        public int getSshPort() {
            InputStream is = null;
            try {
                File f = new File(location, "etc/org.apache.karaf.shell.cfg");
                is = new FileInputStream(f);
                Properties props = new Properties();
                props.load(is);
                String loc = props.getProperty("sshPort");
                return Integer.parseInt(loc);
            } catch (Exception e) {
                return 0;
            } finally {
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        // Ignore
                    }
                }
            }
        }

        public synchronized String getState() {
            int port = getSshPort();
            if (!exists() || port <= 0) {
                return ERROR;
            }
            checkProcess();
            if (this.process == null) {
                return STOPPED;
            } else {
                try {
                    Socket s = new Socket("localhost", port);
                    s.close();
                    return STARTED;
                } catch (Exception e) {
                    // ignore
                }
                return STARTING;
            }
        }

        protected void checkProcess() {
            if (this.process != null) {
                try {
                    this.process.exitValue();
                    this.process = null;
                } catch (IllegalThreadStateException e) {
                }
            }
        }

        private static String getProperty(Map<String,String> props, String key, String deflt) {
            String res = props.get(key);
            if (res == null) {
                res = deflt;
            }
            return res;
        }

        public void stop() throws Exception {
            checkProcess();
            if (this.process != null) {
                // Try a clean shutdown
                cleanShutdown();
                if (this.process != null) {
                    this.process.destroy();
                }
            }
        }

        protected void cleanShutdown() {
            try {
                File file = new File(new File(location, "etc"), CONFIG_PROPERTIES_FILE_NAME);
                URL configPropURL = file.toURI().toURL();
                Map<String,String> props = loadPropertiesFile(configPropURL);
                props.put("karaf.base", new File(location).getCanonicalPath());
                props.put("karaf.home", new File(location).getCanonicalPath());
                props.put("karaf.data", new File(new File(location), "data").getCanonicalPath());
                InterpolationHelper.performSubstitution(props);
                int port = Integer.parseInt(getProperty(props, KARAF_SHUTDOWN_PORT, "0"));
                String host = getProperty(props, KARAF_SHUTDOWN_HOST, "localhost");
                String portFile = props.get(KARAF_SHUTDOWN_PORT_FILE);
                String shutdown = getProperty(props, KARAF_SHUTDOWN_COMMAND, DEFAULT_SHUTDOWN_COMMAND);
                if (port == 0 && portFile != null) {
                    BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(portFile)));
                    String portStr = r.readLine();
                    port = Integer.parseInt(portStr);
                    r.close();
                }
                // We found the port, try to send the command
                if (port > 0) {
                    Socket s = new Socket(host, port);
                    s.getOutputStream().write(shutdown.getBytes());
                    s.close();
                    long t = System.currentTimeMillis() + 5000;
                    do {
                        Thread.sleep(100);
                        checkProcess();
                    } while (System.currentTimeMillis() < t && process != null);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        protected static Map<String,String> loadPropertiesFile(URL configPropURL) throws Exception {
            // Read the properties file.
            Properties configProps = new Properties();
            InputStream is = null;
            try {
                is = configPropURL.openConnection().getInputStream();
                configProps.load(is);
                is.close();
            }
            catch (Exception ex) {
                System.err.println(
                        "Error loading config properties from " + configPropURL);
                System.err.println("Main: " + ex);
                try {
                    if (is != null) is.close();
                }
                catch (IOException ex2) {
                    // Nothing we can do.
                }
                return null;
            }
            return (Map) configProps;
        }

    }

    protected static final boolean IS_WINDOWS_OS = System.getProperty("os.name").toLowerCase().contains("windows");

    public static void extractKit(File targetDir) throws Exception {
        if (isWindowsOs()) {
            extractWindowsKit(targetDir);
        } else {
            extractUnixKit(targetDir);
        }
    }

    public static boolean isWindowsOs() {
        return IS_WINDOWS_OS;
    }

    protected static void extractWindowsKit(File targetDir) throws Exception {
        InputStream is = Helper.class.getResourceAsStream("/karaf.zip");
        extract(new ZipArchiveInputStream(is), targetDir);
    }

    protected static void extractUnixKit(File targetDir) throws Exception {
        InputStream is = Helper.class.getResourceAsStream("/karaf.tar.gz");
        extract(new TarArchiveInputStream(new GzipCompressorInputStream(is)), targetDir);
        File bin = new File(targetDir, "bin");
        String[] files = bin.list();
        List<String> args = new ArrayList();
        Collections.addAll(args, "chmod", "+x");
        Collections.addAll(args, files);
        Process chmod = new ProcessBuilder()
            .directory(new File(targetDir, "bin"))
            .command(args)
            .start();
        PumpStreamHandler pump = new PumpStreamHandler(System.in, System.out, System.err);
        pump.attach(chmod);
        pump.start();
        waitForProcessEnd(chmod, 5000);
    }

    protected static void extract(ArchiveInputStream is, File targetDir) throws IOException {
        try {
            if (targetDir.exists()) {
                FileUtils.forceDelete(targetDir);
            }
            targetDir.mkdirs();
            ArchiveEntry entry = is.getNextEntry();
            while (entry != null) {
                String name = entry.getName();
                name = name.substring(name.indexOf("/") + 1);
                File file = new File(targetDir, name);
                if (entry.isDirectory()) {
                    file.mkdirs();
                } else {
                    file.getParentFile().mkdirs();
                    OutputStream os = new FileOutputStream(file);
                    try {
                        IOUtils.copy(is, os);
                    } finally {
                        IOUtils.closeQuietly(os);
                    }
                }
                entry = is.getNextEntry();
            }
        } finally {
            is.close();
        }
    }

    public static Process launchScript(File homeDir, String script, String args) throws IOException {
        ProcessBuilder builder = new ProcessBuilder();
        builder.directory(homeDir);
        if (isWindowsOs()) {
            builder.command("cmd.exe", "/c", new File(homeDir, "bin\\" + script + ".bat").getAbsolutePath(), args);
        } else {
            builder.command("/bin/sh", new File(homeDir, "bin/" + script).getAbsolutePath(), args);
        }
        builder.redirectErrorStream(true);
        Process process = builder.start();
        PumpStreamHandler pump = new PumpStreamHandler(System.in, System.out, System.err);
        pump.attach(process);
        pump.start();
        return process;
    }

    public static Instance startKaraf(File home) throws Exception {
        Process karaf = launchScript(home, "karaf", "server");
        return new Instance(home.getAbsolutePath(), karaf);
//        InstanceImpl instance = new InstanceImpl(null, "root", home.getAbsolutePath(), null, true);
//        instance.attach(karaf.getPid());
//        return instance;
    }

    public static void waitForKarafStarted(Instance karaf, long timeout) throws Exception {
        for (int i = 0; i < timeout / 100; i++) {
            if (Instance.STARTING.equals(karaf.getState())) {
                Thread.sleep(100);
            } else {
                break;
            }
        }
        if (!Instance.STARTED.equals(karaf.getState())) {
            throw new Exception("Karaf did not start correctly");
        }
    }

    public static void waitForKarafStopped(Instance karaf, long timeout) throws Exception {
        waitForProcessEnd(karaf.process, timeout);
    }

    public static void waitForProcessEnd(Process process, long timeout) throws Exception {
        for (int i = 0; i < timeout / 100; i++) {
            try {
                process.exitValue();
                return;
            } catch (IllegalThreadStateException e) {
            }
            Thread.sleep(100);
        }
        throw new Exception("Process is still running");
    }

    public static void kill(Process process) {
        try {
            process.destroy();
        } catch (Throwable e) {
        }
    }

    public static void kill(Instance instance) {
        try {
            instance.stop();
        } catch (Throwable e) {
        }
    }

}
