/**
 * constructor for ServiceInfo class
 * TODO update to respect user config (ignored layers)
 *
 * Properties:
 *    dom
 *    envelope
 *    layers
 *    forbiddenTags
 *    disabledTypes
 *    coordSeparator
 *    tupleSeparator
 *    mapUnits
 *
 * Methods:
 *    toString
 *    getLayerNames
 *    getLayerIds
 *    getLayerById
 *    getLayers
 *    filterLayersByScale
 *    setActiveLayer
 *    getActiveLayer
 *    loadConfig
 *    getVisibleLayers
 *
 */
function ServiceInfo(dom) {
   this.dom = dom;

   //Initial_Extent envelope
   var propertiesNode = dom.getElementsByTagName("PROPERTIES")[0];
   this.envelope = new Envelope(propertiesNode.getElementsByTagName("ENVELOPE")[0]);

   layerInfoNodeList = dom.getElementsByTagName("LAYERINFO");
   //reverse this list to get TOC order
   this.layers = new Array();
   for (var i=0; i<layerInfoNodeList.length; i++) {
      this.layers[i] = new LayerInfo(layerInfoNodeList[i]);
   }
   
   //reverse to put list in legacy TOC order
   this.layers.reverse();
   
   //assign the legacy TOC index
   for (var i=0; i<this.layers.length; i++) {
      this.layers[i].tocIndex = i;
   }
   
   //default the active layer to the first layer in list (last in AXL file)
   this.setActiveLayer(this.layers[0].id);
   
   this.forbiddenTags = this.dom.getElementsByTagName('CAPABILITIES')[0].getAttribute('forbidden');
   this.disabledTypes = this.dom.getElementsByTagName('CAPABILITIES')[0].getAttribute('disabledtypes');     

   this.coordSeparator = this.dom.getElementsByTagName('SEPARATORS')[0].getAttribute('cs');
   this.tupleSeparator = this.dom.getElementsByTagName('SEPARATORS')[0].getAttribute('ts');
   this.mapUnits = this.dom.getElementsByTagName('MAPUNITS')[0].getAttribute('units'); 
   
}


//@TODO XMLSerializer will not pickup changes to object after node passed in
ServiceInfo.prototype.toString=function() {
   return((new XMLSerializer()).serializeToString(this.dom));
}

/**
 * return an array of layer names. reversed from AXL definition
 */
ServiceInfo.prototype.getLayerNames=function() {
   var names = new Array();
   for (var i=0; i<this.layers.length; i++) {
      names[i] = this.layers[i].name;
   }
   return(names);
}

/**
 * return an array of layer ids. reversed from AXL definition
 */
ServiceInfo.prototype.getLayerIds=function() {
   var ids = new Array();
   for (var i=0; i<this.layers.length; i++) {
      ids[i] = this.layers[i].id;
   }
   return(ids);
}


/**
 * return a LayerInfo object for the layer w/ specified id. returns null if none
 */
ServiceInfo.prototype.getLayerById=function(id) {
   for (var i=0; i<this.layers.length; i++) {
      if (this.layers[i].id == id) {
         return(this.layers[i]);
      }
   }
   return(null);
}


/**
 * return a LayerInfo object for the layer w/ specified name. returns null if none
 * if more than one layer w/ same name, returns first
 */
ServiceInfo.prototype.getLayerByName=function(name) {
   for (var i=0; i<this.layers.length; i++) {
      if (this.layers[i].name == name) {
         return(this.layers[i]);
      }
   }
   return(null);
}


/**
 * return an array of LayerInfo objects. reversed from AXL definition
 */
ServiceInfo.prototype.getLayers=function() {
   return(this.layers);
}


/**
 * return an array of LayerInfo objects filtered by scale. reversed from AXL definition
 */
ServiceInfo.prototype.filterLayersByScale=function(mapscale) {
   var filteredLayers = new Array();
   for (var i=0; i<this.layers.length; i++) {
      if (mapscale >= this.layers[i].minScale && mapscale <= this.layers[i].maxScale) {
         filteredLayers.push(this.layers[i]);
      }
   }
   return(filteredLayers);
}


