package usda.weru.mcrew.timeline;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import javax.help.CSH;
import javax.help.HelpBroker;
import javax.help.HelpSet;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.SwingUtilities;

import usda.weru.mcrew.JulianCalendar;
import usda.weru.mcrew.MCREWConfig;
import usda.weru.mcrew.MCREWWindow;
import usda.weru.mcrew.ManageData;
import usda.weru.mcrew.RowInfo;
import usda.weru.mcrew.TablePanel;
import usda.weru.mcrew.timeline.TimelinePanelData.ManageRow;

/**
 * This class will drive the positioning of all items in the table.
 * @author jonathanhornbaker
 */
public class TimelinePanel extends JPanel
{
    public static final long serialVersionUID = 56L;
    private StepType scaleType = StepType.DAY;
    private int pixelVal = 10;
    private double zoom = 10;  //zoom is the number of pixels allocated per day.
                            //If zoom ends up less than 1, that means we are using
                            //date ranges.  Anything less than 3 sets the table to
                            //be non-editable. 
    private int offset = 0;     //The location of 1 Jan 01.
    /**
     * Sets whether the user specifies their zoom based of days weeks months or years.
     */
    public enum StepType
    {
        DAY(1),
        WEEK(7),
        MONTH(31),
        YEAR(365);
        
        public final int days;
        
        StepType(int type) { days = type; }
    }
    private final int[] dPM = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    private final TimelinePanelData information;
    private int maxHeight;
    private int headerHeight = 25;
    private int headerWidth = 25;
    private TimelineControls controls;
    private OperationIcon cache = null;
    private boolean dragging = true;
    private ArrayList<OperationIcon> selected = new ArrayList<OperationIcon>();
    private OperPopup popup;
    private int regiRow;
    private int regiPixel;
    private boolean changed = true;
    private MCREWWindow coordinator;
    private int currentlySelected = 0;
    private int parentHeight;
    private int parentWidth;
    /**
     * The adjustment factor to the panel given by the scroll bar.
     */
    private int vertScrollAdjust = 0;
    private int hortScrollAdjust = 0;
    
    private JScrollBar vertScrollBar;
    private JScrollBar hortScrollBar;
    
    private boolean opIcs = true;
   
