/**
 * 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.wildfly.camel.test.common.zookeeper;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.TimeUnit;

import org.apache.camel.util.IOHelper;
import org.apache.zookeeper.server.NIOServerCnxnFactory;
import org.apache.zookeeper.server.ZooKeeperServer;
import org.apache.zookeeper.server.persistence.FileTxnSnapLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wildfly.camel.test.common.utils.AvailablePortFinder;

public class EmbeddedZookeeper {

    private static final Logger LOG = LoggerFactory.getLogger(EmbeddedZookeeper.class);

    private final NIOServerCnxnFactory connectionFactory;
    private final ZooKeeperServer zkServer;
    private final Path zookeeperBaseDir;
    private final int port;

    public EmbeddedZookeeper() throws Exception {
        this(AvailablePortFinder.getNextAvailable(), Files.createTempDirectory(Paths.get("target").toAbsolutePath(), "zktemp"));
    }

    public EmbeddedZookeeper(int port, Path baseDir) throws Exception {
        this.port = port;

        zookeeperBaseDir = baseDir;

        zkServer = new ZooKeeperServer();
        File dataDir = zookeeperBaseDir.resolve("log").toFile();
        File snapDir = zookeeperBaseDir.resolve("data").toFile();
        FileTxnSnapLog ftxn = new FileTxnSnapLog(dataDir, snapDir);
        zkServer.setTxnLogFactory(ftxn);
        zkServer.setTickTime(1000);
        connectionFactory = new NIOServerCnxnFactory() {
            @Override
            protected void configureSaslLogin() throws IOException {
                // do nothing
            }
        };
        connectionFactory.configure(new InetSocketAddress("localhost", port), 0);
    }

    public int getServerPort() {
        return port;
    }

    public String getConnection() {
        return "localhost:" + port;
    }

    public EmbeddedZookeeper startup(int timeout, TimeUnit unit) throws Exception {
        connectionFactory.startup(zkServer);
        if (timeout > 0) {
            waitForServerUp(getConnection(), unit.toMillis(timeout));
        }
        return this;
    }

    public void shutdown() throws Exception {
        try {
            connectionFactory.shutdown();
            connectionFactory.join();
            zkServer.shutdown();

            while (zkServer.isRunning()) {
                zkServer.shutdown();
                Thread.sleep(100);
            }
        } finally {
            try {
                cleanZookeeperDir();
            } catch (Exception e) {
                LOG.warn("Error cleaning up ZK data directory {}", e);
            }
        }
    }

    public static boolean waitForServerUp(String hp, long timeout) {
        long start = System.currentTimeMillis();
        while (true) {
            try {
                // if there are multiple hostports, just take the first one
                hp = hp.split(",")[0];
                String result = send4LetterWord(hp, "stat");
                if (result.startsWith("Zookeeper version:")) {
                    return true;
                }
            } catch (IOException e) {
                LOG.info("server {} not up {}", hp, e);
            }

            if (System.currentTimeMillis() > start + timeout) {
                break;
            }
            try {
                Thread.sleep(250);
            } catch (InterruptedException e) {
                // ignore
            }
        }
        return false;
    }

    private static String send4LetterWord(String hp, String cmd) throws IOException {
        String split[] = hp.split(":");
        String host = split[0];
        int port;
        try {
            port = Integer.parseInt(split[1]);
        } catch (RuntimeException e) {
            throw new RuntimeException("Problem parsing " + hp + e.toString());
        }

        Socket sock = new Socket(host, port);
        BufferedReader reader = null;
        try {
            OutputStream outstream = sock.getOutputStream();
            outstream.write(cmd.getBytes());
            outstream.flush();

            reader = IOHelper.buffered(new InputStreamReader(sock.getInputStream()));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line + "\n");
            }
            return sb.toString();
        } finally {
            sock.close();
            if (reader != null) {
                reader.close();
            }
        }
    }

    private void cleanZookeeperDir() throws Exception {
        Files.walkFileTree(zookeeperBaseDir, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exception) throws IOException {
                exception.printStackTrace();
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exception) throws IOException {
                if (exception == null) {
                    Files.delete(dir);
                }
                return FileVisitResult.CONTINUE;
            }
        });
    }
}
