package rodex;

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import overlay.OverlayExample;
import ow.id.ID;
import uk.ac.bbk.dcs.automed.qproc.iql.ASG;
import uk.ac.bbk.dcs.automed.qproc.iql.ParseException;
import uk.ac.ic.doc.automed.p2p.overlay.OverlayController;
import uk.ac.ic.doc.automed.p2p.overlay.OverlayException;
import uk.ac.ic.doc.automed.p2p.overlay.OverlayToolKit;
import uk.ac.ic.doc.automed.util.Debugger;
import uk.ac.ic.doc.automed.util.LogOutputHandler;
import uk.ac.ic.doc.automed.util.Logger;
import uk.ac.ic.doc.automed.util.NumberToolkit;
import uk.ac.ic.doc.automed.util.db.DBAccess;
import uk.ac.ic.doc.automed.util.db.DBAccessFactory;
import uk.ac.ic.doc.automed.util.db.DBRecord;
import uk.ac.ic.doc.automed.util.db.DBRecordWithConstraints;
import uk.ac.ic.doc.automed.wrappers.AutoMedQueryToken;
import uk.ac.ic.doc.automed.wrappers.SQLQuery;
import uk.ac.ic.doc.rodex.p2p.Peer;
import uk.ac.ic.doc.rodex.qproc.QueryAnalyser;
import uk.ac.ic.doc.rodex.qproc.QueryFunction;
import uk.ac.ic.doc.rodex.qproc.QueryIntegrityException;
import uk.ac.ic.doc.rodex.qproc.QueryVariable;
import utils.AutoMedUtils;

/**
 * RodexDON.java
 *
 * Test application for using Chord to form a Data Overlay Network (DON) over a
 * network of AutoMed peers.
 *
 * <br> The primary objective behind DON is that a a peer does not need to know
 * the desired global schema before executing query Q over the network
 *
 * <br> Features:
 * <ol>
 *  <li>A network of 10 peers</li>
 *  <li>All peers have the same database schema: person(id, name, yob)</li>
 *  <li>Each peer Pi stores a partition of the data as follows:
 *    <ul>
 *      <li>(i - 1) * 100 + 1 < id < i * 100
 *      <li>(i - 1) * 10 + 1900 + 1 < yob < (i * 10) + 1900
 *    </ul>
 * <li>Use the DON to answer queries and observe the performance when using the
 * following enhancements:
 *    <ul>
 *      <li>no range guards
 *      <li>using range guard
 *    </ul>
 * </li>
 * <li>Distribute tuples based on range guards on the <b> numeric PK field</b> (id)<br>
 * <li>Requires that the peers IDs are scaled with the size of the PK field's domain -->
 *     <br>may result in a large number of peers
 * <li>Does not yet support range guards on other numeric, index attributes (e.g. yob)
 * </ol>
 *
 * <br> References: "Towards a Unifying Framework for Complex Query
 *        Processing over Structured Peer-to-Peer Data Networks"
 *
 * @author Duc M Le  <a href="mailto:dmle@doc.ic.ac.uk"><i>dmle@doc.ic.ac.uk</i></a>
 * Created on 28 July 2006
 * Department of Computing, Imperial College
 */

public class RodexDON extends OverlayExample {
  protected static int debugSection = Debugger.DEBUG_P2P_OVERLAY;
  protected static int debugLevel = Debugger.DEBUG_LEVEL_STD;
  protected static final String PROPS_FILE = "etc/rodexdon.properties";
  protected static Properties props;
  
  protected ResultSet rs;
  
  // cache the number ranges
  protected static int[][] rgID;
  protected static int[][] rgYOB;
  
