/*
 * $Id: FunctionSpec.java,v 1.34 2013/01/10 21:17:15 agoubard Exp $
 *
 * See the COPYRIGHT file for redistribution and use restrictions.
 */
package org.xins.common.spec;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Element;

import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.xml.ElementList;
import org.xins.common.xml.ElementFormatter;
import org.xml.sax.SAXException;

/**
 * Specification of a function.
 *
 * @version $Revision: 1.34 $ $Date: 2013/01/10 21:17:15 $
 * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
 *
 * @since XINS 1.3.0
 */
public final class FunctionSpec {

   /**
    * Name of the function, cannot be <code>null</code>.
    */
   private final String _functionName;

   /**
    * The number of seconds that the result of this function could be cached.
    */
   private int _cache = 0;

   /**
    * Description of the function, cannot be <code>null</code>.
    */
   private String _description;

   /**
    * The input parameters of the function.
    */
   private Map<String,ParameterSpec> _inputParameters = new LinkedHashMap<String,ParameterSpec>();

   /**
    * The input param combos of the function.
    */
   private List<ParamComboSpec> _inputParamCombos = new ArrayList<ParamComboSpec>();

   /**
    * The input data section elements of the function.
    */
   private Map<String,DataSectionElementSpec> _inputDataSectionElements = new LinkedHashMap<String,DataSectionElementSpec>();

   /**
    * The defined error code that the function can return.
    */
   private Map<String,ErrorCodeSpec> _errorCodes = new LinkedHashMap<String,ErrorCodeSpec>();

   /**
    * The output parameters of the function.
    */
   private Map<String,ParameterSpec> _outputParameters = new LinkedHashMap<String,ParameterSpec>();

   /**
    * The output param combos of the function.
    */
   private List<ParamComboSpec> _outputParamCombos = new ArrayList<ParamComboSpec>();

   /**
    * The output data section elements of the function.
    */
   private Map<String,DataSectionElementSpec> _outputDataSectionElements = new LinkedHashMap<String,DataSectionElementSpec>();

   /**
    * Creates a new <code>Function</code> by parsing the function specification file.
    *
    * @param functionName
    *    the name of the function, cannot be <code>null</code>.
    *
    * @param reference
    *    the reference class used to get the defined type class, cannot be <code>null</code>.
    *
    * @param baseURL
    *    the base URL path where are located the specifications, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>functionName == null || reference == null || baseURL == null</code>.
    *
    * @throws InvalidSpecificationException
    *    if the specification is incorrect or cannot be found.
    */
   FunctionSpec(String functionName, Class  reference, String baseURL)
   throws IllegalArgumentException, InvalidSpecificationException {
      MandatoryArgumentChecker.check("functionName", functionName, "reference", reference, "baseURL", baseURL);
      _functionName = functionName;
      try {
         Reader reader = APISpec.getReader(baseURL, functionName + ".fnc");
         parseFunction(reader, reference, baseURL);
      } catch (IOException ioe) {
         throw new InvalidSpecificationException("[Function: " + functionName + "] Cannot read function.", ioe);
      }
   }

   /**
    * Gets the name of the function.
    *
    * @return
    *    the name of the function, never <code>null</code>.
    */
   public String getName() {
      return _functionName;
   }

   /**
    * Gets the number of seconds the result of this method could be cached.
    *
    * @return
    *    the number of seconds the result of this method could be cached,
    *    or <code>0</code> if the result should not be cached.
    *
    * @since XINS 3.1
    */
   public int getCache() {
      return _cache;
   }


   /**
    * Sets the number of seconds the result of this method could be cached.
    *
    * @param cache
    *    the number of seconds the result of this method could be cached,
    *    or <code>0</code> if the result should not be cached.
    *
    * @since XINS 3.1
    */
   public void setCache(int cache) {
      _cache = cache;
   }

   /**
    * Gets the description of the function.
    *
    * @return
    *    the description of the function, never <code>null</code>.
    */
   public String getDescription() {
      return _description;
   }

   /**
    * Gets the input parameter for the specified name.
    *
    * @param parameterName
    *    the name of the parameter, cannot be <code>null</code>.
    *
    * @return
    *    the parameter, never <code>null</code>.
    *
    * @throws EntityNotFoundException
    *    if the function does not contain any input parameter with the specified name.
    *
    * @throws IllegalArgumentException
    *    if <code>parameterName == null</code>.
    */
   public ParameterSpec getInputParameter(String parameterName)
   throws EntityNotFoundException, IllegalArgumentException {

      MandatoryArgumentChecker.check("parameterName", parameterName);

      ParameterSpec parameter = (ParameterSpec) _inputParameters.get(parameterName);
      if (parameter == null) {
         throw new EntityNotFoundException("Input parameter \"" + parameterName + "\" not found.");
      }

      return parameter;
   }