    /**
     * The basic constructor for the timeline panel.  Needs an array of the filenames
     * it should access, and to have access to the MCREWWindow it is embedded in.
     * @param files
     * @param parent 
     */
    public TimelinePanel(String[] files, MCREWWindow parent)
    {
        zoom = TimelineConfig.height;
        pixelVal = TimelineConfig.height;
        coordinator = parent;
        information = new TimelinePanelData(this, files);
        information.initialize();
        maxHeight = information.getNumFiles() * TimelineConfig.height;
        MouseHandler handler = new MouseHandler();
        addMouseMotionListener(handler);
        addMouseListener(handler);
        setLayout(null);
        this.setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(0, 0, 0)));
        vertScrollBar = new JScrollBar(JScrollBar.VERTICAL);
        vertScrollBar.addAdjustmentListener(evt -> { vertScrollAction(evt); });
        vertScrollBar.setMaximum(getVertScrollMax());
        this.add(vertScrollBar);
        vertScrollBar.setVisible(true);
        hortScrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
        hortScrollBar.addAdjustmentListener(evt -> { hortScrollAction(evt); });
        hortScrollBar.setMaximum(getVertScrollMax());
        this.add(hortScrollBar);
        hortScrollBar.setVisible(true);
    }
    
    /**
     * Since the controls are a separate entity and need a reference to this panel,
     * we need to link controls outside of the constructor.  Note that this means
     * the placement of the timeline controls must already be done before this
     * method is called, as the controls panel is not a part of the timeline panel.
     * @param ctrl 
     */
    public void linkControls(TimelineControls ctrl) 
    { 
        controls = ctrl;
        controls.resetZoom(TimelineConfig.height);
    }
    
    /**
     * Returns the controls, so the user can directly manipulate state somewhere else.
     * Deliberately package private.
     * @return 
     */
    TimelineControls getControls() { return controls; }
    /**
     * Returns the coordinator for the timeline to the tables.
     * Deliberately package private.
     * @return 
     */
    MCREWWindow getCoordinator() { return coordinator; }
    
    /**
     * Respecifies the locations of all the intervals and icons.
     */
    private void recalibrate()
    {
        List<ManageRow> bands = information.getBands();
        bands.stream().forEach((band) -> 
        { 
            band.reInitData();
            positionIntervals(band.intervals); 
            band.intervals.stream().forEach((intr) ->
            {
                reposition(intr.getIcons());
            });
        });
    }
    
    public void recalibrateDates()
    {
        this.removeAll();
        List<ManageRow> bands = information.getBands();
        bands.stream().forEach((band) -> 
        { 
            band.reInitDataDates();
            positionIntervals(band.intervals); 
            band.intervals.stream().forEach((intr) ->
            {
                reposition(intr.getIcons());
            });
        });
        this.add(vertScrollBar);
        this.add(hortScrollBar);
    }
    
    /**
     * Initializes the position of the intervals by passing off to the interval
     * solver.
     * @param intrs 
     */
    public void positionIntervals(List<TimelineInterval> intrs)
    {
        intrs.stream().forEach((intr) -> 
        { 
            positionInterval(intr); 
        });
    }
    
    /**
     * Initializes the placement and range of a specific interval.
     * @param intr 
     */
    public void positionInterval(TimelineInterval intr)
    {
        int upperPixelY = getUpperBound(intr);
        int lowerPixelY = getLowerBound(intr);
        int upperPixelX = dateToPixels(intr.getStart());
        int lowerPixelX = dateToPixels(intr.getEnd());
        intr.setLocation(upperPixelX, upperPixelY, lowerPixelX - upperPixelX, lowerPixelY - upperPixelY);
    }
    
    /**
     * Iterates through all icons and repositions them.
     * @param icons 
     */
    public void reposition(List<OperationIcon> icons)
    {
        icons.stream().forEach((ico) -> 
        { 
            remove(ico);
            reposition(ico);
            add(ico);
        });
    }
    
    /**
     * Takes one icon, and relocates it's origin for repositioning.
     * @param ico 
     */
    public void reposition(OperationIcon ico)
    {
        RowInfo row = ico.getRow();
        int pixelX = dateToPixels(row.getDate());
        int pixelY = getPixelRow(row);
        ico.specifyLocation(pixelX, pixelY);
    }
    
    /**
     * Takes the row of the management file and returns the pixel location that info
     * should lie at.
     * @param row
     * @return 
     */
    private int getPixelRow(RowInfo row)
    {
        int manRow = information.getRowOrigin(row);
        return getUpperBound(manRow);
    }
    
    /**
     * Finds the number of management row that contains this pixel.
     * @param pixelY
     * @return 
     */
    public int getBandFromPixel(int pixelY)
    {
        pixelY -= headerHeight;
        int index = 0;
        for(; index < maxHeight; index ++)
        {
            if(pixelY < ((index + 1) * (TimelineConfig.height + 5))) return index + vertScrollAdjust;
        }
        return -1;
    }
    
    /**
     * Takes the interval and finds the lower bound of the strip.
     * @param intr
     * @return 
     */
    private int getLowerBound(TimelineInterval intr)
    {
        int manRow = information.getIntervalOrigin(intr) + 1 - vertScrollAdjust;
        return (manRow * TimelineConfig.height) + ((manRow -1) * 5) + headerHeight;
    }
    
    /**
     * Takes the interval and finds the upper bound of the strip.
     * @param intr
     * @return 
     */
    private int getUpperBound(TimelineInterval intr)
    {
        int manRow = information.getIntervalOrigin(intr) - vertScrollAdjust;
        return manRow * (TimelineConfig.height + 5) + headerHeight;
    }
    
    /**
     * Takes the interval and finds the lower bound of the strip.
     * @param intr
     * @return 
     */
    private int getLowerBound(int intr)
    {
        int manRow = intr + 1 - vertScrollAdjust;
        return (manRow * TimelineConfig.height) + ((manRow -1) * 5) + headerHeight;
    }
    
    /**
     * Takes the interval and finds the upper bound of the strip.
     * @param intr
     * @return 
     */
    private int getUpperBound(int intr)
    {
        int manRow = intr - vertScrollAdjust;
        return manRow * (TimelineConfig.height + 5) + headerHeight;
    }
    
    /**
     * Takes a date, and turns that into the number of pixels between the date and
     * 01 Jan 01, assuming one pixel per day.
     * @param date
     * @return 
     */
    private int dateToPixels(JulianCalendar date)
    {
        int total = (date.get(Calendar.YEAR) - 1) * 365;
        if(scaleType == StepType.MONTH)
        {
            total += (date.get(Calendar.MONTH)) * 31;
        }
        else
        {
            for(int month = 0; month < date.get(Calendar.MONTH); month ++)
            {
                total += dPM[month];
            }
        }
        total += date.get(Calendar.DATE);
        total --; //We need to decrement to get base 1 dates to line up with base 0 pixels.
        total = applyScale(total);
        return total;
    }
    
    /**
     * Takes the given pixel, removes the scale and finds the date corresponding
     * to that pixel.
     * @param pixelX
     * @return 
     */
    public JulianCalendar pixelsToDate(int pixelX)
    {
        pixelX = removeScale(pixelX);
        pixelX ++; //We need to increment to get base 0 pixels to line up with base 1 dates.
        int year = 1;
        while((pixelX - 365) > 0)
        {
            pixelX -= 365;
            year ++;
        }
        int month = 0;
        if(scaleType == StepType.MONTH)
        {
            while((pixelX - 31) > 0)
            {
                pixelX -= 31;
                month ++;
            }
        }
        else
        {
            for(int item : dPM)
            {
                if((pixelX - item) > 0)
                {
                    pixelX -= item;
                    month ++;
                }
                else break;
            }
        }
        JulianCalendar total = new JulianCalendar(year, month, pixelX);
        return total;
    }
    
    /**
     * This takes a pixelLocation and removes any offsets necessary (If a month
     * resolution is selected, we normalize each month to have 31 "days" by inserting
     * dead space where necessary.)
     * @param pixelLocation
     * @return 
     */
    public int applyScale(int pixelLocation)
    {
//        if(scaleType == StepType.MONTH)
//        {
//            int pixel = pixelLocation;
//            while((pixel - 365) > 0)
//            {
//                pixel -= 365;
//                pixelLocation += 7;
//            }
//            int month = 0;
//            for(; (pixel - dPM[month]) > 0; month ++)
//            {
//                pixel -= dPM[month];
//            }
//            if(month == 0) return (int) ((pixelLocation * zoom) - offset);
//            if(month == 1)
//            {
//                //If the month is February, we have 3 dead zones, one before
//                //the first, one after the 28 and one inbetween 14 and 15,
//                //so we need to take the first and second deadzones into account
//                //during the calculation of pixels in the month.
//                pixelLocation ++;
//                if(pixel > 14) pixelLocation ++;
//                return (int) ((pixelLocation * zoom) - offset);
//            }
//            else
//            {
//                pixelLocation += 3; //We added three "days" to February. 
//                if(month < 4) return (int) ((pixelLocation * zoom) - offset);
//                pixelLocation ++;   //We add a "day" to April
//                if(month < 6) return (int) ((pixelLocation * zoom) - offset);
//                pixelLocation ++;   //We add a "day" to June
//                if(month < 9) return (int) ((pixelLocation * zoom) - offset);
//                pixelLocation ++;   //We add a "day" to September
//                if(month < 11) return (int) ((pixelLocation * zoom) - offset);
//                pixelLocation ++;   //We add a "day" to November
//                return (int) ((pixelLocation * zoom) - offset);
//            }
//        }
//        else 
            return (int) ((pixelLocation * zoom) - offset + headerWidth);
    }
    
    /**
     * This takes a pixelLocation and applies any offsets necessary (If a month
     * resolution is selected, we normalize each month to have 31 "days" by inserting
     * dead space where necessary.)
     * @param pixelLocation
     * @return 
     */
    private int removeScale(int pixelLocation)
    {
        return pixelLocation = (int) ((pixelLocation + offset - headerWidth) / zoom);
//        if(scaleType == StepType.MONTH)
//        {
//            int pixel = pixelLocation;
//            while((pixel - 365) > 0)
//            {
//                pixel -= 365;
//                pixelLocation -= 7;
//            }
//            int month = 0;
//            for(; (pixel - dPM[month]) > 0; month ++)
//            {
//                pixel -= dPM[month];
//            }
//            if(month == 0) return pixelLocation;
//            if(month == 1)
//            {
//                    //If the month is February, we have 3 dead zones, one before
//                    //the first, one after the 28 and one inbetween 14 and 15,
//                    //so we need to take the first and second deadzones into account
//                    //during the calculation of pixels in the month.
//                    pixelLocation --;
//                    if(pixel > 14) pixelLocation --;
//                    return pixelLocation;
//            }
//            else
//            {
//                pixelLocation -= 3; //We added three "days" to February. 
//                if(month < 4) return pixelLocation;
//                pixelLocation --;   //We add a "day" to April
//                if(month < 6) return pixelLocation;
//                pixelLocation --;   //We add a "day" to June
//                if(month < 9) return pixelLocation;
//                pixelLocation --;   //We add a "day" to September
//                if(month < 11) return pixelLocation;
//                pixelLocation --;   //We add a "day" to November
//                return pixelLocation;
//            }
//        }
//        else return pixelLocation;
    }
    
    /**
     * Returns the level of zoom applied to the timeline.
     * @return 
     */
    public double getZoom() { return zoom; }
    
    /**
     * Returns the number of pixels per scale.
     * @return 
     */
    public int getPixelZoom() { return pixelVal; }
    
    /**
     * Returns the scale applied to the timeline. 
     * @return 
     */
    public StepType getScale() { return scaleType; }
    
    /**
     * Returns the last pixel to be rendered.
     * @return 
     */
    public int getEnd() { return (int) ((information.getCycle() * 365 * getZoom()) - offset); }
    
    /**
     * These three functions are necessary to set the zoom.  The setZoom function sets the 
     * number of pixels per scale.  The setZoomType sets the enumeration to days/weeks/months/years.
     * The resetZoom function must be called whenever either is changed, as it will recalculate the
     * zoom vaue that is actualy used.
     * @param value 
     */
    public void setZoom(int value) 
    { 
        pixelVal = value;
        controls.resetZoom(value);
        resetZoom(); 
        if(value < (TimelineConfig.height * getScale().days)) opIcs = false;
        else opIcs = true;
    }
    public void setZoomType(StepType value) 
    { 
        if(scaleType != value)
        {
            controls.resetZoomType(value);
            pixelVal /= scaleType.days;
            if(pixelVal == 0) pixelVal = 1;
            pixelVal *= value.days;
            controls.resetZoom(pixelVal);
            scaleType = value; 
            resetZoom(); 
        }
    }
    public void resetZoom() 
    { 
        zoom = ((double) pixelVal / (double) scaleType.days); 
        if(removeScale(getPanelEnd()) > removeScale(getEnd())) 
        {
            offset = 0;
            offset = applyScale(removeScale(getEnd()) - removeScale(getPanelEnd()));
        }
        hortScrollBar.setMaximum(getHortScrollMax());
        repaint(); 
    }
    
    /**
     * This method will handle the swapping of an operation icon between rows,
     * if necessary.
     * @param opIc
     * @param orig
     * @param drop 
     */
    public void swap(OperationIcon opIc, int orig, int drop)
    {
        if(opIc instanceof MultiOperationIcon) information.cutAndSew(((MultiOperationIcon) opIc).getAllRows(), orig, drop);
        else information.cutAndSew(opIc.getRow(), orig, drop);
        recalibrateDates();
        if(MCREWConfig.autosort) this.getTablePanelNoSelect(drop).SortJMI_actionPerformed(null);
        repaint();
    }
    
    /**
     * Will change the location of all selected Operation Icons by the pixel range specified.
     */
    public void repositionSelectionByPixels(int pixels)
    {
        for(OperationIcon opIc : selected)
        {
            JulianCalendar date = opIc.getRow().getDate();
            int loc = dateToPixels(date);
            loc += pixels;
            date = pixelsToDate(loc);
            opIc.setDate(date);
            getTablePanelNoSelect(information.getRowOrigin(opIc.getRow())).setDataChanged(true);
        }
        coordinator.dateChange(this);
    }
    
    /**
     * Will copy all operationicons selected and then change by the pixel range specified.
     * @param pixels 
     */
    public void copySelectionByPixels(int pixels)
    {
        for(OperationIcon orig : selected)
        {
            OperationIcon opIc = orig.copyAction();
            JulianCalendar date = opIc.getRow().getDate();
            int loc = dateToPixels(date);
            loc += pixels;
            date = pixelsToDate(loc);
            opIc.setDate(date);
            getTablePanelNoSelect(information.getRowOrigin(opIc.getRow())).setDataChanged(true);
        }
        coordinator.dateChange(this);
    }
    
    /**
     * Moves the view window one screen length left.
     */
    public void shiftLeft() 
    { 
        offset -= getPanelEnd(); 
        if(offset < headerWidth) offset = 0;
        repaint(); 
    }
    /**
     * Moves the view window one delimiter length left.
     */
    public void moveLeft() 
    { 
        offset -= pixelVal / scaleType.days;  
        if(offset < headerWidth) offset = 0;
        repaint(); 
    }
    /**
     * Moves the view window one delimiter length left.
     */
    public void moveRight() 
    { 
        offset += pixelVal / scaleType.days; 
        if(removeScale(getPanelEnd()) > removeScale(getEnd()))
        {
            offset = 0;
            offset = applyScale(removeScale(getEnd()) - removeScale(getPanelEnd()));
        }
        repaint(); 
    }
    /**
     * Moves the view window one screen length left.
     */
    public void shiftRight() 
    { 
        offset += getPanelEnd(); 
        if(removeScale(getPanelEnd()) > removeScale(getEnd())) 
        {
            offset = 0;
            offset = applyScale(removeScale(getEnd()) - removeScale(getPanelEnd()));
        }
        repaint(); 
    }
    
    /**
     * This will slot the current rowinfo into our set of selected, for generation
     * of a popup.
     * @param row 
     */
    public void addSelected(OperationIcon row) 
    { 
        if(!selected.contains(row)) selected.add(row);
        else selected.remove(row);
    }
    
    /**
     * Removes all entries from being selected.
     */
    public void clearSelected() 
    { 
        for(OperationIcon opIc : selected) 
        {
            opIc.deselect();
            opIc.repaint();
        }
        selected.clear(); 
    }
    
    /**
     * We will occasionally need to know if nothing is selected.
     * @return 
     */
    public boolean noSelection() { return selected.size() == 0; }
    
    /**
     * Fires off the generating event for the popup.
     */
    public void generatePopup(int locX, int locY)
    {
        if(selected.size() == 0) return;
        if(popup == null) popup = new OperPopup(this);
        if(selected.size() == 1) 
        {
            popup.setManagement(information.getBands().get(information.getRowOrigin
                    (selected.get(0).getRow())));
            coordinator.refocus(getBandFromPixel(locY));
        }
        else popup.setManagement(null);
        if(selected.size() == 1 && selected.get(0) instanceof MultiOperationIcon)
        {
            RowInfo[] rows = ((MultiOperationIcon) selected.get(0)).getAllRows();
            popup.setStretch(rows, true);
        }
        else
        {
            RowInfo[] rows = new RowInfo[selected.size()];
            for(int index = 0; index < selected.size(); index ++) rows[index] = selected.get(index).getRow();
            popup.setStretch(rows);
        }
        popup.show(this, locX, locY);
        popup.setVisible(true);
    }
    
    /**
     * Registers the last click as having been at the specified x y location.
     * @param locX
     * @param locY 
     */
    public void registerClick(int locX, int locY) 
    {
        regiPixel = locX;
        regiRow = getBandFromPixel(locY);
    }
    
    /**
     * Closes the span of a shift click, adding all contained entries to the selected
     * cells.
     * @param locX
     * @param locY 
     */
    public void closeInterval(int locX, int locY)
    {
        int closePixel = locX;
        int closeRow = getBandFromPixel(locY);
        if(closeRow != regiRow) return;
        JulianCalendar start;
        JulianCalendar end;
        if(closePixel > regiPixel)
        {
            start = pixelsToDate(regiPixel);
            end = pixelsToDate(closePixel);
        }
        else
        {
            start = pixelsToDate(closePixel);
            end = pixelsToDate(regiPixel);
        }
        while(start.after(new JulianCalendar(information.getBands().get(closeRow).getRepeat() + 1, 0, 1)))
        {
            start = start.cloneDate().decrementYear(information.getBands().get(closeRow).getRepeat());
        }
        while(end.after(new JulianCalendar(information.getBands().get(closeRow).getRepeat() + 1, 0, 1)))
        {
            end = end.cloneDate().decrementYear(information.getBands().get(closeRow).getRepeat());
        }
        List<OperationIcon> icons = information.getBands().get(
                getBandFromPixel(locY)).inDateRange(start, end);
        for(OperationIcon opIc : icons) 
        {
            selected.add(opIc);
            opIc.select();
        }
    }
    
    /**
     * Registers a change.
     */
    public void setChanged() { changed = true; }
    
    /**
     * Used to ensure month header actually prints the start of months at the start
     * of months.
     * @return 
     */
    public int pixelsToMonthEnd()
    {
        JulianCalendar date = pixelsToDate(headerHeight);
        int day = date.get(Calendar.DATE);
        int offset = 0;
        int window = 31 / pixelVal;
        while (day != 1)
        {
            offset += (zoom < 1 ? 1 : zoom);
            date = pixelsToDate(headerHeight + offset);
            day = date.get(Calendar.DATE);
            if((day - window) <= 1) break;
        }
        return offset;
    }
    /**
     * Used to ensure month header actually prints the start of years at the start
     * of years.
     * @return 
     */
    public int pixelsToYearEnd()
    {
        JulianCalendar date = pixelsToDate(headerHeight);
        int day = date.get(Calendar.MONTH);
        int offset = 0;
        int window = 12 / pixelVal;
        while (day != 0)
        {
            offset += (zoom < 1 ? 1 : zoom);
            date = pixelsToDate(headerHeight + offset);
            day = date.get(Calendar.MONTH);
            if((day - window) <= 0) break;
        }
        return offset;
    }
    
    /**
     * This method paints the header, by rendering a line, rendering the string form
     * of the date at that line and rendering another line at the end of the date string.
     * @param grph 
     */
    public void paintHeader(Graphics grph)
    {
        grph.clearRect(0, 0, getPanelEnd(), headerHeight);
        int width = headerWidth;
        switch(this.scaleType)
        {
            case DAY:
                while(true)
                {
                    JulianCalendar date = pixelsToDate(width);
                    grph.setColor(Color.BLACK);
                    grph.drawLine(width, 0, width, 24);
                    grph.drawString(date.getDate(), width + 4, 12);
                    int offset = (grph.getFontMetrics().stringWidth(date.getDate()) + 8) > pixelVal ? 
                            (grph.getFontMetrics().stringWidth(date.getDate()) + 8) : pixelVal;
                    width += offset;
                    if(width > (getPanelEnd())) break;
                }
                break;
            case WEEK:
                while(true)
                {
                    JulianCalendar date = pixelsToDate(width);
                    grph.setColor(Color.BLACK);
                    grph.drawLine(width, 0, width, 24);
                    grph.drawString(date.getDate(), width + 4, 12);
                    int offset = (grph.getFontMetrics().stringWidth(date.getDate()) + 8) > pixelVal ? 
                            (grph.getFontMetrics().stringWidth(date.getDate()) + 8) : pixelVal;
                    width += offset;
                    if(width > (getPanelEnd())) break;
                }
                break;
            case MONTH:
                width += pixelsToMonthEnd();
                if(width > this.getPanelEnd())
                {
                    grph.setColor(Color.BLACK);
                    JulianCalendar date = pixelsToDate(headerHeight);
                    grph.drawString(getMonthString(date.get(Calendar.MONTH)), headerHeight + 4, 12);
                }
                while(true)
                {
                    JulianCalendar date = pixelsToDate(width);
                    grph.setColor(Color.BLACK);
                    grph.drawLine(width, 0, width, 24);
                    grph.drawString(getMonthString(date.get(Calendar.MONTH)), width + 4, 12);
                    int offset = (grph.getFontMetrics().stringWidth(getMonthString(
                            date.get(Calendar.MONTH))) + 8) > pixelVal ? 
                            (grph.getFontMetrics().stringWidth(getMonthString(
                            date.get(Calendar.MONTH))) + 8) : pixelVal;
                    width += offset;
                    if(width > (getPanelEnd())) break;
                }
                break;
            case YEAR:
                width += pixelsToYearEnd();
                if(width > this.getPanelEnd())
                {
                    grph.setColor(Color.BLACK);
                    JulianCalendar date = pixelsToDate(headerHeight);
                    grph.drawString(Integer.toString(date.get(Calendar.YEAR)), headerHeight + 4, 12);
                }
                while(true)
                {
                    JulianCalendar date = pixelsToDate(width);
                    grph.setColor(Color.BLACK);
                    grph.drawLine(width, 0, width, 24);
                    grph.drawString(Integer.toString(date.get(Calendar.YEAR)), width + 4, 12);
                    int offset = (grph.getFontMetrics().stringWidth(Integer.toString(
                            date.get(Calendar.YEAR))) + 8) > pixelVal ? 
                            (grph.getFontMetrics().stringWidth(Integer.toString(
                            date.get(Calendar.YEAR))) + 8) : pixelVal;
                    width += offset;
                    if(width > (getPanelEnd())) break;
                }
                break;
        }
        grph.clearRect(0, 0, headerWidth, this.getSize().height);
        for(int index = 0; index < information.getBands().size(); index ++)
        {
            if(index < vertScrollAdjust) continue;
            int height = getUpperBound(index);
            int low = getLowerBound(index);
            if(getLowerBound(index) > (getSize().height - 17)) break;
            grph.setColor(Color.BLACK);
            grph.drawLine(0, height, 0, 24);
            grph.drawString(((Integer)information.getBands().get(index).data.getOrder()).toString(), 5, low);
            grph.drawLine(0, low + 5, 0, 24);
        }
    }
    
    /**
     * Returns the full name of the month associated with said number.
     * @param month
     * @return 
     */
    public String getMonthString(int month)
    {
        switch(month)
        {
            case Calendar.JANUARY:
                return "January";
            case Calendar.FEBRUARY:
                return "February";
            case Calendar.MARCH:
                return "March";
            case Calendar.APRIL:
                return "April";
            case Calendar.MAY:
                return "May";
            case Calendar.JUNE:
                return "June";
            case Calendar.JULY:
                return "July";
            case Calendar.AUGUST:
                return "August";
            case Calendar.SEPTEMBER:
                return "September";
            case Calendar.OCTOBER:
                return "October";
            case Calendar.NOVEMBER:
                return "November";
            case Calendar.DECEMBER:
                return "December";
            default:
                return "Not a Month";
        }
    }
    
    /**
     * Returns the preferred size of the timeline.
     * @return 
     */
    @Override
    public Dimension getPreferredSize()
    {
        Dimension top = new Dimension();
        top.width -= 2355;
        top.height = information.getNumFiles() * (TimelineConfig.height + 5) + headerHeight + 10;
        return top;
    }
    
    /**
     * This method will bring in changes from the component directly above this componet.
     * 
     * Needed to be jury rigged as a consequence of the forms.
     * @param height 
     */
    public void newParentSize(int height, int width) 
    {
        if(height < TimelineConfig.height) parentHeight = 0;
        else parentHeight = height; 
        if(width < TimelineConfig.height) parentWidth = 0;
        else parentWidth = width;
        vertScrollBar.setMaximum(getVertScrollMax());
        vertScrollBar.setBounds(getSize().width - 17, headerHeight, 17, parentHeight - headerHeight - 17);
        hortScrollBar.setMaximum(getHortScrollMax());
        hortScrollBar.setBounds(0, getSize().height - 17, parentWidth, 17);
    }
    
    private int getVertScrollMax()
    {
        int pixelY = parentHeight;
        pixelY -= headerHeight;
        pixelY -= hortScrollBar != null ? hortScrollBar.getSize().height : 17;
        int index = 0;
        for(; index < maxHeight; index ++)
        {
            if(pixelY < ((index + 1) * (TimelineConfig.height + 5))) break;
        }
        /**
         * Total number of bands minus visible bands plus weird adjustment factor
         * to get the scroll bar to display the proper number of nodes.
         */
        return information.getNumFiles() + 10 - index;
    }
    
    private int getHortScrollMax()
    {
        int cyc = information.getCycle();
        cyc *= 365;
        int daysToCover = getPanelEnd() - headerWidth;
        daysToCover /= zoom;
        int width = cyc - daysToCover;
        return width + 10;
//return 21128;
    }
    
    /**
     * 
     * @param evt 
     */
    private void vertScrollAction(AdjustmentEvent evt)
    {
        vertScrollAdjust = evt.getValue();
        repaint();
    }
    boolean lock = true;
    private void hortScrollAction(AdjustmentEvent evt)
    {
        hortScrollAdjust = evt.getValue();
        offset = ((int) (hortScrollAdjust * zoom));
        repaint();
    }
    
    public int getPanelEnd() { return parentWidth - (vertScrollBar.isVisible() ? vertScrollBar.getSize().width : 0); }
    
    /**
     * Paints each interval as a rectangle and slots the operationicons into place.
     * @param grph 
     */
    @Override
    public void paintComponent(Graphics grph)
    {
        if(!changed) return;
        super.paintComponent(grph);
        for(Component child : getComponents()) child.setVisible(opIcs);
        vertScrollBar.setVisible(true);
        hortScrollBar.setVisible(true);
        recalibrate();
        for(int index = 0; index < information.getNumFiles(); index ++)
        {
            ManageRow band = information.getBands().get(index);
            if(index < vertScrollAdjust) 
            {
                for(OperationIcon opIc : band.icons) opIc.setVisible(false);
                continue;
            }
            boolean sortError = !band.isSorted();
            if(band.intervals.size() == 1)
            {
                int hello = 21;
            }
            for(TimelineInterval intr : band.intervals)
            {
                JulianCalendar intrStart = intr.getStart();
                JulianCalendar intrEnd = intr.getEnd();
                JulianCalendar viewStart = pixelsToDate(headerWidth);
                JulianCalendar viewEnd = pixelsToDate(getPanelEnd());
                if(sortError)
                {
                    //We don't want to render intervals when the table is unsorted.
                    //We still have to render all the icons contained within the interval.
                    if(opIcs) intr.renderIcons(grph, viewStart, viewEnd);
                    continue;
                }
                //We need to find the fist area that is in the view screen.
                while(!(viewEnd.after(intrStart) || intrEnd.after(viewStart))
                        || (viewStart.after(intrStart) && viewStart.after(intrEnd)))
                {
                    intrStart = intrStart.cloneDate().incrementYear(band.getRepeat());
                    intrEnd = intrEnd.cloneDate().incrementYear(band.getRepeat());
                }
                //Paint all valid rectangles.
                while(intrEnd.after(viewStart) && !intrStart.after(viewEnd))
                {
                    if(viewStart.after(intrStart))
                    {
                        int origin = 0;
                        int termin = dateToPixels(intrEnd) - origin;
                        grph.setColor(intr.getBackground());
                        grph.fillRect(origin, intr.getYOrigin(), termin, intr.getYEdge());
                    }
                    else if(intrEnd.after(viewEnd))
                    {
                        int origin = dateToPixels(intrStart);
                        int termin = getPanelEnd() - origin;
                        grph.setColor(intr.getBackground());
                        grph.fillRect(origin, intr.getYOrigin(), termin, intr.getYEdge());
                    }
                    else
                    {
                        int origin = dateToPixels(intrStart);
                        int termin = dateToPixels(intrEnd) - origin;
                        grph.setColor(intr.getBackground());
                        grph.fillRect(origin, intr.getYOrigin(), termin, intr.getYEdge());
                    }
                    //Repeat for mirrors of the interval
                    intrStart = intrStart.cloneDate().incrementYear(band.getRepeat());
                    intrEnd = intrEnd.cloneDate().incrementYear(band.getRepeat());
                }
                //We have to render all the icons contained within the visible interval.
                if(opIcs) intr.renderIcons(grph, viewStart, viewEnd);
            }
        }
        this.paintChildren(grph);
        vertScrollBar.repaint();
        hortScrollBar.repaint();
        paintHeader(grph);
        changed = false;
    }
    
    /**
     * This method will set the changed variable so as to allow a repaint of the
     * table.
     */
    @Override
    public void repaint()
    {
        setChanged();
        super.repaint();
    }
    
    private class MouseHandler extends  MouseAdapter
    {       
        private boolean inHeader = false;
        private int labelBand = -1;
        
        /**
         * The mouse motion listening allows us to constantly update the current
         * date field in the controls panel.
         * @param evt 
         */
        @Override
        public void mouseMoved(MouseEvent evt)
        {
            if(evt.getSource() == TimelinePanel.this)
            {
                if(controls == null) return;
                JulianCalendar day = pixelsToDate(evt.getX());
                controls.currentDate.setValue(day);
            }
        }
        
        @Override
        public void mouseClicked(MouseEvent evt)
        {
            if(SwingUtilities.isLeftMouseButton(evt))
            {
                int band = getBandFromPixel(evt.getPoint().y);
                if(!(evt.getPoint().x < headerWidth))
                {
                    if((evt.getModifiers() & (InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK)) == (InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK))
                    {
                        closeInterval((evt.getPoint().x), (evt.getPoint().y));
                    }
                    else if((evt.getModifiers() & InputEvent.SHIFT_MASK) == InputEvent.SHIFT_MASK)
                    {
                        clearSelected();
                        closeInterval((evt.getPoint().x), (evt.getPoint().y));
                    }
                    else 
                    {
                        clearSelected();
                        registerClick((evt.getPoint().x), (evt.getPoint().y));
                        coordinator.refocus(band);
                    }
                    if((evt.getPoint().y > getUpperBound(band)) && (getLowerBound(band) > evt.getPoint().y))
                    {
                        //Fire MCREW table change.
                    }
                }
            }
        }
        
        @Override
        public void mousePressed(MouseEvent evt)
        {
            if((evt.getPoint().x < headerWidth))
            {
                labelBand = getBandFromPixel(evt.getPoint().y);
                inHeader = true;
            }
        }
        
        @Override
        public void mouseReleased(MouseEvent evt)
        {
            if(!inHeader) return;
            inHeader = false;
            int band = getBandFromPixel(evt.getPoint().y);
            if(labelBand == band) return;
            information.swapRequest(labelBand, band);
            repaint();
        }
        
        /**
         * We want to reset the date in the control panel when the mouse leaves
         * the panel.
         * @param evt 
         */
        @Override
        public void mouseExited(MouseEvent evt)
        {
            if(evt.getSource() == TimelinePanel.this)
            {
                if(controls == null) return;
                JulianCalendar day = pixelsToDate(headerWidth);
                controls.currentDate.setValue(day);
            }
        }
    }
    
    /**
     * Sets the currently selected row to the last row.  Coordinates with the table
     * to display said table in the MCREW window.
     */
    public void selectLast()
    {
        
    }
    
    /**
     * Sets the currently selected row.  Coordinates with the table to display said
     * table in the MCREW window.
     * @param row 
     */
    public void selectRow(int row)
    {
        
    }
    
    /**
     * Returns the manage data associated with the given number.
     * @param order
     * @return 
     */
    public ManageData getManageData(int order)
    {
        return information.getBands().get(order).data;
    }
    
    /**
     * Adds a new file to the end of the set.
     */
    public void appendNewFile() { information.addBlank(); }
    
    /**
     * Adds an existing file to the end of the set.
     */
    public void appendExistingFile(String filename) { information.addFile(filename); }
    
    /**
     * Adds field help.
     * @param hs_mcrew 
     */
    public void addCSH(HelpSet hs_mcrew) {
        HelpBroker hb_mcrew = hs_mcrew.createHelpBroker();
        hb_mcrew.enableHelpKey(getRootPane(), "McrewIntro_html", hs_mcrew);
        ActionListener helper = new CSH.DisplayHelpFromSource(hb_mcrew);
    } //end of addCSH
    
    /**
     * Returns the number of files associated with the timeline.
     * @return 
     */
    public int getNumFiles() { return information.getNumFiles(); }
    
    /**
     * Iterates through each file and calls the save on that file.
     */
    public void saveAllFiles() { information.getBands().stream().forEach(ManageRow::save); }
    
    /**
     * Clears all of the old data in the selected row, then reassembles the data.
     */
    public void resynchSelected()
    {
        ManageRow row = information.getBands().get(currentlySelected);
        row.clearData();
        row.initData();
    }
    
    /**
     * Returns the index of whatever interval is currently selected.
     * @return 
     */
    public int getSelectedIndex() { return currentlySelected; } 
    
    /**
     * Selects the table at a given index, and then returns that table.
     * @param index
     * @return 
     */
    public TablePanel getTablePanel(int index)
    {
        currentlySelected = index;
        return getSelected();
    }
    
    /**
     * Returns the table at the given index.
     * @param index
     * @return 
     */
    public TablePanel getTablePanelNoSelect(int index) { return information.getTable(index); }
    
    /**
     * Returns the currently selected table.
     * @return 
     */
    public TablePanel getSelected()
    {
        if(information.getNumFiles() == 0)
        {
            //If there are no files, no file can be selected.
            currentlySelected = 0;
            return null;
        }
        else if(information.getNumFiles() <= currentlySelected)
        {
            //If we somehow select something above range, default to selecting
            //the last item in the file.
            currentlySelected = information.getNumFiles() - 1;
        }
        else if(currentlySelected < 0)
        {
            //If we are somehow below the range, default to selecting the first item.
            currentlySelected = 0;
        }
        return information.getTable(currentlySelected);
    }
}