/**
 * return an Array of LayerInfo objects filtered by visibility
 */
ServiceInfo.prototype.getVisibleLayers=function() {
   return(
      this.layers.findAll(function(l) {return(l.visible); })
   );
}


/**
 * return a LayerInfo object at the specified index. Index refers to the TOC 
 * position (reverse of AXL order)
 */
ServiceInfo.prototype.getLayer=function(index) {
   if (index < this.layers.length) {
      return(this.layers[index]);
   } else {
      return(null);
   }
}


/**
 * return a LayerInfo object corresponding to the given TocIndex. Used for
 * connection to legacy code
 */
/*
ServiceInfo.prototype.getLayerByTocIndex=function(index) {
   for (var i=0; i<this.layers.length; i++) {
      if (this.layers[i].tocIndex == index) {
         return(this.layers[i]);
      }
   }
   return(null);
}
*/


/**
 * sets the active layer. There can be only one active layer per collection. 
 * this has the side-effect of setting all other layers to inactive
 * returns true if successful, false otherwise
 *
 * WARNING: if invalid layer ID given, all set to inactive
 */
ServiceInfo.prototype.setActiveLayer=function(id) {
   //log.debug('setActiveLayer method called on ServiceInfo w/ '+id);
   var success = false;
   for (var i=0; i<this.layers.length; i++) {
      if (this.layers[i].id == id) {
         this.layers[i].active = true;
         success = true;
      } else {
         this.layers[i].active = false;
      }
   }
   return(success);
}


/**
 * returns the LayerInfo object currently set to active. returns null if none
 * found.  There should only be one active layer, but if by some error there 
 * is more than one, only the first will be returned
 */
ServiceInfo.prototype.getActiveLayer=function() {
   for (var i=0; i<this.layers.length; i++) {
      if (this.layers[i].active) {
         return(this.layers[i]);
      }
   }
   return(null);
}


/**
 * update the default ServiceInfo response with the configuration elements
 * 
 * given a DOM representing the configuration files
 */
