/* ManageData
 *
 * @version 1.1
 *
 * @date 03-25-03
 *
 * @author Sada, Manmohan
 *
 */

package usda.weru.mcrew;

import com.klg.jclass.table.EditableTableDataModel;
import com.klg.jclass.table.data.AbstractDataSource;

import javax.swing.*;
import java.util.*;

import org.w3c.dom.*;
import org.w3c.dom.traversal.*;
import org.w3c.dom.traversal.NodeFilter;
import com.klg.jclass.table.JCCellRange;
import com.sun.org.apache.xml.internal.serialize.OutputFormat;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;
import de.schlichtherle.io.File;
import de.schlichtherle.io.FileReader;
import de.schlichtherle.io.FileWriter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.Format;
import java.text.SimpleDateFormat;
import org.apache.log4j.Logger;
import usda.weru.util.WepsFileTypes;



/**
 * This is the class that holds the data for each row (through RowInfo object). It also
 * has various functions to deal with the operations like cut, copy, paste, read and
 * write data files.
 */
public class ManageData extends AbstractDataSource implements EditableTableDataModel {
    private static final Logger LOGGER = Logger.getLogger(ManageData.class);
    public enum WriteFileMode {

        NORMAL, UPDATE, FROM_NRCS
    }
    /**
     * String that indicates the start of the document.
     */
    public static final String sSTART="START";
    /**
     * String that indicates the end of the document.
     */
    public static final String sEND="END";
    /**
     * String that indicates the version number of the document.
     */
    public static final String sVersion="Version:";
    /**
     * String that indicates that we reached the end of the file.
     */
    public static final String sEOF="EOF";
    /**
     * String that preceeds the comments. It tells us where the comments are in
     * a .MAN file.
     */
    public static final String sCommentStr="#------------";
    /**
     * If the document doesn't have a version number when it is being created then a default
     * version is attached to the document. This valiable store it.
     * 1.50 = Added ofuel to O03 and O04
     */

    public static final double VERSION_MINIMUM= 1.40;       //minimum version the code can gracefully load
    public static final double VERSION_FUELADDED= 1.50;
    public static final double VERSION_CURRENT= 1.50;
    /**
     * THis indicates whether or not to ignore this version of the document.
     */
    public static final String sIgnoreVersion="IGNORE";
    // '#' followed by any other characters of any number
    //is also the currentVersion (now handles notes in crops and ops and man)
    //sDefaultVersion = "1.30", //is also the currentVersion
    //sOldVersion = "1.10" // ManageData cannot read old management file (New version are 1.2 or above)
    //sVersion = sDefaultVersion,
    /**
     * Indicates null line.
     */
    public static final char cNullLine='?';
    /**
     * Indicates start (*START in file) or end (*END in file) line.
     */
    public static final char cStartEndLine='*';
    /**
     * Indicator of version line that starts with character 'V' in .MAN file.
     */
    public static final char cVersionLine='V';
    /**
     * Indicator of start of the line with notes section of a .MAN file with character 'N'.
     */
    public static final char cManfileNotes='N';
    /**
     * Any line with first character as '#' is ignorable in weps management (.MAN) file as it
     * indicates that it is a comment line.
     */
    public static final char cCommentLine='#';
    /**
     * This indicates that the line contains the details about the date when these operations
     * were done.
     */
    public static final char cDateLine='D';
    /**
     * This character indicates a start of an operation data line.
     */
    public static final char cOperationLine='O';
    /**
     * This character indicates a start of a process data line.
     */
    public static final char cProcessLine='P';
    /**
     * This character indicates a start of a group data line.
     */
    public static final char cGroupLine='G';
    /**
     * This character indicates us about the data that is available for various crop or
     * operations parameters from the .MAN file.
     */
    public static final char cParameterLine='+';
    /**
     * This character indicates us about the textual description available for various crop
     * or operations parameter data accesssible from a .MAN file.
     */
    public static final char cParameterLineText='T';
    /**
     * Character that indicates the end of line for a data line.
     */
    public static final char cEndofLine='<';
    /**
     * A line such as #---..repeated till 72nd column indicates the end of this set (one
     * row) of data.
     */
    public static final char cEndofRowDataLine='-';
    /**
     * Constant value that is returned when a file is read successfully
     */
    public static final int kSuccess=1;
    /**
     * Constant integer value returned when the errors occur due to an unknown reason
     * i:e for which and exception is not caught, etc.
     */
    public static final int kUnknown_Error=-1;
    /**
     * If the version of the .MAN file we are reffering to is below the minimum mentioned we
     * return this value to indicate that its a wrong version to pull the data from for our
     * analysis purposes.
     */
    public static final int kWrong_Version=-2;
    /**
     * If the appropriate file is not found we return this number
     */
    public static final int kFile_NotFound=-3;
    /**
     * Number of lines that are present in the notes section of a weps management file.
     */
    public static final int kCorruptedFile=-666;

    private static int mwepsmanfilenoteslines=0;
    /**
     * Tells us the number of years it will take to make that soil arable for the same crop.
     */
    public int kRotationYears=1;
    /**
     * The current version of the .MAN file.
     */
    public double version;
    /**
     * The text for the notes in a WEPS management file.
     */
    public String mwepsmanfilenotes="";
    /**
     * Holds the RowInfo objects which in turn contains the data for each row.
     */
    private Vector<RowInfo> mRows;

    /**
     * Default constructor that initialises vector datastructure that stores the row data
     * from .MAN files for future use in the model.
     */
    public ManageData() {
        mRows=new Vector<RowInfo>();
    }

    /**
     * This method fetches the number of rows that exist in the MCREW table as data sets.
     * @return The number of rows in a vector.
     */
    public int size() {
        return mRows.size();
    }

    /**
     * Method that clears all the row information associated with the MCREW table.
     */
    public void clear() {
        mRows.clear();
        kRotationYears=1;
        version=VERSION_MINIMUM;
        fireDataReset();
    }

    /**
     * This method fetches the vector containing a rowInfo objects that holds all the
     * information about the table rows.
     * @return The vector containing the rowInfo objects that hold the information
     * on each row of the main MCREW table.
     */
    public Vector<RowInfo> getRows() {
        return mRows;
    }

    public RowInfo getRow(int index) {
        if(mRows == null) {
            return null;
        }
        return mRows.get(index);
    }

    /**
     * This method fetches the notes associated with a management file i:e the file
     * level notes on who, what, etc. was modified from previous working copy of the
     * file and what new changes would help in accomplishing the task.
     * @return The text string that is stored as file level notes.
     */
    public String getWepsManFileNotes() {

        return mwepsmanfilenotes;
    }

    /**
     * Adds a new Row to the hashtable of rows
     * @param pRowNum The place at which the new row would be added.
     * @param pRow RowInfo Object holding the information for that row.
     */
    public void addRow(int pRowNum, RowInfo pRow) {
        clearDeleted();
        mRows.add(pRowNum, pRow);
        fireRowsAdded(pRowNum, 1);
    }

    /**
     * The new date object that will be added to the table for the row
     * numbered pRowNum.
     * @param pRowNum The row to which the new date will be added.
     * @param pDate The date object needed to be set for row pRowNum.
     */
    public void addDate(int pRowNum, JulianCalendar pDate) {
        RowInfo row;

        if(pRowNum >= mRows.size()) {
            row=new RowInfo();
            row.setDate(pDate);
            insertRow(pRowNum, row);
            return;
        }
        row=(RowInfo) mRows.get(pRowNum);
        row.setDate(pDate);
    }

    /**
     * Fetches the date object associated with the row passed as argument to this method.
     * @param pRowNum The row number whose date object is being requested.
     * @return Tbe date object associated with the row number pRowNum
     */
    public JulianCalendar getDate(int pRowNum) {
        if(pRowNum >= mRows.size()) {
            return new JulianCalendar(01, 00, 01);
        }
        if(pRowNum < 0) {
            return new JulianCalendar(01, 00, 01);
        }
        return ((RowInfo) mRows.get(pRowNum)).getDate();
    }

    /**
     * Method that is used for changing the dates associated with multiple rows assigning
     * same values to all the row objects sitting in the vector holding the row that needs
     * change of dates.
     * @param scv The vector that holds all the row objects whose date needs to be changed.
     * @param dc The new DateChange object that holds the new values for the date to be set.
     */
    public boolean changeDates(Vector scv, DateChange dc) {
        //test only
        for(Iterator sci=scv.iterator(); sci.hasNext();) {
            JCCellRange jccr=(JCCellRange) sci.next();
            int sRow=jccr.start_row;
            int eRow=jccr.end_row;

            if(sRow > eRow) {
                int pivot=sRow;
                sRow=eRow;
                eRow=pivot;
            }
            if(!changeDates(sRow, eRow, dc.type, dc.delta, true)) {
                return false;
            }
        }

        //Actually update
        for(Iterator sci=scv.iterator(); sci.hasNext();) {
            JCCellRange jccr=(JCCellRange) sci.next();
            int sRow=jccr.start_row;
            int eRow=jccr.end_row;

            if(sRow > eRow) {
                int pivot=sRow;
                sRow=eRow;
                eRow=pivot;
            }
            changeDates(sRow, eRow, dc.type, dc.delta);
        }
        return true;
    }

