package com.superliminal.uiutil;

import javax.swing.*;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Calendar;
import java.util.Arrays;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.text.DateFormat;

/**
 * Presents two or more selectable dates arrayed on a timeline.<br>
 * 
 * Created Apr 28, 2006
 *
 * @author Melinda Green
 */
public class TimePicker extends JPanel {
    private final static String MONSTRS[] = {
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    };
    private final static int PAD=30, BW=20, DAYTIC=8;
    private final static String FAMILY = "Sans Serif";
    private final static Font
        NONTH_FONT = new Font(FAMILY, Font.BOLD,  14),
        DAY_FONT   = new Font(FAMILY, Font.BOLD,  12),
        HOUR_FONT  = new Font(FAMILY, Font.PLAIN, 10);
    private Date[] dates;
    private JRadioButton butts[];

    public static interface SelectionListener {
        public void selectionChanged(int selection);
    }

    /**
     * Convenience constructor assumes no initial selection or selection listener.
     */
    public TimePicker(Date[] dates) {
        this(dates, -1, null);
    }

    public Dimension getMinimumSize() {
        return new Dimension(2*PAD, 2*PAD);
    }

    /**
     * @param dates must have 2 or more entries sorted from newest to oldest.
     * @param initialSelection index of date to initially select or -1 if none.
     * @param dl optional callback object notified whenever selection changes.
     */
    public TimePicker(Date[] dates, int initialSelection, final SelectionListener dl) {
        this.dates = dates;
        this.setPreferredSize(new Dimension(300, 70));
        this.setLayout(null); // because we will position the buttons explicitly
        ActionListener selectionWatcher = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if(dl != null)
                    dl.selectionChanged(getSelection());
            }
        };
        ButtonGroup group = new ButtonGroup();
        butts = new JRadioButton[dates.length];
        for(int i=0; i<butts.length; i++) {
            JRadioButton butt = new JRadioButton();
            butts[i] = butt;
            butt.setSize(BW, BW);
            butt.setOpaque(false);
            butt.setToolTipText(dates[i].toString());
            group.add(butt);
            butt.addActionListener(selectionWatcher);
            butt.setSelected(i == initialSelection);
            this.add(butt);
        }
    }

    /**
     * @return the index of the currently selected date or -1 if none.
     */
    public int getSelection() {
        for(int i=0; i<butts.length; i++)
            if(butts[i].isSelected())
                return i;
        return -1;
    }

    /**
     * Positions all the radio buttons.
     */
    public void doLayout() {
        int w = getWidth(), h = getHeight();
        float spanInMillis = dates[0].getTime() - dates[dates.length-1].getTime();
        float pixelsPerMilli = (w-2*PAD) / spanInMillis;
        // first arrange them all in a line ignoring overlaps
        for(int i=0; i<butts.length; i++) {
            float offset = (dates[0].getTime() - dates[i].getTime()) * pixelsPerMilli;
            butts[i].setLocation((int)(w-PAD-offset-BW/2), h-2*PAD);
        }
        // next, make a pass looking for overlaps and adjust vertically when that fixes them, otherwise too bad.
        int availableVerticalSpace = butts[0].getY() + butts[0].getHeight();
        int minx[] = new int[availableVerticalSpace/BW]; // leftmost edges for each row.
        Arrays.fill(minx, Integer.MAX_VALUE);
        for (JRadioButton cur : butts) { // place each button
            int rightEdge = cur.getX() + cur.getWidth();
            for (int row = 0; row < minx.length; row++) { // counting up from base row
                if (rightEdge < minx[row]) {
                    minx[row] = cur.getX(); // set the new minimum for this row
                    cur.setLocation(cur.getX(), cur.getY() - row * BW);
                    break; // go to positioning next button
                }
            }
        }
    }

    public void paintComponent(Graphics g) {
        Font origFont = g.getFont();
        int w = getWidth(), h = getHeight();
        g.setColor(getBackground());
        g.fillRect(0, 0, w, h);
        float spanInMillis = dates[0].getTime() - dates[dates.length-1].getTime();
        float pixelsPerMilli = (w-2*PAD) / spanInMillis;
        GregorianCalendar monthStart = round(dates[0], Calendar.MONTH);
        monthStart.add(Calendar.MONDAY, 1);
        int xend = w;
        Color bg = getBackground();
        Color fg = Color.black;
        Color other = StaticUtils.slightlyDifferentColor(bg);
        g.setFont(NONTH_FONT);
        boolean even = true;
        while(xend > 0) {
            g.setColor(other);
            even = !even;
            float offset = (dates[0].getTime() - monthStart.getTimeInMillis()) * pixelsPerMilli;
            int xst = (int)(w-PAD-offset);
            if(even)
                g.fillRect(xst,0,xend-xst,h);
            g.setColor(fg);
            g.drawString(MONSTRS[monthStart.get(Calendar.MONTH)], xst+3, h-3);
            xend = xst;
            monthStart.add(Calendar.MONDAY, -1);
        }
        g.setColor(fg);
        // vertical line showing "now"
        //int now = w-PAD-(int)((dates[0].getTime() - System.currentTimeMillis()) * pixelsPerMilli);
        //g.drawLine(now, h-PAD, now, 20);
        //g.drawString("now", now-width("now",g)/2, g.getFontMetrics().getAscent());
        // horizontal time axis
        g.drawLine(0, h-PAD, w, h-PAD);
        // for short time spans, draw tick marks for days and possibly hours.
        float pixelsPerMinute = pixelsPerMilli * 1000 * 60;
        float pixelsPerHour = pixelsPerMinute * 60;
        float pixelsPerDay = pixelsPerHour * 24;
        if(pixelsPerDay > 0) {
            GregorianCalendar midnight = round(dates[dates.length-1], Calendar.DAY_OF_MONTH);
            GregorianCalendar end = new GregorianCalendar();
            end.setTime(dates[0]);
            while(midnight.before(end)) {
                int daytic = PAD+(int)((midnight.getTimeInMillis() - dates[dates.length-1].getTime()) * pixelsPerMilli);
                g.fillRect(daytic, h-PAD-DAYTIC, 2, DAYTIC);
                if(pixelsPerDay > 18) { // enough room so day-of-month numbers don't overlap?
                    String daystr = ""+midnight.get(Calendar.DAY_OF_MONTH);
                    if(pixelsPerDay > 35) // enough room for month number too?
                        daystr = (midnight.get(Calendar.MONTH)+1) + "/" + daystr;
                    g.setFont(DAY_FONT);
                    g.drawString(daystr, daytic-width(daystr,g)/2, h-PAD+12);
                    g.setFont(HOUR_FONT);
                    if(pixelsPerHour > 5) { // enough room for hour data too?
                        for(int hour=1; hour<24; hour++) {
                            int hx = (int)(daytic+hour*pixelsPerHour+.5);
                            if(pixelsPerHour > 18) { // enough room so hour-of-day numbers don't overlap?
                                String hourstr = ""+hour;
                                g.drawString(hourstr, hx-width(hourstr,g)/2, h-PAD-2);
                                if(pixelsPerMinute > 5) { // enough room for minute data too?
                                    for(int minute=1; minute<60; minute++) {
                                        int mx = (int)(hx+minute*pixelsPerMinute+.5);
                                        String minutestr = ""+minute;
                                        if(pixelsPerMinute > 20) { // enough room so minute numbers don't overlap?
                                            if(pixelsPerMinute > 35)
                                                minutestr = hourstr + ':' + minutestr; // yes, and room for hour too
                                            g.drawString(minutestr, mx-width(minutestr,g)/2, h-PAD-2);
                                        }
                                        else // no, only enough room for a minute tic mark
                                            g.drawLine(mx, h-PAD, mx, h-PAD-DAYTIC/3);
                                    }
                                }
                            }
                            else // no, only enough room for an hour tic mark
                                g.drawLine(hx, h-PAD, hx, h-PAD-DAYTIC/2);
                        }
                    }
                }
                midnight.add(Calendar.DAY_OF_MONTH, 1);
            }
            // paint labels in the lower corners with the dates of the first and last dates in the range
            g.setFont(HOUR_FONT); // because it's a good small font
            DateFormat df = DateFormat.getDateInstance();
            StaticUtils.fillString(df.format(dates[dates.length-1]), 3, h-3, bg, g);
            String date0 = df.format(dates[0]);
            Rectangle2D strrect = g.getFontMetrics().getStringBounds(date0, null);
            StaticUtils.fillString(date0, w-(int)strrect.getWidth()-3, h-3, bg, g);

            g.setFont(origFont);
        }
    }

    private static int width(String str, Graphics g) {
        return (int)(g.getFontMetrics().getStringBounds(str, null).getWidth()+.5);
    }

    /**
     * Rounds a given date to a specified unit.
     * @param date date to round
     * @param unit one of Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH, Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND.
     * @return date rounded to the nearest specified unit.
     */
    public static GregorianCalendar round(Date date, int unit) {
        GregorianCalendar cal = new GregorianCalendar();
        cal.setTime(date);
        switch(unit) {
            case Calendar.YEAR: return
                new GregorianCalendar(cal.get(Calendar.YEAR), 0, 1);
            case Calendar.MONTH: return
                new GregorianCalendar(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), 1);
            case Calendar.DAY_OF_MONTH: return
                new GregorianCalendar(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH));
            case Calendar.HOUR_OF_DAY: return
                new GregorianCalendar(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY), 0);
            case Calendar.MINUTE: return
                new GregorianCalendar(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
            case Calendar.SECOND: return
                new GregorianCalendar(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), 0);
        }
        return null;
    }

    /**
     * A simple example test program.
     */
    public static void main(String[] args) {
        JFrame frame = new StaticUtils.QuickFrame("TimePicker Test");
        frame.setSize(600, 154);
        long now = System.currentTimeMillis();
        long daylen = 1000 * 60 * 60 * 24;
        frame.add(new TimePicker(
            //new Date[] {new Date(now), new Date(now-daylen/4), new Date(now-daylen*2)},
            new Date[] {
                new Date(now), new Date(now-daylen/4), new Date(now-daylen/3), new Date(now-daylen/2), new Date(now-daylen*2),
                new Date(now-daylen*8), new Date(now-daylen*8-daylen/2), new Date(now-daylen*8-daylen),
                new Date(now-daylen*20)
            },
            0,
            new TimePicker.SelectionListener() {
                public void selectionChanged(int selection) {
                    System.out.println("Selected date " + selection);
                }
            }
        ));
        frame.setVisible(true);
    }
}