   /**
    * Gets the input parameter specifications defined in the function.
    * The key is the name of the parameter, the value is the {@link ParameterSpec} object.
    *
    * @return
    *    the input parameters, never <code>null</code>.
    */
   public Map<String,ParameterSpec> getInputParameters() {
      return Collections.unmodifiableMap(_inputParameters);
   }

   /**
    * Gets the output parameter of the specified name.
    *
    * @param parameterName
    *    the name of the parameter, cannot be <code>null</code>.
    *
    * @return
    *    the parameter, never <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>parameterName == null</code>.
    *
    * @throws EntityNotFoundException
    *    if the function does not contain any output parameter with the specified name.
    */
   public ParameterSpec getOutputParameter(String parameterName)
   throws IllegalArgumentException, EntityNotFoundException {

      MandatoryArgumentChecker.check("parameterName", parameterName);

      ParameterSpec parameter = (ParameterSpec) _outputParameters.get(parameterName);
      if (parameter == null) {
         throw new EntityNotFoundException("Output parameter \"" + parameterName + "\" not found.");
      }

      return parameter;
   }

   /**
    * Gets the output parameter specifications defined in the function.
    * The key is the name of the parameter, the value is the {@link ParameterSpec} object.
    *
    * @return
    *    the output parameters, never <code>null</code>.
    */
   public Map<String,ParameterSpec> getOutputParameters() {
      return Collections.unmodifiableMap(_outputParameters);
   }

   /**
    * Gets the error code specification for the specified error code.
    *
    * @param errorCodeName
    *    the name of the error code, cannot be <code>null</code>.
    *
    * @return
    *    the error code specifications, never <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>errorCodeName == null</code>.
    *
    * @throws EntityNotFoundException
    *    if the function does not define any error code with the specified name.
    */
   public ErrorCodeSpec getErrorCode(String errorCodeName)
   throws IllegalArgumentException, EntityNotFoundException {

      MandatoryArgumentChecker.check("errorCodeName", errorCodeName);
      ErrorCodeSpec errorCode = _errorCodes.get(errorCodeName);
      if (errorCode == null) {
         throw new EntityNotFoundException("Error code \"" + errorCodeName + "\" not found.");
      }

      return errorCode;
   }

   /**
    * Gets the error code specifications defined in the function.
    * The standard error codes are not included.
    * The key is the name of the error code, the value is the {@link ErrorCodeSpec} object.
    *
    * @return
    *    the error code specifications, never <code>null</code>.
    */
   public Map<String,ErrorCodeSpec> getErrorCodes() {
      return Collections.unmodifiableMap(_errorCodes);
   }

   /**
    * Gets the specification of the element of the input data section with the
    * specified name.
    *
    * @param elementName
    *    the name of the element, cannot be <code>null</code>.
    *
    * @return
    *    the specification of the input data section element, never <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>elementName == null</code>.
    *
    * @throws EntityNotFoundException
    *    if the function does not define any input data element with the specified name.
    */
   public DataSectionElementSpec getInputDataSectionElement(String elementName)
   throws IllegalArgumentException, EntityNotFoundException {

      MandatoryArgumentChecker.check("elementName", elementName);

      DataSectionElementSpec element = _inputDataSectionElements.get(elementName);
      if (element == null) {
         throw new EntityNotFoundException("Input data section element \"" + elementName + "\" not found.");
      }

      return element;
   }

   /**
    * Gets the specification of the elements of the input data section.
    * The key is the name of the element, the value is the {@link DataSectionElementSpec} object.
    *
    * @return
    *    the input data section elements, never <code>null</code>.
    */
   public Map<String,DataSectionElementSpec> getInputDataSectionElements() {
      return _inputDataSectionElements;
   }

   /**
    * Gets the specification of the element of the output data section with the
    * specified name.
    *
    * @param elementName
    *    the name of the element, cannot be <code>null</code>.
    *
    * @return
    *   The specification of the output data section element, never <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>elementName == null</code>.
    *
    * @throws EntityNotFoundException
    *    if the function does not define any output data element with the specified name.
    */
   public DataSectionElementSpec getOutputDataSectionElement(String elementName)
   throws IllegalArgumentException, EntityNotFoundException {

      MandatoryArgumentChecker.check("elementName", elementName);

      DataSectionElementSpec element = (DataSectionElementSpec) _outputDataSectionElements.get(elementName);
      if (element == null) {
         throw new EntityNotFoundException("Output data section element \"" + elementName + "\" not found.");
      }

      return element;
   }

