package usda.weru.mcrew.timeline;

import de.schlichtherle.truezip.file.TFile;
import java.awt.Color;
import java.awt.Dimension;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JOptionPane;
import usda.weru.mcrew.Action;
import usda.weru.mcrew.Identity;

import usda.weru.mcrew.JulianCalendar;
import usda.weru.mcrew.MCREWConfig;
import usda.weru.mcrew.ManageData;
import usda.weru.mcrew.OperationObject;
import usda.weru.mcrew.RowInfo;
import usda.weru.mcrew.Table;
import usda.weru.mcrew.TablePanel;
import usda.weru.mcrew.XMLConstants;
import usda.weru.mcrew.timeline.TimelineInterval.IntervalType;

/**
 * This class will handle the interfacing of the graphical components of the timeline
 * with the data stored in managedata files.
 * @author jonathanhornbaker
 */
public class TimelinePanelData 
{
    private final TimelinePanel panel;
    private List<ManageRow> dataStore;
    private int yearCycle = -1;
        
    /**
     * Basic constructor for the data.  It needs a link to the display component
     * of the timeline and an array of filenames to initialize.
     * @param parentPanel
     * @param files 
     */
    public TimelinePanelData(TimelinePanel parentPanel, String[] files)
    {
        panel = parentPanel;
        dataStore = new ArrayList<ManageRow>();
        int index = 0;
        for(String file : files)
        {
            ManageRow temp = new ManageRow();
            if(temp.openFile(file)) dataStore.add(temp);
            temp.data.setOrder(index);
            index ++;
        }
    }
    
    /**
     * Actually initializes the intervals and operation icons.
     */
    public void initialize() { dataStore.stream().forEach((row) -> { row.initData(); }); }
        
    /**
     * Returns the number of managements tied to the timeline.
     * @return 
     */
    public int getNumFiles() { return dataStore.size(); }
    
    /**
     * Iterates over the managedata to find the one that contains the exact row(
     * must have the same reference).
     * @param row
     * @return 
     */
    public int getRowOrigin(RowInfo row)
    {
        for(int ind = 0; ind < dataStore.size(); ind ++) 
        {
            for(RowInfo inf : dataStore.get(ind).data.getRows()) if(row == inf) return ind;
        }
        return -1;
    }
    
    /**
     * Finds the index of the managerow that this interval originated from.
     * @param intr
     * @return 
     */
    public int getIntervalOrigin(TimelineInterval intr)
    {
        for(int ind = 0; ind < dataStore.size(); ind ++) 
        {
            for(TimelineInterval inf : dataStore.get(ind).intervals) if(intr == inf) return ind;
        }
        return -1;
    }
    
    /**
     * Gets the color tied to the interval at the current date of the current row.
     * @param date
     * @param row
     * @return 
     */
    public Color getPeriodColor(int date, int row)
    {
        ManageRow data = dataStore.get(panel.getBandFromPixel(row));
        TimelineInterval intr = data.getIntervalOfDate(panel.pixelsToDate(date));
        return intr.getBackground();
    }
    
    /**
     * Returns the number of years needed to fully represent all possible combinations
     * of the rotations.
     * @return 
     */
    public int getCycle()
    {
        if(yearCycle == -1)
        {
            int[] rots = new int[dataStore.size()];
            for(int index = 0; index < dataStore.size(); index ++)
            {
                rots[index] = dataStore.get(index).data.kRotationYears;
            }
            yearCycle = TimelineConfig.getRotationYears(rots);
        }
        return yearCycle;
    }
    
    /**
     * A utility function for checking to see if the second dimension lies within
     * the first.
     * @param first
     * @param second
     * @return 
     */
    public static boolean inside(Dimension first, Dimension second)
    { 
        return ((first.height <= second.height) || (first.width <= second.width)); 
    }
    
    /**
     * Returns the data of the timeline as a list of rows.
     * @return 
     */
    public List<ManageRow> getBands() { return dataStore; } 
    
    /**
     * Returns the TablePanel that holds the same information contained within
     * the ManageRow specified by the index.
     * @param index
     * @return 
     */
    public TablePanel getTable(int index) { return dataStore.get(index).mcrewTable; }
    