    /**
     * Method that is used for changing the dates associated with multiple rows assigning
     * same values to all the row objects sitting in the vector holding the row that needs
     * change of dates.
     * @param scv The vector that holds all the row objects whose date needs to be changed.
     * @param jc The date object to be assigned to all the rows sitting in the vector scv.
     */
    public void changeDates(Vector scv, JulianCalendar jc) {
        for(Iterator sci=scv.iterator(); sci.hasNext();) {
            JCCellRange jccr=(JCCellRange) sci.next();
            int sRow=jccr.start_row;
            int eRow=jccr.end_row;

            if(sRow > eRow) {
                int pivot=sRow;
                sRow=eRow;
                eRow=pivot;
            }

            for(int rdx=sRow; rdx <= eRow; rdx++) {
                RowInfo ri;
                try {
                    ri=(RowInfo) mRows.get(rdx);
                } catch(ArrayIndexOutOfBoundsException e) {
                    System.err.println("ManageData:ChangeDates(Vector, dateChange): " + " Theres no row " + rdx);
                    continue;
                }
                ri.changeDate((JulianCalendar) jc.clone());
                fireRowChanged(ri);
            }
        }
    }

    /**
     * Method that is used for changing the dates associated with multiple rows assigning
     * same values to all the row objects sitting in the vector holding the row that needs
     * change of dates.
     * @param scv The vector that holds all the row objects whose date needs to be changed.
     * @param field The field or table cell which holds the date values which need to be modified.
     * @param value The new value to be assigned as date to the row fields.
     */
    public boolean changeDates(Vector scv, int field, int value) {
        //Test only
        for(Iterator sci=scv.iterator(); sci.hasNext();) {
            JCCellRange jccr=(JCCellRange) sci.next();
            int sRow=jccr.start_row;
            int eRow=jccr.end_row;

            if(sRow > eRow) {
                int pivot=sRow;
                sRow=eRow;
                eRow=pivot;
            }

            if(!changeDates(sRow, eRow, field, value, true)) {
                return false;
            }
        }

        //Actually update
        for(Iterator sci=scv.iterator(); sci.hasNext();) {
            JCCellRange jccr=(JCCellRange) sci.next();
            int sRow=jccr.start_row;
            int eRow=jccr.end_row;

            if(sRow > eRow) {
                int pivot=sRow;
                sRow=eRow;
                eRow=pivot;
            }

            changeDates(sRow, eRow, field, value);
        }
        return true;
    }

    public void changeDates(int startRow, int endRow, int field, int value) {
        changeDates(startRow, endRow, field, value, false);
    }

    public boolean changeDates(int startRow, int endRow, int field, int value, boolean testonly) {
        for(int rdx=startRow; rdx <= endRow; rdx++) {
            RowInfo ri;
            try {
                ri=(RowInfo) mRows.get(rdx);
            } catch(ArrayIndexOutOfBoundsException e) {
                //No row info, go to the next one
                continue;
            }

            JulianCalendar jd=ri.getDate();
            jd.add(field, value);
            if(jd.get(jd.ERA) < 1 || jd.get(jd.YEAR) < 1 || jd.get(jd.YEAR) > 99) {
                JOptionPane.showMessageDialog(null, "Invalid rotation year.\nMinimum: 1\nMaximum: 99", "Error changing dates", JOptionPane.ERROR_MESSAGE);
                if(testonly) {
                    jd.add(field, -value);
                }
                return false;
            }

            if(testonly) {
                jd.add(field, -value);
            } else {
                fireRowChanged(ri);
            }
        }
        return true;
    }

    /**
     * This function is not really worth. If Collections.max(mRows) yield null, then it means all dates
     * are null. This is because null date preecedes the valid dates. This is taken care in the
     * comapreTo functions of RowInfo class and JulianCalendar class.
     * This function is provided for completion sake as getMinCal() is a required function.
     */
    private JulianCalendar getMaxCal() {


        JulianCalendar maxCal=((RowInfo) Collections.max(mRows)).getDate();

        return maxCal;
    }

    /**
     * This 'min' function on Collections interface ( RowInfo objects implement Comaprable Interface)
     * will not yeild correct results always because few Rows may not have any Dates at all.
     * Since all the null dates preeeced the one with valid dates, if minCal == null, then it means
     * that some of the dates are null, but there can be few rows with valid dates
     */
    private JulianCalendar getMinCal() {
        JulianCalendar minCal=((RowInfo) Collections.min(mRows)).getDate();

        if(minCal != null) {
            return minCal;
        }
        for(int i=0; i < mRows.size(); i++) {
            JulianCalendar tempCal=((RowInfo) mRows.get(i)).getDate();
            if(tempCal != null) {
                if(minCal == null) {
                    minCal=tempCal;
                } else if(minCal.compareTo(tempCal) > 0) {
                    // i.e. tempCal preeceds minCal
                    minCal=tempCal;
                }
            }
        }

        return minCal;
    }

    /**
     * Moves all the dates to "rotYears" year forward making sure they dont cross the
     * maxYear i.e. it just rotates them 1 yr ahead
     * @param rotYears The number of years after which the rotation of crops occur.
     */
    public void cycleForward(int rotYears) {
        JulianCalendar maxCal=getMaxCal();
        JulianCalendar minCal=getMinCal();

        if((minCal == null) || (maxCal == null)) {
            return;
        }

        //This means its a one-year rotation, dont do anything.
        if(rotYears == 1) {
            return;
        }
        //	//System.out.println("ManageData:cycleFwd->"+ "Maxyear = "+ maxYear + "MinYear =" +minYear);
        for(int i=0; i < mRows.size(); i++) {
            JulianCalendar cal=((RowInfo) mRows.get(i)).getDate();
            if(cal == null) {
                //Date is not yet set for thsi row
                continue;
            }
            int yearValue=cal.get(Calendar.YEAR);
            //	//System.out.println("yearValue:"+yearValue);
            yearValue+=1; // cycle forward one year
            if(yearValue > rotYears) {
                yearValue=yearValue % rotYears;
            }

            cal.set(Calendar.YEAR, yearValue);
            //System.out.println("CycleFwd "+ yearValue);
            fireRowChanged(i);
        }
    }

    /**
     * Moves all the dates by "rotYears" year backward making sure they dont cross the
     * minYear i.e. it just rotates them then to the current yr
     * @param rotYears The number of years by which the rotation of crops is reduced.
     */
    public void cycleBackward(int rotYears) {
        JulianCalendar maxCal=getMaxCal();
        JulianCalendar minCal=getMinCal();

        if((minCal == null) || (maxCal == null)) {
            return;
        }

        //This means its a one-year rotation, dont do anything.
        if(rotYears == 1) {
            return;

            //	//System.out.println("ManageData:cycleBackwd->"+ "Maxyear = "+ maxYear + "MinYear =" +minYear);
        }
        for(int i=0; i < mRows.size(); i++) {
            JulianCalendar cal=((RowInfo) mRows.get(i)).getDate();
            if(cal == null) {
                // No date is set for this row
                return;
            }
            int yearValue=cal.get(Calendar.YEAR);

            yearValue-=1; //cycle backward one year
            if(yearValue <= 0) {
                yearValue=rotYears;
            }
            /*
            if(yearValue < minYear)
            yearValue = maxYear - (minYear - yearValue  - 1); //rotate it back
            //if its 1 lesser than minYear, it has to go back to maxYear not maxYear - 1
             */

            cal.set(Calendar.YEAR, yearValue);
            fireRowChanged(i);
        }
    }

    /**
     * This method fetches the value currently assigned to the row pRowNum cell of\
     * the column with column name pColName.
     * @param pRowNum The row number whose cell value is being requested for column pColName.
     * @param pObjectName The name of the operation or crop object whose data will
     * @param pColName The name of the column which is usually the tagname from the
     * crop or operation files from where the data is pulled to populate these column cells.
     * @return The value currently assigned to cell of row pRowNum and colum whose name is pColName.
     */
//    public String getColumnValueAsString(int pRowNum, String pObjectName, String pColName) {
//
//    }
    public String getColumnValueAsString(int pRowNum, String pObjectName, String pColName) {
        Object o=getColumnValue(pRowNum, pObjectName, pColName);
        if(o != null) {
            return o.toString();
        } else {
            return null;
        }
    }

    public Object getColumnValue(int pRowNum, String pObjectName, String pColName) {
        if(pRowNum < 0 || pRowNum >= size()) {
            return null;
        }        

        try {

            RowInfo rowInfo=(RowInfo) mRows.get(pRowNum);
            if(pObjectName.equals("date")) {
                return rowInfo.getDate();
            }
            
            String[] values = rowInfo.getValues(pObjectName, pColName, ConfigData.getDisplayUnit());
            if (values == null || values.length == 0){
                return null;
            }
            else if (values.length == 1){
                return values[0];
            }
            else{
                return values;
            }
           
        } catch(ArrayIndexOutOfBoundsException e) {
            System.err.println("ManageData:getColumnValue: Array Index out of Bounds, RowNum = " + pRowNum);
        }
        return null;
    }

    /**
     * This method sets new value to the cell whose column name is pColName & row is
     * pRowNum and the objectname is pObjectName.
     * @param pRowNum The row number whose cell value is being requested for column pColName.
     * @param pObjectName The name of the operation or crop object whose data is being reset to a new value
     * @param pColName The name of the column which is usually the tagname from the
     * crop or operation files from where the data is pulled and is inserted in these column cells.
     * @param pValue The new value that needs to be set for the cell whose column name is
     * pColName and row number is pRowNum.
     */
    public void setColumnValue(int pRowNum, String pObjectName, String pColName, Object pValue) {
        if(pValue == null) {
            return;
        }
        try {
            RowInfo rowInfo=(RowInfo) mRows.get(pRowNum);
            if(rowInfo != null) {
                if (pValue instanceof String){
                    rowInfo.set(pObjectName, pColName, (String) pValue, ConfigData.getDisplayUnit());
                }
                else if(pValue.getClass().isArray()){
                    rowInfo.setValues(pObjectName, pColName, (String[]) pValue, ConfigData.getDisplayUnit());
                }
                fireRowChanged(pRowNum);
            }
        } catch(Exception e) {
            System.err.println("ManageData:setColumnValue:Exception " + e);
        }

        return;
    }