   /**
    * Gets the specification of the elements of the output data section.
    * The key is the name of the element, the value is the {@link DataSectionElementSpec} object.
    *
    * @return
    *    the output data section elements, never <code>null</code>.
    */
   public Map<String,DataSectionElementSpec> getOutputDataSectionElements() {
      return _outputDataSectionElements;
   }

   /**
    * Gets the input param combo specifications.
    *
    * @return
    *    the list of the input param combos specification
    *    ({@link ParamComboSpec}), never <code>null</code>.
    */
   public List<ParamComboSpec> getInputParamCombos() {
      return Collections.unmodifiableList(_inputParamCombos);
   }

   /**
    * Gets the output param combo specifications.
    *
    * @return
    *    the list of the output param combos specification
    *    ({@link ParamComboSpec}), never <code>null</code>.
    */
   public List<ParamComboSpec> getOutputParamCombos() {
      return Collections.unmodifiableList(_outputParamCombos);
   }

   /**
    * Parses the function specification file.
    *
    * @param reader
    *    the reader that contains the content of the result code file, cannot be <code>null</code>.
    *
    * @param reference
    *    the reference class used to get the defined type class, cannot be <code>null</code>.
    *
    * @param baseURL
    *    the base URL path where are located the specifications, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>reader == null || reference == null || baseURL == null</code>.
    *
    * @throws IOException
    *    if the parser cannot read the content.
    *
    * @throws InvalidSpecificationException
    *    if the result code file is incorrect.
    */
   private void parseFunction(Reader reader, Class reference, String baseURL)
   throws IllegalArgumentException, IOException, InvalidSpecificationException {

      MandatoryArgumentChecker.check("reader", reader, "reference", reference, "baseURL", baseURL);

      Element function;
      try {
         function = ElementFormatter.parse(reader);
      } catch (SAXException pe) {
         throw new InvalidSpecificationException("[Function: " + _functionName + "] Cannot parse function.", pe);
      }
      String cacheValue = function.getAttribute("cache");
      if (!cacheValue.equals("")) {
         _cache = Integer.parseInt(cacheValue);
      }
      ElementList descriptionElementList = new ElementList(function, "description");
      if (descriptionElementList.isEmpty()) {
         throw new InvalidSpecificationException("[Function: " + _functionName + "] No definition specified.");
      }
      Element descriptionElement = descriptionElementList.get(0);
      _description = descriptionElement.getTextContent();
      ElementList input = new ElementList(function, "input");
      if (!input.isEmpty()) {

         // Input parameters
         Element inputElement = input.get(0);
         _inputParameters = parseParameters(reference, inputElement);

         // Param combos
         _inputParamCombos = parseCombos(inputElement, _inputParameters, true);

         // Data section
         ElementList dataSections = new ElementList(inputElement, "data");
         if (!dataSections.isEmpty()) {
            Element dataSection = dataSections.get(0);
            _inputDataSectionElements = parseDataSectionElements(reference, dataSection, dataSection);
         }
      }

      ElementList output = new ElementList(function, "output");
      if (!output.isEmpty()) {
         Element outputElement = output.get(0);

         // Error codes
         ElementList errorCodesList = new ElementList(outputElement, "resultcode-ref");
         for (Element nextErrorCode : errorCodesList) {
            String errorCodeName = nextErrorCode.getAttribute("name");
            if (errorCodeName.equals("")) {
               throw new InvalidSpecificationException("[Function: " + _functionName + "] Missing name attribute for a error code.");
            }
            if (errorCodeName.indexOf('/') != -1) {
               errorCodeName = errorCodeName.substring(errorCodeName.indexOf('/') + 1);
            }
            ErrorCodeSpec errorCodeSpec = new ErrorCodeSpec(errorCodeName, reference, baseURL);
            _errorCodes.put(errorCodeName, errorCodeSpec);
         }

         // Output parameters
         _outputParameters = parseParameters(reference, outputElement);

         // Param combos
         _outputParamCombos = parseCombos(outputElement, _outputParameters, true);

         // Data section
         ElementList dataSections = new ElementList(outputElement, "data");
         if (!dataSections.isEmpty()) {
            Element dataSection = dataSections.get(0);
            _outputDataSectionElements = parseDataSectionElements(reference, dataSection, dataSection);
         }
      }
   }

