/*
 * 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.geode.cache.client.internal;

import static org.apache.geode.distributed.ConfigurationProperties.ENABLE_CLUSTER_CONFIGURATION;
import static org.apache.geode.distributed.ConfigurationProperties.GROUPS;
import static org.apache.geode.distributed.ConfigurationProperties.LOCATORS;
import static org.apache.geode.distributed.ConfigurationProperties.LOG_LEVEL;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.apache.geode.distributed.ConfigurationProperties.START_LOCATOR;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.lang.StringUtils;

import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.client.Pool;
import org.apache.geode.cache.client.PoolManager;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.cache.server.ServerLoadProbe;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.distributed.Locator;
import org.apache.geode.internal.cache.PoolFactoryImpl;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.Invoke;
import org.apache.geode.test.dunit.LogWriterUtils;
import org.apache.geode.test.dunit.NetworkUtils;
import org.apache.geode.test.dunit.SerializableCallable;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;

/**
 *
 */
public abstract class LocatorTestBase extends JUnit4DistributedTestCase {
  protected static final String CACHE_KEY = "CACHE";
  protected static final String LOCATOR_KEY = "LOCATOR";
  protected static final String REGION_NAME = "A_REGION";
  protected static final String POOL_NAME = "daPool";
  protected static final Object CALLBACK_KEY = "callback";
  /**
   * A map for storing temporary objects in a remote VM so that they can be used between calls.
   * Cleared after each test.
   */
  protected static final HashMap remoteObjects = new HashMap();

  public LocatorTestBase() {
    super();
  }

  @Override
  public final void preTearDown() throws Exception {

    SerializableRunnable tearDown = new SerializableRunnable("tearDown") {
      public void run() {
        Locator locator = (Locator) remoteObjects.get(LOCATOR_KEY);
        if (locator != null) {
          try {
            locator.stop();
          } catch (Exception e) {
            // do nothing
          }
        }

        Cache cache = (Cache) remoteObjects.get(CACHE_KEY);
        if (cache != null) {
          try {
            cache.close();
          } catch (Exception e) {
            // do nothing
          }
        }
        remoteObjects.clear();

      }
    };
    // We seem to like leaving the DS open if we can for
    // speed, but lets at least destroy our cache and locator.
    Invoke.invokeInEveryVM(tearDown);
    tearDown.run();

    postTearDownLocatorTestBase();
  }

  protected void postTearDownLocatorTestBase() throws Exception {}

  protected void startLocator(final String hostName, final int locatorPort,
      final String otherLocators) {
    final String testName = getUniqueName();
    disconnectFromDS();
    Properties props = new Properties();
    props.setProperty(MCAST_PORT, String.valueOf(0));
    props.setProperty(LOCATORS, otherLocators);
    props.setProperty(LOG_LEVEL, LogWriterUtils.getDUnitLogLevel());
    props.setProperty(ENABLE_CLUSTER_CONFIGURATION, "false");
    try {
      File logFile = new File(testName + "-locator" + locatorPort + ".log");
      InetAddress bindAddr = null;
      try {
        bindAddr = InetAddress.getByName(hostName);
      } catch (UnknownHostException uhe) {
        Assert.fail("While resolving bind address ", uhe);
      }
      Locator locator = Locator.startLocatorAndDS(locatorPort, logFile, bindAddr, props);
      remoteObjects.put(LOCATOR_KEY, locator);
    } catch (IOException ex) {
      Assert.fail("While starting locator on port " + locatorPort, ex);
    }
  }

  protected void startLocatorInVM(final VM vm, final int locatorPort, final String otherLocators) {
    vm.invoke(new SerializableRunnable("Create Locator") {
      public void run() {
        String hostName = NetworkUtils.getServerHostName(vm.getHost());
        startLocator(hostName, locatorPort, otherLocators);
      }
    });
  }

  protected void stopLocatorInVM(VM vm) {
    vm.invoke(new SerializableRunnable("Stop Locator") {
      public void run() {
        stopLocator();
      }
    });
  }

  protected void stopLocator() {
    Locator locator = (Locator) remoteObjects.remove(LOCATOR_KEY);
    locator.stop();
  }

  protected int startBridgeServerInVM(VM vm, String[] groups, String locators) {
    return startBridgeServerInVM(vm, groups, locators, new String[] {REGION_NAME});
  }

  protected int addCacheServer(final String[] groups) throws IOException {
    Cache cache = (Cache) remoteObjects.get(CACHE_KEY);
    CacheServer server = cache.addCacheServer();
    server.setPort(0);
    server.setGroups(groups);
    server.start();
    return new Integer(server.getPort());
  }

