/*
 * 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.uima.collection.impl.cpm.container.deployer.vns;

import java.net.ConnectException;
import java.util.ArrayList;

import org.apache.uima.UIMAFramework;
import org.apache.uima.collection.impl.cpm.utils.CPMUtils;
import org.apache.uima.util.Level;
import org.apache.vinci.transport.BaseClient;
import org.apache.vinci.transport.VinciFrame;

/**
 * 
 * Connects to and querries a given VNS server for a list a services bound to a given name.
 * 
 */

public class VNSQuery {
  private VinciVNSQuery vnsQuery = null;

  /**
   * Connects to a VNS server identified by host and port
   * 
   * @param aVnsHost
   *          - VNS host name
   * @param aVnsPort
   *          - VNS port number
   * 
   * @throws Exception
   *           - when unable to connect to VNS
   */
  public VNSQuery(String aVnsHost, int aVnsPort) throws Exception {
    vnsQuery = new VinciVNSQuery(aVnsHost, aVnsPort);
  }

  /**
   * Returns a list of services registered in the VNS and bound to a given name.
   * 
   * @param aName
   *          - name of the service
   * 
   * @return - ArrayList of {@link VinciServiceInfo} instances
   * 
   * @throws Exception
   *           - unable to get a list
   */
  public ArrayList getServices(String aName) throws Exception {
    return vnsQuery.getVinciServices(aName);
  }

  /**
   * Returns a list of services that have not yet been assigned to any CPM proxy. It diffs the
   * current list and a new list as returned from the VNS.
   * 
   * @param aName
   *          - name of the service
   * @param assignedServices
   *          - a list of services currently in use
   * @return - ArrayList of {@link VinciServiceInfo} instances
   * 
   * @throws Exception
   *           - unable to get a list
   */
  public ArrayList getUnassignedServices(String aName, ArrayList assignedServices)
          throws Exception {
    // Retrieve a new list from the VNS
    ArrayList newList = getServices(aName);
    // Do a diff between current and new service list
    findUnassigned(assignedServices, newList);
    return newList;
  }

  /**
   * Diffs two lists of services and returns those that have not yet been assigned
   * 
   * @param oldList
   *          - current (in-use) list of services
   * @param newList
   *          - new list of services
   * 
   * @return - number of un-assigned services
   */
  public static int findUnassigned(ArrayList oldList, ArrayList newList) {
    int newServiceCount = 0;
    for (int i = 0; i < newList.size(); i++) {

      VinciServiceInfo service = (VinciServiceInfo) newList.get(i);
      if (!newService(service, oldList)) {
        ((VinciServiceInfo) newList.get(i)).setAvailable(false);
      } else {
        newServiceCount++;
      }
    }
    return newServiceCount;
  }

  /**
   * Checks if a service identified by {@link VinciServiceInfo} instance is in use. If a service
   * exists in the service list but is not assigned, that means that is available. If the service
   * does not exist in the list it is also considered available.
   * 
   * @param aService
   *          - {@link VinciServiceInfo} instance to locate in the list
   * @param oldList
   *          - list of current (in-use) services
   * 
   * @return - true, if service is available. false, otherwise
   */
  private static boolean newService(VinciServiceInfo aService, ArrayList oldList) {
    for (int i = 0; i < oldList.size(); i++) {
      VinciServiceInfo service = (VinciServiceInfo) oldList.get(i);

      if (aService.getHost().equals(service.getHost()) && aService.getPort() == service.getPort()
              && !service.isAvailable()) {
        return false;
      }
    }
    return true;
  }

  /**
   * 
   * Inner class used for accessing the VNS server.
   * 
   */
  public class VinciVNSQuery {
    private String vnsHost;

    private int vnsPort;

    BaseClient vnsConnection = null;

    /**
     * Establishes connection to a given VNS server
     * 
     * @param aVnsHost
     *          - name of the host where the VNS is running
     * @param aVnsPort
     *          - port on which the VNS is listening
     * @throws Exception
     *           - unable to connect to VNS
     */
    public VinciVNSQuery(String aVnsHost, int aVnsPort) throws Exception {
      vnsHost = aVnsHost;
      vnsPort = aVnsPort;
      vnsConnection = new BaseClient(vnsHost, vnsPort);
    }