  static {
    props = new Properties();
    
    try {
      props.load(RodexDON.class.getResourceAsStream(PROPS_FILE));
      
      // populate PK range
      int NUM_SUB_RANGES_ID = Integer.parseInt(getProperty("NUM_SUB_RANGES_ID"));
      int DOMAIN_ID_MIN = Integer.parseInt(getProperty("DOMAIN_ID_MIN"));
      int DOMAIN_ID_MAX = Integer.parseInt(getProperty("DOMAIN_ID_MAX"));
      rgID = NumberToolkit.createNumberRanges(DOMAIN_ID_MIN,
        DOMAIN_ID_MAX, NUM_SUB_RANGES_ID);
      
      // populate index attribute range
      int NUM_SUB_RANGES_YOB = Integer.parseInt(getProperty("NUM_SUB_RANGES_YOB"));
      int DOMAIN_YOB_MIN = Integer.parseInt(getProperty("DOMAIN_YOB_MIN"));
      int DOMAIN_YOB_MAX = Integer.parseInt(getProperty("DOMAIN_YOB_MAX"));
      rgYOB = NumberToolkit.createNumberRanges(DOMAIN_YOB_MIN, DOMAIN_YOB_MAX,
        NUM_SUB_RANGES_YOB);
    } catch (IOException ioe) {
      ioe.printStackTrace();
    }
  }
  
  /**
   * Creates a new instance of RodexDON
   */
  public RodexDON() throws Exception {
    super();
  }
    
  /**
   * Return the configured value for a given application property
   */
  public static String getProperty(String propName) {
    if (props != null) {
      return props.getProperty(propName);
    }
    
    return null;
  }
  
  /**
   * calculate number of peers to initiate from the number of ID ranges
   */
  public static int getRequiredNumPeers() {

    // number of peers is equal to the lower bound value of the last PK range
    int numPeers = rgID[rgID.length-1][0];
    
    Logger.debug("","","Need at least "+numPeers+" peers",
      debugSection, debugLevel);
    
    return numPeers;
  }
  
  public void distributeTableData(int peerIndex) throws Exception {
    distributeTableDataByAttribute(peerIndex, "id");
  }
  
  /**
   * Distribute table data by the field name
   */
  public void distributeTableDataByAttribute(int peerIndex, String field) throws Exception {
    // access db and get database records
    /**@TODO: divide into multiple reads*/
    
    String driver = DBAccessFactory.DRIVER_POSTGRES;
    String dbName = getProperty("DB_NAME");
    String user = getProperty("DB_USER");
    String pwd = getProperty("DB_PWD");
    String db = getProperty("URL_BASE")+dbName;
    
//    if (rs == null) {
      DBAccess dba = DBAccessFactory.getDBAccessInstance(driver, db, user, pwd, null);

      String sql = "SELECT * FROM person order by " + field;
      rs = dba.executeQuery(sql);
//    } else {
//      rs.first();
//    }
    
    List fields = new ArrayList();
    fields.add("id");
    fields.add("name");
    fields.add("yob");
    Vector v = null;
//    DBRecordWithConstraints rec = null;
    
    int TTL = Integer.parseInt(getProperty("TTL"));
    
    // do with ID first
    String[] indexAttrs = new String[] { field };
    Map idHashStack = new HashMap();
    Peer p = super.getPeers()[peerIndex];
    while (rs.next()) {
      int id = rs.getInt("id");
      Integer idObj = new Integer(id);
      String name = rs.getString("name");
      Integer yob = new Integer(rs.getInt("yob"));
      
      // get range guard for the specified attribute
      int[][] rgSet = getRangeGuardDef(field);
      int val = rs.getInt(field);
      
      int[][] inRG = getRangeGuard(val, rgSet);
      int rgMin = inRG[0][0];
      int rgMax = inRG[0][1];
      
      // get hash key for this range
      ID hid = (ID) idHashStack.get(rgMin+"");
      if (hid == null) {
        //hid = OverlayToolKit.createIDFromNumber(rgMin);
        hid = getHashKeyForRange(field, rgMin, rgMax);
        idHashStack.put(rgMin+"", hid);
      }
      
      // prepare record object to put into overlay network
      v = new Vector(3);
      v.add(idObj);
      v.add(name);
      v.add(yob);
      ASG tableQuery = null, tuple = null;
      try {
        tableQuery = new ASG("[{id,name,yob} | " +
          "{t,id} <- <<person,id>>;" +
          "{t,name} <- <<person,name>>;" +
          "{t,yob} <- <<person,yob>>; id = "+id+"; yob = "+yob+"]");
        tuple = new ASG("[{"+id+",'"+name+"',"+yob+"}]");
      } catch (ParseException ex) {
        Logger.error("RodexDON","distributeTableData()", ex.getMessage(), ex);
        throw ex;
      }

      Logger.debug("","","Peer "+p.getName()+": Record id: "+id+", attribute val: " + val,
        debugSection, debugLevel);
      
      distributeDBRecord(peerIndex, tableQuery, tuple, hid, TTL);
      
//      rec = new DBRecordWithConstraints(fields, v, tableQuery, tuple);
//      rec = new DBRecordWithConstraints(tableQuery, tuple);
//      rec.setRangeGuardsOnAttributes(indexAttrs, inRG);
//      
//      OverlayController.waitBeforeAccess();
//      Logger.debug("","","Peer "+p.getName()+": Record id: "+id+", attribute val: " + val,
//        debugSection, debugLevel);
//      
//      /**@TODO: Send several objects per put */
//      this.put(peerIndex, hid, rec, TTL);
    }
  }
  
