/* Title: ParameterMeta.java
 * Version 1.1
 * Author		Manmohan, Sada
 * Company: USDA-ARS
 * Date:    June, 2005
 * 
 */
package usda.weru.mcrew;

import java.text.NumberFormat;
import java.util.Hashtable;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.traversal.DocumentTraversal;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.TreeWalker;
import usda.weru.util.Caster;
import org.apache.log4j.Logger;

/**
 * This class represents the Mcrew parameter. Holds information about a parameter 
 * such as name, type, prompt, display attribute and value attribute.
 * Edit history
 * @author Manmohan, Sada, Sudhir Kaul  
 * @date June 2005
 */
public class ParameterMeta {

    static NumberFormat numberFormat = NumberFormat.getInstance();

//	private String name;
//	private String prompt;
//	private String type;
//	private String unit;
//	private String displayAttribute;
//	private String valueAttribute;
    Hashtable<String, Object> mParameter;
    Hashtable<String, Hashtable<String, String>> mGroup;
    Hashtable<String, Boolean> inclusion;
    
    InputLimits il;
    private static final Logger LOGGER = Logger.getLogger(ParameterMeta.class);
    
    /**
     * The nuimber of digits permitted after the decimal point for a fractional number.
     */
    public static final int kMaximumFractionDigits = 10;

    static {
        numberFormat.setGroupingUsed(false);
        numberFormat.setParseIntegerOnly(true);
    }

    /**
     * Default constructor for creating a new object.
     */
    public ParameterMeta() {
        mParameter = new Hashtable<>();
        mGroup = new Hashtable<String, Hashtable<String, String>>();
        inclusion = new Hashtable<String, Boolean>();
    }
    

    /**
     * Initializes the meta object with the node data that is being passed as an argument.
     * @param pNode The node object whose data is used for initialization purpose.
     */
    public void initialize(Node pNode) {
        DocumentTraversal traversable;

        if (pNode.getOwnerDocument() == null) {
            traversable = (DocumentTraversal) pNode;
        } else {
            traversable = (DocumentTraversal) pNode.getOwnerDocument();
        }

        TreeWalker walker = traversable.createTreeWalker(pNode, NodeFilter.SHOW_ALL, null, false);

        Node nodeChild = walker.firstChild();
        while (nodeChild != null) {
            String nodeChildName = nodeChild.getNodeName();
            String nodeChildData = "";

            if (nodeChildName.equals(XMLConstants.scomment)) {
                nodeChild = walker.nextSibling();
                continue; // Theres no need to put comments into the data structure
            }

					//Node textNode = walker.firstChild();
            nodeChildData = XMLDoc.getTextData(nodeChild);
            
            if(nodeChildName.equals(XMLConstants.sabs_max) || 
                    nodeChildName.equals(XMLConstants.sabs_min) ||
                    nodeChildName.equals(XMLConstants.srec_min) ||
                    nodeChildName.equals(XMLConstants.srec_max))
            {
                NamedNodeMap attributes = nodeChild.getAttributes();
                if(attributes == null) inclusion.put(nodeChildName, null);
                else
                {
                    for(int index = 0; index < attributes.getLength(); index ++)
                    {
                        Node item = attributes.item(index);
                        String attrname = item.getNodeName();
                        if(attrname.equalsIgnoreCase("exclude"))
                        {
                            String attrval = item.getNodeValue().trim();
                            inclusion.put(nodeChildName, !attrval.equalsIgnoreCase("true"));
                            if(!attrval.equalsIgnoreCase("true") && !attrval.equalsIgnoreCase("false"))
                            {
                                LOGGER.warn(nodeChildName + " in file " + 
                                        pNode.getOwnerDocument().getLocalName() +
                                        " has an exlusion value not \"true\", \"false\" or \"\" "
                                        + "set to false by default.");
                            }
                        }
                        
                    }
                }
            }

            if (nodeChildData == null) {
//                            //System.out.println( " ParameterMeta : initialize() : nodeChildName With NULL-DATA inserted (PARENT) : " + nodeChild.getParentNode().getNodeName() + " & Current Node is : " + nodeChild.getNodeName() );
                nodeChildData = "";
            }
				//if(textNode != null) //Some nodes have no data eg some paramtype in crop_defn
            //	nodeChildData = textNode.getNodeValue();
//                        //System.out.println( " ParameterMeta : initialize() : nodeChildName inserted  : " + nodeChildName + " & nodeChildData inserted  : " + nodeChildData );
            mParameter.put(nodeChildName, nodeChildData);
                        //System.out.println(nodeChildName);

            nodeChild = walker.nextSibling();
        }
        InitializeInputLimits();
    }