    /**
     * Returns a list of services bound to a given name. It ONLY returns those services that are
     * actually running. The VNS may return services that are stale. Those will be filtered out.
     * 
     * @param aVinciServiceName
     *          - name of the service
     * @return - list of services bound to a given name.
     * 
     * @throws Exception
     *           - error while looking up the service
     */
    public ArrayList getVinciServices(String aVinciServiceName) throws Exception {
      ArrayList serviceList = new ArrayList();
      BaseClient client = null;
      // make sure we got a valid connection to VNS
      if (vnsConnection != null && vnsConnection.isOpen()) {
        // Set up VNS query
        VinciFrame queryFrame = new VinciFrame();
        queryFrame.fadd("vinci:COMMAND", "getlist");
        queryFrame.fadd("PREFIX", aVinciServiceName);
        // System.out.println("Query Frame:::"+queryFrame.toXML());
        // Query the VNS
        VinciFrame response = (VinciFrame) vnsConnection.sendAndReceive(queryFrame);
        ArrayList serviceFrames = response.fget("SERVICE");
        // Each service is returned in its own SERVICE frame. So cycle through those now
        // one at a time
        for (int i = 0; i < serviceFrames.size(); i++) {
          VinciFrame serviceFrame = (VinciFrame) serviceFrames.get(i);
          // Copy data from the frame ( host, port etc)
          VinciServiceInfo serviceInfo = getServiceInfo(serviceFrame);
          if (serviceInfo != null) {
            // Test the service for availability. Use only those services that respond. The list
            // may contain stale services that are not running
            try {
              // Establish a brief connection to test for availability. This test fails gracefully
              // Its not an error if the service does not respond. The retry logic is done
              // elsewhere.
              client = new BaseClient(serviceInfo.getHost(), serviceInfo.getPort());
              if (client.isOpen()) {
                if (UIMAFramework.getLogger().isLoggable(Level.FINEST)) {
                  UIMAFramework.getLogger(this.getClass()).logrb(Level.FINEST,
                          this.getClass().getName(), "initialize", CPMUtils.CPM_LOG_RESOURCE_BUNDLE,
                          "UIMA_CPM_service_active_on_port__FINEST",
                          new Object[] { Thread.currentThread().getName(),
                              serviceInfo.getServiceName(), serviceInfo.getHost(),
                              String.valueOf(serviceInfo.getPort()) });
                }
                // Service is ok, so add it to the list
                serviceList.add(serviceInfo);
              }
            } catch (ConnectException ce) {
              if (UIMAFramework.getLogger().isLoggable(Level.WARNING)) {
                UIMAFramework.getLogger(this.getClass()).logrb(Level.WARNING,
                        this.getClass().getName(), "initialize", CPMUtils.CPM_LOG_RESOURCE_BUNDLE,
                        "UIMA_CPM_service_not_active_on_port__WARNING",
                        new Object[] { Thread.currentThread().getName(),
                            serviceInfo.getServiceName(), serviceInfo.getHost(),
                            String.valueOf(serviceInfo.getPort()) });
              }
            } finally {
              // Drop the connection if necessary.
              if (client != null) {
                try {
                  client.close();
                } catch (Exception ex) {
                }
              }
            }
          }
        }
      }

      return serviceList;
    }

    /**
     * Copy service information from Vinci frame.
     * 
     * @param aServiceFrame
     *          - Vinci frame containing service info
     * 
     * @return- instance of {@link VinciServiceInfo} containing service info
     */
    private VinciServiceInfo getServiceInfo(VinciFrame aServiceFrame) {
      String serviceName = aServiceFrame.fgetString("NAME");
      String service_host_IP = aServiceFrame.fgetString("IP");
      int service_port = aServiceFrame.fgetInt("PORT");

      VinciServiceInfo serviceInfo = new VinciServiceInfo(serviceName, service_host_IP,
              service_port);
      return serviceInfo;
    }
  }

  public static void main(String[] args) {
    try {
      VNSQuery vq = new VNSQuery(args[0], Integer.parseInt(args[1]));

      ArrayList list = vq.getServices(args[2]);
      System.out.println("Got::" + list.size() + " Services");
      BaseClient client = null;
      for (int i = 0; i < list.size(); i++) {
        Object ob = list.get(i);
        if (ob != null) {
          System.out.println(((VinciServiceInfo) ob).toString());
          VinciServiceInfo serviceInfo = (VinciServiceInfo) ob;
          if (serviceInfo != null) {
            try {
              client = new BaseClient(serviceInfo.getHost(), serviceInfo.getPort());
              if (client.isOpen()) {
                System.out.println("Service::" + serviceInfo.getServiceName() + " is alive");
              }
            } catch (ConnectException ce) {
              System.out.println("Service::" + serviceInfo.getServiceName() + " is down");
            }
          }
        }

      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