   /**
    * Parse an element in the data section.
    *
    * @param reference
    *    the reference class used to locate the files, cannot be <code>null</code>.
    *
    * @param topElement
    *    the element to parse, cannot be <code>null</code>.
    *
    * @param dataSection
    *    the data section, cannot be <code>null</code>.
    *
    * @return
    *    the top elements of the data section, or an empty array there is no
    *    data section.
    *
    * @throws IllegalArgumentException
    *    if <code>reference == null || topElement == null || dataSection == null</code>.
    *
    * @throws InvalidSpecificationException
    *    if the specification is incorrect.
    */
   static Map<String,DataSectionElementSpec> parseDataSectionElements(Class reference, Element topElement, Element dataSection)
   throws IllegalArgumentException, InvalidSpecificationException {

      MandatoryArgumentChecker.check("reference", reference, "topElement", topElement, "dataSection", dataSection);

      Map<String,DataSectionElementSpec> dataSectionElements = new LinkedHashMap<String,DataSectionElementSpec>();

      // The <data> may have a "contains" attribute.
      String dataContainsAttr = topElement.getAttribute("contains");
      if (!dataContainsAttr.equals("")) {
         DataSectionElementSpec dataSectionElement = getDataSectionElement(reference, dataContainsAttr, dataSection);
         dataSectionElements.put(dataContainsAttr, dataSectionElement);
      }

      // Gets the sub elements of this element
      ElementList dataSectionContains = new ElementList(topElement, "contains");
      if (!dataSectionContains.isEmpty()) {
         Element containsElement = dataSectionContains.get(0);
         ElementList contained = new ElementList(containsElement, "contained");
         for (Element containedElement : contained) {
            String name = containedElement.getAttribute("element");
            DataSectionElementSpec dataSectionElement = getDataSectionElement(reference, name, dataSection);
            dataSectionElements.put(name, dataSectionElement);
         }
      }
      return dataSectionElements;
   }

   /**
    * Gets the specified element in the data section.
    *
    * @param reference
    *    the reference class used to locate the files, cannot be <code>null</code>.
    *
    * @param name
    *    the name of the element to retreive, cannot be <code>null</code>.
    *
    * @param dataSection
    *    the data section, cannot be <code>null</code>.
    *
    * @return
    *    the data section element or <code>null</code> if there is no element
    *    with the specified name.
    *
    * @throws IllegalArgumentException
    *    if <code>reference == null || name == null || dataSection == null</code>.
    *
    * @throws InvalidSpecificationException
    *    if the specification is incorrect.
    */
   static DataSectionElementSpec getDataSectionElement(Class reference, String name, Element dataSection)
   throws IllegalArgumentException, InvalidSpecificationException {
      MandatoryArgumentChecker.check("reference", reference, "name", name, "dataSection", dataSection);
      ElementList elements = new ElementList(dataSection, "element");
      for (Element nextElement : elements) {
         String nextName = nextElement.getAttribute("name");
         if (name.equals(nextName)) {

            String description = new ElementList(nextElement, "description").get(0).getTextContent();

            Map subElements = parseDataSectionElements(reference, nextElement, dataSection);

            boolean isPcdataEnable = false;
            ElementList dataSectionContains = new ElementList(nextElement, "contains");
            if (!dataSectionContains.isEmpty()) {
               Element containsElement = dataSectionContains.get(0);
               List pcdata = new ElementList(containsElement, "pcdata");
               if (!pcdata.isEmpty()) {
                  isPcdataEnable = true;
               }
            }

            ElementList attributesList = new ElementList(nextElement, "attribute");
            Map<String, ParameterSpec> attributes = new LinkedHashMap();
            for (Element nextAttribute : attributesList) {
               ParameterSpec attribute = parseParameter(reference, nextAttribute);
               attributes.put(attribute.getName(), attribute);
            }

            List attributeCombos = parseCombos(nextElement, attributes, false);
            DataSectionElementSpec result = new DataSectionElementSpec(nextName,
                  description, isPcdataEnable, subElements, attributes, attributeCombos);
            return result;
         }
      }
      return null;
   }