ServiceInfo.prototype.loadConfig=function(config) {
   log.trace('inside loadConfig...');
   var layerNodeList = config.getElementsByTagName("layer");

   //layer attributes
   var name,visible,active,id;

   var layerNode = null; //the <layer> element from the config file
   var axlLayer = null;  //the existing LayerInfo instance built from AXL definition
   for (var i=0; i<layerNodeList.length; i++) {         
      layerNode = layerNodeList[i];
         
      //required
      try {
         id = layerNode.getAttributeNode("id").nodeValue;
         axlLayer = serviceInfo.getLayerById(id);           
      } catch (e) {
         log.fatal('Error: <layer> missing required id attribute'+e);
      }
            
      if (axlLayer == null) {
         //invalid layerid provided, continue processing the other <layer> elements in file
         log.warn('invalid layer id provided: '+id);
         continue;
      }
        
      if (layerNode.getAttributeNode("name")) {
         //update name
         axlLayer.name = layerNode.getAttributeNode("name").nodeValue;
      }
      
      if (layerNode.getAttributeNode("queryable") &&  
          layerNode.getAttributeNode("queryable").nodeValue == "false") {
         axlLayer.queryable = false;
      }
      
      //startDate, endDate work in pairs - both must be present or neither used
      if (layerNode.getAttributeNode("startDate") && layerNode.getAttributeNode("endDate")) {
         axlLayer.startColumn = layerNode.getAttributeNode("startDate").nodeValue;
         axlLayer.endColumn = layerNode.getAttributeNode("endDate").nodeValue;      
      }
        
      if (layerNode.getAttributeNode("active") && 
         layerNode.getAttributeNode("active").nodeValue == "true") {
         //update active
         log.info('config file set active layer to '+axlLayer.name);
         serviceInfo.setActiveLayer(axlLayer.id);
      }
      if (layerNode.getAttributeNode("visible")) {
         if (layerNode.getAttributeNode("visible").nodeValue == "true") { 
            //update visible
            axlLayer.visible = true;
         } else {
            axlLayer.visible = false;
         }
      }
             
      if (layerNode.getAttributeNode("description") != null) {
         axlLayer.description =layerNode.getAttributeNode("description").nodeValue;
      }
      
      if (layerNode.getElementsByTagName("href")[0]) {
         axlLayer.href = new Href(layerNode.getElementsByTagName("href")[0]);
         //log.debug(axlLayer.name+': '+axlLayer.href);
         
      }
            
      //@TODO this should be an attribute of layer no a separate node
      if (layerNode.getElementsByTagName("meta")[0] && 
         layerNode.getElementsByTagName("meta")[0].getAttributeNode("url")) {
         axlLayer.meta = layerNode.getElementsByTagName("meta")[0].getAttributeNode("url").nodeValue;
      }
      
      //station and inventory elements used in pairs
      if (layerNode.getElementsByTagName("station")[0] && layerNode.getElementsByTagName("inventory")[0]) {
         axlLayer.station = new Station(layerNode.getElementsByTagName("station")[0]);
         axlLayer.inventory = new Inventory(layerNode.getElementsByTagName("inventory")[0]);
      }
            
      //fields
      if (! layerNode.getElementsByTagName("fields")[0]) {
         continue;
      }
      //replace the existing array of FieldInfo objects
      var fieldNodeList = layerNode.getElementsByTagName("fields")[0].getElementsByTagName("field");
            
      var newFields = new Array();
      var theField = null;
      var fieldName = null;
      for (var j=0; j<fieldNodeList.length; j++) {
         fieldName = fieldNodeList[j].getAttributeNode("name").nodeValue;
       
         //FieldInfo configured from AXL
         theField = axlLayer.getFieldByName(fieldName);
         if (theField == null) {
            log.warn('field "'+fieldName+'" not found in AXL layer - suggests error in config file');
            continue;
         } 
         //update the existing FieldInfo and add to new array
         if (fieldNodeList[j].getAttributeNode("alias")) {
             theField.alias = fieldNodeList[j].getAttributeNode("alias").nodeValue;
         }
         //build an href if necessary
         if (fieldNodeList[j].getAttributeNode("prefix") ||
            fieldNodeList[j].getAttributeNode("suffix")) {
            theField.href = new Href(fieldNodeList[j]);
         }
         //format attribute not currently used
         if (fieldNodeList[j].getAttributeNode("format")) {
            theField.format = fieldNodeList[j].getAttributeNode("format").nodeValue;
         }
   
         newFields[j] = theField;
               
      }
      //replace with new (potentially shortened) array
      axlLayer.fields = newFields;
   }   

}



/*****************************************************************************/
/*****************************************************************************/
/**
 * LayerInfo class
 * 
 * @TODO merge LayerConfig properties
 *
 * Properties:
 *    node
 *    type
 *    visible
 *    id
 *    name
 *    fclass
 *    envelope
 *    href   //config for hyperlink tool
 *    meta   //URL for layer metadata
 *    minScale
 *    maxScale
 *    fields //array of FieldInfo objects for this layer
 *
 * Methods:
 *    toString
 *    getFieldNames    //constrained by selectFields
 *    getFieldAliases  //provided via LayerConfig
 *    getFieldByName   //constrained by selectFields
 *    
 */