    public void fireRowChanged(RowInfo... rows) {
        for(RowInfo row : rows) {
            int i=mRows.indexOf(row);
            fireRowChanged(i);
        }
    }

    /**
     * Get the required Data object of the row at pRowNum
     * @param pRowNum The row number in the main MCREW table where the data object
     * resides.
     * @param pObjectName The name of the object like the operation or crop names that
     * are assigned to the data object.
     * @return The data object associated with that row that was requested.
     */
    public DataObject getDataObject(int pRowNum, String pObjectName) {
        if(mRows.size() <= pRowNum) {
            return null;
        }
        return ((RowInfo) mRows.get(pRowNum)).getDataObject(pObjectName);
    }

    /**
     * Reads a single Operation Object or CropObject and puts it into the given row
     * @param pRowNum The row number in the main MCREW table where the data object
     * resides.
     * @param pObjectName The name of the object like the operation or crop names that
     * are assigned to the data object whose data is being read.
     * @param pFileName The file from whom the dataobjects are being populated.
     */
    public void readDataObject(int pRowNum, String pObjectName, String pFileName) {

        DataObject dataObject=null;
        RowInfo row;

        if(pObjectName.equals(XMLConstants.soperation)) {
            dataObject=new OperationObject(pObjectName);
        } else if(pObjectName.equals(XMLConstants.scrop)) {
            dataObject=new CropObject(pObjectName);
        }
        int result=0;
        if(dataObject != null) {
            result=dataObject.readXMLFile(pFileName);
        }
        if((result < 0) || (dataObject == null)) {
            System.err.println("ManageData:readDataObject:" + " Data reading unsuccessfull for " + pObjectName);
            return;
        }


        if((pRowNum < 0) || (dataObject == null)) {
            return;
        }
        if(pRowNum >= mRows.size()) {

            row=null;
            if(pRowNum < 1) {
                row=new RowInfo();
            } else {
                JulianCalendar beforeDate=getDate(pRowNum - 1);
                row=new RowInfo((JulianCalendar) beforeDate.clone());
            }
            row.addDataObject(pObjectName, dataObject);
            insertRow(pRowNum, row);
        } else {
            row=(RowInfo) mRows.get(pRowNum);
            if(row != null) {
                row.addDataObject(pObjectName, dataObject);
                fireRowChanged(pRowNum);
            }
        }
    }
    //Above function taken from ReadData

    /**
     * Reads the data file ( XML or .MAN ) contiaing the operation or crop data for
     * various crops.
     * @param pFileName Tne name of the file that contains this data.
     * @return If read successfully returns true else false.
     */
    public int readDataFile(String pFileName) {
        manFile=new File(new File(pFileName).getAbsoluteFile());
        if(pFileName.toLowerCase().endsWith(XMLConstants.sXMLFileExtension)) {
            int result=readXMLFile(pFileName);
            fixup();
            fireDataReset();
            return result;
        } else if(WepsFileTypes.Management.accept(manFile) || WepsFileTypes.Rotation.accept(manFile)) {
            int result=readManFile(pFileName);
            fixup();
            fireDataReset();
            return result;
        }

        return kUnknown_Error;
    }

    private void fixup(){
        try{
            if(version < VERSION_FUELADDED){    //1.5
                for(RowInfo row : getRows()){
                    OperationObject op = (OperationObject) row.getDataObject(XMLConstants.soperation);
                    Action action = op.getOperationAction();
                    if(action.getIdentity().id == 3 || action.getIdentity().id==4){
                        action.addParameter(new Parameter("ofuel", " "));
                    }
                }
            }
        }
        catch(Exception e){
            LOGGER.error("Unable to fixup management file." + manFile.getAbsolutePath(), e);
        }
    }

    private int readXMLFile(String pFileName) {
        int returnCode=kSuccess;
        Document doc;
        DocumentTraversal traversable;
        TreeWalker walker;
        Node root;
        Node node;
        Node nodeChild;
        mwepsmanfilenotes="";
        Vector rowList;

        rowList=new Vector();
        doc=XMLDoc.getDocument(pFileName);
        if(doc == null) {
            mRows=null;
            return kFile_NotFound;
        }
        doc.normalize();

        root=doc.getDocumentElement();
        if(root.getOwnerDocument() == null) {
            traversable=(DocumentTraversal) root;
        } else {
            traversable=(DocumentTraversal) root.getOwnerDocument();
        }
        walker=traversable.createTreeWalker(root, NodeFilter.SHOW_ALL, null, false);

        node=walker.firstChild();
        while(node != null) {
            String nodeName=node.getNodeName();
            if(nodeName.equals(XMLConstants.swepsmanvalue)) {
                RowInfo row=new RowInfo();
                row.initialize(node);

                rowList.add(row);

            } else if(nodeName.equals(XMLConstants.sversion)) {

                try{
                    String versionText = XMLDoc.getTextData(node);
                    versionText = versionText != null ? versionText.trim() : "0.0";
                    version = Double.parseDouble(versionText);
                }
                catch(Exception e){
                    LOGGER.error("Unable to parse management version number", e);
                    version = 0;
                }

                if(version < VERSION_MINIMUM){
                    LOGGER.error("Management version less than " + VERSION_MINIMUM);
                    returnCode=kWrong_Version;
                }
                if(version > VERSION_CURRENT){
                    LOGGER.error("Management version greater than " + VERSION_CURRENT);
                    returnCode=kWrong_Version;
                }
            } else if(nodeName.equals(XMLConstants.srotationyears)) {
                try {
                    kRotationYears=(new Integer(XMLDoc.getTextData(node))).intValue();
                } catch(NumberFormatException e) {
                    System.err.println("ManageData:readXMLfile()->" + " Rotation years is not a number in the data file");
                    kRotationYears=1;
                }
            } else if(nodeName.equals(XMLConstants.swepsmanfilenotes)) {
                try {
                    mwepsmanfilenotes=XMLDoc.getTextData(node);

                    if(mwepsmanfilenotes == null) {
                        mwepsmanfilenotes="";
                    }
                    else{
                        mwepsmanfilenotes = mwepsmanfilenotes.trim();
                    }
                } catch(Exception e) {
                    System.err.println("ManageData:readXMLfile()->" + " There are NO Management file level notes ");
                    mwepsmanfilenotes="";
                }
            }

            node=walker.nextSibling();
        } // end while(node!= null)
        mRows=rowList; // Update the meber variable
        return returnCode;
    }
    public File manFile;

    private int writeXMLFile(String pFileName, WriteFileMode mode) {
        Node root;
        String[] nonEscapingElements={XMLConstants.soperationname, XMLConstants.sname};

        RowInfo rowInfo;

        try {


            Document wepsmanDoc=XMLDoc.createDocument(XMLConstants.smanagement_template);
            /* createDocument function cretes a new Document with appropriate Doctype and stylesheet attached */
            root=wepsmanDoc.getDocumentElement();

            Node versionNode=wepsmanDoc.createElement(XMLConstants.sversion);
            version = VERSION_CURRENT;

            XMLDoc.setTextData(versionNode, String.valueOf(version), wepsmanDoc);
            root.appendChild(versionNode);

            Node rotyearsNode=wepsmanDoc.createElement(XMLConstants.srotationyears);
            XMLDoc.setTextData(rotyearsNode, Integer.toString(kRotationYears), wepsmanDoc);
            root.appendChild(rotyearsNode);

            //Write the notes.  We check here for the type of writing we're doing.
            if(mwepsmanfilenotes == null || mwepsmanfilenotes.length() == 0) {
                mwepsmanfilenotes="";
            } else {
                mwepsmanfilenotes=mwepsmanfilenotes.trim();
            }
            switch(mode) {
                case NORMAL:
                    //Normal write, most likely a save.  We don't add anything to the notes.
                    break;
                case FROM_NRCS:
                    //The file was converted from an NRCS skel file.  We add a line to the notes.
                    Format dateFormat1 = new SimpleDateFormat("MMM dd, yy");
                    mwepsmanfilenotes=mwepsmanfilenotes + "Management Conversion From [NRCS Standard XML Format] on " + dateFormat1.format(new Date()) + "\n";
                    break;
                case UPDATE:
                    Format dateFormat2 = new SimpleDateFormat("MMM dd, yy");
                    mwepsmanfilenotes=mwepsmanfilenotes + "Management Updated on " + dateFormat2.format(new Date()) + "\n";
                    break;
            }

            Node wepsmanfilenotesNode=wepsmanDoc.createElement(XMLConstants.swepsmanfilenotes);
            XMLDoc.setTextData(wepsmanfilenotesNode, mwepsmanfilenotes.trim(), wepsmanDoc);
            root.appendChild(wepsmanfilenotesNode);

            int totalRows=mRows.size();
            for(int row=0; row < totalRows; row++) {
                rowInfo=(RowInfo) mRows.get(row);

                Node wepsmanvalueNode=rowInfo.getNode(wepsmanDoc);
                root.appendChild(wepsmanvalueNode);
            }

            OutputFormat format=new OutputFormat(wepsmanDoc);
            format.setIndenting(true);
            format.setIndent(XMLConstants.INDENT);
            format.setEncoding(XMLConstants.sEncoding);
            format.setNonEscapingElements(nonEscapingElements);
            format.setMediaType(XMLConstants.sMediaType);
            format.setVersion(XMLConstants.sVersion);

            //BufferedWriter bw = new BufferedWriter(new FileWriter(pFileName));
            FileWriter fw=new FileWriter(pFileName);

            XMLSerializer serializer=new XMLSerializer(fw, format);
            serializer.serialize(wepsmanDoc);
            //mwepsmanfilenotes = "";
        } catch(DOMException e) {
            e.printStackTrace();
            return kUnknown_Error;
        } catch(IOException e) {
            e.printStackTrace();
            return kUnknown_Error;
        }
        return kSuccess;
    }

