/* See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Esri Inc. 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 com.esri.gpt.catalog.discovery;
import com.esri.gpt.catalog.lucene.Storeables;
import com.esri.gpt.framework.collection.CaseInsensitiveMap;
import com.esri.gpt.framework.collection.StringSet;
import com.esri.gpt.framework.geometry.Envelope;
import com.esri.gpt.framework.util.Val;

import java.util.Map;
import java.util.logging.Logger;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/** 
 * Contains the configured collection of discoverble/storable property meanings.
 */
@SuppressWarnings("serial")
public class PropertyMeanings extends CaseInsensitiveMap<PropertyMeaning> {
  
  /** class variables ========================================================= */
  
  /** The Logger. */
  private static Logger LOGGER = Logger.getLogger(PropertyMeanings.class.getName());
  
  /** Date modified property name = "dateModified" */
  public static final String NAME_DATEMODIFIED = "dateModified";
  
  /** Document UUID (primary key) name = "uuid" */
  public static final String NAME_UUID = "uuid";
  
  /** instance variables ====================================================== */
  private AliasedDiscoverables allAliased = new AliasedDiscoverables();
  private PropertySets         dcPropertySets = new PropertySets();
  private IStoreables          storeables = new Storeables();
  
  /** constructors ============================================================ */
  
  /** Default constructor. */
  public PropertyMeanings() {
    super(false);
  }
  
  /** properties ============================================================== */
  
  /** 
   * Gets the entire map of aliased discoverables. 
   * @return the map of aliased discoverables
   */
  public AliasedDiscoverables getAllAliased() {
    return allAliased;
  }
  
  /**
   * Gets the configured Dublin Core property sets.
   * @return the property sets
   */
  public PropertySets getDcPropertySets() {
    return dcPropertySets;
  }
  
  /** methods ================================================================= */
    
  /**
   * Adds a property meaning to the collection.
   * @param meaning the property meaning to add
   */
  private void add(PropertyMeaning meaning) {
    if (meaning != null) {
      put(meaning.getName(),meaning);
    }
  }
  
  /**
   * Loads property meanings from an XML document.
   * <p/>
   * The XML document  is based upon the configuration file:<br/>
   * gpt/metadata/property-meanings.xml
   * @param dom the XML document associated with the property meanings configuration file
   * @throws XPathExpressionException if an invalid XPath expression was encountered
   */
  public void load(Document dom) throws XPathExpressionException {
    this.clear();
    
    XPath xpath = XPathFactory.newInstance().newXPath();
    Node root = (Node)xpath.evaluate("/property-meanings",dom,XPathConstants.NODE);
    if (root != null) {
      NodeList nlProps = (NodeList)xpath.evaluate("property-meaning",root,XPathConstants.NODESET);
      String name;
      PropertyMeaning meaning;
      PropertyMeaningType meaningType;
      PropertyValueType valueType;
      PropertyComparisonType compType;
      
      // default allow leading wildcard setting
      String alwExpr = "/property-meanings/property-meaning[@name='anytext']/@allowLeadingWildcard";
      boolean alwDefault = Val.chkStr(xpath.evaluate(alwExpr,dom)).equalsIgnoreCase("true");

      // loop through the properties
      for (int i=0;i<nlProps.getLength();i++) {
        Node ndProp = nlProps.item(i);
        name = Val.chkStr(xpath.evaluate("@name",ndProp));        
        meaningType = PropertyMeaningType.from(xpath.evaluate("@meaningType",ndProp));
        valueType = PropertyValueType.from(xpath.evaluate("@valueType",ndProp));
        compType = PropertyComparisonType.from(xpath.evaluate("@comparisonType",ndProp));
        
        meaning = new PropertyMeaning(name,meaningType,valueType,compType);
        
        if (meaning.getMeaningType().equals(PropertyMeaningType.ANYTEXT)) {
          meaning = new PropertyMeaning.AnyText();
          String names = xpath.evaluate("consider/text()",ndProp);
          ((PropertyMeaning.AnyText)meaning).getNamesToConsider().addDelimited(names);
          
        } else if (meaning.getMeaningType().equals(PropertyMeaningType.GEOMETRY)) {
          meaning = new PropertyMeaning.Geometry();
          Envelope envelope = null;
          String tokens = xpath.evaluate("defaultEnvelope/text()",ndProp);
          String[] coords = Val.tokenize(tokens," ");
          if (coords.length == 4) {
            envelope = new Envelope();
            envelope.put(coords[0],coords[1],coords[2],coords[3]);
            if (envelope.isValid()) {
              ((PropertyMeaning.Geometry)meaning).setDefaultEnvelope(envelope);
            }
          }
          
        }
        
        meaning.setAllowLeadingWildcard(
            Val.chkBool(xpath.evaluate("@allowLeadingWildcard",ndProp),alwDefault));
        this.add(meaning);
        
        NodeList nlChildren = ndProp.getChildNodes();
        for (int j=0;j<nlChildren.getLength();j++) {
          Node ndChild = nlChildren.item(j);
          if (ndChild.getNodeType() == Node.ELEMENT_NODE) { 
            String qName = Val.chkStr(xpath.evaluate("@name",ndChild));
            StringSet qAliases = new StringSet();
            if (qName.length() > 0) {
              
              // Dublin Core elements
              if (ndChild.getNodeName().equalsIgnoreCase("dc")) {
                DcElement dc = new DcElement(qName);
                dc.getAliases().add(dc.getElementName());
                dc.getAliases().addDelimited(xpath.evaluate("@aliases",ndChild));
                dc.setScheme(xpath.evaluate("@scheme",ndChild));
                meaning.setDcElement(dc);
                qAliases = dc.getAliases();
                
              // other elements
              } else {
                qAliases.add(qName);
                qAliases.addDelimited(xpath.evaluate("@aliases",ndChild));
              }
              
              // append to the all aliased discoverables collection
              IStoreable storeable = storeables.connect(meaning);
              if (storeable != null) {
                Discoverable discoverable = new Discoverable(qName);
                discoverable.setMeaning(meaning);
                discoverable.setStoreable(storeable);
                for (String alias: qAliases) {
                  this.getAllAliased().put(alias,discoverable);
                }
                this.getAllAliased().put(meaning.getName(),discoverable);
              }
              
            }
          }
        }        
         
      }
    }
    
    // load explicit aliases
    if (root != null) {
      NodeList nl = (NodeList)xpath.evaluate("property-alias",root,XPathConstants.NODESET);      
      for (int i=0;i<nl.getLength();i++) {
        Node nd = nl.item(i);
        String name = Val.chkStr(xpath.evaluate("@meaning-name",nd));        
        String value = Val.chkStr(xpath.evaluate("@value",nd)); 
        PropertyMeaning meaning = this.get(name);
        if (meaning == null) {
          LOGGER.warning("property-meanings.xml/property-alias@meaning-name="+name+" is invalid.");
        } else {
          Discoverable discoverable = this.getAllAliased().get(name);
          if (discoverable != null) {
            StringSet aliases = new StringSet();
            aliases.addDelimited(value);
            for (String alias: aliases) {
              this.getAllAliased().put(alias,discoverable);
            }
          }
        }
      }
    }
    
    // make the Dublin Core aliased map
    AliasedDiscoverables dcAliased = dcPropertySets.getAllAliased();
    for (PropertyMeaning meaning: values()) {
      DcElement dcElement = meaning.getDcElement();
      if ((dcElement != null) && (dcElement.getElementName().length() > 0)) {
        IStoreable storeable = storeables.connect(meaning);
        if (storeable != null) {
          Discoverable discoverable = new Discoverable(dcElement.getElementName());
          discoverable.setMeaning(meaning);
          discoverable.setStoreable(storeable);
          
          StringSet aliases = dcElement.getAliases();
          for (String alias: aliases) {
            dcAliased.put(alias,discoverable);
          }
          dcAliased.put(meaning.getName(),discoverable);
        }
      }
    }
    
    // parse the property sets
    if (root != null) {
      Node ndSets = (Node)xpath.evaluate("propertySets",root,XPathConstants.NODE);
      if (ndSets != null) {
        Node ndMinimal = (Node)xpath.evaluate("minimal",ndSets,XPathConstants.NODE);
        Node ndBrief = (Node)xpath.evaluate("brief",ndSets,XPathConstants.NODE);
        Node ndSummary = (Node)xpath.evaluate("summary",ndSets,XPathConstants.NODE);
        Node ndFull = (Node)xpath.evaluate("full",ndSets,XPathConstants.NODE);
        loadDcPropertySet(ndMinimal,xpath,dcAliased,dcPropertySets.getMinimal());
        loadDcPropertySet(ndBrief,xpath,dcAliased,dcPropertySets.getBrief());
        loadDcPropertySet(ndSummary,xpath,dcAliased,dcPropertySets.getSummary());
        loadDcPropertySet(ndFull,xpath,dcAliased,dcPropertySets.getFull());
      }
    }
    
  }
  