function LayerInfo(node) {
   this.node = node;
   //featureclass, acetate, image
   this.type = node.getAttributeNode("type").nodeValue;
   this.visible = true;
   this.id = node.getAttributeNode("id").nodeValue;
   this.name = node.getAttributeNode("name").nodeValue;
   this.href = null;  //Href object
   this.active = false;
   this.meta = null;
   this.fclass = null;
   this.envelope = null;
   this.fields = null;  //array of FieldInfo objects
   this.minScale = 0;
   this.maxScale = 1.7976931348623157E308;
   this.isFeature = true;  //featureclass, expect FCLASS element
   this.rowIdField = null;
   this.shapeField = null;
   this.queryable = true;
   //temporary attribute to facilitate connections w/ legacy code
   this.tocIndex = null;
   this.startColumn = null;
   this.endColumn = null;
   //rarely-used station, inventory elements
   this.station = null;
   this.inventory = null;
   this.description = null;
      
   if (node.getAttributeNode("visible").nodeValue == "false") {
      this.visible = false;
   }

   //minscale,maxscale attributes are optional, override defaults if present
   if (node.getAttributeNode("minscale") != null) {
      this.minScale = parseFloat(node.getAttributeNode("minscale").nodeValue);
   }
   if (node.getAttributeNode("maxscale") != null) {
      this.maxScale = parseFloat(node.getAttributeNode("maxscale").nodeValue);
   }
   
   //should only be 1 FCLASS element in case of featureclass, 0 in case of image  
   var fclassNodeList = node.getElementsByTagName("FCLASS");
   if (fclassNodeList.length == 1) {
      //featureclass - should be point, line, polygon
      this.fclass = fclassNodeList[0].getAttributeNode("type").nodeValue;
      this.isFeature = true;
      //only one ENVELOPE element
      this.envelope = new Envelope(fclassNodeList[0].getElementsByTagName("ENVELOPE")[0]);
      var fieldNodeList = fclassNodeList[0].getElementsByTagName("FIELD");
      this.fields = new Array();
      for (var i=0; i<fieldNodeList.length; i++) {
         this.fields[i] = new FieldInfo(fieldNodeList[i]);
         //set the shape, rowId fields for this layer - should only be one each, but doesn't check
         if (this.fields[i].type == '-99') { this.rowIdField = this.fields[i]; }
         if (this.fields[i].type == '-98') { this.shapeField = this.fields[i]; }
      }
   } else {
      //image
      this.fclass = null;
      this.isFeature = false;
      this.fields = null;
      this.envelope = new Envelope(node.getElementsByTagName("ENVELOPE")[0]);
   }
      
}

//@TODO XMLSerializer will not pickup changes to object after node passed in
LayerInfo.prototype.toString=function() {
   return((new XMLSerializer()).serializeToString(this.node));
}

LayerInfo.prototype.isVisible=function() {
   if (this.visible == "true") {
      return(true);
   } else {
      return(false);
   }
}


/**
 * returns true if this layer is available at the given map scale
 */
LayerInfo.prototype.isVisibleAtScale=function(sFactor) {
	if ( sFactor >= this.minScale && sFactor <= this.maxScale) {
	   return(true);
	} else {
	   return(false);
	}
}


LayerInfo.prototype.getFields=function() {
   return(this.fields);
}
      
LayerInfo.prototype.getFieldNames=function() {
  var names = new Array();
   for (var i=0; i<this.fields.length; i++) {
      names[i] = this.fields[i].name;
   }
   return(names);
}


/**
 * return an array of field aliases. If none have been defined, this is the same
 * result as getFieldNames()
 */
LayerInfo.prototype.getFieldAliases=function() {
  var aliases = new Array();
   for (var i=0; i<this.fields.length; i++) {
//      if (this.fields[i].alias) {         
         aliases[i] = this.fields[i].alias;
//      } else {
//         aliases[i] = this.fields[i].name;
//      }
   }
   return(aliases);
}

//fieldName always unique, alias not guaranteed to be
LayerInfo.prototype.getFieldByName=function(name) {
  for (var i=0; i<this.fields.length; i++) {
      if (this.fields[i].name == name) {
         return(this.fields[i]);
      }
   }
   return(null);
}

/**
 * return a list of FieldInfo objects filtered the the specified type
 * @TODO check for valid fieldType 
 */
LayerInfo.prototype.getFieldsByType=function(theType) {
   var theFields = new Array();
   for (var i=0; i<this.fields.length; i++) {
      if (this.fields[i].type == theType) {
         theFields.push(this.fields[i]);
      }
   }
   return(theFields);
}