    private int readManFile(String pFileName) {
        //int lineNum = 0;
        int returnCode=kSuccess;
        Vector rowList=null;
        int rowCount=0;
        OperationObject operationObject=null;
        RowInfo row=null;

        String dataLine=null;
        String delimeter=" "; // delimeter is single space
        String actionCode=null;
        String actionId=null;
        Action action=null;
        boolean textLine=false;
        String displayText="";        
        mwepsmanfilenotes="";

        StringTokenizer tokenizer;        

        BufferedReader br = null;
        try {
            br=new BufferedReader(new FileReader(pFileName));
            dataLine=br.readLine();
            while(dataLine != null) {
                //Saftey check for a blank line.
                if(dataLine.length() == 0) {
                    dataLine=br.readLine();
                    continue;
                }

                // Remove the '<' char at the end of each line which is present in old man files
                int endIndex=dataLine.indexOf(cEndofLine); // 72nd char is '<' in older files only
                if(endIndex != -1) {
                    dataLine=dataLine.substring(0, endIndex);
                }
                char code=dataLine.charAt(0); // check the first char in the line
                ////System.out.println("starting char in Mcrew file:"+code);
                switch(code) {
                    case cStartEndLine:
                        if(dataLine.toLowerCase().indexOf(sSTART.toLowerCase()) != -1) {
                            rowList=new Vector();
                            //also get the rotation years value
                            int rotationIndex=dataLine.toLowerCase().indexOf(sSTART.toLowerCase()) + sSTART.length();
                            String rotationStr=dataLine.substring(rotationIndex).trim();
                            kRotationYears=(new Integer(rotationStr)).intValue();
                        } else if(dataLine.toLowerCase().indexOf(sEND.toLowerCase()) != -1) {
                            //check if there has bee any previous row
                            if(operationObject != null) {
                                row.addDataObject(operationObject.getObjectName(), operationObject);
                                rowList.add(row);
                                rowCount++;
                                operationObject=null;
                            }
                            mRows=rowList;
                            return returnCode;
                        }
                        break;
                    case cVersionLine:
                        // Read the version value
                        int versionIndex=dataLine.toLowerCase().indexOf(sVersion.toLowerCase()) + sVersion.length();

                        String versionText = dataLine.substring(versionIndex).trim();
                        try{
                            version = Double.parseDouble(versionText);
                        }
                        catch(Exception e){
                            LOGGER.error("Unable to parse management version number", e);
                            version= 0;
                        }

                        if(version < VERSION_MINIMUM){
                            LOGGER.error("Management version less than " + VERSION_MINIMUM);
                            returnCode=kWrong_Version;
                        }
                        if(version > VERSION_CURRENT){
                            LOGGER.error("Management version greater than " + VERSION_CURRENT);
                            returnCode=kWrong_Version;
                        }
     
                        // Now substring starts two character after 'Version'. Thats OK as we have ':' after 'Version' in the file.
                        break;
                    case cManfileNotes:
                        // Read the version value
                        try {
                            if(mwepsmanfilenoteslines == 0) {

                                mwepsmanfilenotes=mwepsmanfilenotes + dataLine.substring(2);
                                mwepsmanfilenoteslines++;
                            } else {
                                if(mwepsmanfilenotes.trim().equals("")) {

                                    mwepsmanfilenotes=mwepsmanfilenotes + dataLine.substring(2);
                                    mwepsmanfilenoteslines++;
                                } else {

                                    mwepsmanfilenotes=mwepsmanfilenotes + "\n" + dataLine.substring(2);
                                    mwepsmanfilenoteslines++;
                                }
                            }
                            mwepsmanfilenotes=XMLDoc.getStringFromXMLSafeString(mwepsmanfilenotes);
                            if (mwepsmanfilenotes != null){
                                mwepsmanfilenotes = mwepsmanfilenotes.trim();
                            }
                        } catch(Exception e) {
                            LOGGER.warn("ManageData:readManFile:" + "No Management File Level Notes ", e);                            
                            mwepsmanfilenotes="";
                        }

                        break;
                    case cCommentLine:
                        // nothing to do with the comment line
                        /*if(dataLine.charAt(1) == cEndofRowDataLine)
                        {
                        row.addDataObject(operationObject.getObjectName(), operationObject);
                        rowList.add(row);
                        rowCount++;
                        operationObject = null;
                        }*/
                        break;
                    case cDateLine:
                        //check if there has bee any previous row
                        if(operationObject != null) {
                            row.addDataObject(operationObject.getObjectName(), operationObject);
                            rowList.add(row);
                            rowCount++;
                            operationObject=null;
                        }
                        row=new RowInfo();
                        tokenizer=new StringTokenizer(dataLine.substring(1), delimeter); //leave out the first char
                        String date=null;
                        if(tokenizer.hasMoreElements()) {
                            date=tokenizer.nextToken();
                        }
                        row.setDate(date);
                        operationObject=new OperationObject();

                        break;
                    case cOperationLine:
                        String operationName=null;
                        try {
                            actionCode=dataLine.substring(0, 1);
                            // a string tokeinzer cannot be use as there no delimeter. Space cannot
                            //be used as a operation/ action name can have spaces
                            actionId=dataLine.substring(2, 4);
                            operationName=dataLine.substring(5).trim(); // Every line has '<' at the end
                        } catch(IndexOutOfBoundsException e) {
                            e.printStackTrace();
                        }

                        operationObject.setOperationName(operationName);

                        action=new Action(actionId, actionCode);
                        operationObject.addAction(action);


                        /* Note: the 'action' is not yet initialized with values. actionis initialized when you get next Parameter Line.
                         * This takes care of actions with no parameters as the new action line would be initialized before
                         * next parameter line
                         */
                        break;
                    case cProcessLine:
                    case cGroupLine:

                        try {
                            actionCode=dataLine.substring(0, 1);
                            // a string tokeinzer cannot be use as there no delimeter.Space cannot be used
                            // as a operation /action name can have spaces
                            actionId=dataLine.substring(2, 4);
                            String actionname=dataLine.substring(5).trim();
                            // action name not used. Action name can be obtained from actionMetas
                        } catch(IndexOutOfBoundsException e) {
                            e.printStackTrace();
                            System.err.println(" Line " + dataLine + " is shorter than normal" + " @ManageData:readManFile()");
                        }
                        action=new Action(actionId, actionCode);
                        operationObject.addAction(action);
                        /* Note: the 'action' is not yet initialized with values. actionis initialized when you get next Parameter Line.
                         * This takes care of actions with no parameters as the new action line would be initialized before
                         * next parameter line
                         */

                        break;
                    case cParameterLine:
                    case cParameterLineText:                        
                        int lineNum=0;
                        //System.out.printf("%s\n", action != null ? action.getIdentity() : "null");
                        // Some actions have parameter values in more than line, so we need a lineNum count.
                        do {
                            if(action != null) {
                                //action is initilized under the case operationline,
                                //processLine or GroupLine in the previous run of the loop
                                
                                if(dataLine.charAt(0) == cParameterLine) {
                                    
                                    //test to make sure we're not expecting a T line that is missing in the file                                    
                                    
                                    List lineInfo= ConfigData.getManFileFormatInfo(action.getIdentity(), lineNum);
                                    
                                    if(lineInfo != null && lineInfo.size() > 0){
                                        String expectedLineCode = (String) lineInfo.get(0);                                        
                                        if(expectedLineCode.equals("T")){
                                            action.initialize("", lineNum++);
                                        }
                                    }
                                          
                                    
                                    
                                    textLine=false;
                                    action.initialize(dataLine.substring(1).trim(), lineNum++);
                                    dataLine=br.readLine();
                                    endIndex=dataLine.indexOf(cEndofLine); // 72nd char is '<' in older files only
                                    if(endIndex != -1) {
                                        dataLine=dataLine.substring(0, endIndex); // Remove '<'
                                    }
                                }
                                
                                else if(dataLine.charAt(0) == cParameterLineText) {                                
                                    StringBuilder buffer = new StringBuilder();
                                    do {
                                        //if( textLine == false){
                                        buffer.append(dataLine.substring(1));
                                        buffer.append("\n");
                                        dataLine=br.readLine();
                                        //}
                                    } while(dataLine.charAt(0) == cParameterLineText);

                                    action.initialize(buffer.toString(), lineNum++);
                                }
                            }
                            
                        } while((dataLine != null) && (dataLine.charAt(0) == cParameterLine || dataLine.charAt(0) == cParameterLineText));

                        
                        //test if there was an expected text line missing in the file
                        try{
                            List lineInfo= ConfigData.getManFileFormatInfo(action.getIdentity(), lineNum);

                            if(lineInfo != null && lineInfo.size() > 0){
                                String expectedLineCode = (String) lineInfo.get(0);                                
                                if(expectedLineCode.equals(String.valueOf(cParameterLineText))){
                                    //add blank text into the notes value.
                                    action.initialize("", lineNum++);
                                }
                            }
                        }
                        catch(ArrayIndexOutOfBoundsException aioobe){
                            //do nothing
                        }
                        
                        // Now since dataLine is positioned at line after the paramLine, we dont need to read new line
                        continue;


                    default:
                        LOGGER.error("Unknown line in weps manangement data file: " + pFileName + "\n" + dataLine);
                        returnCode=kCorruptedFile;
                        break;
                } // end switch
                dataLine=br.readLine();
            } // end while
            mwepsmanfilenoteslines=0;
        } // end try
        catch(FileNotFoundException e) {
            LOGGER.warn("File \"" + new File(pFileName).getAbsolutePath() + "\" does not exist.");
            return kFile_NotFound;
        } catch(IOException e) {
            LOGGER.warn("Unable to read manage file.", e);
            return kUnknown_Error;
        }
        finally{
            try{
                if (br != null){
                    br.close();
                }
            }
            catch(Exception e){
                LOGGER.error("Error reading management file.", e);
            }
        }

        mRows=rowList;
        manFile=new File(new File(pFileName).getAbsoluteFile());
        return returnCode;
    }