    /**
     * This method updates the meta data of the parameteMeta object's node that is 
     * being passed as an argument. 
     * @param pNode The node object whose data is being updated.
     */
    public void update(Node pNode) {
        Hashtable<String, String> paramChoiceTable = null; // to store paramchoice strings if any exists
        DocumentTraversal traversable;
        if (pNode.getOwnerDocument() == null) {
            traversable = (DocumentTraversal) pNode;
        } else {
            traversable = (DocumentTraversal) pNode.getOwnerDocument();
        }

        TreeWalker walker = traversable.createTreeWalker(pNode, NodeFilter.SHOW_ALL, null, false);

        Node nodeChild = walker.firstChild(); // can be paramname, paramprompt, paramunit, displayattribute and valueattribute
        while (nodeChild != null) {
            String nodeChildName = nodeChild.getNodeName();

            if ((nodeChildName.equals(XMLConstants.sparamname)) || (nodeChildName.equals(XMLConstants.scomment))) { // Theres no need to put comment data into data structure. param name is already there
                nodeChild = walker.nextSibling();
                continue;
            } else if (nodeChildName.equals(XMLConstants.sconversion)) {
                String addendValue = "0";
                String factorValue = "1.0"; //either one or both vcan be present

                Node grandChild1 = walker.firstChild(); //Can be factor or addend i.e either one or both can be present in the file
                Node grandChild2 = walker.nextSibling();
                if (grandChild1 != null) {
                    if (grandChild1.getNodeName().equals(XMLConstants.sfactor)) {
                        factorValue = XMLDoc.getTextData(grandChild1); // walker.firstChild().getNodeValue();
//                                                //System.out.println( " ParameterMeta : Update() : Conversion : Factor Name is  : " + grandChild1.getNodeName() + " & Factor-VALUE is  : " + factorValue );
                    } else {
                        addendValue = XMLDoc.getTextData(grandChild1); // walker.firstChild().getNodeValue();
//                                              //System.out.println( " ParameterMeta : Update() : Conversion : Addend Name is  : " + grandChild1.getNodeName() + " & Addend-VALUE is  : " + addendValue );
                    }
                    //walker.parentNode();
                }
                if (grandChild2 != null) {
                    {
                        if (grandChild2.getNodeName().equals(XMLConstants.saddend)) {
                            addendValue = XMLDoc.getTextData(grandChild2); // walker.firstChild().getNodeValue();
                        } else {
                            factorValue = XMLDoc.getTextData(grandChild2); // walker.firstChild().getNodeValue();
                        }
                        //walker.parentNode();
                    }
                }
                ////System.out.println( " ParameterMeta : Update() : Conversion : Factor Name before INSERTION is  : " + XMLConstants.sfactor + " & Factor-VALUE is before INSERTION : " + factorValue );
                mParameter.put(XMLConstants.sfactor, factorValue);
                ////System.out.println( " ParameterMeta : Update() : Conversion : saddend Name before INSERTION is  : " + XMLConstants.saddend + " & addend-VALUE is before INSERTION : " + addendValue );
                mParameter.put(XMLConstants.saddend, addendValue);

                /* Node myCurrentNode = walker.getCurrentNode();
                 // //System.out.println( " ParameterMeta : Update() : Conversion : NODE-NAME is  : " + myCurrentNode.getNodeName() + " & NODE-VALUE is  : " + myCurrentNode.getNodeValue() );
                                
                 Node myParentNode = walker.parentNode()
                                
                 Node myGrandParentNode = walker.parentNode();
                 //System.out.println( " ParameterMeta : Update() : Conversion : PARENT Node-Name is  : " + myParentNode.getNodeName() + " & PARENT Node-Value is  : " + myParentNode.getNodeValue() );
                 //System.out.println( " ParameterMeta : Update() : Conversion : GrandPARENT Node-Name is  : " + walker.getCurrentNode().getNodeName() + " & GrandPARENT Node-Value is  : " + walker.getCurrentNode().getNodeValue() );
                 */
                walker.parentNode().getParentNode();

            } else if (nodeChildName.equals(XMLConstants.sparamchoice)) {
                String nodeChildData = XMLDoc.getTextData(nodeChild);
                NamedNodeMap attributeMap = nodeChild.getAttributes();
                Node valueAttributeNode = null;

                if (attributeMap != null) {
                    valueAttributeNode = attributeMap.getNamedItem(XMLConstants.svalue);
                }

                if (valueAttributeNode == null) {
                    //System.err.println("  ==== ParameterMeta === : == update(): == " + "Theres are no values with the the paramchoice "+ nodeChildData + " Discarding paramchoice");
                    nodeChild = walker.nextSibling();
                    continue;
                }

                String choiceValue = XMLDoc.getTextData(valueAttributeNode);   // VERY IMPORTANT
                //System.out.println("====ParameterMeta ==== : == update(): === The CHOICE values with : " + choiceValue + " IS : "  + nodeChildData );

                if (paramChoiceTable == null) // VERY IMPORTANT
                {  /* Every paramchoice has a integer value associated with it. The values are in no order, eg 
                     * one choice can have a value of 0 and other 14 for some parameter. A hash table is used to store 
                     * each paramchoice string with the paramchoice value as the key.
                     */

                    paramChoiceTable = new Hashtable<>();  // VERY IMPORTANT
                    mParameter.put(XMLConstants.sparamchoice, paramChoiceTable);  // VERY IMPORTANT
                    //System.out.println("====ParameterMeta ==== : == update(): === paramChoice is " + XMLConstants.sparamchoice );
                }

                paramChoiceTable.put(choiceValue, nodeChildData); // VERY IMPORTANT
            } else {       ////System.out.println("ParameterMeta : update(): NODECHILD INFO : nodeChildName : " + nodeChildName + " & nodeChildData : " + nodeChildData ); 
                String nodeChildData = XMLDoc.getTextData(nodeChild); // walker.firstChild().getNodeValue();
                if (nodeChildData != null) { // if in case value null
                    mParameter.put(nodeChildName, nodeChildData);
                    if (nodeChildName.equals(XMLConstants.sdisplayattribute) ||
                            nodeChildName.equals(XMLConstants.svalueattribute) ||
                            nodeChildName.equals(XMLConstants.sformat)) {
                        Node nodeParent = findParentNode(XMLConstants.scategory, nodeChild);
                        if (nodeParent != null) {
                            Node nodeCategoryName = nodeParent.getFirstChild();
                            String groupName = XMLDoc.getTextData(nodeCategoryName);
                            setGroupAttribute(groupName, nodeChildName, nodeChildData);
                        }

                    }
                }
            }

            nodeChild = walker.nextSibling();
        } // end while(nodeChild != null)
    }

