/**
 * Copyright 2014 Cloudera Inc.
 *
 * Licensed 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.kitesdk.minicluster;

import com.google.common.base.Preconditions;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.List;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileUtil;
import org.apache.zookeeper.server.NIOServerCnxnFactory;
import org.apache.zookeeper.server.ZooKeeperServer;
import org.apache.zookeeper.server.persistence.FileTxnLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A Zookeeper minicluster service implementation.
 * 
 * This class was ripped from MiniZooKeeperCluster from the HBase tests. Changes
 * made include:
 * 
 * 1. It will now only launch 1 zookeeper server.
 * 
 * 2. It will only attempt to bind to the port specified, and will fail if it
 * can't.
 * 
 * 3. The startup method now takes a bindAddress, which allows us to configure
 * which IP the ZK server binds to. This was not configurable in the original
 * class.
 * 
 * 4. The ZK cluster will re-use a data dir on the local filesystem if it
 * already exists instead of blowing it away.
 */
public class ZookeeperService implements Service {

  private static final Logger logger = LoggerFactory
      .getLogger(ZookeeperService.class);

  static {
    MiniCluster.registerService(ZookeeperService.class);
  }

  private static final int TICK_TIME = 2000;
  private static final int CONNECTION_TIMEOUT = 30000;

  /**
   * Configuration settings
   */
  private Configuration hadoopConf;
  private String workDir;
  private Integer clientPort = 2828;
  private String bindIP = "127.0.0.1";
  private Boolean clean = false;
  private int tickTime = 0;

  /**
   * Embedded ZooKeeper cluster
   */
  private NIOServerCnxnFactory standaloneServerFactory;
  private ZooKeeperServer zooKeeperServer;
  private boolean started = false;

  public ZookeeperService() {
  }

  @Override
  public void configure(ServiceConfig serviceConfig) {
    this.workDir = serviceConfig.get(MiniCluster.WORK_DIR_KEY);
    if (serviceConfig.contains(MiniCluster.BIND_IP_KEY)) {
      bindIP = serviceConfig.get(MiniCluster.BIND_IP_KEY);
    }
    if (serviceConfig.contains(MiniCluster.CLEAN_KEY)) {
      clean = Boolean.parseBoolean(serviceConfig.get(MiniCluster.CLEAN_KEY));
    }
    if (serviceConfig.contains(MiniCluster.ZK_PORT_KEY)) {
      clientPort = Integer.parseInt(serviceConfig.get(MiniCluster.ZK_PORT_KEY));
    }
    hadoopConf = serviceConfig.getHadoopConf();
  }

  @Override
  public Configuration getHadoopConf() {
    return hadoopConf;
  }

  @Override
  public void start() throws IOException, InterruptedException {
    Preconditions.checkState(workDir != null,
        "The localBaseFsLocation must be set before starting cluster.");

    setupTestEnv();
    stop();

    File dir = new File(workDir, "zookeeper").getAbsoluteFile();
    recreateDir(dir, clean);
    int tickTimeToUse;
    if (this.tickTime > 0) {
      tickTimeToUse = this.tickTime;
    } else {
      tickTimeToUse = TICK_TIME;
    }
    this.zooKeeperServer = new ZooKeeperServer(dir, dir, tickTimeToUse);
    standaloneServerFactory = new NIOServerCnxnFactory();

    // NOTE: Changed from the original, where InetSocketAddress was
    // originally created to bind to the wildcard IP, we now configure it.
    logger.info("Zookeeper force binding to: " + this.bindIP);
    standaloneServerFactory.configure(
        new InetSocketAddress(bindIP, clientPort), 1000);

    // Start up this ZK server
    standaloneServerFactory.startup(zooKeeperServer);

    String serverHostname;
    if (bindIP.equals("0.0.0.0")) {
      serverHostname = "localhost";
    } else {
      serverHostname = bindIP;
    }
    if (!waitForServerUp(serverHostname, clientPort, CONNECTION_TIMEOUT)) {
      throw new IOException("Waiting for startup of standalone server");
    }

    started = true;
    logger.info("Zookeeper Minicluster service started on client port: "
        + clientPort);
  }

  @Override
  public void stop() throws IOException {
    if (!started) {
      return;
    }

    standaloneServerFactory.shutdown();
    if (!waitForServerDown(clientPort, CONNECTION_TIMEOUT)) {
      throw new IOException("Waiting for shutdown of standalone server");
    }

    // clear everything
    started = false;
    standaloneServerFactory = null;
    zooKeeperServer = null;

    logger.info("Zookeeper Minicluster service shut down.");
  }

  @Override
  public List<Class<? extends Service>> dependencies() {
    return null;
  }

  private void recreateDir(File dir, boolean clean) throws IOException {
    if (dir.exists() && clean) {
      FileUtil.fullyDelete(dir);
    } else if (dir.exists() && !clean) {
      // the directory's exist, and we don't want to clean, so exit
      return;
    }
    try {
      dir.mkdirs();
    } catch (SecurityException e) {
      throw new IOException("creating dir: " + dir, e);
    }
  }

  // / XXX: From o.a.zk.t.ClientBase
  private static void setupTestEnv() {
    // during the tests we run with 100K prealloc in the logs.
    // on windows systems prealloc of 64M was seen to take ~15seconds
    // resulting in test failure (client timeout on first session).
    // set env and directly in order to handle static init/gc issues
    System.setProperty("zookeeper.preAllocSize", "100");
    FileTxnLog.setPreallocSize(100 * 1024);
  }

  // XXX: From o.a.zk.t.ClientBase
  private static boolean waitForServerDown(int port, long timeout) {
    long start = System.currentTimeMillis();
    while (true) {
      try {
        Socket sock = new Socket("localhost", port);
        try {
          OutputStream outstream = sock.getOutputStream();
          outstream.write("stat".getBytes());
          outstream.flush();
        } finally {
          sock.close();
        }
      } catch (IOException e) {
        return true;
      }

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

  // XXX: From o.a.zk.t.ClientBase
  private static boolean waitForServerUp(String hostname, int port, long timeout) {
    long start = System.currentTimeMillis();
    while (true) {
      try {
        Socket sock = new Socket(hostname, port);
        BufferedReader reader = null;
        try {
          OutputStream outstream = sock.getOutputStream();
          outstream.write("stat".getBytes());
          outstream.flush();

          Reader isr = new InputStreamReader(sock.getInputStream());
          reader = new BufferedReader(isr);
          String line = reader.readLine();
          if (line != null && line.startsWith("Zookeeper version:")) {
            return true;
          }
        } finally {
          sock.close();
          if (reader != null) {
            reader.close();
          }
        }
      } catch (IOException e) {
        // ignore as this is expected
        logger.info("server " + hostname + ":" + port + " not up " + e);
      }

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