    /**
     * If the parameter pFileName has no extension then this function calls save function for saving both xml and
     * man files. If hte fileName alredy has some extension then it just saves for that file
     * @param pFileName The name of the file where the data needs to be written.
     * @return True if the data file is written successfully else false.
     */
    public int writeDataFile(String pFileName) {
        return writeDataFile(pFileName, WriteFileMode.NORMAL);
    }

    public int writeDataFile(String pFileName, WriteFileMode mode) {
        //Safety Check Build directory structure for the file if needed.
        File dir = (File) new File(pFileName).getParentFile();
        if (!dir.exists()){
            boolean created = dir.mkdirs();
            if(!created){
                LOGGER.error("Unable to create required directory structure: " + dir.getAbsolutePath());
                return kUnknown_Error;
            }
        }

        if(pFileName.endsWith(XMLConstants.sXMLFileExtension)) {
            return writeXMLFile(pFileName, mode);
        } else {
            return writeManFile(pFileName, mode);
        }
    }

 private int writeManFile(String pFileName, WriteFileMode mode) {
        String dataLine;        

        String symbolParameterLine=Character.valueOf(cParameterLine).toString();
        String symbolParameterLineText=Character.valueOf(cParameterLineText).toString();

        try {
            PrintWriter pw=new PrintWriter(new BufferedWriter(new FileWriter(pFileName)));

            dataLine=sVersion;

            //always write the current version
            version = VERSION_CURRENT;

            dataLine=prettyConcat(dataLine, String.valueOf(version));
            pw.write(dataLine);
            pw.write("\n");

            dataLine=(new Character(cStartEndLine)).toString().concat(sSTART);
            dataLine=prettyConcat(dataLine, Integer.toString(kRotationYears));
            pw.write(dataLine);
            pw.write("\n");

            //Write the notes.  We check here for the type of writing we're doing.
            if(mwepsmanfilenotes == null || mwepsmanfilenotes.length() == 0) {
                mwepsmanfilenotes="";
            } else {
                mwepsmanfilenotes=mwepsmanfilenotes + "\n";
            }
            switch(mode) {
                case NORMAL:
                    //Normal write, most likely a save.  We don't add anything to the notes.
                    break;
                case FROM_NRCS:
                    //The file was converted from an NRCS skel file.  We add a line to the notes.
                    mwepsmanfilenotes=mwepsmanfilenotes + "This management file was originally converted from the NRCS standard XML format on - " + new Date().toString();
                    break;
                case UPDATE:
                    mwepsmanfilenotes=mwepsmanfilenotes + "This management file was updated to contain the most current crop and operation records for WEPS on - " + new Date().toString();
                    break;
            }

            dataLine=(new Character(cManfileNotes)).toString();
            dataLine=prettyConcat(dataLine, XMLDoc.getXMLSafeString(mwepsmanfilenotes));
            pw.write(dataLine);
            pw.write("\n");

            dataLine=sCommentStr;
            pw.write(dataLine);
            pw.write("\n");

            int numRows=mRows.size();
            for(int rowidx=0; rowidx < numRows; rowidx++) {

                RowInfo row=(RowInfo) mRows.get(rowidx);

                String date=row.getDate().toString();
                dataLine=(new Character(cDateLine)).toString();
                dataLine=prettyConcat(dataLine, date);

                //System.out.println(dataLine + " Its my Date inserted");
                pw.write(dataLine);
                pw.write("\n");

                //Only operation objects are existing in any row at present
                OperationObject operationObj=(OperationObject) row.getDataObject(XMLConstants.soperation);

                String operationName=operationObj.getOperationName();
                //System.out.println("ManageData:writemanfile()->" + "Writing out parameters for operation: "+ operationName + " dtd " + date);
                Vector actionIdVec=operationObj.getAllIds();
                //Hashtable actions = operationObj.getAllActions();
                int actionVecSize=actionIdVec.size();

                ////System.out.println("number of action " + actionVecSize);
                //First get the operation action
                for(int actionidx=0; actionidx < actionVecSize; actionidx++) {
                    Action action=operationObj.getAction(actionidx);

                    Identity actionId=action.getIdentity();
                    String actionName=action.getActionName();
                    //System.out.println("ManageData:writemanfile() "+  "There actionname IS : " + actionName + " - IDENTITY is : " + actionId.toString());
                    if(actionName == null) {
                        System.err.println("ManageData:writemanfile()->" + "Theres no actionname for action " + actionId + "in operation_defn.xml");
                        actionName=XMLConstants.sNoName;
                    }
                    dataLine=actionId.toString();
                    /* Note: Operation name is to be appended with first operation action. All other actions (process and group) will be
                    appended with their action name.
                     */
                    if(actionidx == 0) {
                        dataLine=prettyConcat(dataLine, operationName);
                    } else {
                        dataLine=prettyConcat(dataLine, actionName);
                    }
                    pw.write(dataLine);
                    pw.write("\n");


                    /* Get the information from man_fileformat.xml on how to print the data in the .man file */
                    int numLines=ConfigData.getManFormatNumLines(actionId); // get the number of lines in which the params are divided
                    for(int i=0; i < numLines; i++) {
                        /*Vector paramFormatLine = (Vector)ConfigData.getManFileFormat(actionId, i);
                        Vector paramFormatLine = (Vector)ConfigData.getManFileFormatInfo(actionId, i); */
                        Vector paramFormatLineInfo=(Vector) ConfigData.getManFileFormatInfo(actionId, i);
                        String symbol=(String) paramFormatLineInfo.get(0);
                        String spaceAfterSymbol="";

                        /* if( symbol.equals( symbolParameterLine ) || symbol.equals(symbolParameterLineText) ){
                        spaceAfterSymbol = (String)paramFormatLineInfo.get(1);
                        }*/

                        if(symbol.equals(symbolParameterLine)) {
                            Vector paramFormatLine=(Vector) paramFormatLineInfo.get(1);

                            if(paramFormatLine == null) {
                                System.err.println("Action:initialize()" + "Theres is no man_format.xml data for the dataLine " + dataLine + " of action" + actionId);
                                continue;
                            }
                            /*Enumeration e1 = paramFormatLine.elements();
                            while( e1.hasMoreElements() ){
                            //System.out.println(" ManangeData : WriteManFile() : Elements in paramFormatLine are : " + (String)( e1.nextElement() ));
                            }*/
                            dataLine=symbolParameterLine;

                            for(int j=0; j < paramFormatLine.size(); j++) {
                                String paramName=((String) paramFormatLine.get(j)).trim();
                                String value=action.get(paramName);
                                //System.out.println(" ManangeData : WriteManFile() : Element in paramFormatLine is : == ParamName : " + paramName + " ParamValue : " + value );
                                if(value == null) {
                                    ///System.err.println("\tManageData:writemanfile()->"+ "Theres no value for parameter '" +paramName +
                                    //        "' in the action "+ actionId);
                                    continue;
                                }
                                dataLine=prettyConcat(dataLine, value);
//                                                        //System.out.println("\t ManageData : writemanfile() : The dataLine value is : " + dataLine );
                            }

                            if(dataLine.equals("")) {

                                dataLine=symbolParameterLine + "\n";
                            }

                            if(dataLine != null) {
                                pw.write(dataLine);
                                pw.write("\n");
                            }
                        }

                        if(symbol.equals(symbolParameterLineText)) {
                            // if( spaceAfterSymbol.equals(" ") ){
                            Vector paramFormatLine=(Vector) paramFormatLineInfo.get(1);

                            if(paramFormatLine == null) {
                                System.err.println("Action:initialize()" + "Theres is no man_format.xml data for the dataLine " + dataLine + " of action" + actionId);
                                continue;
                            }
                            /*Enumeration e1 = paramFormatLine.elements();
                            while( e1.hasMoreElements() ){
                            //System.out.println(" ManangeData : WriteManFile() : Elements in paramFormatLine are : " + (String)( e1.nextElement() ));
                            }*/
                            dataLine=symbolParameterLineText;
                            String dataLineText="";

                            for(int j=0; j < paramFormatLine.size(); j++) {
                                String paramName=((String) paramFormatLine.get(j)).trim();
                                String value=action.get(paramName);
                                //System.out.println(" ManangeData : WriteManFile() : Element in paramFormatLine is : == ParamName : " + paramName + " ParamValue : " + value );
                                if(value == null) {
                                    //System.err.println("\tManageData:writemanfile()->"+ "Theres no value for parameter '" +paramName +
                                    //      "' in the action "+ actionId);
                                    continue;
                                }
                                //String line = value.substring( 0, value.indexOf('\n') );
                                String[] line=value.split("\\n");
                                int lineLength=line.length;
                                for(int x=0; x < lineLength; x++) {
//                                                             //System.out.println(" ManageData : writeManFile() : Line string value is : " + line[x] + "Number of newLie tokens : " + line.length  );
                                    dataLine=prettyConcat(symbolParameterLineText, line[x]);
                                    dataLineText=dataLineText + dataLine;
                                }

                                //if( lineLength == 0 ){
                                if(dataLineText.equals("")) {

                                    dataLineText=symbolParameterLineText + "\n";
                                }

                                if(dataLineText != null) {
                                    pw.write(dataLineText);
                                    //pw.write("\n");
                                }

                                dataLine=symbolParameterLineText;
                            }
                            /*  }
                            if( !( spaceAfterSymbol.equals(" ") ) ){
                            pw.write(symbolParameterLineText);
                            pw.write("\n");
                            } */
                        }

                        /*if(dataLine != null){
                        pw.write(dataLine);
                        pw.write("\n");
                        }*/
                    } // end for paramForamtVec
                } // end for actionVec
                //dataLine = (new Character(cCommentLine)).toString();
                dataLine=sCommentStr;
                pw.write(dataLine);
                pw.write("\n");
            }

            dataLine=(new Character(cStartEndLine)).toString();
            dataLine=dataLine.concat(sEND);
            pw.write(dataLine);
            pw.write("\n");

            dataLine=(new Character(cStartEndLine)).toString();
            dataLine=dataLine.concat(sEOF);
            pw.write(dataLine);
            pw.write("\n");

            //mwepsmanfilenotes = "";
            pw.flush();
            pw.close();
        } catch(FileNotFoundException e) {
            e.printStackTrace();
            return kFile_NotFound;
        } catch(IOException e) {
            e.printStackTrace();
            return kUnknown_Error;
        }

        return kSuccess;
    }
 
