/* Title:		Action.java
 * Version	
 * Author		Sada, Manmohan
 * Company: USDA-ARS
 * Date:    July, 2003
 * Description:  This class defines an Action. This is the data class which holds the action data for an
 * Operation Object.
 */
package usda.weru.mcrew;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.ListIterator;
import java.util.StringTokenizer;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.traversal.DocumentTraversal;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.TreeWalker;

/**
 * This class defines an Action. This is the data class which holds the action data for an
 * Operation Object
 * Class that puts the data in a format described in the XML file from Action element.
 * Action nodes are passed and objects are initialized with the data that goes along with
 * the descriptions and stored into a datastructure like vector, hashtable etc.
 */
public class Action implements Cloneable {

    private Identity mActionId;
    Hashtable<String, Parameter> mParameters;
    List<String> mParameterNames;
    
    /**
     * Default Constructor that instantiates the Lists & Hashtables to store the data from
     * the files according to the XML description.
     */
    public Action() {
        mActionId = new Identity();
        mParameters = new Hashtable<>();
        mParameterNames = new ArrayList<>();
    }

    /**
     * Constructor that instantiates an identity object & stores the value for that identity.
     * Hashtable storing different parameters and a vector storing those parameter names is
     * instantiated too.
     * @param pId The parameter identity name
     * @param pCode The parameter identity value
     */
    public Action(String pId, String pCode) {
        mActionId = new Identity(pId, pCode);
        mParameters = new Hashtable<>();
        mParameterNames = new ArrayList<>();
    }

    /**
     * Constructor for Action object that assignes an existing identity object to itself.
     * Hashtable storing different parameters and a vector storing those parameter names are
     * instantiated too.
     * @param pActionId Identity of the Action object
     */
    public Action(Identity pActionId) {
        mActionId = pActionId;
        mParameters = new Hashtable<>();
        mParameterNames = new ArrayList<>();
    }

    /**
     * Constructor that assigns an identity. Hashtable storing different parameters and a 
     * vector storing those parameter names are instantiated too.
     * @param pActionId Identity of the Action object
     * @param pParameter Object storing the list of parameters associated with that Action
     */
    public Action(Identity pActionId, Parameter pParameter) {
        mActionId = pActionId;
        mParameters = new Hashtable<>();
        mParameters.put(pParameter.getName(), pParameter);
        mParameterNames = new ArrayList<>();
        mParameterNames.add(pParameter.getName());
    }

    /**
     * Method that adds a new parameter to the existing hashtable or creates a new
     * mParameters hashtable to populate it with the first parameter object.
     * @param pParameter The parameter object to be added
     */
    public void addParameter(Parameter pParameter) {
        if (mParameters == null) {
            mParameters = new Hashtable<>();
        }

        mParameters.put(pParameter.getName(), pParameter);
        mParameterNames.add(pParameter.getName());

    }

    /**
     * The only time this function is called is when there is a need to change
     * the crop name. This is called from change crop in OperationObject class
     * @param pName Name of the parameter whose value needs to be changed
     * @param pValue The new value of the parameter
     */
    public void changeParameterValue(String pName, String pValue) {
        if (mParameters != null) {
            mParameters.remove(pName); // get the parameter named 'gcropname'
            Parameter parameter = new Parameter();
            parameter.initialize(pName, pValue);
            mParameters.put(pName, parameter); // parameters are stored according to their names
        }
    }

    /**
     * The only time this function is called is when there is a need to change
     * the values of all the crop names and their values. This is called from change crop
     * in OperationObject class
     * @param pCrop The crop object whose parameter names and values need to be changed.
     */
    public void changeAllParameterValues(CropObject pCrop) {
        if (mParameters != null && pCrop != null && pCrop.getParameterNames() != null) {
            pCrop.getParameterNames().forEach(paramName -> {
                Parameter oldParameter = mParameters.get(paramName);
                Parameter newParameter = pCrop.getParameter(paramName);
                if (oldParameter != null) {
                    oldParameter.changeValue(newParameter);
                }
                else {
                    mParameters.put(paramName, newParameter);
                    mParameterNames.add(paramName);
                }
            }); /* -ihaas- This is the old version. The new way above cycles through
            all of the new crop parameters instead of the old parameters
            This way, only the parameters that are in the new crop
            will be updated. I also added the else clause which adds
            any parameters that exist in the new crop but do not
            currently exist in the old crop.
            Enumeration<Parameter> e = mParameters.elements();
            while (e.hasMoreElements()) {
            Parameter oldParameter = e.nextElement();
            Parameter newParameter = pCrop.getParameter(oldParameter.get(XMLConstants.sparamname));
            if (newParameter != null) {
            oldParameter.changeValue(newParameter);
            }
            }
             */

        }

    }