    /**
     * Add a prexisting file to the set.
     * @param filename 
     */
    public boolean addFile(String filename)
    {
        ManageRow temp = new ManageRow();
        boolean success = temp.openFile(filename);
        if(success) dataStore.add(temp);
        return success;
    }
    
    /**
     * Add a black file to the set.
     */
    public boolean addBlank()
    {
        ManageData data = new ManageData();
        data.setWepsManFileNotes("");
        data.clear();
        ManageRow temp = new ManageRow();
        boolean success = temp.openFile(data);
        if(success) dataStore.add(temp);
        return success;
    }
    
    private boolean swap = false;
    /**
     * This method handles the moving of entire managerows on the table (drag and
     * drop of timeline).
     * @param orig
     * @param dest 
     */
    public void swapRequest(int orig, int dest)
    {
        if(dest > dataStore.size() - 1) dest = dataStore.size() - 1;
        if(swap)
        {
            //swap
            ManageRow temp = dataStore.get(orig);
            dataStore.set(orig, dataStore.get(dest));
            dataStore.set(dest, temp);
        }
        else
        {
            List<ManageRow> temp = new ArrayList<ManageRow>(dataStore.size());
            int index = 0;
            for(; index < (orig < dest ? orig : dest); index ++)
            {
                temp.add(index, dataStore.get(index));
            }
            //insert
            if(orig < dest)
            {
                for(; index < dest; index ++)
                {
                    temp.add(index, dataStore.get(index + 1));
                }
                temp.add(index, dataStore.get(orig));
                index ++;
            }
            else
            {
                temp.add(index, dataStore.get(orig));
                index ++;
                for(; index < orig + 1; index ++)
                {
                    temp.add(index, dataStore.get(index - 1));
                }
            }
            for(; index < dataStore.size(); index ++)
            {
                temp.add(index, dataStore.get(index));
            }
            dataStore = temp;
        }
    }
    
    /**
     * This method cuts the row out of it's origin managedata and ports it to the
     * new managedata.
     * @param row
     * @param orig
     * @param dest 
     */
    public void cutAndSew(RowInfo row, int orig, int dest)
    {
        dataStore.get(orig).data.getRows().remove(row);
        dataStore.get(dest).data.getRows().add(row);
    }
    
    public void cutAndSew(RowInfo[] rows, int orig, int dest)
    {
        for(RowInfo row : rows)
        {
            dataStore.get(orig).data.getRows().remove(row);
            dataStore.get(dest).data.getRows().add(row);
        }
    }
    
    public class ManageRow
    {
        List<TimelineInterval> intervals;
        ManageData data;
        List<OperationIcon> icons; //This is the sum total list
                                                    //of icons needed for the row.
        
        Identity initialization = new Identity(0, "O");
                
        private TablePanel mcrewTable;
        /**
         * Basic constructor.  Makes an essentially empty container.  
         */
        public ManageRow()
        {
            data = new ManageData();
            intervals = new ArrayList<TimelineInterval>();
            icons = new ArrayList<OperationIcon>();
        }
        