  protected int addCacheServerInVM(VM vm, final String[] groups) {
    SerializableCallable connect = new SerializableCallable("Add Bridge server") {
      public Object call() throws Exception {
        return addCacheServer(groups);
      }
    };
    Integer port = (Integer) vm.invoke(connect);
    return port.intValue();
  }

  protected int startBridgeServerInVM(VM vm, final String[] groups, final String locators,
      final String[] regions) {
    return startBridgeServerInVM(vm, groups, locators, regions, CacheServer.DEFAULT_LOAD_PROBE,
        false);
  }

  protected int startBridgeServerInVM(VM vm, String[] groups, String locators,
      boolean useGroupsProperty) {
    return startBridgeServerInVM(vm, groups, locators, new String[] {REGION_NAME},
        CacheServer.DEFAULT_LOAD_PROBE, useGroupsProperty);
  }

  protected int startBridgeServerInVM(VM vm, final String[] groups, final String locators,
      final String[] regions, final ServerLoadProbe probe, final boolean useGroupsProperty) {
    SerializableCallable connect = new SerializableCallable("Start bridge server") {
      public Object call() throws IOException {
        return startBridgeServer(groups, locators, regions, probe, useGroupsProperty);
      }
    };
    Integer port = (Integer) vm.invoke(connect);
    return port.intValue();
  }

  protected int startBridgeServer(String[] groups, String locators) throws IOException {
    return startBridgeServer(groups, locators, new String[] {REGION_NAME});
  }

  protected int startBridgeServer(final String[] groups, final String locators,
      final String[] regions) throws IOException {
    return startBridgeServer(groups, locators, regions, CacheServer.DEFAULT_LOAD_PROBE, false);
  }

  protected int startBridgeServer(final String[] groups, final String locators,
      final String[] regions, final ServerLoadProbe probe, final boolean useGroupsProperty)
      throws IOException {
    Properties props = new Properties();
    props.setProperty(MCAST_PORT, "0");
    props.setProperty(LOCATORS, locators);
    if (useGroupsProperty && groups != null) {
      props.setProperty(GROUPS, StringUtils.join(groups, ","));
    }
    DistributedSystem ds = getSystem(props);
    Cache cache = CacheFactory.create(ds);
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.DISTRIBUTED_ACK);
    factory.setEnableBridgeConflation(true);
    factory.setDataPolicy(DataPolicy.REPLICATE);
    RegionAttributes attrs = factory.create();
    for (int i = 0; i < regions.length; i++) {
      cache.createRegion(regions[i], attrs);
    }
    CacheServer server = cache.addCacheServer();
    server.setPort(0);
    if (!useGroupsProperty) {
      server.setGroups(groups);
    }
    server.setLoadProbe(probe);
    server.start();

    remoteObjects.put(CACHE_KEY, cache);

