/* CropObject This class holds all crop data
 * Operation objects have all the values of crop under their actions (G:03, P:51). Only time a crop object 
 * is used is when the user selects new crop from the table. Then the values from crop object are later
 * copied onto the appropriate paramters in the actions of Operation object.
 */
package usda.weru.mcrew;

import de.schlichtherle.truezip.file.TFile;
import java.util.Hashtable;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import javax.swing.JOptionPane;
import org.apache.log4j.Logger;
import org.openide.util.Exceptions;
import org.w3c.dom.DOMException;
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;

import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;

/**
 * This object holds all the information about a crop. It helps to read the
 * information specific to this crop from the management(.man) and/or XML
 * files and be able to initialize/update the data coming from various
 * screens of the GUI model.
 */
public class CropObject extends DataObject implements Cloneable {

    private final static Logger logger = Logger.getLogger(CropMeta.class);
    /**
     * The name of the current crop object
     */
    public String mCropName; // Name of this particular operation
    private Hashtable<String, Parameter> mParameters; // to hold all parameter values;
    Vector<String> mParameterNames;

    /**
     * Instance of CropMeta to store limits and datatypes.
     */
    CropMeta mCropMeta = (CropMeta) MCREWConfig.getObjectMeta(XMLConstants.scrop);
    
    /**
     * If the .crop file is found it returns 1
     */
    public static final int kSuccess = 1;

    /** Flag if true indicates that atleast one category for this object
     * does not have any parameter and so need not display that category 
     * in the crop drilldown screen- neha
     */
    public boolean categoryEmpty = false;

    /** Flag - true if all the parameters in a particular category are 
     * hidden (a H display attribute), if true we hide that category
     * in the drilldown screen - neha
     */
    public boolean allParamsHidden = false;
    /**
     * If an error type relating the file is not found we use this value.
     */
    public static final int kUnknown_Error = -1;

    /**
     * If the .crop file we are looking for is not found we return -2
     */
    public static final int kFile_NotFound = -2;
    

    /**
     * The constructor for crop object that defines the hashtable holding
     * all the parameter objects for the crop.
     */
    public CropObject() {
        mObjectName = XMLConstants.scrop;
        mCropName = "No Crop";
        mParameters = new Hashtable<>();
        mParameterNames = null;
    }

    /**
     * The constructor for crop object that defines the hashtable holding
     * all the parameter objects for the crop
     * @param pObjectName The name of the object that is being created.
     */
    public CropObject(String pObjectName) {
        mObjectName = XMLConstants.scrop;
        mCropName = "No Crop";
        mParameters = new Hashtable<>();

    }

    /**
     * The constructor for crop object that defines the hashtable holding
     * all the parameter objects for the crop
     * @param pObjectName The name of the object that is being created.
     * @param pCropName The name of the crop whose object is being created.
     */
    public CropObject(String pObjectName, String pCropName) {
        mObjectName = XMLConstants.scrop;
        mCropName = pCropName;
        mParameters = new Hashtable<>();
    }

    /**
     * A method that sets the name of the crop to whatever string is being
     * passed as an argument.
     * @param pCropName The string that stores the name of the crop.
     */
    public void setCropName(String pCropName) {
        mCropName = pCropName;
    }
    
    /**
     * A method that fetches the current object name.
     * @return The name of the crop object
     */
    @Override
    public String getObjectName() {
        return mObjectName;
    }

    /**
     * A method that fetches the current crop name.
     * @return The name of the crop itself 
     */
    public String getCropName() {
        return mCropName;
    }