  /**
   * Distribute a {@link DBRecordWithConstraints} object to the network
   */
  public void distributeDBRecord(int peerIndex, ASG query, ASG tuple, ID id, int ttl) throws Exception {
      DBRecordWithConstraints rec = new DBRecordWithConstraints(query, tuple);
      OverlayController.waitBeforeAccess();      
      this.put(peerIndex, id, rec, ttl);
  }
  
  /**
   * Use this method to calculate a hashed-key for a numeric range of an index
   * attribute.
   */
  public static ID getHashKeyForRange(String field, int rgMin, int rgMax) {
    // this method calculates hashed-key from lower bound of the range
    ID hid = OverlayToolKit.createIDFromNumber(rgMin);
    return hid;
  }
  
  
  public void put(int peerIndex, ID key, DBRecord value, int ttl) throws OverlayException {
    //
    Peer p = super.getPeers()[peerIndex];
    Logger.debug("","","Peer "+p.getName()+": Putting (key,value) = ("+key.toString()+
      ","+value+"); TTL="+(ttl/1000),
      debugSection, debugLevel);
    p.put(key, value, ttl);
  }
  
  
  public void getValuesForNumericKey(int peerIndex) throws OverlayException {
    Peer p = getPeers()[peerIndex];
    
    String key = AutoMedUtils.getCmdLineInput("Enter an INTEGER value as key: ");
    int intVal = Integer.parseInt(key);
    
    // look up range guard for value
    int[][] inRG = getRangeGuard(intVal, rgID);
    if (inRG == null) {
      System.out.println("Invalid record id value!");
    } else {
      int rgMin = inRG[0][0];
      int rgMax = inRG[0][1];
      
//      ID id = OverlayToolKit.createIDFromNumber(rgMin);
      
      ID id = getHashKeyForRange("id", rgMin, rgMax);
      
      Logger.debug("","","Peer "+p.getName()+": Looking up value(s) for record id:"+intVal+
        " (key="+id.toString()+")",
        debugSection, debugLevel);
      
      get(peerIndex, id);
    }
  }
  