        /**
         * This function will read a given file by path, and return false if the load
         * failed at any point.
         * @param filename
         * @return 
         */
        public boolean openFile(String filename)
        {
            if (filename.equals("")) {
                System.err.println("File not selected error");
                return false;
            }
            /* Read data on a new ManageData object and assign it to 'data'only on success
             'data' is the ManageData object associated with the 'table'[Table object],
             'table' in associated with this Mcrew object. */
            ManageData newData = new ManageData();
            int dataReadValue = newData.readDataFile(filename);
            switch (dataReadValue) {
                case ManageData.kCorruptedFile:
                    JOptionPane.showMessageDialog(panel, "Unable to read management file " + filename
                            + "\nThe file appears to be corrupted.", "Corrupted File", JOptionPane.ERROR_MESSAGE);
                    return false;
                case ManageData.kWrong_Version:
                    String[] errorMsg = new String[2];
                    errorMsg[0] = "Management file " + filename + "\nis of version " + newData.version
                            + ", whereas the current version is " + ManageData.VERSION_MINIMUM + ".";

                    errorMsg[1] = "Do you want to continue reading ?";

                    int reply = JOptionPane.showConfirmDialog(panel, errorMsg, "Version", JOptionPane.YES_NO_OPTION,
                            JOptionPane.QUESTION_MESSAGE);
                    if (reply == JOptionPane.NO_OPTION) {
                        return false;
                    }
                    break;
                case ManageData.kFile_NotFound:
                    System.err.println("Mcrew:openFile() " + "Cannot find the file. Strange!!");
                    return false;
                case ManageData.kUnknown_Error:
                    System.err.println("Mcrew:openFile() " + "Unknown error reading data file");
                    return false;
                case ManageData.kSuccess:
                    break;
                default:
                    break;
            }
            while (data == null) { Thread.yield(); }
            data.readDataFile(filename);
            mcrewTable = new TablePanel(data, panel.getCoordinator().getCD(), panel.getCoordinator());
            mcrewTable.getTable().checkData();

            TFile file = new TFile(filename);
            mcrewTable.fileName = file.getName();
            mcrewTable.savePath = file.getParent();
            mcrewTable.absFileName = file.getAbsolutePath();

            //Set the Text Field to the absolute path of current opened file.
            try {
                mcrewTable.filePathName = file.getCanonicalPath();
                mcrewTable.changeRotFileName(mcrewTable.filePathName);
                //RotationJT.setText(filePathName);
            } catch (IOException e) {
                System.err.println("Unable to read file: " + file.getAbsolutePath() + e);
            }
            //For now, since the timeline is yet to be part of mcrew, we need to
            //set the panel itself as the parent, so we can find the parent frame
            //(for no immediately apparent reason.)
            panel.add(mcrewTable);
            mcrewTable.setVisible(false);
            return true;
        }
        
        /**
         * This function will set the data to be the given managedata.  data is assumed
         * to be accurate.
         * @param dataIn
         * @return 
         */
        public boolean openFile(ManageData dataIn)
        {
            data = dataIn;
            mcrewTable = new TablePanel(data, panel.getCoordinator().getCD(), panel.getCoordinator());
            //For now, since the timeline is yet to be part of mcrew, we need to
            //set the panel itself as the parent, so we can find the parent frame
            //(for no immediately apparent reason.)
            panel.add(mcrewTable);
            mcrewTable.setVisible(false);
            return true;
        }
        
        /**
         * Returns the table associated with the data contained in the row.
         * @return 
         */
        public Table getTable() { return mcrewTable.getTable(); }
        
        /**
         * We need this to be called seperately so we don't try to call panel's reference
         * to this without it having been initialized.
         */
        public void initData()
        {
            initTimeIntervals();
            initOperationIcons();
            panel.positionIntervals(intervals);
            panel.reposition(icons);
        }
        
        /**
         * We need this to be called seperately so we don't try to call panel's reference
         * to this without it having been initialized.
         */
        public void reInitData()
        {
            intervals = new ArrayList<TimelineInterval>();
//            icons = new ArrayList<OperationIcon>();
            initTimeIntervals();
            reallocateOpIcons();
            panel.positionIntervals(intervals);
            panel.reposition(icons);
        }
        
        /**
         * We need this to be called seperately so we don't try to call panel's reference
         * to this without it having been initialized.
         */
        public void reInitDataDates()
        {
            intervals = new ArrayList<TimelineInterval>();
            icons = new ArrayList<OperationIcon>();
            initTimeIntervals();
            initOperationIcons();
            panel.positionIntervals(intervals);
            panel.reposition(icons);
        }
        /**
         * Returns the panel associated with the data.
         * @return 
         */
        public TimelinePanel getPanel() { return panel; }
        
        /**
         * Returns the year at which this interval should repeat.
         * @return 
         */
        public int getRepeat() { return data.kRotationYears; }
        
        /**
         * Returns the pixel at which this interval should repeat.
         * @return 
         */
        public int getPixelRepeat() { return (int) (data.kRotationYears * 365 * panel.getZoom()); }
    
        /**
         * This function will take the given date and return the relevent
         * timelineinterval for that date.
         * @param date
         * @return 
         */
        public TimelineInterval getIntervalOfDate(JulianCalendar date)
        {
            if(intervals.size() == 1) return intervals.get(0);
            for(TimelineInterval intr : intervals)
            {
                if(intr.contains(date)) return intr;
            }
            return null;
        }
        