    return new Integer(server.getPort());
  }

  protected int startBridgeServerWithEmbeddedLocator(final String[] groups, final String locators,
      final String[] regions, final ServerLoadProbe probe) throws IOException {
    Properties props = new Properties();
    props.setProperty(MCAST_PORT, String.valueOf(0));
    props.setProperty(START_LOCATOR, locators);
    props.setProperty(LOCATORS, locators);
    DistributedSystem ds = getSystem(props);
    Cache cache = CacheFactory.create(ds);
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.DISTRIBUTED_ACK);
    factory.setEnableBridgeConflation(true);
    factory.setDataPolicy(DataPolicy.REPLICATE);
    RegionAttributes attrs = factory.create();
    for (int i = 0; i < regions.length; i++) {
      cache.createRegion(regions[i], attrs);
    }
    CacheServer server = cache.addCacheServer();
    server.setGroups(groups);
    server.setLoadProbe(probe);
    server.setPort(0);
    server.start();

    remoteObjects.put(CACHE_KEY, cache);

    return new Integer(server.getPort());
  }

  protected int startBridgeServerWithEmbeddedLocatorInVM(VM vm, final String[] groups,
      final String locators, final String[] regions, final ServerLoadProbe probe) {
    SerializableCallable connect = new SerializableCallable("Start bridge server") {
      public Object call() throws IOException {
        return startBridgeServerWithEmbeddedLocator(groups, locators, regions, probe);
      }
    };
    Integer port = (Integer) vm.invoke(connect);
    return port.intValue();
  }

  protected void startBridgeClientInVM(VM vm, final String group, final String host, final int port)
      throws Exception {
    startBridgeClientInVM(vm, group, host, port, new String[] {REGION_NAME});
  }

  protected void startBridgeClientInVM(VM vm, final String group, final String host, final int port,
      final String[] regions) throws Exception {
    PoolFactoryImpl pf = new PoolFactoryImpl(null);
    pf.addLocator(host, port).setServerGroup(group).setPingInterval(200)
        .setSubscriptionEnabled(true).setSubscriptionRedundancy(-1);
    startBridgeClientInVM(vm, pf.getPoolAttributes(), regions);
  }

  protected void startBridgeClientInVM(VM vm, final Pool pool, final String[] regions)
      throws Exception {
    SerializableRunnable connect = new SerializableRunnable("Start bridge client") {
      public void run() throws Exception {
        startBridgeClient(pool, regions);
      }
    };
    if (vm == null) {
      connect.run();
    } else {
      vm.invoke(connect);
    }
  }

  protected void startBridgeClient(final String group, final String host, final int port)
      throws Exception {
    startBridgeClient(group, host, port, new String[] {REGION_NAME});
  }

  protected void startBridgeClient(final String group, final String host, final int port,
      final String[] regions) throws Exception {
    PoolFactoryImpl pf = new PoolFactoryImpl(null);
    pf.addLocator(host, port).setServerGroup(group).setPingInterval(200)
        .setSubscriptionEnabled(true).setSubscriptionRedundancy(-1);
    startBridgeClient(pf.getPoolAttributes(), regions);
  }

  protected void startBridgeClient(final Pool pool, final String[] regions) throws Exception {
    Properties props = new Properties();
    props.setProperty(MCAST_PORT, String.valueOf(0));
    props.setProperty(LOCATORS, "");
    DistributedSystem ds = getSystem(props);
    Cache cache = CacheFactory.create(ds);
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.LOCAL);
    factory.setPoolName(POOL_NAME);
    PoolFactoryImpl pf = (PoolFactoryImpl) PoolManager.createFactory();
    pf.init(pool);
    LocatorDiscoveryCallback locatorCallback = new MyLocatorCallback();
    remoteObjects.put(CALLBACK_KEY, locatorCallback);
    pf.setLocatorDiscoveryCallback(locatorCallback);
    pf.create(POOL_NAME);

    RegionAttributes attrs = factory.create();
    for (int i = 0; i < regions.length; i++) {
      cache.createRegion(regions[i], attrs);
    }

    remoteObjects.put(CACHE_KEY, cache);
  }

  protected void stopBridgeMemberVM(VM vm) {
    vm.invoke(new SerializableRunnable("Stop bridge member") {
      public void run() {
        Cache cache = (Cache) remoteObjects.remove(CACHE_KEY);
        cache.close();
        disconnectFromDS();
      }
    });
  }

  public String getLocatorString(Host host, int locatorPort) {
    return getLocatorString(host, new int[] {locatorPort});
  }

  public String getLocatorString(Host host, int[] locatorPorts) {
    StringBuffer str = new StringBuffer();
    for (int i = 0; i < locatorPorts.length; i++) {
      str.append(NetworkUtils.getServerHostName(host)).append("[").append(locatorPorts[i])
          .append("]");
      if (i < locatorPorts.length - 1) {
        str.append(",");
      }
    }

    return str.toString();
  }

  protected static class MyLocatorCallback extends LocatorDiscoveryCallbackAdapter {

    private final Set discoveredLocators = new HashSet();
    private final Set removedLocators = new HashSet();

    public synchronized void locatorsDiscovered(List locators) {
      discoveredLocators.addAll(locators);
      notifyAll();
    }

    public synchronized void locatorsRemoved(List locators) {
      removedLocators.addAll(locators);
      notifyAll();
    }

    public boolean waitForDiscovery(InetSocketAddress locator, long time)
        throws InterruptedException {
      return waitFor(discoveredLocators, locator, time);
    }

    public boolean waitForRemove(InetSocketAddress locator, long time) throws InterruptedException {
      return waitFor(removedLocators, locator, time);
    }

    private synchronized boolean waitFor(Set set, InetSocketAddress locator, long time)
        throws InterruptedException {
      long remaining = time;
      long endTime = System.currentTimeMillis() + time;
      while (!set.contains(locator) && remaining >= 0) {
        wait(remaining);
        remaining = endTime - System.currentTimeMillis();
      }
      return set.contains(locator);
    }

    public synchronized Set getDiscovered() {
      return new HashSet(discoveredLocators);
    }

  }

}