  /**
   * Filter result based on an IQL query. This is a recursive method which loops through
   * the entire range guard space of the record id to search for possible values
   */
  public void getValuesFromQuerySimple(int peerIndex, String query) throws OverlayException {
    Peer p = getPeers()[peerIndex];
    
    if (query == null)
      query = AutoMedUtils.getCmdLineInput("Enter an IQL query: ");
    
    // loop through all range guards of the record id to find results
    int numIDRGs = rgID.length;
    Set result = new HashSet();
    for (int i = 0; i < numIDRGs; i++) {
      // look up range guard for value
      int rgMin = rgID[i][0];
      int rgMax = rgID[i][1];
      
//      ID id = OverlayToolKit.createIDFromNumber(rgMin);
      ID id = getHashKeyForRange("id", rgMin, rgMax);
      
      Logger.debug("","","Peer "+p.getName()+": Looking up value(s) from id range ["+
        rgMin+","+rgMax+"] with key="+id.toString()+")",
        debugSection, debugLevel);
      try {
        Set vals = p.get(id, new ASG(query));
        if (vals != null)
          result.addAll(vals);
      } catch (ParseException ex) {
        Logger.error("","", ex.getMessage(), ex);
      }
    }
    
    displayVals(query, result);
  }
  
  /**
   * Overloading {@link #getValuesFromQuery(int peerIndex)}
   */
  public void getValuesFromQuery(int peerIndex, String queryStr) throws OverlayException {
    Peer p = getPeers()[peerIndex];
    
    String q = AutoMedUtils.getCmdLineInput(
      "Enter an IQL query or press ENTER to use the default: \n" + queryStr + "\n");
    if (q != null && !q.equals("null")) {
      queryStr = q;
    }
    
    ASG query = null;
    try {
      query = new ASG(queryStr);
    } catch (ParseException ex) {
      Logger.error("","", ex.getMessage(), ex);
    }
    
    QueryAnalyser qa = new QueryAnalyser(query);
    try {
      qa.analyse();
    } catch (QueryIntegrityException ex) {
      Logger.error("","", ex.getMessage(), ex);
    }
    
    Map vars = qa.getQueryVariables();
    //Have to use object map here so that when it is null we know
    // that min range guard does not exist.
    Map rgMins = new HashMap();
    
    // get all associated functions to each variable, then find the lowest
    // range guard value (range-guard-min)
    for (Iterator it = vars.values().iterator(); it.hasNext();) {
      QueryVariable qv = (QueryVariable) it.next();
      // assume one-to-one mapping b/w variable and field
      String field = (String) qv.getBoundFields().get(0);
      List funcs = qv.getBoundFunctions();
      
      Integer[] minMax = getMinRangeGuard(field, funcs);
      if (minMax != null)
        rgMins.put(field, minMax);
      
    }
    
    // if no ranges specified then loop through all id ranges to get result
    if (rgMins.size() == 0) {
      Logger.debug("","","Peer "+p.getName()+": Query does not have any ranges! " +
        "Using recursive look-up on record id", debugSection, debugLevel);
      getValuesFromQuerySimple(peerIndex, queryStr);
    } else {
      // loop through all Chord's ID spaces identified by min range guard values
      // to find results
      Set result = new HashSet();
      for (Iterator vit = rgMins.keySet().iterator(); vit.hasNext();) {
        String field = (String) vit.next();
        Integer[] minMax = (Integer[]) rgMins.get(field);
        int rgMin = minMax[0].intValue();
        Integer fieldMax = minMax[1];
        
        Logger.debug("","","Peer "+p.getName()+": Looking up value(s) from range('"+
          field+"') starting with "+rgMin + " until value: " + fieldMax,
          debugSection, debugLevel);
        Set vals = getValuesPerAttribute(peerIndex, field, rgMin, fieldMax, query);
        if (vals != null)
          result.addAll(vals);
        
        displayVals(queryStr, result);
      }
    }    
  }
  
  /**
   * Filter result based on an IQL query. This works better than {@link getValuesFromQuerySimple()}
   * method in that it first analyses user query to identify the lowest range
   * guard that covers the query's predicate(s)
   */
  public void getValuesFromQuery(int peerIndex) throws OverlayException {
    Peer p = getPeers()[peerIndex];
    
    String queryStr = AutoMedUtils.getCmdLineInput("Enter an IQL query: ");
    getValuesFromQuery(peerIndex, queryStr);
  }
  