        /**
         * Initializes the growth and fallow intervals of the timeline.
         */
        public void initTimeIntervals()
        {
            //If there are no rows, there are no crops, thus there are no growth intervals.
            //or fallow intervals;
            //We need to find all P51s and P31s so that we can find all kill rows for crops.
            ArrayList<Integer> plants = new ArrayList<Integer>();
            ArrayList<Integer> kills = new ArrayList<Integer>();
            for(int index = 0; index < data.getRows().size(); index ++)
            {
                RowInfo row = data.getRows().get(index);
                OperationObject op = (OperationObject) row.getDataObject(XMLConstants.soperation);
                if(op == null) continue;
                if(op.getAllIds().contains(new Identity(51, "P"))) 
                {
                    plants.add(index);
                }
                if(op.getAllIds().contains(new Identity(31, "P"))) kills.add(index);
            }
            if(data.getRows().size() < 2)
            {
                JulianCalendar start = new JulianCalendar(1, 0, 1);
                JulianCalendar end = new JulianCalendar(data.kRotationYears, 11, 31);
                intervals.add(new TimelineInterval(start, end, plants.isEmpty() ? IntervalType.FALLOW : IntervalType.GROWTH,
                    this));
                return;
            }

            //If there are no crops, there is no growth interval.  The entire field
            //is always fallow
            if(plants.isEmpty()) 
            {
                JulianCalendar start = data.getRows().get(0).getDate();
                JulianCalendar end = data.getRows().get(data.getRows().size() - 1).getDate();
                intervals.add(new TimelineInterval(start, end, IntervalType.FALLOW,
                    this));
            }
            else
            {
                //Growths start at plant, go until kill.  Fallows start at kill, go
                //until plant.  If the kill is a plant operation, it does not count
                //as a fallow period.
                int killInd = -1;
                for(int plantInd = 0; plantInd < plants.size(); plantInd++)
                {
                    int plant = plants.get(plantInd);
                    if(killInd != -1)
                    {
                        //Our last crop ended with a kill, rather than a plant.  Thus
                        //We have a fallow interval that starts with a kill and ends
                        //with a plant.
                        intervals.add(new TimelineInterval(data.getRows().get(killInd).getDate().cloneDate().increment(), 
                                    data.getRows().get(plant).getDate(), IntervalType.FALLOW, this));
                        killInd = -1;
                    }
                    OperationObject plantOp = (OperationObject) data.getRows().get(plant).getDataObject(XMLConstants.soperation);
                    //We need to know whether the plant is an annual or anything else, to determine what kills it.
                    String tempVal = (String) plantOp.getCrop().getParameter("idc").getValue();
                    int cropType = Integer.parseInt(tempVal);
                    int nextPlant = plantInd != (plants.size() - 1) ? plants.get(plantInd + 1) : data.getRows().size();
                    for(int kill : kills)
                    {
                        //If the kill is before the plant (with the exception of the first crop,
                        //which will be handled elsewhere) it will not end the growth interval.
                        if(kill <= plant) continue;
                        //If the next plant happens before a kill that affects this plant,
                        //that kill is the termination of the old plant.
                        if(kill > nextPlant) 
                        {
                            intervals.add(new TimelineInterval(data.getRows().get(plant).getDate(), 
                                    data.getRows().get(nextPlant).getDate(), IntervalType.GROWTH, this));
                            break;
                        }
                        OperationObject killOp = (OperationObject) data.getRows().get(kill).getDataObject(XMLConstants.soperation);
                        int index = killOp.getAllIds().indexOf(new Identity(31, "P"));
                        if(index == -1) continue;
                        Action act = killOp.getAction(index);
                        //If it's a Kill op, we need to see what the  kill flag is
                        //to determine whether it killed the crop.
                        tempVal = act.getValue("kilflag");
                        int killType = Integer.parseInt(tempVal);
                        switch(killType)
                        {
                            case 2:
                                intervals.add(new TimelineInterval(data.getRows().get(plant).getDate(), 
                                    data.getRows().get(kill).getDate().cloneDate().increment(), IntervalType.GROWTH, this));
                                killInd = kill;
                                break;
                            case 1:
                                //If the kill is an annual, we need to see if the corresponding
                                //crop is an annual.  If it is, this is the kill we're looking for.
                                //If not, we don't care.
                                switch(cropType)
                                {
                                    case 1:/*FALLTHROUGH*/
                                    case 2:/*FALLTHROUGH*/
                                    case 4:/*FALLTHROUGH*/
                                    case 5:
                                        intervals.add(new TimelineInterval(data.getRows().get(plant).getDate(), 
                                            data.getRows().get(kill).getDate().cloneDate().increment(), IntervalType.GROWTH, this));
                                        killInd = kill;
                                        break;
                                    default:
                                }
                                break;
                            default:
                        }
                        if(killInd != -1) break;
                    }
                }
                if(killInd != -1)
                {
                    //The last plant was killed, and thus the fallow period does the
                    //wrap.
                    //To wrap, the fallow goes to the end of the last rotation year (31 Dec).
                    //It then starts at the beginning (1 Jan 1) and continues to the first
                    //plant.
                    //If we ended with a kill on 31 Dec we don't need the end of year section.
                    if(!data.getRows().get(killInd).getDate().equals(new JulianCalendar(data.kRotationYears, 11, 31)))
                    {
                        intervals.add(new TimelineInterval(data.getRows().get(killInd).getDate().cloneDate().increment(), 
                                    new JulianCalendar(data.kRotationYears, 11, 31), IntervalType.FALLOW, this));
                    }
                    if(!data.getRows().get(plants.get(0)).getDate().equals(new JulianCalendar(1, 0, 1)))
                    {
                        intervals.add(new TimelineInterval(new JulianCalendar(1, 0, 1), 
                                    data.getRows().get(plants.get(0)).getDate(), IntervalType.FALLOW, this));
                    }
                }
                else
                {
                    boolean nofallow = false;
                    //The last plant wasn't killed.  We need to start the loop from
                    //the start.
                    int plant = plants.get(plants.size() - 1);
                    if(!data.getRows().get(plant).getDate().equals(new JulianCalendar(data.kRotationYears, 11, 31)))
                    {
                        intervals.add(new TimelineInterval(data.getRows().get(plant).getDate(), 
                                    new JulianCalendar(data.kRotationYears, 11, 31), IntervalType.GROWTH, this));
                    }
                    OperationObject plantOp = (OperationObject) data.getRows().get(plant).getDataObject(XMLConstants.soperation);
                    //We need to know whether the plant is an annual or anything else, to determine what kills it.
                    String tempVal = (String) plantOp.getCrop().getParameter("idc").getValue();
                    int cropType = Integer.parseInt(tempVal);
                    int nextPlant = plants.get(0);
                    for(int kill : kills)
                    {
                        //If the next plant happens before a kill that affects this plant,
                        //that kill is the termination of the old plant.
                        if(kill > nextPlant) 
                        {
                            if(!data.getRows().get(nextPlant).getDate().equals(new JulianCalendar(1, 0, 1)))
                            {
                                intervals.add(new TimelineInterval(new JulianCalendar(1, 0, 1), 
                                            data.getRows().get(nextPlant).getDate(), IntervalType.GROWTH, this));
                            }
                            break;
                        }
                        OperationObject killOp = (OperationObject) data.getRows().get(kill).getDataObject(XMLConstants.soperation);
                        int index = killOp.getAllIds().indexOf(new Identity(31, "P"));
                        if(index == -1) continue;
                        Action act = killOp.getAction(index);
                        //If it's a Kill op, we need to see what the  kill flag is
                        //to determine whether it killed the crop.
                        tempVal = act.getValue("kilflag");
                        int killType = Integer.parseInt(tempVal);
                        switch(killType)
                        {
                            case 2:
                                if(!data.getRows().get(kill).getDate().equals(new JulianCalendar(1, 0, 1)))
                                {
                                    intervals.add(new TimelineInterval(new JulianCalendar(1, 0, 1), 
                                                data.getRows().get(kill).getDate().cloneDate().increment(), IntervalType.GROWTH, this));
                                }
                                break;
                            case 1:
                                //If the kill is an annual, we need to see if the corresponding
                                //crop is an annual.  If it is, this is the kill we're looking for.
                                //If not, we don't care.
                                switch(cropType)
                                {
                                    case 1:/*FALLTHROUGH*/
                                    case 2:/*FALLTHROUGH*/
                                    case 4:/*FALLTHROUGH*/
                                    case 5:
                                        if(!data.getRows().get(kill).getDate().equals(new JulianCalendar(1, 0, 1)))
                                        {
                                            intervals.add(new TimelineInterval(new JulianCalendar(1, 0, 1), 
                                                        data.getRows().get(kill).getDate().cloneDate().increment(), IntervalType.GROWTH, this));
                                        }
                                        break;
                                    default:
                                }
                                break;
                            default:
                                if(!data.getRows().get(plant).getDate().equals(new JulianCalendar(1, 0, 1)))
                                {
                                    intervals.add(new TimelineInterval(new JulianCalendar(1, 0, 1), 
                                                data.getRows().get(plant).getDate().cloneDate().increment(), IntervalType.GROWTH, this));
                                }
                                nofallow = true;
                        }
                    }
                    if(!nofallow && ((kills.size() == 1) && (plants.size() == 1)) && (kills.get(0) < plants.get(0)) && intervals.size() == 2)
                    {
                        intervals.add(new TimelineInterval(data.getRows().get(kills.get(0)).getDate().cloneDate().increment(), 
                                        data.getRows().get(plants.get(0)).getDate().cloneDate().increment(), IntervalType.FALLOW, this));
                    }
                }
            }
        }
        