    /**
     * This method individually confirms if date cell in each row has dates entered in
     * it.
     * @return The vector that contains the collection of row objects that have empty
     * date cells
     */
    public Vector <Integer> checkIfAllDatesPresent() {
        Vector <Integer> noDateRows=null;

        for(int i=0; i < mRows.size(); i++) {
            RowInfo row=(RowInfo) mRows.get(i);
            JulianCalendar tempJCal=row.getDate();
            ////System.out.println("ManageData:checkIfAllDatesPresent: " + "Era = " + tempJCal.get(Calendar.ERA) );
            /* BC dates are invalid dates */
            if((tempJCal == null) || (tempJCal.get(Calendar.ERA) == GregorianCalendar.BC)) {
                if(noDateRows == null) {
                    noDateRows=new Vector <Integer>();
                }
                noDateRows.add(i);
            }
        }
        return noDateRows;
    }

    /**
     * Checks if the table rows are empty or not.
     * @return The vector that contains a collection of row objects that are empty.
     */
    public Vector <Integer> checkEmptyRows() {
        //Check for blank lines in the data
        Vector <Integer> emptyRows=null;

        for(int i=0; i < mRows.size(); i++) {
            RowInfo row=(RowInfo) mRows.get(i);
            if(row.getDataObject(XMLConstants.soperation) == null) {
                if(emptyRows == null) {
                    emptyRows=new Vector <Integer>();
                }
                emptyRows.add(i);
            }
        }

        return emptyRows;
    }

    /**
     * This function concates the string parameters with a space in between them
     * and returns the concatenated string
     */
    private String prettyConcat(String data, String param) {
        if(data == null || param == null) {
            return "";
        }
        StringBuffer buffer = new StringBuffer(data);
        buffer.append(" "); // Append a single space


        if(data.equals("N")) {
            buffer = new StringBuffer(data);            

            String[] result=param.split("\\n");
            for(int i=0; i < result.length; i++) {
                if(i == 0) {
                    buffer.append(" " + result[i]);                    
                } else {
                    buffer.append("\n" + "N " + result[i]);                    
                }
            }

            return buffer.toString();
        }

        if(data.equals("T")) {
            //  //System.out.println("param String:"+param+" size:"+param.length());
            if(param.equals("") || param.length() == 0) {
                return data + "\n";
            } else if(!param.equals("") && ((param.trim().equals("")))) {
                return data + param + "\n";
            } else if((param.substring(0, 1).equals(" ")) && (!(param.trim().equals("")))) {
                return data + param + "\n";
            } else if((param.substring(0, 1).equals(" ")) && ((param.trim().equals("")))) {
                return data + " " + param + "\n";
            } else if(!(param.substring(0, 1).equals(" "))) {
                return data + " " + param + "\n";
            }

            return buffer.toString();
        }

        if(data.equals("+") && !param.equals("") && param.trim().equals("")) {
            return data + param;                        
        } else {            
            buffer.append(param);            
        }

        return buffer.toString();
    }

    // ADD ERROR HANDLING
    /**
     * This method deletes the rows that are selected from the table along with the
     * data associated with it.
     * @param scv The vector containing the selected row number objects to be deleted.
     */
    public void deleteSelectedRows(Vector scv) {
        clearDeleted();
        try {
            for(Iterator sci=scv.iterator(); sci.hasNext();) {
                JCCellRange jccr=(JCCellRange) sci.next();
                int sRow=jccr.start_row;
                int eRow=jccr.end_row;
                if(sRow > eRow) {
                    int pivot=sRow;
                    sRow=eRow;
                    eRow=pivot;
                }
                int max=getRows().size();
                for(int i=sRow; i <= eRow; i++) {
                    if(i >= max) {
                        continue;
                    }
                    deletedRowNums.add(sRow);
                    deletedRows.add((RowInfo) mRows.remove(sRow));
                    fireRowDeleted(sRow, 1);
                }
            }
        } catch(ArrayIndexOutOfBoundsException e) {
            System.err.println("ManageData:deleteRows->" + "Trying to delete non existing row ");
        }
    }
    Vector<Integer> deletedRowNums=new Vector();
    Vector<RowInfo> deletedRows=new Vector();

    /**
     * This method deletes the rows that are mentioned in the vector passed as argument
     * to the method from the table.
     * @param pRowNums The vector containing the rows to be deleted.
     */
    public void deleteRows(Vector pRowNums) {
        clearDeleted();
        try {
            for(int i=0; i < pRowNums.size(); i++) {
                deletedRowNums.add(i);
                int indexToDelete=((Integer) pRowNums.get(i)).intValue() - i;
                deletedRows.add((RowInfo) mRows.remove(indexToDelete)); // subtract i from as number of elements in mRows are decreasing
                fireRowDeleted(indexToDelete, 1);
            }
        } catch(ArrayIndexOutOfBoundsException e) {
            System.err.println("ManageData:deleteRows->" + "Trying to delete non existing row ");
        }
    }

    private void clearDeleted() {
        deletedRowNums.removeAllElements();
        deletedRows.removeAllElements();
    }

    public int[] undoDeleteRows() {
        Integer[] r=deletedRowNums.toArray(new Integer[0]);
        int[] rtn=new int[r.length];
        for(int i=0; i < r.length; i++) {
            rtn[i]=r[i].intValue();
        }
        int numReplaced=0;
        if(deletedRowNums.size() == 0) {
            return null;
        }
        while(!deletedRowNums.isEmpty()) {
            insertRow(deletedRowNums.lastElement().intValue(), deletedRows.lastElement());
            deletedRows.remove(deletedRows.size() - 1);
            deletedRowNums.remove(deletedRowNums.size() - 1);
            numReplaced++;
        }
        clearDeleted();
        return rtn;
    }

    /**
     * This method inserts all the rows from vector "pRows" starting at pRowNum
     * whose row data object is pRow.
     * @param pRowNum The place in the table where this row needs to be inserted.
     * @param pRows The vector that holds all the rows to be inserted.
     */
    public void insertRows(int pRowNum, Vector pRows) {
        clearDeleted();
        int addAtRow=pRowNum;
        ////System.out.println("ManageData:insertRows:" + "mrows Size = " + mRows.size() + "Ad at " + addAtRow);
        for(int i=0; i < pRows.size(); i++) {

            mRows.add(addAtRow++, (RowInfo) pRows.get(i));
            fireRowsAdded(addAtRow - 1, 1);
            //System.out.println("ManageData:insertData" + "Inserted row " + ((RowInfo)pRows.get(i)).getDate());
        }
    }

    /**
     * This method inserts a row at pRowNum whose row data object is pRow.
     * @param pRowNum The place in the table where this row needs to be inserted.
     * @param pRow The row info object that holds the data for that row which is
     * being inserted.
     */
    public void insertRow(int pRowNum, RowInfo pRow) {
        mRows.add(pRowNum, pRow);
        fireRowsAdded(pRowNum, 1);
    }