  /**
   * Perform an recursive look up of values per index field (attribute) starting
   * from a specific range
   */
  public Set getValuesPerAttribute(int peerIndex, String field,
    int startValue, Integer endValue, ASG query) {
    
    Peer p = getPeers()[peerIndex];
    
    Set result = new HashSet();
    
    int[][] rgSet = getRangeGuardDef(field);
    
    for (int i = 0; i < rgSet.length; i++) {
      // look up range guard for value
      int rgMin = rgSet[i][0];
      int rgMax = rgSet[i][1];
      
      // moves to the first range that contains start value
      if (rgMin < startValue)
        continue;
      
//      ID id = OverlayToolKit.createIDFromNumber(rgMin);
      ID id = getHashKeyForRange(field, rgMin, rgMax);
      
      Logger.debug("","","Peer: "+p.getName()+": Looking up value(s) from id range ["+
        rgMin+","+rgMax+"] with key="+id.toString()+")",
        debugSection, debugLevel);
      Set vals = p.get(id, query);
      if (vals != null)
        result.addAll(vals);

      // stops when end value is also contained in this range
      if (endValue != null && endValue.intValue() <= rgMax) {
        break;
      }        
    }
    
    if (result.isEmpty()) {
      return null;
    } else {
      return result;
    }
  }
  
  /**
   * Return a range guard [min,max] for an integer
   */
  protected int[][] getRangeGuard(int i, int[][] rgs) {
    for (int j = 0; j < rgs.length; j++) {
      int min = rgs[j][0];
      int max = rgs[j][1];
      if (min <= i && i <= max) {
        return new int[][] {
          {min, max}
        };
      }
    }
    
    return null;
  }
  
  /**
   * Return the min range guard value of all function ranges associated to a variable
   * @return Integer[]  contains 2 elements: (1) the lower bound value of the first
   *            range that contains the functions' min (2) the functions' max value
   */
  private Integer[] getMinRangeGuard(String field, List functions) {
    int min = 0;
    int max = 0;
    boolean foundMin = false, foundMax = false;
    for (Iterator it = functions.iterator(); it.hasNext();) {
      QueryFunction qf = (QueryFunction) it.next();
      AutoMedQueryToken opt = qf.getOperator();
      int exp = (int) qf.getExpression().doubleValue();
      if (opt.equals(SQLQuery.GREATERTHAN) ||
        opt.equals(SQLQuery.GREATERTHANEQUAL)) { // min
        // e.g. x >= 2
        if (foundMin == false)
          min = exp;
        else if (exp < min)
          min = exp;
        
        foundMin = true;
      } else if (opt.equals(SQLQuery.LESSTHAN) || 
        opt.equals(SQLQuery.LESSTHANEQUAL)) {
        // max
        // e.g. x >= 2
        if (foundMax == false)
          max = exp;
        else if (exp > max)
          max = exp;
        
        foundMax = true;        
      } else if (opt.equals(SQLQuery.EQUAL)) {  // min and max
        if (foundMax == false)
          max = exp;
        else if (exp > max)
          max = exp;

        if (foundMin == false)
          min = exp;
        else if (exp < min)
          min = exp;

        foundMin = true;
        foundMax = true;
      }        
    }
    
    // throw exception if not found
    if (foundMin == false && foundMax == false) {
      return null;
    }
    
    // otherwise find the corresponding set of range guards for the given variable
    // and lookup the range guard for min value
    int[][] rgSet = getRangeGuardDef(field);
    if (rgSet == null) {
      Logger.debug("","","Could not find range guard " +
        "definition for field '"+field+"')",
        debugSection, debugLevel);
      return null;
    }
    
    int[][] rg = getRangeGuard(min, rgSet);
    if (rg == null) {
      Logger.debug("","","Could not find range guard " +
        "for value "+min+" of field '"+field+"'",
        debugSection, debugLevel);
      return null;
    }
    
    // Now, minObj is the lower bound value of the first range that contains min
    // while maxObj is the actual value found in the query functions (if any)
    Integer minObj = new Integer(rg[0][0]);
    Integer maxObj = null;
    if (foundMax != false) {
      // actual max value from query functions
      maxObj = new Integer(max);
    }
        
    return new Integer[] {minObj, maxObj};
  }
  