    /**
     * Returns true if the two actions have the same id and parameters.
     * Returns false otherwise.
     * @param obj
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Action other = (Action) obj;
        if (this.mActionId != other.mActionId && (this.mActionId == null || !this.mActionId.equals(other.mActionId))) {
            return false;
        }
        if (this.mParameters != other.mParameters && (this.mParameters == null || !this.mParameters.equals(other.mParameters))) {
            return false;
        }
        return !(this.mParameterNames != other.mParameterNames && (this.mParameterNames == null
                || !this.mParameterNames.equals(other.mParameterNames)));
    }

    /**
     *
     * @return
     */
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 19 * hash + (this.mActionId != null ? this.mActionId.hashCode() : 0);
        return hash;
    }

    /**
     * This method initializes the Action node (from weps .man file) with the values in the text file according to
     * the XML tree structure mentioned in the file that is passed.
     * @param pNode The node whose values are being used to populate the datastructure.
     */
    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(); // nodeChild is 'identity' or 'actioname' or 'paramdefn'

        while (nodeChild != null) {
            String nodeChildName = nodeChild.getNodeName();
            
            if (nodeChildName.equals(XMLConstants.sidentity)) {
                Node code = walker.firstChild();
                String codeValue = XMLDoc.getTextData(code);

                //String codeValue = walker.firstChild().getNodeValue();
                //walker.parentNode(); // move the walker to the level of code / id. 
                Node id = walker.nextSibling();
                String idValue = XMLDoc.getTextData(id);
                //String idValue = walker.firstChild().getNodeValue();
                //walker.parentNode(); //get to the level of id /code

                Identity identity = new Identity(idValue, codeValue);

                mActionId = identity;
                
                walker.parentNode(); // get to the level of identity
            } else if (nodeChildName.equals(XMLConstants.sparam)) {
                Parameter parameter = new Parameter();

                Node paramNode = nodeChild;
                parameter.initialize(paramNode);

                addParameter(parameter);

                //walker is already at the level of paramdefn
            }
            nodeChild = walker.nextSibling();
        } // end while
    }

    /**
     * Note: The member variable mParameterNames contains only the names of the parameter for which
     * this action has the value, whereas the parameterNames from the paramFormatvec has names of 
     * all parameters this action can have.
     * Initializes the node with the data from corresponding line of the .man file.
     * @param pDataLine The data string from the .man file
     * @param pLineNum The line number on which this data string exists
     */
    public void initialize(String pDataLine, int pLineNum) {

        String paramName;

        // Get the order of the parameters from ConfigData
        List<String> paramFormatLine = MCREWConfig.getManFileFormat(mActionId, pLineNum);
        if (paramFormatLine == null) {
            return;
        }

        int numParams = paramFormatLine.size();
        if (numParams == 1) { // don't use tokenizer
            paramName = paramFormatLine.get(0);
            Parameter parameter = new Parameter(paramName, pDataLine);
            addParameter(parameter);
        } else {	// Now handle the normal case where in a data line contains various param 
            // values separated through single space (delimeter)
            int paramCount = 0;
            StringTokenizer tokenizer = new StringTokenizer(pDataLine, " ");
            while (tokenizer.hasMoreElements()) {
                String token = tokenizer.nextToken(); //the token is the parameter Value

                try {
                    paramName = paramFormatLine.get(paramCount++);
                    Parameter parameter = new Parameter(paramName, token);
                    addParameter(parameter);
                } catch (IndexOutOfBoundsException e) {
                    //EL - 12/21 : this is catching a lot of index out of bounds exceptions (just catching silently)
                    //EL - this has to do with ManageData -> readManFile method.
                }
            }
        }
    }

    /**
     * Returns the identity object associated with the ACTION object. This is generally used in
     * figuring out the object in the hashtable that stores it.
     * @return The identity object associated with the Action object.
     */
    public Identity getIdentity() {
        return mActionId;
    }

    /**
     * Getter Method returning the list of parameter names
     * @return Returns the vector containing the parameter names contained in the action
     * object.
     */
    public List<String> getParameterNames() {
        return mParameterNames;
    }

    /**
     * Getter Method returning the Parameter object associated with the parameter
     * name
     * @param pName The name of the parameter whose parameter object is requested
     * @return The requested parameter object
     */
    public Parameter getParameter(String pName) {
        if (mParameters != null) {
            return mParameters.get(pName);
        }

        return null;
    }

    /**
     * Getter method to get the name of the Action object
     * @return Return the action name by fetching it from the appropriate ActionMeta class
     */
    public String getActionName() {
        String actionName = null;
        ActionMeta actionMeta = MCREWConfig.getObjectMeta(XMLConstants.soperation).getActionMeta(mActionId);
        if (actionMeta != null) {
            actionName = actionMeta.getActionName();
        }

        return actionName;
    }

    /**
     * Tells whether its a crop object or not
     * @return Return true if the object is of type CropObject, otherwise false
     */
    public boolean isCrop() {
        return mActionId.equals(new Identity(XMLConstants.kCropId, XMLConstants.sCropCode));
    }

    /**
     * Tells whether its a OperationObject or not by checking if the identity is an operation 
     * i.e. checks if code = "O"
     * @return Return true if the object is of type OperationObject, otherwise false
     */
    public boolean isOperation() {
        return mActionId.code.equals(XMLConstants.sOperationCode);
    }

    /**
     * Getter method that fetches the value from the object whose name is pParamName
     * @param pParamName The name of the parameter whose object is to be retrieve and value fetched
     * @return Returns the value of the parameter object with name pParamName
     */
    public String getValue(String pParamName) {
        if (mParameters != null) {
            Parameter parameter = mParameters.get(pParamName);
            if (parameter != null) {
                return parameter.get(XMLConstants.svalue);
            }
        }

        return null;
    }

    /**
     * Getter method that returns the action node of the document.
     * @param doc The XML document from where we contruct the tree nodes using the
     * template to organize the data from our database for future access
     * using objects(DOM)
     * @return The node formed using this data
     */
    public Node getNode(Document doc) {
        Node actionvalueNode = doc.createElement(XMLConstants.sactionvalue);

        Node identityNode = mActionId.getNode(doc);
        actionvalueNode.appendChild(identityNode);

        for (String paramName : mParameterNames) {
            Parameter parameter = mParameters.get(paramName);
            Node paramNode = parameter.getNode(doc);

            actionvalueNode.appendChild(paramNode);
        }
        /* Old implementation
         Enumeration enum = mParameters.elements();
         while(enum.hasMoreElements())
         {
         Parameter parameter = (Parameter)enum.nextElement();
         Node paramNode = parameter.getNode(doc);
			
         actionvalueNode.appendChild(paramNode);
         }
         */
        return actionvalueNode;
    }

    /**
     * The method that makes a copy of the action object that stores the data in
     * a format defined in the operation (def, display, lang ) & crop
     * (def, display, lang ) XML files.
     * @return Returns the object that was created after the copy object.
     */
    @Override
    public Object clone() {
        try {
            ////System.err.println("Action:clone" + " Cloning "+ mActionId);
            Action copy = (Action) super.clone();

            // copy.mParameters = (Hashtable)mParameters.clone(); // This just creates a shallow copy of the Hashtable
            copy.mParameters = new Hashtable<>();

            Enumeration<String> e = mParameters.keys();
            while (e.hasMoreElements()) {
                String key = e.nextElement();
                Parameter parameter = mParameters.get(key);

                String keyCopy = key;
                Parameter parameterCopy = (Parameter) parameter.clone();

                copy.mParameters.put(keyCopy, parameterCopy);

            }

            //	copy.mParameterNames = (List)mParameterNames.clone(); //This just creates a shallow copy of List.
            copy.mParameterNames = new ArrayList<>();
            ListIterator<String> le = mParameterNames.listIterator();
            while (le.hasNext()) {
                String paramName = le.next();
                String paramNameCopy = paramName;

                copy.mParameterNames.add(paramNameCopy);
            }

            return copy;
        } catch (CloneNotSupportedException e) {
            //System.err.println("Action:Clone();" + "Clone Not Supported!");
            //throw new Error("This should never happen!"); 
        }

        return null;
    }

    /**
     * Finds any missing or extra parameters by comparing to the ActionMeta
     * @return invalidParams. The list containing parameter names that are either:
     *          missing (if there are any),
     *          extra (if there are no missing)
     *          otherwise null
     */
    public List<String> checkParameterConsistency() {
        int check = getCheckForParameterConsistency();
        
        if (check == ManageData.CHECK_PASSED || check == -1){
            return null;
        }
        
        List<String> invalidParams = new ArrayList<>();

        ActionMeta actionMeta = MCREWConfig.getObjectMeta(XMLConstants.soperation).getActionMeta(mActionId);
        if (actionMeta == null) {
            return null;
        }

        List<String> metaParameters = actionMeta.getParameterNames();

        if (check == ManageData.CHECK_WARNING){
            // this has extra parameters
            for (String p : mParameterNames) {
                if (!metaParameters.contains(p)) {
                    invalidParams.add(p + " (extra)");
                }
            }
        }
        else {
            // this is missing 1 or more parameters that are defined in the meta
            for (String p : metaParameters) {
                if (!mParameterNames.contains(p)) {
                    if (actionMeta.getParameterMeta(p).isRequired())
                        invalidParams.add(p + " (MISSING)");
                }
            }
        }
                
        return invalidParams.isEmpty() ? null : invalidParams;
    }
    
    /**
     * Determines if there are any missing or extra parameters by comparing to the ActionMeta
     * @return ManageData.CHECK_PASSED (No missing or extra parameters)
     *         ManageData.CHECK_WARNING (No missing but at least one extra parameter)
     *         ManageData.CHECK_FAILED (At least one missing parameter)
     */
    public int getCheckForParameterConsistency(){
        ActionMeta actionMeta = MCREWConfig.getObjectMeta(XMLConstants.soperation).getActionMeta(mActionId);
        if (actionMeta == null) {
            return -1;
        }

        List<String> metaParameters = actionMeta.getParameterNames();

        if (mParameterNames.containsAll(metaParameters)) {
            if (mParameterNames.size() == metaParameters.size()) {
                return ManageData.CHECK_PASSED;
            }
            else {
                // this has extra params. give warning
                return ManageData.CHECK_WARNING;
            }
        }
        else {
            // this is missing 1 or more parameters that are defined in the meta
            return ManageData.CHECK_FAILED;
        }
    }
    
    /**
     * Creates P101 from P51 when a .upgm is chosen to replace a .crop
     * @return The P101 Action
     */
    public Action createP101FromP51() {
        final String[] CROP_INDEPENDANT_PARAMETERS = {"rowflag", "rowspac", "rowridge"};
        
        Action P101 = new Action("101", "P");
        
        for(String param : CROP_INDEPENDANT_PARAMETERS) {
            P101.addParameter(this.getParameter(param));
        }
        return P101;
    }
    
    /**
     * Compares this identity with parameter
     * @param id The numeric part of the Identity to compare
     * @param code The character part of the Identity to compare
     * @return true if the Identities are the same, otherwise false
     */
    public boolean checkId(String id, String code) {
        if (mActionId != null)
            return mActionId.equals(new Identity(id, code));
        else
            return false;
    }
    
    /**
     * Compares Checks if this is an UPGM Action
     * @return true if this is an UPGM, otherwise false
     */
    public boolean isUpgmAction() {
        Identity identity = getIdentity();
        String code = identity.code;
        int id = identity.id;
        
        return code.equals("P") && id >= 100 && id != 101;
    }
    
    /**
     * Creates P51 from P101 when a .crop is chosen to replace a .upgm
     * @return The created P51 Action
     */
    public Action createP51FromP101() {
        final String[] CROP_INDEPENDANT_PARAMETER_NAMES = {"rowflag", "rowspac", "rowridge"};

        OperationMeta opMeta = (OperationMeta) MCREWConfig.getObjectMeta(XMLConstants.soperation);
        ActionMeta aMeta = opMeta.getActionMeta(new Identity("51", "P"));
        List<String> p51ParameterNames = aMeta.getParameterNames();
        
        Action P51 = new Action("51", "P");
        
        p51ParameterNames.forEach(paramName -> {
            P51.addParameter(new Parameter(paramName, ""));
        });
        for(String param : CROP_INDEPENDANT_PARAMETER_NAMES) {
            P51.getParameter(param).changeValue(this.getParameter(param));
        }
        return P51;
    }
    
    /**
     * Removes all parameters that are not defined in the meta
     */
    public void removeExtraParameters() {
        List<String> extraParams = new ArrayList<>();
        
        ActionMeta actionMeta = MCREWConfig.getObjectMeta(XMLConstants.soperation).getActionMeta(mActionId);
        if (actionMeta == null) {
            return;
        }

        List<String> metaParameters = actionMeta.getParameterNames();

        mParameterNames.stream().filter(p -> (!metaParameters.contains(p))).forEachOrdered(p -> {
            extraParams.add(p);
        });
        
        if (extraParams.isEmpty()) {
            return;
        }
        mParameterNames.removeAll(extraParams);
        extraParams.forEach(param -> {
            mParameters.remove(param);
        });
    }
}