        /**
         * Initializes the operation Icons, and places them in their respective
         * intervals.
         */
        public void initOperationIcons()
        {
            RowInfo next = null;
            MultiOperationIcon sink = null;
            JulianCalendar second = null;
            if(data.getNumRows() == 1) return; 
            next = data.getRow(0);
            second = data.getRow(0).getDate();
            for(int index = 0; index < data.getNumRows() - 2; index ++)
            {
                RowInfo row = next;
                next = data.getRow(index + 1);
                OperationObject op = (OperationObject) row.getDataObject(XMLConstants.soperation);
                if(op.getAllIds().contains(initialization)) continue;
                JulianCalendar first = second;
                second = next.getDate();
                if(sink != null)
                {
                    sink.add(row);
                    if(!first.equals(second)) sink = null;
                }
                else
                {
                    if(first.equals(second))
                    {
                        sink = new MultiOperationIcon(row, TimelinePanelData.this, panel);
                        icons.add(sink);
                        TimelineInterval intr = getIntervalOfDate(row.getDate());
                        intr.pushIcon(sink);
                    }
                    else
                    {
                        OperationIcon opIc = new OperationIcon(row, TimelinePanelData.this, panel);
                        icons.add(opIc);
                        TimelineInterval intr = getIntervalOfDate(row.getDate());
                        intr.pushIcon(opIc);
                    }
                }
            }
            if(sink != null)
            {
                sink.add(next);
                sink = null;
            }
            else
            {
                OperationIcon opIc = new OperationIcon(next, TimelinePanelData.this, panel);
                icons.add(opIc);
                TimelineInterval intr = getIntervalOfDate(next.getDate());
                intr.pushIcon(opIc);
            }
            
        }
        