  /**
   * Loads a Dublin core property set.
   * @param parent the parant node for the set
   * @param xpath and XPath that can be used for dom queries
   * @param aliased the full set of aliased discoverables
   * @param discoverables the collection of discoverables to populate
   * @throws XPathExpressionException if an invalid XPath expression was encountered
   */
  private void loadDcPropertySet(Node parent,  
                                 XPath xpath, 
                                 AliasedDiscoverables aliased,
                                 Discoverables discoverables) 
   throws XPathExpressionException {
   StringSet names = new StringSet();
   if (parent != null) {
     Node ndDc = (Node)xpath.evaluate("dc",parent,XPathConstants.NODE);
     if (ndDc != null) {
       NodeList nlNames = (NodeList)xpath.evaluate("meaning-names",ndDc,XPathConstants.NODESET);
       for (int i=0;i<nlNames.getLength();i++) {
         names.addDelimited(nlNames.item(i).getTextContent());
       }
     }
     for (String name: names) {
       Discoverable discoverable = aliased.get(name);
       if ((discoverable != null) && (discoverable.getMeaning().getDcElement() != null)) {
         String dcName = Val.chkStr(discoverable.getMeaning().getDcElement().getElementName());
         if (dcName.length() > 0) {
           discoverables.add(aliased.get(name));
         }
       }
     }
   }
   
  }
    
  /**
   * Appends property information for the component to a buffer.
   * @param sb the buffer to use when appending information
   */
  public void echo(StringBuffer sb) {
    if (size() == 0) {
      sb.append(" (No meanings.)");
    } else {
      for (Map.Entry<String,PropertyMeaning> entry: this.entrySet()) {
        sb.append("\nkey=\"").append(entry.getKey()).append("\"\n");
        ((PropertyMeaning)entry.getValue()).echo(sb,1);
      }
    }   
  }
  
  /**
   * Returns the string representation of the object.
   * @return the string
   */
  @Override
  public String toString() {
    StringBuffer sb = new StringBuffer(getClass().getName()).append(" (");
    echo(sb);
    sb.append("\n) ===== end ").append(getClass().getName());
    return sb.toString();
  }
  
}