    /**
     * Recursively traces up the tree to find the node whose name matches the 
     * parentName
     * @param parentName
     * @param node
     * @return 
     */
    private Node findParentNode(String parentName, Node node) {
        if (node == null) {
            return null;
        }
        String nodeName = node.getNodeName();
        if (nodeName.equals(parentName)) {
            return node;
        } else {
            return findParentNode(parentName, node.getParentNode());
        }
    }

    /**
     * Display category is unused for any parameters except for crops. This method 
     * sets the name of each category that will be used in a crop classifying parameters
     * that go under them. 
     * @param pCategoryName The name to be set for the category.
     */
    public void setDisplayCategory(String pCategoryName) {
        mParameter.put(XMLConstants.sdisplaycategory, pCategoryName); // 
        ////System.out.println("ParameterMeta:" + "display category set to "+ pCategoryName);
    }

    /**
     * Fetches the data value associated with the parameter name pName being passed
     * as argument to the method. 
     * @param pName The name of the parameter whose value is being seeked.
     * @return The meta data for the parameter with name pName
     */
    public String get(String pName) {
        return (String) mParameter.get(pName);
    }

    /* All the below function values can also be retrieved by the above get() function by passing the 
     * appropriate parameter
     */
    /**
     * 45
     * @return are
     */
    public String getName() {
        return (String) mParameter.get(XMLConstants.sname);
    }

    /**
     * returns the Prompt for the parameter.
     * @return 
     */
    public String getPrompt() {
        return (String) mParameter.get(XMLConstants.sparamprompt);
    }

    /**
     * wer
     * @return ert
     */
    public String getType() {
        return (String) mParameter.get(XMLConstants.sparamtype);
    }

    /**
     * ert
     * @param pUnitId at
     * @return asdf
     */
    public String getUnit(int pUnitId) {
        if (pUnitId == XMLConstants.kPrimaryUnit) {
            return (String) mParameter.get(XMLConstants.sparamunit);
        } else {
            return (String) mParameter.get(XMLConstants.sparamaltunit);
        }
    }