   /**
    * Parses a function parameter or an attribute of a data section element.
    *
    * @param reference
    *    the reference class used to locate the files, cannot be <code>null</code>.
    *
    * @param paramElement
    *    the element that contains the specification of the parameter, cannot be <code>null</code>.
    *
    * @return
    *    the parameter, never <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>reference == null || paramElement == null</code>.
    *
    * @throws InvalidSpecificationException
    *    if the specification is incorrect.
    */
   static ParameterSpec parseParameter(Class reference, Element paramElement)
   throws IllegalArgumentException, InvalidSpecificationException {
      MandatoryArgumentChecker.check("reference", reference, "paramElement", paramElement);
      String parameterName = paramElement.getAttribute("name");
      if (parameterName.equals("")) {
         throw new InvalidSpecificationException("Missing name for a parameter.");
      }
      String parameterTypeName = paramElement.getAttribute("type");
      boolean requiredParameter = "true".equals(paramElement.getAttribute("required"));
      ElementList descriptionElementList = new ElementList(paramElement, "description");
      String parameterDefaultValue = paramElement.getAttribute("default");
      if (descriptionElementList.isEmpty()) {
         throw new InvalidSpecificationException("No definition specified for a parameter.");
      }
      String parameterDescription = descriptionElementList.get(0).getTextContent();
      ParameterSpec parameter = new ParameterSpec(reference ,parameterName,
            parameterTypeName, requiredParameter, parameterDescription, parameterDefaultValue);
      return parameter;
   }

   /**
    * Parses the input or output parameters.
    *
    * @param reference
    *    the reference class used to locate the files, cannot be <code>null</code>.
    *
    * @param topElement
    *    the input or output element, cannot be <code>null</code>.
    *
    * @return
    *    a map containing the parameter names as keys, and the
    *    <code>Parameter</code> objects as value, never <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>reference == null || topElement == null</code>.
    *
    * @throws InvalidSpecificationException
    *    if the specification is incorrect.
    */
   static Map<String, ParameterSpec> parseParameters(Class reference, Element topElement)
   throws IllegalArgumentException, InvalidSpecificationException {
      MandatoryArgumentChecker.check("reference", reference, "topElement", topElement);
      ElementList parametersList = new ElementList(topElement, "param");
      Map<String, ParameterSpec> parameters = new LinkedHashMap<String, ParameterSpec>();
      for (Element nextParameter : parametersList) {
         ParameterSpec parameter = parseParameter(reference, nextParameter);
         parameters.put(parameter.getName(), parameter);
      }
      return parameters;
   }

   /**
    * Parses the param-combo element.
    *
    * @param topElement
    *    the input or output element, cannot be <code>null</code>.
    *
    * @param parameters
    *    the list of the input or output parameters or attributes, cannot be <code>null</code>.
    *
    * @param paramCombo
    *    <code>true</code> if a param-combo should be parsed, <code>false</code>
    *    if an attribute-combo should be parsed.
    *
    * @return
    *    the list of the param-combo elements or an empty array if no
    *    param-combo is defined, never <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>topElement == null || parameters == null</code>.
    *
    * @throws InvalidSpecificationException
    *    if the format of the param-combo is incorrect.
    */
   static List parseCombos(Element topElement, Map parameters, boolean paramCombo)
   throws IllegalArgumentException, InvalidSpecificationException {
      MandatoryArgumentChecker.check("topElement", topElement, "parameters", parameters);
      String comboTag = paramCombo ? "param-combo" : "attribute-combo";
      String referenceTag = paramCombo ? "param-ref" : "attribute-ref";
      ElementList paramCombosList = new ElementList(topElement, comboTag);
      List paramCombos = new ArrayList(paramCombosList.size());
      for (Element nextParamCombo : paramCombosList) {
         String type = nextParamCombo.getAttribute("type");
         if (type.equals("")) {
            throw new InvalidSpecificationException("No type defined for " + comboTag + ".");
         }
         ElementList paramDefs = new ElementList(nextParamCombo, referenceTag);
         Map paramComboParameters = new LinkedHashMap();
         for (Element paramDef : paramDefs) {
            String parameterName = paramDef.getAttribute("name");
            if (parameterName.equals("")) {
               throw new InvalidSpecificationException("Missing name for a parameter in " + comboTag + ".");
            }
            ParameterSpec parameter = (ParameterSpec) parameters.get(parameterName);
            if (parameter == null) {
               throw new InvalidSpecificationException("Incorrect parameter name \"" +
                     parameterName + "\" in " + comboTag + ".");
            }
            paramComboParameters.put(parameterName, parameter);
         }
         if (paramCombo) {
            ParamComboSpec paramComboSpec = new ParamComboSpec(type, paramComboParameters);
            paramCombos.add(paramComboSpec);
         } else {
            AttributeComboSpec paramComboSpec = new AttributeComboSpec(type, paramComboParameters);
            paramCombos.add(paramComboSpec);
         }
      }
      return paramCombos;
   }
}