/*
 * 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.vinci.transport.vns.service;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Hashtable;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.vinci.transport.document.XMLToVinci;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * Primary interface into the services database.
 * 
 */
public class ServiceRegistry {

  /* Constants */
  static int minAutoPort = 10000;

  static int maxAutoPort = 11000;

  static int sizeAutoPort = 10;

  static int MAX_ALIAS_LINKS = 25; // Make number of continuous alias links

  /* Instance vars */
  ServiceTree services = null;

  Hashtable bindings = null;

  /* constructor */
  public ServiceRegistry() {
    services = new ServiceTree();
    bindings = new Hashtable();
  }

  /* Methods */

  /* Method to add a binding */
  public void addBinding(Service S) {
    String ip = S.realhost;

    ArrayList v = null;
    if (ip != null)
      v = (ArrayList) bindings.get(ip);
    if (v == null)
      v = new ArrayList();

    v.add(new PortRange(S.minport, S.maxport));
    bindings.put(ip, v);
  }

  /* Method to remove a binding */
  public void delBinding(Service S) {
    ArrayList v = (ArrayList) bindings.get(S.realhost);
    if (v == null)
      return;

    v.remove(new PortRange(S.minport, S.maxport));

    if (v.size() < 1)
      bindings.remove(S.realhost);
  }

  /* Method to check for port range conflict */
  public boolean checkConflict(String ip, int min, int max) {
    ArrayList v = (ArrayList) bindings.get(ip);
    if (v == null) {
      return false;
    }
    PortRange p;
    for (int i = 0; i < v.size(); i++) {
      p = (PortRange) v.get(i);
      if ((p.minPort <= min && min <= p.maxPort) || (p.minPort <= max && max <= p.maxPort)) {
        return true;
      }
    }

    return false;
  }

  /* Method to check for binding */
  public boolean checkBinding(Service S) {
    if (S.name == null || S.name.trim().equals("")) {
      pr("Service not added: Null name");
      return false;
    }

    int mylevel = getLevel(S.name, S.level);
    S.level = "" + mylevel;

    if (services.find(S.name) != null) {
      Object O = services.find(S.name);
      if (!(O instanceof ServiceAlias)) {
        Object[] objs = ((ServiceStack) O).get("None");
        Service srv;
        for (int i = 0; i < objs.length; i++) {
          srv = (Service) objs[i];
          if (srv == null)
            continue;
          if (srv.realhost.equals(S.realhost) && srv.level.equals(S.level)
                  && srv.instance == S.instance) {
            pr("Service not added: Found one with same everything");
            return false;
          }
        }
      }
    }

    String ip = S.realhost;

    if (ip == null) {
      pr("No IP provided");
      return false; // added by RiK
    }

    pr("Checking binding for : " + ip);

    if (!(S.minport > 0 && S.maxport > 0)) {
      if (!(bindings.containsKey(ip))) {
        S.minport = minAutoPort;
        S.maxport = minAutoPort + sizeAutoPort - 1;
        pr("Binding not found");
        return true;
      }

      for (int p = minAutoPort; p < maxAutoPort; p += sizeAutoPort) {
        if (!checkConflict(ip, p, p + sizeAutoPort - 1)) {
          S.minport = p;
          S.maxport = p + sizeAutoPort - 1;
          return true;
        }
      }

      return false;
    } else {
      if (ip == null || !(bindings.containsKey(ip))) {
        return true;
      }

      // Okay to add if there isn't a conflict
      return !checkConflict(ip, S.minport, S.maxport);
    }

  }

  /* Method to add a service to the tree */
  public boolean addService(Service S) {
    pr("Adding Service : " + S.name);
    if (!checkBinding(S))
      return false;
    ServiceStack stack = getStack(S.name, true);
    if (stack == null)
      return false;
    stack.add(S);
    addBinding(S);

    return true;
  }

  /* Method to add an alias to the tree */
  public boolean addAlias(ServiceAlias S) {
    if (services.find(S.name) != null)
      return false;
    if (services.find(S.target) == null)
      return false;
    services.setitem(S.name, S);
    return true;
  }

  /* Method to del an alias from the tree */
  public boolean delAlias(String name) {
    Object o = services.find(name);
    if (o == null || !(o instanceof ServiceAlias)) {
      return false;
    }

    services.setitem(name, null);
    return true;
  }

  /* Method to add a general entry */
  public boolean addEntry(Object o) {
    if (o == null)
      return false;
    if (o instanceof ServiceAlias)
      return addAlias((ServiceAlias) o);
    else
      return addService((Service) o);
  }