/*****************************************************************************/
/*****************************************************************************/
/**
 * constructor for FieldInfo class
 * 
 * Properties:
 *    href
 *    type
 *    typeName
 *    size
 *    precision
 *    name
 *    alias
 *
 * Methods:
 *    toString
 *        
 * @TODO merge FieldConfig properties
 *
 */
function FieldInfo(node) {
   this.node = node;
   this.type = node.getAttributeNode("type").nodeValue;
   this.size = node.getAttributeNode("size").nodeValue;
   this.precision = node.getAttributeNode("precision").nodeValue;
   this.name = node.getAttributeNode("name").nodeValue;
   this.alias = this.name;   //may be overridden by FieldConfig
   this.typeName = this.fieldTypes[this.type]; 
   this.href = null;
   this.format = null;
   
   //@TODO merge FieldConfig
}

//@TODO XMLSerializer will not pickup changes to object after node passed in
FieldInfo.prototype.toString=function() {
   return((new XMLSerializer()).serializeToString(this.node));
}

FieldInfo.prototype.fieldTypes = new Array();
FieldInfo.prototype.fieldTypes['-99'] = 'Row_id'; 
FieldInfo.prototype.fieldTypes['-98'] = 'Shape'; 
FieldInfo.prototype.fieldTypes['-7'] = 'Boolean'; 
FieldInfo.prototype.fieldTypes['-5'] = 'Big integer';
FieldInfo.prototype.fieldTypes['1'] = 'CHAR';
FieldInfo.prototype.fieldTypes['4'] = 'Integer';  
FieldInfo.prototype.fieldTypes['5'] = 'Small integer';  
FieldInfo.prototype.fieldTypes['6'] = 'Float';  
FieldInfo.prototype.fieldTypes['8'] = 'Double';  
FieldInfo.prototype.fieldTypes['12'] = 'String'; 
FieldInfo.prototype.fieldTypes['91'] = 'Date'; 


/*****************************************************************************/
/*****************************************************************************/
/**
 * Href class
 * hyperlink elements for a LayerInfo or FieldInfo class
 *
 * given a <field> element from the configuration file, parses attributes to be
 * used in constructing a hyperlink
 */

function Href(node) {
      this.target = '_blank';
      this.prefix = "";
      this.fieldName = null;
      this.suffix = "";
      this.label = null;  //not used in LayerInfo hrefs
      //JavaScript operator to be applied to value be applied to the value
      //before constructing URL
      this.operator = null;  

      if (node.getAttributeNode("prefix")) {
         this.prefix = node.getAttributeNode("prefix").nodeValue;
      }
      //required attribute
      if (node.getAttributeNode("name")) {
         this.fieldName = node.getAttributeNode("name").nodeValue;
      } else {
         log.fatal("Error: <href> missing required attribute \"name\"");
         throw new Error("Error: <href> missing required attribute \"name\"");
      }     
      if (node.getAttributeNode("label")) {
         this.label = node.getAttributeNode("label").nodeValue;
      }
     
      if (node.getAttributeNode("suffix")) {
         this.suffix = node.getAttributeNode("suffix").nodeValue;
      }
      if (node.getAttributeNode("operator")) {
         this.operator = node.getAttributeNode("operator").nodeValue;
      }
      
}

/**
 * given a field value, construct and return the <A> element
 */
Href.prototype.evaluate=function(fieldValue) {
   
   var str = '<a href="'+this.getUrl(fieldValue);
   str += '" target="'+this.target+'">';
   if (this.label != null) {
      str += this.label;
   } else {
      str += fieldValue;
   }
   str += '</a>';
   return(str);
}


/**
 * given a field value, construct and return the URL
 */
Href.prototype.getUrl=function(fieldValue) {
   var str = this.prefix;
   if (this.operator != null) {
      //TODO assumes the fieldValue is a String. Should test for types
      try {
         str += eval('"'+fieldValue+'".'+this.operator);
      } catch (exception) {
         log.fatal('Error applying operator "'+this.operator+'" to value "'+fieldValue+'"');
         str += fieldValue;
      } 
   } else {
      str += fieldValue;
   }
   str += this.suffix;
   return(str);
}