    /**
     * Returns primary unit of the parameter
     * @return The unit in which the parameter exists.
     */
    public String getUnit() {
        return (String) mParameter.get(XMLConstants.sparamunit);
    }

    /*
     public String getAlternateUnit() 
     {
     return (String)mParameter.get(XMLConstants.sparamaltunit);
     }
     */
    /**
     * This methid is replace with the generalized method below
     * @return The nodename's data value.
     */
    public String getDispAtt() {
        //return (String)mParameter.get(XMLConstants.sdisplayattribute);
        String x = (String) mParameter.get(XMLConstants.sdisplayattribute);
        return x;
    }

    /**
     * This method gets the values of attributes like display or value which are the node names whose 
     * actual values are being requested.
     * @param pAttributeName This argument can be either XMLConstants.sdisplayAtribute or svalueAttribute
     * @return The value associated with that attribute i:e display or value.
     */
    public String getAttribute(String pAttributeName) {
        return (String) mParameter.get(pAttributeName);
    }

    /**
     * Used for the tabs in the crop dialog
     * @param pGroupName
     * @param pAttributeName
     * @return 
     */
    public String getGroupAttribute(String pGroupName, String pAttributeName) {
        Hashtable<String,String> groupHashTable = mGroup.get(pGroupName);
        //Was there a group hashtable?  return null
        if (groupHashTable == null) {
            return (String) mParameter.get(pAttributeName);
        }
        return groupHashTable.get(pAttributeName);
    }

    /**
     *
     * @param pGroupName
     * @param pAttributeName
     * @param pAttributeValue
     */
    public void setGroupAttribute(String pGroupName, String pAttributeName, String pAttributeValue) {
        //First we need the group's hashtable;
        Hashtable<String, String> groupHashTable = mGroup.get(pGroupName);
        //Was there a group hashtable?  If not create one.
        if (groupHashTable == null) {
            groupHashTable = new Hashtable<>();
            mGroup.put(pGroupName, groupHashTable);
        }

        groupHashTable.put(pAttributeName, pAttributeValue);

    }

    /**
     * The choices that exists in the choice list of each parameter for a crop or 
     * operation object.
     * @return The hashtable that stores all the vectors containing each choice list 
     * options or details.
     */
    public Hashtable<String,String> getChoices() {
        return Caster.<Hashtable<String,String>>cast(mParameter.get(XMLConstants.sparamchoice));
    }

    /*
     public double getAlternateValue(double pPrimaryValue)
     {
     double factor = 1.0, addend = 0.0;
		
     if(mParameter != null)
     {
     factor = ((Double)(mParameter.get(XMLConstants.sfactor))).doubleValue();
     addend = ((Double)(mParameter.get(XMLConstants.saddend))).doubleValue();
     }
		
     double returnValue = pPrimaryValue * factor + addend;
		
     return returnValue;
     }
     */
    /**
     * Method converts the values of each parameter's data from its primary unit
     * to the alternate or secondary units. Like from English units to Metric units.
     * @param pPrimaryValue The value of a parameter in primary units.
     * @return The value of a parameter after conversion to alternate or secondary units.
     */
    public String convertToAlternateUnits(String pPrimaryValue) {
        double factor = 1.0, addend = 0.0;

        ////System.out.println("ParameterMeta:converToAlternateUnits, " + "Value = " + pPrimaryValue);		
        String type = getType();
        if ((type != null) && (type.equals(XMLConstants.sstring))) {
            return pPrimaryValue;
        }

        try {
            if (mParameter != null) {
                Object factorObj = mParameter.get(XMLConstants.sfactor);
                Object addendObj = mParameter.get(XMLConstants.saddend);

                if (factorObj != null) {
                    factor = (new Double(factorObj.toString())).doubleValue();
                }

                if (addendObj != null) {
                    addend = (new Double(addendObj.toString())).doubleValue();
                }
            }

            double toConvert = (new Double(pPrimaryValue)).doubleValue();

            Double result = new Double(toConvert * factor + addend);

            Object returnValue = result;

            return convertToProperType(returnValue.toString());

        } catch (NumberFormatException e) {
            //System.err.println("ParameterMeta:converToAlternatUnits: " + "NumberFormatException on input value " + pPrimaryValue);
        }

        return pPrimaryValue; // return old value if something goes wrong
    }