  /**
   * Basically return the pre-configured set of range guards for a table column
   */
  public int[][] getRangeGuardDef(String field) {
    // only supports id in this version
    if (field.equalsIgnoreCase("id")) {
      return rgID;
    }
    return null;
  }
  
  public void finalize() {
    super.finalize();
    this.rgID = null;
    this.rgYOB = null;
  }
  
  public static void main(String[] args) throws Exception {
//    Debugger.setSectionDebugLevel(Debugger.DEBUG_HELLO, Debugger.DEBUG_LEVEL_ALL);
//    Debugger.setSectionDebugLevel(Debugger.DEBUG_P2P, Debugger.DEBUG_LEVEL_STD);
//    Debugger.setSectionDebugLevel(Debugger.DEBUG_P2P_OVERLAY, Debugger.DEBUG_LEVEL_DETAILED);
//    Debugger.setSectionDebugLevel(Debugger.DEBUG_QUERY, Debugger.DEBUG_LEVEL_ALL);
//    Debugger.setSectionDebugLevel(Debugger.DEBUG_QUERY_REWRITE, Debugger.DEBUG_LEVEL_INFO);
//    Debugger.setSectionDebugLevel(Debugger.DEBUG_GENERAL, Debugger.DEBUG_LEVEL_INFO);
    // e.g. -d "9.8 20.5 21.1"
    Debugger.setSectionDebugLevel(args);
    
    Logger.setOutputHandler(new LogOutputHandler(LogOutputHandler.CONSOLE +
      LogOutputHandler.FILE));
    
//    int numPeers = OverlayController.DEF_NUM_HOSTS;
    int numPeers = RodexDON.getRequiredNumPeers();
    
//    System.out.println("numPeers = " + numPeers);
    
    Peer.initOverlay(numPeers);
    
    RodexDON oex = null;
    boolean withSuperPeer = true;
    
    do {
      if (oex != null && AutoMedUtils.dialogYesNo("Start a new set of peers ?", 0) == 1) {
        oex.finalize();
        try {Thread.sleep(5 * 1000);} catch (InterruptedException ie) {}
        oex = new RodexDON();
        oex.startPeers(numPeers, false);
//        oex.startAutoMedPeers(numPeers, withSuperPeer);
      } else if (oex == null) {
        oex = new RodexDON();
        oex.startPeers(numPeers, false);
//        oex.startAutoMedPeers(numPeers, withSuperPeer);
      }
      
      if (AutoMedUtils.dialogYesNo("Display Overlay Network status ?", 0) == 1) {
        oex.displayOverlayStatus();
      }
      
      // prompt user for a peer
      int peerIndex  = oex.displayPeerList();
      
      // Publish person table data to the network according to a given scheme
      if (AutoMedUtils.dialogYesNo("Distribute person table data to network ?", 1) == 1) {
        oex.distributeTableData(peerIndex);
      }
      
      if (AutoMedUtils.dialogYesNo("Peer "+peerIndex+": display overlay status ?", 1) == 1) {
        oex.displayOverlayStatus(peerIndex);
      }
      
      // try query objects using exact match of record id
      if (AutoMedUtils.dialogYesNo("Peer "+peerIndex+": get values for a key from the network ?", 1) == 1) {
        oex.getValuesForNumericKey(peerIndex);
      }
      
      while (AutoMedUtils.dialogYesNo("Peer "+peerIndex+": try another key ?", 1) == 1) {
        oex.getValuesForNumericKey(peerIndex);
      }
      
      // try query objects using query
      while (AutoMedUtils.dialogYesNo("Peer "+peerIndex+": query object ?", 1) == 1) {
        oex.getValuesFromQuery(peerIndex);
      }
      
    } while (AutoMedUtils.dialogYesNo("Run again ?", 0) == 1);
    
    AutoMedUtils.banner("~END~");
    System.exit(0);
  }
}