  /* Method to update the service */
  public boolean updateService(Service S) {
    ServiceStack stack = getStack(S.name);
    if (stack == null)
      return false;
    return stack.update(S);
  }

  /* Method to del the service */
  public boolean delService(Service S) {
    ServiceStack stack = getStack(S.name);
    if (stack == null)
      return false;
    Object[] dellist = stack.delete(S);
    if (dellist != null) {
      for (int i = 0; i < dellist.length; i++)
        delBinding((Service) dellist[i]);
      return (dellist.length > 0);
    }

    return false;
  }

  /* Method to get a list of a particular service */
  public Service[] getServices(String name) {
    return getServices(name, null, false);
  }

  public Service[] getServices(String name, String level) {
    return getServices(name, level, false);
  }

  public Service[] getServices(String name, int level) {
    return getServices(name, "" + level, false);
  }

  public Service[] getServices(String name, int level, boolean resolveAlias) {
    return getServices(name, "" + level, resolveAlias);
  }

  // Accepts a Vinci level
  public Service[] getServices(String name, String level, boolean resolveAlias) {
    // Find the ServiceStack / ServiceAlias that matches the names specified
    Object O = services.find(name);

    if (O != null) {
      if (resolveAlias) {
        int i = 0; // Prevent an alias infinite loop
        while (ServiceAlias.isAlias(O) && i < MAX_ALIAS_LINKS) {
          O = services.find(((ServiceAlias) O).target);
          i++;
        }
      }

      if (ServiceAlias.isAlias(O))
        return null;
      else {
        // Get all the services from the ServiceStack
        // that are at or below the level specified
        Object[] temp = ((ServiceStack) O).get(level);
        Service[] result = new Service[temp.length];
        for (int i = 0; i < temp.length; i++)
          result[i] = (Service) temp[i];
        return result;
      }
    } else
      return null;
  }

  /* Helper methods */

  // Returns the absLevel as determined by the ServiceStack
  int getLevel(String name, String level) {
    ServiceStack S = getStack(name);
    if (S == null)
      return ServiceStack.getAbsLevel(level);
    return S.absLevel(level);
  }

  // Returns the ServiceStack that corresponds to the given name
  ServiceStack getStack(String name) {
    return getStack(name, false);
  }

  // Returns the ServiceStack that corresponds to the given name
  // Creates a stack if create is true, and no ServiceStack is found
  ServiceStack getStack(String name, boolean create) {
    Object o = services.find(name);

    if (o == null) {
      if (create) {
        o = new ServiceStack(name);
        services.setitem(name, o);
      }
    } else {
      if (o instanceof ServiceAlias)
        o = null;
    }

    return (ServiceStack) o;
  }

  // Returns all entries corresponding to a given name
  Object[] getEntries(String name) {
    return getEntries(name, -1);
  }

  Object[] getEntries(String name, int level) {
    return getEntries(name, "" + level);
  }

  Object[] getEntries(String name, String level) {
    Object o = services.find(name);

    if (o == null)
      return null;

    if (o instanceof ServiceAlias) {
      Object[] result = new Object[1];
      result[0] = o;
      return result;
    }

    return ((ServiceStack) o).get(level);
  }

  // Returns a list of all the services in the registry
  public Object[] listServices(String prefix, int level) {
    return listServices(prefix, "" + level);
  }

  public Object[] listServices(String prefix, String level) {
    if (prefix == null)
      prefix = "";

    ArrayList res = new ArrayList();
    Object[] items = services.findprefix(prefix);
    if (items == null)
      return res.toArray();

    for (int i = 0; i < items.length; i++) {
      if (ServiceAlias.isAlias(items[i])) {
        res.add(items[i]);
      } else {
        Object[] subitems = ((ServiceStack) items[i]).get(level);
        if (subitems != null) {
          for (int j = 0; j < subitems.length; j++)
            res.add(subitems[j]);
        }
      }
    }

    return res.toArray();
  }

  // Returns a list of all the services' names in the registry
  public String[] listNames(String prefix, int level) {
    return listNames(prefix, "" + level);
  }