    /**
     * Method converts the values of each parameter's data from its alternate  unit
     * to the primary units. Like from English units to Metric units.
     * @return The value of a parameter after conversion to primary units.
     * @param pAlternateValue The value of a parameter in alternate units.
     */
    public String convertToPrimaryUnits(String pAlternateValue) {
        double factor = 1.0, addend = 0.0;

        String type = getType();
        if ((type != null) && (type.equals(XMLConstants.sstring))) {
            return pAlternateValue;
        }

        try {
            if (mParameter != null) {
                Object factorObj = mParameter.get(XMLConstants.sfactor);
                Object addendObj = mParameter.get(XMLConstants.saddend);

                if (factorObj != null) {
                    factor = (new Double(factorObj.toString())).doubleValue();
                }

                if (addendObj != null) {
                    addend = (new Double(addendObj.toString())).doubleValue();
                }

            }

            double toConvert = (new Double(pAlternateValue)).doubleValue();

            Double result = new Double((toConvert - addend) / factor);

            Object returnValue = result;

            return convertToProperType(returnValue.toString());

        } catch (NumberFormatException e) {
            //System.err.println("ParameterMeta:convertoToprimaryUnits: " + "NumberFormatException on input value " + pAlternateValue);
        }

        return pAlternateValue;
    }

    /**
     * This functions converts the value to Integer.toString() or Double.toString()
     * so as to put the correct value into the parameter. Writing a interger as 
     * double would crash WEPS
     * @param pParamValue The param value to be converted to the proper type
     * @return The converted value represented in string format.
     */
    public String convertToProperType(String pParamValue) {
        try {

            NumberFormat format = NumberFormat.getInstance();

            String type = getType();
            if (type == null) {
                return pParamValue;
            }

            if (type.equals(XMLConstants.sstring)) {
                return pParamValue;
            } else if (type.equals(XMLConstants.sint)) {
                format.setParseIntegerOnly(true);
            } else {
                format.setParseIntegerOnly(false);
                format.setMaximumFractionDigits(kMaximumFractionDigits);
                ////System.out.println("parameterMeta: convertoProperType()" + "Setting max fraction digits to 6 " + pParamValue);
            }

            Number number = format.parse(pParamValue);

            return number.toString();

            /*			
             Double value = new Double(pParamValue);
             Object returnValue = value;
		
             String type = getType();
             if( (type != null) && (type.equals(XMLConstants.sint)) )
             returnValue = new Integer(result.intValue());
			
             return returnValue.toString();
             */
        } catch (NumberFormatException e) {
            //System.err.println("ParameterMeta:convertoToProperType: " + "NumberFormatException on input value " + pParamValue);
        } catch (java.text.ParseException e) {
            //System.err.println("ParameterMeta:convertoToProperType: " + "ParseException on input value " + pParamValue);
        } catch (NullPointerException e) {
            //System.err.println("ParameterMeta:convertoToProperType: " + "NullPointerException");
        }

        return pParamValue;
    }
    /*
     public String getConversionAddend()
     {
     return (String)mParameter.get(XMLConstants.saddend);
     }
	
     public String getConversionFactor()
     {
     return (String)mParameter.get(XMLConstants.sfactor);
     }
     */
    
    
    /**
     * Initializes the Input Limits to what were read from the xml file.
     * If there are no limits, sets them to "", the empty String.
     *                          -Sincerely, Jonathan Hornbaker.
    */
    private void InitializeInputLimits()
    {
        String aMin = (String) mParameter.get("abs_min");
        String aMax = (String) mParameter.get("abs_max");
        String rMin = (String) mParameter.get("rec_min");
        String rMax = (String) mParameter.get("rec_max");
        String dType = (String) mParameter.get("paramtype");
        Boolean aMinInc = inclusion.get("abs_min");
        Boolean aMaxInc = inclusion.get("abs_max");
        Boolean rMinInc = inclusion.get("rec_min");
        Boolean rMaxInc = inclusion.get("rec_max");
        if(aMin == null) aMin = "";
        if(aMax == null) aMax = "";
        if(rMin == null) rMin = "";
        if(rMax == null) rMax = "";
        if(dType == null) dType = "";
        il = new InputLimits(aMin, aMax, rMin, rMax, aMinInc, aMaxInc, rMinInc, rMaxInc, dType);
    }
    
    /**
     * Returns the current instance of InputLimits.
     * @return 
     */
    public InputLimits getLimits()
    {
        return il;
    }
}