        /**
         * Replaces the operation Icons within their correct interval.
         */
        public void reallocateOpIcons()
        {
            for(OperationIcon opIc : icons)
            {
                TimelineInterval intr = getIntervalOfDate(opIc.getRow().getDate());
                intr.pushIcon(opIc);
            }
        }
        
        /**
         * Finds all Operation Icons within the specified date range.
         * @param start
         * @param end
         * @return 
         */
        public List<OperationIcon> inDateRange(JulianCalendar start, JulianCalendar end)
        {
            ArrayList<OperationIcon> icos = new ArrayList<OperationIcon>(icons.size());
            for(OperationIcon opIc : icons)
            {
                JulianCalendar curDate = opIc.getRow().getDate();
                if(curDate.after(end) || curDate.before(start)) continue;
                else icos.add(opIc);
            }
            return icos;
        }
        
        /**
         * Saves the information that lies under the table.
         */
        public void save() { mcrewTable.saveCurrent(); }
        
        /**
         * Removes all relevant information tied to the ManageRow.
         */
        public void clearData()
        {
            intervals.stream().forEach(TimelineInterval::clear);
            icons.stream().forEach(panel::remove);
            intervals = new ArrayList<TimelineInterval>();
            icons = new ArrayList<OperationIcon>();
        }
        
        public boolean isSorted()
        {
            return data.checkSort();
        }
    }
}