Href.prototype.toString=function() {
      var str = '<href ';
      str += 'prefix="'+this.prefix+'" ';
      str += 'name="'+this.fieldName+'" ';
      str += 'suffix="'+this.suffix+'" ';
      str += 'label="'+this.label+'" ';
      str += '>';
      return(str);   
}



/*****************************************************************************/
/*****************************************************************************/
/**
 * Station class
 * optional element used along w/ Inventory class in viewers like SPIDR where 
 * date columns are in separate (inventory) table from spatial features (station
 * table) and spatial table has a one to many relationship with the inventory table 
 *    "id" is the column name for the join column
 *    "table" is the fully qualified table name
 *    "startDate" column name for the start date
 *    "endDate" column name for the end date
 *
 * given a <station> element from the configuration file, parses attributes
 */

function Station(node) {
   this.idColumn = null;
   this.tableName = null;
   this.unqualifiedIdColumn = null;

   if (node.getAttributeNode("table")) {
      this.tableName = node.getAttributeNode("table").nodeValue;
   } else {
      log.fatal("table attribute is required for station elements");
      return;
   }

   if (node.getAttributeNode("id")) {
      this.unqualifiedIdColumn = node.getAttributeNode("id").nodeValue;
      this.idColumn = this.tableName + '.' + node.getAttributeNode("id").nodeValue;
   } else {
      log.fatal("id attribute is required for station elements");
      return;
   }
}


Station.prototype.toString=function() {
   var str = '<station ';
   str += 'id="'+this.unqualifiedIdColumn+'" ';
   str += 'table="'+this.tableName+'" ';
   str += '>';
   return(str);   
}


/*****************************************************************************/
/*****************************************************************************/
/**
 * Inventory class
 * optional element used along w/ Station class in viewers like SPIDR where 
 * date columns are in separate (inventory) table from spatial features (station
 * table) and spatial table has a one to many relationship with the inventory table 
 *    "id" is the column name for the join column
 *    "table" is the fully qualified table name
 *    "startDate" column name for the start date
 *    "endDate" column name for the end date
 *
 * given a <inventory> element from the configuration file, parses attributes
 */

function Inventory(node) {
   this.idColumn = null;
   this.startColumn = null;
   this.endColumn = null;
   this.tableName = null;
   this.unqualifiedIdColumn = null;
   this.unqualifiedStartColumn = null;
   this.unqualifiedEndColumn = null;

   if (node.getAttributeNode("table")) {
      this.tableName = node.getAttributeNode("table").nodeValue;
   } else {
      log.fatal("table attribute is required for inventory elements");
      return;
   }

   if (node.getAttributeNode("id")) {
      this.unqualifiedIdColumn = node.getAttributeNode("id").nodeValue;
      this.idColumn = this.tableName + '.' + node.getAttributeNode("id").nodeValue;
   } else {
      log.fatal("id attribute is required for inventory elements");
      return;
   }
   if (node.getAttributeNode("startDate")) {
      this.unqualifiedStartColumn = node.getAttributeNode("startDate").nodeValue;
      this.startColumn = this.tableName + '.' + node.getAttributeNode("startDate").nodeValue;
   } else {
      log.fatal("startDate attribute is required for inventory elements");
      return;
   }
      if (node.getAttributeNode("endDate")) {
      this.unqualifiedEndColumn = node.getAttributeNode("endDate").nodeValue;
      this.endColumn = this.tableName + '.' + node.getAttributeNode("endDate").nodeValue;
   } else {
      log.fatal("id attribute is required for inventory elements");
      return;
   }
   
}


Inventory.prototype.toString=function() {
   var str = '<inventory ';
   str += 'id="'+this.unqualifiedIdColumn+'" ';
   str += 'table="'+this.tableName+'" ';
   str += 'startDate="'+this.unqualifiedStartColumn+'" ';
   str += 'endDate="'+this.unqualifiedEndColumn+'" ';
   str += '>';
   return(str);   
}
    