    /**
     * This method inserts a row at "rowNum" in the MCREW table.
     * @param rowNum The place in the table where this row needs to be inserted.
     */
    public void insertRow(int rowNum) {
        RowInfo pRow=new RowInfo();
        if(rowNum <= 0) {
            //Will be first row, nothing before
            JulianCalendar afterCal=(JulianCalendar) getDate(0).clone();
            pRow.setDate(afterCal);
        } else {
            //Somewhere between, something before and after.
            JulianCalendar beforeCal=(JulianCalendar) getDate(rowNum - 1).clone();
            pRow.setDate(beforeCal);
        }
        insertRow(rowNum, pRow);
    }
    Vector clipBoardRows=new Vector();

    /**
     * This method coopies all the selected rows to be replicated in thesame table
     * but elsewhere.
     * @param scv The selection of rows that need to be replicated.
     */
    public void copyRows(Vector scv) {
        clearDeleted();
        clipBoardRows.clear();

        for(Iterator sci=scv.iterator(); sci.hasNext();) {
            JCCellRange jccr=(JCCellRange) sci.next();
            int sRow=jccr.start_row;
            int eRow=jccr.end_row;

            if(sRow > eRow) {
                int pivot=sRow;
                sRow=eRow;
                eRow=pivot;
            }

            for(int i=sRow; i <= eRow; i++) {
                try {
                    clipBoardRows.add((RowInfo) mRows.get(i));
                } catch(ArrayIndexOutOfBoundsException e) {
                    System.err.println("ManageData:cutRows()" + "Trying to cut row with no data (might be last row), row num: " + i);
                }
            }
        }
    }

    /**
     * This method cuts the selected rows from the table whose IDs are stored in
     * the vector passed as argument to it.
     * @param scv The vector that holds the row IDs that need to be removed.
     */
    public void cutRows(Vector scv) {
        clearDeleted();
        clipBoardRows.clear();

        for(Iterator sci=scv.iterator(); sci.hasNext();) {
            JCCellRange jccr=(JCCellRange) sci.next();
            int sRow=jccr.start_row;
            int eRow=jccr.end_row;

            if(sRow > eRow) {
                int pivot=sRow;
                sRow=eRow;
                eRow=pivot;
            }

            for(int i=sRow; i <= eRow; i++) {
                try {
                    clipBoardRows.add(mRows.remove(sRow));
                    fireRowDeleted(sRow, 1);
                } catch(ArrayIndexOutOfBoundsException e) {
                    System.err.println("ManageData:cutRows()" + "Trying to cut row with no data (might be last row), row num: " + i);
                }
            }
        }
    }

/*	public void pasteRows(int rowNum)
    {
    Collections.sort(clipBoardRows);
    for (Iterator cbri = clipBoardRows.iterator(); cbri.hasNext(); ) {
    mRows.add(rowNum++, cbri.next());
    }
    }*/
    /**
     * The method pastes the rows that were copied and stored in the vector with
     * their IDs.
     * @param scv The vector that holds the IDs of all the rows to be pasted in the table.
     */
    public void pasteRows(Vector scv) {
        clearDeleted();
        if(scv.size() == 0) {
            //System.out.println("MD_pR: paste faild; no scv");
            return;
        }
        Collections.sort(clipBoardRows);
        //System.out.println("MD_pR: " + scv);
        JCCellRange jccr=(JCCellRange) scv.get(0);
        int rowNum=jccr.start_row;
        for(Iterator cbri=clipBoardRows.iterator(); cbri.hasNext();) {
            RowInfo current=(RowInfo) cbri.next();
            RowInfo clone=(RowInfo) current.clone();
            mRows.add(rowNum++, clone);
//			mRows.add(rowNum++, ( (RowInfo)cbri.next()).clone() );
        }
    }

    /**
     * This method pastes the row at a place in the table specified by the argument.
     * @param rowNum The place where the row will be pasted.
     * @return True if the row was successfully pasted else false.
     */
    public int pasteRows(int rowNum) {
        clearDeleted();
        Collections.sort(clipBoardRows);
        //System.out.println("MD_pR: " + rowNum);
        int rowCnt=0;
        for(Iterator cbri=clipBoardRows.iterator(); cbri.hasNext();) {
            RowInfo current=(RowInfo) cbri.next();
            RowInfo clone=(RowInfo) current.clone();
            insertRow(rowNum++, clone);
            rowCnt++;
//			mRows.add(rowNum++, ( (RowInfo)cbri.next()).clone() );
        }
        return rowCnt;
    }

    public void sortData() {        
        Collections.sort(mRows, new Comparator <RowInfo> (){

            public int compare(RowInfo r1, RowInfo r2) {
                if (r1.getDate().equals(r2.getDate())){
                    int r1Index = mRows.indexOf(r1);
                    int r2Index = mRows.indexOf(r2);
                    return r1Index - r2Index;
                    
                }
                else{
                    //Compare by dates.
                    return r1.getDate().compareTo(r2.getDate());
                }
            }
            
        });
        fireDataReset();
    }
    //CHECKS
    public static final int CHECK_PASSED=0;
    public static final int CHECK_FAILED=1;

    /**
     * This method makes sure that the rotation years are set right.
     * @return True if set correct else false
     */
    public int checkRotationYears() {
        //Implement natural ordering according to dates
        if(mRows.isEmpty()) {
            JOptionPane.showMessageDialog(null, "CANNOT SAVE : Check for either empty rows or empty file itself.", " Cannot Save file ", JOptionPane.INFORMATION_MESSAGE);
            return -1;
        }
        RowInfo maxRow=(RowInfo) Collections.max(mRows);        

        int maxYear=((Calendar) maxRow.getDate()).get(Calendar.YEAR);        

        //return maxYear - minYear + 1;
        /* We will be returning the maximum year in the data-rather than the
         * number of years between the maximum year and minimum year-added by Neha.
         */
        return maxYear;                
    }

    /**
     * Checks if all the parameters are consistent with the way we want them in the
     * model so that the application has no problem reading and writting the data
     * from those paramenters when requested.
     * @return This vector contains all the parameters that are insonsitent with
     * the model.
     */
    public Vector checkParameterConsistency() {
        Vector invalidOprns=null;

        for(int i=0; i < size(); i++) {
            OperationObject obj=(OperationObject) getDataObject(i, XMLConstants.soperation);
            Vector invalidActions=obj.checkParameterConsistency();
            if(invalidActions != null) {
                if(invalidOprns == null) {
                    invalidOprns=new Vector();
                }
                invalidOprns.add(obj);
            }
        }
        return invalidOprns;

        //return null;
    }

    public int checkAllConditions(List<String> msgs) {

        int results=0;

        results|=checkVersion(msgs);

        results|=checkIfAllDatesPresent(msgs);

        results|=checkIfSorted(msgs);

        results|=checkEmptyRows(msgs);

        results|=checkRotYears(msgs);

        results|=checkParameterConsistency(msgs);

        //If any of the tests failed, return a failed status
        if((results & CHECK_FAILED) == CHECK_FAILED) {
            return CHECK_FAILED;
        } else {
            return CHECK_PASSED;
        }
    }

    public int checkVersion(List<String> msgs) {
        if(version < VERSION_MINIMUM){
            msgs.add("Management file " + manFile.getAbsolutePath() + " is of version " + version + ", whereas the minimum version is " + VERSION_MINIMUM);
            return CHECK_FAILED;
        }
        if(version > VERSION_CURRENT){
            msgs.add("Management file " + manFile.getAbsolutePath() + " is of version " + version + ", whereas the maximum version is " + VERSION_CURRENT);
            return CHECK_FAILED;
        }
        return CHECK_PASSED;
    }

    /**
     * This method makes sure if rows exist in a table then their date cells are
     * populated.
     * @return True, if date is populated for every row in a table else false.
     */
    public int checkIfAllDatesPresent(List<String> msgs) {
        Vector noDateRows=checkIfAllDatesPresent();

        if(noDateRows == null) {
            return CHECK_PASSED;
        }
        String noDateRowStr=null;
        msgs.add("There are following rows with no dates in them.");

        for(int i=0; i < noDateRows.size(); i++) {
            if(noDateRowStr == null) {
                noDateRowStr=noDateRows.get(i).toString();
            } else {
                noDateRowStr+=", " + noDateRows.get(i).toString();
            }
        }

        msgs.add(noDateRowStr);

        return CHECK_FAILED;
    }

    /**
     * Check if the rows are in incresing order (of dates)
     * @return True if the rows are in increasing order else false
     */
    public int checkIfSorted(List<String> msgs) {
        int lastIndex=mRows.size() - 1;
        for(int i=0; i < lastIndex; i++) {
            RowInfo presentRow=(RowInfo) mRows.get(i);
            RowInfo nextRow=(RowInfo) mRows.get(i + 1);

            if(presentRow.compareTo(nextRow) > 0) {
                msgs.add("The data is not sorted according to the dates.");
                return CHECK_FAILED;
            }
        }
        return CHECK_PASSED;
    }

    public int checkEmptyRows(List<String> msgs) {
        Vector <Integer> emptyRows=checkEmptyRows();

        if(emptyRows == null) {
            return CHECK_PASSED;
        }        
        StringBuffer buffer = null;
        msgs.add("There are some empty rows in the management file as given below.");

        for(int i=0; i < emptyRows.size(); i++) {
            if(buffer == null) {
                buffer = new StringBuffer(emptyRows.get(i).toString());                
            } else {
                buffer.append(", ");
                buffer.append(emptyRows.get(i).toString());
            }
        }
        if (buffer != null){
            msgs.add(buffer.toString());
        }


        return CHECK_FAILED;
    }