  public String[] listNames(String prefix, String level) {
    if (prefix == null)
      prefix = "";

    ArrayList res = new ArrayList();
    Object[] items = services.findprefix(prefix);

    if (items == null)
      return new String[0];

    for (int i = 0; i < items.length; i++) {
      if (ServiceAlias.isAlias(items[i]))
        res.add(((ServiceAlias) items[i]).name);
      if (level == null || !((ServiceStack) items[i]).isEmpty(level)) {
        res.add(((ServiceStack) items[i]).name);
      }
    }

    String[] result = new String[res.size()];
    for (int i = 0; i < res.size(); i++)
      result[i] = res.get(i).toString();

    return result;
  }

  /* Methods to load and save registry info */
  public void load(String fname) throws Exception {

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    try {
      dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    } catch (ParserConfigurationException e) {
      System.err.println(
              "Warning: DocumentBuilderFactory doesn't support the feature http://apache.org/xml/features/disallow-doctype-decl");
    }

    DocumentBuilder docBuilder = dbf.newDocumentBuilder();

    Document doc;
    try (FileReader readme = new FileReader(fname)) {
      doc = docBuilder.parse(new InputSource(readme));
    }

    Element root = doc.getDocumentElement();

    if (!root.getNodeName().equals("CONFIGURATION"))
      throw new RuntimeException("Illegal file specified");

    NodeList services = doc.getElementsByTagName("SERVICE");
    Node N, C;
    Hashtable H;
    Object S;

    for (int i = 0; i < services.getLength(); i++) {
      N = services.item(i);
      H = new Hashtable();

      NodeList children = N.getChildNodes();

      for (int j = 0; j < children.getLength(); j++) {
        C = children.item(j);

        if (C.getNodeType() == Node.TEXT_NODE)
          continue;

        NodeList subchildren;

        subchildren = C.getChildNodes();

        if (subchildren.getLength() == 1 && C.getFirstChild().getNodeType() == Node.TEXT_NODE) {
          // Simple elements
          H.put(C.getNodeName(), C.getFirstChild().getNodeValue());
        } else {
          // Complex elements
          H.put(C.getNodeName(),
                  XMLToVinci.xmlToVinciFrame(new StringReader(constructXMLString(C, true))));
        }
      } // End for j

      // Hack to distinguish Service from ServiceAlias
      if (!(H.get("TARGET") != null && H.get("NAME") != null))
        try {
          S = new Service(H);
        } catch (RuntimeException e) {
          System.err.println("Got exception while adding Service : " + H.get("NAME").toString());
          System.err.println("Exception generated : " + e);
          continue;
        }
      else
        S = new ServiceAlias((String) H.get("NAME"), (String) H.get("TARGET"));

      addEntry(S);
    }

  }

  static String constructXMLString(Node C, boolean include) {
    if (C.getNodeType() == Node.TEXT_NODE || C.getNodeType() == Node.CDATA_SECTION_NODE) {
      if (C.getNodeType() == Node.CDATA_SECTION_NODE)
        return "<![CDATA[" + C.getNodeValue() + "]]>";
      return C.getNodeValue();
    } else {
      String result = "";
      if (include)
        result += "<" + C.getNodeName() + ">";
      NodeList N = C.getChildNodes();
      for (int i = 0; i < N.getLength(); i++) {
        result += constructXMLString(N.item(i), true);
      }
      if (include)
        result += "</" + C.getNodeName() + ">";
      return result;
    }
  }

  public void save(Writer F) throws IOException {

    F.write("<CONFIGURATION>\n");
    Object[] objs = listServices("", -1); // Get all the services
    if (objs != null)
      for (int i = 0; i < objs.length; i++) {
        if (objs[i] == null)
          continue;
        if (ServiceAlias.isAlias(objs[i]))
          F.write(((ServiceAlias) objs[i]).toXML());
        else
          F.write(((Service) objs[i]).toXML());
        F.write("\n");
      }
    F.write("</CONFIGURATION>\n");

  }

  /* Inner class to represent port range as a data structure */
  class PortRange {
    int minPort, maxPort;

    PortRange(int min, int max) {
      minPort = min;
      maxPort = max;
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof PortRange))
        return false;

      PortRange p = (PortRange) o;

      return (p.minPort == minPort) && (p.maxPort == maxPort);
    }

    @Override
    public int hashCode() {
      return minPort * 31 + maxPort;
    }
  }

  /* For testing */
  public static void main(String[] args) throws Exception {
    ServiceRegistry SR = new ServiceRegistry();
    SR.load(args[0]);

    FileWriter F;
    SR.save(F = new FileWriter(args[0] + ".bak"));
    F.close();
  }

  public static void pr(String s) {
    System.out.println(s);
  }

}