    /**
     * Initialize function can be called while reading in a new crop XML file where 
     * the document is traversed through node by node and if its a crop node then 
     * its meta data like name, parameters are pulled, initialised and the data is set.
     * @param pNode Its the node that may consist of an operation or crop details.
     */
    @Override
    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);

        String nodeName = pNode.getNodeName();

        if (nodeName.equals(XMLConstants.scropDB)) {
            Node nodeChild = walker.firstChild();
            //No need to loop as the root itself is passed to action
            while (nodeChild != null) {
                String nodeChildName = nodeChild.getNodeName();
                if (nodeChildName.equals(XMLConstants.scropname)) {
                    String cropName = XMLDoc.getTextData(nodeChild);

                    setCropName(cropName);
                } else if (nodeChildName.equals(XMLConstants.sparam)) {
                    Parameter parameter = new Parameter();
                    parameter.initialize(nodeChild);

                    String pname = parameter.get(XMLConstants.sname);

                    mParameters.put(pname, parameter);
                }
                nodeChild = walker.nextSibling();
            }
        }
    }

    /**
     * This method Only this class knows the structure of a cropDB node and so both reading from
     * (cropDB)Node and creating new node are done here.
     * @param doc The document object that contains the crop data for various crop nodes.
     * @return cropDB node whose node contents were set from the document that was parsed
     */
    @Override
    public Node getNode(Document doc) {
        /* 
         */
        Node cropDBNode = doc.createElement(XMLConstants.scropDB);

        setNodeContents(cropDBNode, doc);

        return cropDBNode;
    }

    /**
     * Sets a given cropDBNode with the present Object contents. The template it uses 
     * comes from the various crop XML files
     * @param pNode THe node whose contents such as crop name, parameters etc. need
     * to be set.
     * @param pDoc The document that contains the data which will be set as node contents
     * of the crop with name cropname.
     */
    private void setNodeContents(Node pNode, Document pDoc) {
        Node cropNameNode = pDoc.createElement(XMLConstants.scropname);
        logger.debug("CropObject :setNodeContents : The Document name is : " + pDoc.getNodeName()
                + "   The Document Value  is : " + pDoc.getNodeValue());
        XMLDoc.setTextData(cropNameNode, mCropName, pDoc);

        pNode.appendChild(cropNameNode);

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

            pNode.appendChild(paramNode);
        }
    }

    /**
     * Sets a given cropDBNode with the present Object contents. The template it uses 
     * comes from the various crop XML files
     * @param pNode THe node whose contents such as crop name, parameters etc. need
     * to be set.
     * @param pDoc The document that contains the data which will be set as node contents
     * of the crop with name cropname.
     * @param newFileName The name of the file being served as the crop name as the
     * data fpr this crop will be stored here in this file.
     */
    private void setNodeContents(Node pNode, Document pDoc, String newFileName) {

        Node cropNameNode = pDoc.createElement(XMLConstants.scropname);
        logger.debug("CropObject :setNodeContents : The Document name is : " + pDoc.getNodeName()
                + "   The Document Value  is : " + pDoc.getNodeValue());
        if (cropNameNode.getNodeName().equals(XMLConstants.scropname) && (!mCropName.equals(newFileName))) {
            mCropName = newFileName;
        }
        XMLDoc.setTextData(cropNameNode, mCropName, pDoc);
        pNode.appendChild(cropNameNode);

        Vector<String> missingParameters = new Vector<String>();
        boolean paramPresent = false;
        String defaultParamName = "";
        //Vector defaultCropParameterNames = null;
        CropObject defaultCrop = null;
        //Atleast one category under this crop object has no parameter objects
        if (categoryEmpty == true) {
            //get the no_crop XML file into a Node
            String fullCropName = new TFile("mcrew_cfg", XMLConstants.sno_cropFileName).getAbsolutePath();
            Node cropNode = SkelImportPanel.loadXMLCropFile(fullCropName);
            //create a CropObject and initialize it with the node from the no_crop XML file
            defaultCrop = new CropObject();
            defaultCrop.initialize(cropNode);
            /** Get the parameters from the no_crop and the parameters from "this" CropObject
             * and get those parameters that are NOT in "this" CropObject and present
             * in no_crop. Add these parameters to the new crop file that is being written - neha
             */
            Enumeration<String> entries = defaultCrop.mParameters.keys();
            while (entries.hasMoreElements()) {
                paramPresent = false;
                defaultParamName = entries.nextElement();
                for (String paramName : mParameterNames) {
                    if (defaultParamName.equals(paramName) == true) {
                        paramPresent = true;
                        break;
                    }
                }
                if (paramPresent == false) {
                    //  //System.out.println("defaultParamName:"+defaultParamName);
                    missingParameters.add(defaultParamName);
                }
            }//end of while loop
            for (String missingParamName : missingParameters) {
                Parameter parameter = defaultCrop.mParameters.get(missingParamName);
                Node paramNode = parameter.getNode(pDoc);
                pNode.appendChild(paramNode);
            }
        }//end of if categoryEmpty is true
        for (String paramName : mParameterNames) {
            ////System.out.println("paramName:"+paramName);
            Parameter parameter = mParameters.get(paramName);
            Node paramNode = parameter.getNode(pDoc);
            pNode.appendChild(paramNode);
        }

    }

    /**
     * Read new crop xml file (*.oprn). Note that initialize() is called directly
     * when reading from management files. This function is called to read from
     * new crop when adding a crop from Table.java. Also when data from a XML file needs 
     * to be displayed on the main MCREW screen and the corresponding crop/operation screens
     * @param pFileName The XML file that needs to be read with the crop/operation 
     * formatted data.
     * @return A positive integer if the file is read successfully else negative 1 which means error code
     *
     */
    @Override
    public int readXMLFile(String pFileName) {
        logger.debug("XML Filename is :-" + pFileName);
        DataObject dataObject = null;
        Node root, node;
        Document doc;
        RowInfo row;

        doc = XMLDoc.getDocument(pFileName);
        if (doc == null) {
            return kUnknown_Error;
        }

        doc.normalize();
        initialize(doc.getDocumentElement());

        return kSuccess;
        //return 1;
    }

    /**
     *
     * Writes a new crop xml file. This function is called when user selcts 
     * to save a new crop from crop Drill down screen.
     * @param pFileName The name of the file to be used for saving the data.
     * @return A positive integer if the file was saved successfully else negative 
     */
    public int writeXMLFile(String pFileName) {
            String newFileName1 = null;
            if (!pFileName.endsWith(".crop")) {
                pFileName = pFileName + ".crop";
            }
            int index = pFileName.lastIndexOf("\\");
            if(index != -1) newFileName1 = pFileName.substring(index + 1, pFileName.lastIndexOf("."));
            else newFileName1 = pFileName.substring(pFileName.lastIndexOf("/") + 1, pFileName.lastIndexOf("."));
            Node root;

            String nonEscapingElements[] = {XMLConstants.scropname, XMLConstants.sname};

            try {

                /* createDocument function creates a new Document with appropriate Doctype and stylesheet attached */
                Document cropDoc = XMLDoc.createDocument(XMLConstants.scrop);
                //System.out.println("CropObject : writeXMLFile : - scrop is : - - -  " + XMLConstants.scrop );
                if (cropDoc == null) {
                    System.err.println("OprnObject:writerXMLFile(): " + "cropDoc is null");
                    return kUnknown_Error;
                }

                root = cropDoc.getDocumentElement();
                //System.out.println("CropObject : writeXMLFile : ROOT NodeName is - - -  " + root.getNodeName() );
                setNodeContents(root, cropDoc, newFileName1);

                DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
                DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
                if (impl == null) {
                    System.out.println("No DOMImplementation found !");
                    return kUnknown_Error;
                }
                LSSerializer serializer = impl.createLSSerializer();
                serializer.getDomConfig().setParameter("format-pretty-print", true);
                LSOutput output = impl.createLSOutput();
                output.setEncoding(XMLConstants.sEncoding);
                output.setCharacterStream(new java.io.FileWriter(pFileName));
                serializer.write(cropDoc, output);
            } catch (DOMException e) {
                System.err.println("DomException");
            } catch (java.io.IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | ClassCastException ex) {
                Exceptions.printStackTrace(ex);
            }
            return kSuccess;
    }
    

    /**
     * This function can be called to initialize all parameter values from an already 
     * existing crop action (P51 or anuy other crop Action) within a OperationObject
     * Note that cropObject by itself does not have actions.
     * Also, P51 has three extra parameters that a crop object does not have. These parameters 
     * are removed.
     * @param pAction The action object that contains these parameters and other data 
     * like the identity, etc.
     */
    public void setCropParameters(Action pAction) {
        mCropMeta = (CropMeta) MCREWConfig.getObjectMeta(XMLConstants.scrop);
        if (mCropMeta == null) {
            System.err.println("CropObject:setCropParameters: " + "No CropMeta object found");
            return;
        }

        /* Get the parameter names from CropMeta object. Retireve the values for those parameters from
         * pAction and then copy it onto the current ctopObject
         */
        mParameterNames = new Vector<>();

        Identity inp = pAction.getIdentity();
        Identity decomp = new Identity(65, "P");
        List<String> cropMetaParameters;
        //We want to only try to read decomp parameters if this is a decomp object.
        if(inp.equals(decomp)) cropMetaParameters = mCropMeta.getDecompNames();
        else cropMetaParameters = mCropMeta.getParameterNames();
        //Vector actionParameters = pAction.getParameterNames();
        for (String name : cropMetaParameters) {
            Parameter parameter = pAction.getParameter(name);

            if (parameter != null) {
                mParameters.remove(name); // Remove older one
                mParameters.put(name, parameter);	 // Add the newer one
                logger.debug("Parameter Name added is :-" + name + "  and its value is : " + ((String) parameter.getValue()));
                ////System.out.println("CropObject: setCropParameters(): Parameter Name is :-" + name);
                mParameterNames.add(name);
            } else {
                if(!name.equals("crop_devnotes")) logger.warn("Cropobject does not have value for parameter " + name);
            }
        }

    }

    /**
     * This method fetches the parameters present in each category of the
     * crop are divided into for their classification purposes. 
     * An example could be found in the crop_display XML file that tells you 
     * about various category parameters.
     * @return The hashtable that contains the various category parameters 
     */
    public Map<String, List<String>> getCategoryParams() {
        mCropMeta = (CropMeta) MCREWConfig.getObjectMeta(XMLConstants.scrop);
        if (mCropMeta != null) {
            Map<String, List<String>> cP = mCropMeta.getCropCatAndParams();

            return cP;
        } else {
            return null;
        }
    }
    /**
     * This method fetches the CropMeta object itself
     * @return CropMeta
     */
    public CropMeta getMeta()
    {
        return mCropMeta;
    }

    /**
     * This method fetches the list of category names for a crop and the
     * crop are divided into for their classification purposes. 
     * An example could be found in the crop_display XML file that tells you 
     * about various category names used as Tab headers in the crop_Dlg screen.
     * @return The vector that contains the list of category names for that crop.
     */
    public List<String> getCategoryNames() {
        mCropMeta = (CropMeta) MCREWConfig.getObjectMeta(XMLConstants.scrop);
        if (mCropMeta != null) {
            List<String> cnVec = mCropMeta.getCategoryNamesVec();
            return cnVec;
        } else {
            return null;
        }
    }

    /**
     * This method fetches the the parameter object associated with the name pName
     * in the hashtable mParameters.
     * @param pName The name whose parameter object is requested.
     * @return The parameter object associated with pName
     */
    public Parameter getParameter(String pName) {
        if (mParameters != null) {
            return mParameters.get(pName);
        }
        return null;
    }

    /**
     * Fetches the parameter names that are associated with the crop object.
     * @return The vector that holds all these parameter names.
     */
    public Vector<String> getParameterNames() {
        return mParameterNames;
    }

    /**
     * Fetches the values associated with the parameter in the crop object for the
     * column with name pColName.
     * @param pColName The name of the column whose parameter value is requested.
     * @return The actual value associated with the parameter.
     */
    public String getValue(String pColName) {
        if (pColName.equals(XMLConstants.scropname)) {
            return mCropName;
        }

        /* If not crop name, then the pColName should be any parameter name */
        Parameter parameter = mParameters.get(pColName);
        if (parameter != null) {
            return parameter.get(XMLConstants.svalue);
        }

        return null;
    }

    /**
     * Fetches the values associated with the parameter in the crop object for the
     * column with name pColName.
     * @param pColName The name of the column whose parameter value is requested.
     * @param pUnit The units in which the parameter meta data is stored or represented
     * This metadata is for the parameter in the column pColName for the crop Object.
     * @return The actual value associated with the parameter.
     */
    @Override
    public String getValue(String pColName, int pUnit) {
        if (pColName.equals(XMLConstants.scropname)) {
            return mCropName;
        }

        /* If not crop name, then the pColName should be any parameter name */
        String returnValue = null;
        Parameter parameter = mParameters.get(pColName);
        if (parameter != null) {
            returnValue = parameter.get("value");
        }

        if ((returnValue != null) && (pUnit == XMLConstants.kAlternateUnit)) // i.e conversion is required
        {
            mCropMeta = (CropMeta) MCREWConfig.getObjectMeta(XMLConstants.scrop);
            if (mCropMeta != null) {
                ParameterMeta paramMeta = mCropMeta.getParameterMeta(pColName);

                returnValue = paramMeta.convertToAlternateUnits(returnValue);
            }
        }
        return returnValue;
        //return null;
    }

    /**
     * Sets the parameter in crop object with name pParamName to the value for
     * the column with name pColName.
     * @param pParamName The name of the parameter whose value is being updated to a
     * new value
     * @param pNewValue The new value of the crop parameter.
     * @param pUnit The units to which the new value for the parameter name will
     * exists.
     */
    @Override
    public void setValue(String pParamName, String pNewValue, int pUnit) {
        String oldValue = null;
        String valueToSet = pNewValue;

        Parameter parameter = mParameters.get(pParamName);
        oldValue = parameter.get(XMLConstants.svalue);

        ParameterMeta paramMeta = ((CropMeta) MCREWConfig.getObjectMeta(XMLConstants.scrop)).getParameterMeta(pParamName);
        if (paramMeta != null) {
            /* Values are always stored in Primary unit, convert it if displayed values are in Alternate unit */
            if (pUnit == XMLConstants.kAlternateUnit) {
                if (paramMeta != null) {

                    valueToSet = paramMeta.convertToPrimaryUnits(pNewValue);
                }

                logger.debug("Converted new value " + pNewValue + " to " + valueToSet);
            }

            if (!(oldValue.equals(valueToSet))) {
                /* Values should be written out in proper type i,e if user types in 5.00 for an integer value, 
                 * then it should be converted to 5 before putting the into the parameter
                 */
                String properTypeValue = valueToSet;
                properTypeValue = paramMeta.convertToProperType(properTypeValue);

                parameter.setValue(properTypeValue);
                return;

            }
        } else {
            logger.warn("Could not find the parameterMeta. Setting new value anyway");

            parameter.setValue(pNewValue);
            return;
        }

    }

    /**
     *
     * @param obj
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final CropObject other = (CropObject) obj;
        if (this.mCropName != other.mCropName && (this.mCropName == null || !this.mCropName.equals(other.mCropName))) {
            return false;
        }
        if (this.mParameters != other.mParameters && (this.mParameters == null || !this.mParameters.equals(other.mParameters))) {
            return false;
        }
        if (this.mParameterNames != other.mParameterNames && (this.mParameterNames == null
                || !this.mParameterNames.equals(other.mParameterNames))) {
            return false;
        }
        return true;
    }

    /**
     *
     * @return
     */
    @Override
    public int hashCode() {
        int hash = 5;
        hash = 79 * hash + (this.mCropName != null ? this.mCropName.hashCode() : 0);
        return hash;
    }

    /**
     * The method that makes an identical copy of the crop object.
     * @return The copy made from the original object.
     */
    @Override
    public Object clone() {

        CropObject copy = (CropObject) super.clone();
        copy.mCropName = mCropName;

        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);
        }
        return copy;

    }
    
    /**
     * Checks all of the data contained within the object.
     * @return tableStatus
     */
    @Override
    public InputLimits.TableStatus checkData()
    {
        InputLimits.TableStatus overall = InputLimits.TableStatus.OKAY;
        for(String paramName : getParameterNames())
        {
            String value = getValue(paramName);
            if(value == null) continue;
            ParameterMeta limits = mCropMeta.getParameterMeta(paramName);
            if(limits.getGroupAttribute(paramName, XMLConstants.svalueattribute).
                    equals(XMLConstants.schoice))
            {
                lim.put(paramName, InputLimits.TableStatus.CHOICE);
                continue;
            }
            if(limits.getGroupAttribute(paramName, XMLConstants.svalueattribute).
                    equals(XMLConstants.smultiline)) 
            {
                lim.put(paramName, InputLimits.TableStatus.CHOICE);
                continue;
            }
            if(getParameter("idc") != null)
            {
                if(getParameter("idc").getValue().equals("0") && !mCropMeta.getDecompNames().contains(paramName))
                {
                    lim.put(paramName, InputLimits.TableStatus.CHOICE);
                    continue;
                }
            }
            InputLimits limiter = limits.getLimits();
            InputLimits.TableStatus currentCellStatus = limiter.evaluateInput(value);
            lim.put(paramName, currentCellStatus);
            if(overall.lessThan(currentCellStatus))
            {
                overall = currentCellStatus;
            }
        }
        if(getParameter("idc") != null)
        {
            if(getParameter("idc").getValue().equals("-1"))
            {
                lim.put("idc", InputLimits.TableStatus.NOSAVE);
                overall = InputLimits.TableStatus.NOSAVE;
            }
        }
        return overall;
    }
}