    public int checkRotYears(List<String> msgs) {
        int correctYears=checkRotationYears(); // returns the correct year value
        int rotYears=getRotationYears();

        if(rotYears < correctYears) {
            msgs.add("The numbers of rotation years is incorrectly set.");
            return CHECK_FAILED;
        } else {
            return CHECK_PASSED;
        }
    }

    public int checkParameterConsistency(List<String> msgs) {
        Vector invalidOprns=checkParameterConsistency();
        if(invalidOprns == null) {
            return CHECK_PASSED;
        }
        msgs.add("The following operations have parameters which might be having incorrect values due to wrong management file version");

        for(Iterator it=invalidOprns.iterator(); it.hasNext();) {
            OperationObject invalidOprn=(OperationObject) it.next();
            String operationName=(String) invalidOprn.getOperationName();
            msgs.add(operationName);

            Vector invalidActions=invalidOprn.checkParameterConsistency();

            for(Iterator jt=invalidActions.iterator(); jt.hasNext();) {
                Action invalidAction=(Action) jt.next();

                String actionId=invalidAction.getIdentity().toString();
                String messageLine="\t" + actionId + ": ";

                Vector invalidParams=invalidAction.checkParameterConsistency();
                for(Iterator kt=invalidParams.iterator(); kt.hasNext();) {
                    messageLine=messageLine.concat((String) kt.next());
                    messageLine=messageLine.concat(", ");
                }

                msgs.add(messageLine.substring(0, messageLine.length() - 2)); // Remove the last  ", "
            }
        }

        return CHECK_FAILED;
    }
    
    public int checkCalibration(List<String> msgs) {

        int results=0;

        results|=checkCropSelectedForCalibration(msgs);
        results |=checkCropHasHarvestForCalibration(msgs);

        //If any of the tests failed, return a failed status
        if((results & CHECK_FAILED) == CHECK_FAILED) {
            return CHECK_FAILED;
        } else {
            return CHECK_PASSED;
        }
    }
    
    public int checkCropSelectedForCalibration(List<String> msgs) {
        
        for(RowInfo row : getRows()){
            DataObject o = row.getDataObject(XMLConstants.soperation);
            if(o instanceof OperationObject){
                OperationObject op = (OperationObject) o;
                
                //is this a planting op?
                if(OperationClassification.Planting == OperationClassification.classify(op)){
                    //is the calibration flag set?
                    String value = op.getValue("cbaflag");
                    
                    if(value != null && Integer.valueOf(value.trim()) == 1){
                        return CHECK_PASSED;
                    }
                }
            }
        }
        
        msgs.add("No crops selected for calibration.");


        return CHECK_FAILED;
    }
    
    public int checkCropHasHarvestForCalibration(List<String> msgs) {
        
        int result = CHECK_PASSED;
        
        for(RowInfo row : getRows()){
            DataObject o = row.getDataObject(XMLConstants.soperation);
            if(o instanceof OperationObject){
                OperationObject op = (OperationObject) o;
                
                //is this a planting op?
                if(OperationClassification.Planting == OperationClassification.classify(op)){
                    //is the calibration flag set?
                    String cbaflag = op.getValue("cbaflag");
                    
                    String cropname;
                    if(cbaflag != null && Integer.valueOf(cbaflag.trim()) == 1){
                        //this crop is set to calibrate, store the name in case we need it later
                        cropname = op.getCropName();
                        
                        
                        int startIndex = getRows().indexOf(row);
                        
                        //collect all the harvests for this crop, aka every harvest op until the next planting.
                        List<OperationObject> harvests = new LinkedList<OperationObject>();
                                                
                        //loop
                        for(int i = startIndex + 1; i < getRows().size() + startIndex; i++){                            
                            Object o2 =getRow(i % getRows().size()).getDataObject(XMLConstants.soperation);
                            if(o instanceof OperationObject){
                                OperationObject op2 = (OperationObject) o2;
                                if(OperationClassification.Harvest == OperationClassification.classify(op2)){
                                    harvests.add(op2);
                                }
                                else if (OperationClassification.Planting == OperationClassification.classify(op2)){
                                    //reached another planting, break out of the harvest seach
                                    break;
                                }
                            }                            
                        }
                        
                        //check that only one harvest is set to calibrate
                        int foundCalibratedHarvest = 0;
                        for (OperationObject harvest : harvests){
                            String harv_calib_flg = harvest.getValue("harv_calib_flg");
                            if(harv_calib_flg != null && Integer.valueOf(harv_calib_flg.trim()) == 1){
                                foundCalibratedHarvest++;
                            }
                        }
                        
                        if (foundCalibratedHarvest == 0){
                            msgs.add("No harvest operation with the calibration flag enable found for " + cropname);
                            result = CHECK_FAILED;
                        }
                        else if (foundCalibratedHarvest > 1){
                            msgs.add("More than one harvest operation with the calibration flag enable found for " + cropname);
                            result = CHECK_FAILED;
                        }
                        
                    }
                }
            }
        }                


        return result;
    }        
        
    
    
    

    /**
     * This method fetches the value assigned to the rotation years of the operation/crop
     * @return The year value when the operation/crops will be rotated.
     */
    public int getRotationYears() {
        return kRotationYears;
    }

    /**
     * This method assigns the value of the rotation years of the operation/crop
     * @param pYears The number of years when the operation/crop will be rotated.
     */
    public void setRotationYears(int pYears) {
        kRotationYears=pYears;
    }

    /**
     * This method sets the notes written in the notes text area of the .MAN file to
     * the variable that stores it for future references.
     * @param notes The notes string to be stored.
     */
    public void setWepsManFileNotes(String notes) {

        mwepsmanfilenotes=notes != null ? notes.trim() : null;
    }
    // check the rotation year: if the date is null, check all. otherwise, check the date is greater than the current year value
    public static final Object EMPTY_OBJECT=new Object();

    //JCTableModel code
    public Object getTableDataItem(int row, int column) {

        String colName=ConfigData.getTagName(column);
        String objectName=ConfigData.getObjectName(column);
        Object result=getColumnValue(row, objectName, colName);
        if(result == null) {
            if(colName.equals("Date") || colName.equals("operationname")) {
                return EMPTY_OBJECT;
            }
                        
        }
        return result;
    }

    public boolean setTableDataItem(Object o, int row, int column) {
        if(row > size() - 1) {
            addRow(size(), new RowInfo());
            return setTableDataItem(o, row, column);
        }

        String colName=ConfigData.getTagName(column);
        String objectName=ConfigData.getObjectName(column);
        try {
            setColumnValue(row, objectName, colName, o);
            return true;
        } catch(Exception e) {
            return false;
        }
    }

    public int getNumRows() {
        //TODO: Cleanup
        int count=size() + 1;
        return count;
    }

    public int getNumColumns() {
        //TODO: Cleanup
        int count=ConfigData.getNumCols();
        return count;
    }
    public Object getTableRowLabel(int row) {
        return getCropIntervalInfo(row);
    }

    public CropIntervalInfo getCropIntervalInfo(int row) {
        //return an integer indicating the crop interval number
        
        int crop = 0;
        boolean lastWasHarvest = isRowHarvest(getRows().size() - 1);
        
        for(int i = 0; i < row; i++){
            lastWasHarvest = false;
            if(isRowHarvest(i)){
                crop++;
                lastWasHarvest = true;
            }
        }
        
        //wrap code.  Make sure the is another harvest before the end of the management file
        boolean anotherHarvestLater = false;
        for(int i = row ; i < getRows().size() - 1; i++){
            if(isRowHarvest(i)){
                anotherHarvestLater = true;
                break;
            }
        }
        
        return new CropIntervalInfo(row, anotherHarvestLater ? crop : 0, lastWasHarvest, isRowHarvest(row));
         
    }
    
    private boolean isRowHarvest(int row){
        if (row < 0 || row >= getRows().size()){
            return false;
        }
        RowInfo info = getRow(row);
        Object temp = info.getDataObject(XMLConstants.soperation);
        if(temp instanceof OperationObject){
            OperationObject op = (OperationObject) temp;
            if(OperationClassification.Harvest == OperationClassification.classify(op)){
                return true;
            }
        }
        return false;
    }

    public Object getTableColumnLabel(int column) {
        return ConfigData.getColumnLabels().get(column);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final ManageData other = (ManageData) obj;
        if (this.kRotationYears != other.kRotationYears) {
            return false;
        }
        if (this.version != other.version) {
            return false;
        }
        if (this.mwepsmanfilenotes != other.mwepsmanfilenotes && (this.mwepsmanfilenotes == null || !this.mwepsmanfilenotes.trim().equals(other.mwepsmanfilenotes.trim()))) {
            return false;
        }
        if (this.mRows != other.mRows && (this.mRows == null || !this.mRows.equals(other.mRows))) {            
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 17 * hash + (this.manFile != null ? this.manFile.hashCode() : 0);
        return hash;
    }
    
    public static class CropIntervalInfo{
        
        private final int row, crop;
        private final boolean first, last;

        public CropIntervalInfo(int row, int crop, boolean first, boolean last) {
            this.row = row;
            this.crop = crop;
            this.first = first;
            this.last = last;
        }
        
        public int getCrop(){
            return crop;
        }
        
        public boolean isFirst(){
            return first;
        }
        
        public boolean isLast(){
            return last;
        }
        
        
    }
    
}
