/* ***************************************************************************
 *  
 *       File:  CalendarWidget.java
 *    Package:  ca.janeg.calendar
 * 
 *   Contains:      ButtonActionListener
 *                  CalendarModel
 *                  CalendarSelectionListener
 *                  InputListener
 * 
 * References:  'Java Rules' by Douglas Dunn
 *              Addison-Wesley, 2002 (Chapter 5, section 13 - 19)
 * 
 *              'Professional Java Custom UI Components'
 *              by Kenneth F. Krutsch, David S. Cargo, Virginia Howlett
 *              WROX Press, 2001 (Chapter 1-3)
 * 
 * Date         Author          Changes
 * ------------ -------------   ----------------------------------------------
 * Oct 24, 2002 Jane Griscti    Created
 * Oct 27, 2002 jg              Cleaned up calendar display
 * Oct 30, 2002 jg              added ctor CalendarComboBox( Calendar )
 * Oct 31, 2002 jg              Added listeners and Popup
 * Nov  1, 2002 jg              Cleaned up InputListener code to only accept
 *                              valid dates
 * Nov  2, 2002 jg              modified getPopup() to handle display when
 *                              component is positioned at the bottom of the screen
 * Nov  3, 2002 jg              changed some instance variables to class variables
 * Mar 29, 2003 jg              added setDate() contributed by James Waldrop
 * *************************************************************************** */
package ca.janeg.calendar;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFormattedTextField;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.SwingConstants;
import javax.swing.border.LineBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.basic.BasicArrowButton;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;

/**
 *  A custom component that mimics a combo box, displaying
 *  a perpetual calendar rather than a 'list'.
 * 
 *  @author     Jane Griscti    jane@janeg.ca
 *  @version    1.0             Oct 24, 2002 
 */
public class CalendarComboBox extends JPanel {

    // -- class fields
    private static final DateFormatSymbols  dfs         = new DateFormatSymbols();      
    private static final String[]           months      = dfs.getMonths();    
    private static final String[]           dayNames    = new String[ 7 ];  
    private static final Toolkit            toolkit     = Toolkit.getDefaultToolkit();
    private static final Dimension          screenSize  = toolkit.getScreenSize();
    private static final PopupFactory       factory     = 
                                                PopupFactory.getSharedInstance();   
    
    // -- instance fields used with 'combo-box' panel           
    private final JPanel                inputPanel  = new JPanel(); 
       
    private final JFormattedTextField   input      
                            = new JFormattedTextField( new Date() );
    private final BasicArrowButton      comboBtn
                            = new BasicArrowButton( SwingConstants.SOUTH ); 
    
    // -- instance fields used with calendar panel                      
    private final JPanel                calPanel    = new JPanel();                         
    private final JTextField            calLabel    = new JTextField( 11 );
    private final Calendar              current     = new GregorianCalendar();  
    private final CalendarModel         display     = new CalendarModel( 6, 6 );    
    private final JTable                table       = new JTable( display );    
                             
    private final BasicArrowButton      nextBtn = 
                        new BasicArrowButton( SwingConstants.EAST );
    private final BasicArrowButton      prevBtn =
                        new BasicArrowButton( SwingConstants.WEST );  
    private final BasicArrowButton      closeCalendarBtn = 
                        new BasicArrowButton( SwingConstants.NORTH );                                                                       
    private Popup popup;    

    /**
     *  Create a new calendar combo-box object set with today's date.
     */
    public CalendarComboBox(){    
        this( new GregorianCalendar() );
    }
    
    /**
     *  Create a new calendar combo-box object set with the given date.
     *  
     *  @param cal  a calendar object
     *  @see java.util.GregorianCalendar
     */
    public CalendarComboBox( final Calendar cal ){
        super();                
         
        // set the calendar and input box date
        Date date = cal.getTime();       
        current.setTime( date );
        input.setValue( date );
         
        // create the GUI elements and assign listeners             
        buildInputPanel();
        buildCalendarDisplay();
        registerListeners();                  
        
        // intially, only display the input panel        
        add( inputPanel );                                                             
    }

    /*
     *  Creates a field and 'combo box' button above the calendar 
     *  to allow user input. 
     */
    private void buildInputPanel(){
        inputPanel.setLayout( new BoxLayout( inputPanel, BoxLayout.X_AXIS ) );
          
        input.setColumns( 12 );
        inputPanel.add( input );
        
        comboBtn.setActionCommand( "combo" );
        inputPanel.add( comboBtn );  
    }   
    
    /*
     *  Builds the calendar panel to be displayed in the popup
     */
    private void buildCalendarDisplay(){
        
        //  Allow for individual cell selection and turn off
        //  grid lines.     
        table.setCellSelectionEnabled(true);
        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);    
        table.setShowGrid( false );

        //  Calendar (table) column headers
        //    Set column headers to weekday names as given by
        //    the default Locale. 
        // 
        //    Need to re-map the retreived names. If used as is,
        //    the table model ends up with an extra empty column as
        //    the returned names begin at index 1, not zero.
        String[] names = dfs.getShortWeekdays();
        
        for( int i=1; i<names.length; i++ ){
            dayNames[ i - 1 ] = "" + names[ i ].charAt( 0 );
        }
        
        display.setColumnIdentifiers( dayNames );
        table.setModel( display );
        
        //  Set the column widths. Need to turn 
        //  auto resizing off to make this work.
        table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);         
        int count = table.getColumnCount();     

        for( int i = 0; i < count; i ++ ){
            TableColumn col = table.getColumnModel().getColumn( i );
            col.setPreferredWidth( 20 );                
        }
                        
        //  Column headers are only displayed automatically
        //  if the table is put in a JScrollPane. Don't want
        //  to use one here, so need to add the headers
        //  manually.
        JTableHeader header = table.getTableHeader();
        header.setFont( header.getFont().deriveFont( Font.BOLD ) );         
        
        JPanel panel = new JPanel();
        panel.setLayout( new BoxLayout( panel, BoxLayout.Y_AXIS ) );
        panel.add( header );
        panel.add( table );  
        
        calPanel.setBorder( new LineBorder( Color.BLACK ) );
        calPanel.setLayout( new BorderLayout() );
        calPanel.add( buildCalendarNavigationPanel(), BorderLayout.NORTH );
        calPanel.add( panel );
    }

    /* 
     * Creates a small panel above the month table to display the month and
     * year along with the 'prevBtn', 'nextBtn' month selection buttons
     * and a 'closeCalendarBtn'.
     */
    private JPanel buildCalendarNavigationPanel(){
        JPanel panel = new JPanel();
        panel.setLayout( new BoxLayout( panel, BoxLayout.X_AXIS ) );
        
        //  Add a text display of the selected month and year.
        //  A JTextField is used for the label instead of a JLabel
        //  as it is easier to ensure a consistent size; JLabel
        //  expands and contracts with the text size
        calLabel.setEditable( false );
        int fontSize = calLabel.getFont().getSize();
        calLabel.setFont( calLabel.getFont().deriveFont( Font.PLAIN, fontSize - 2 ) );
        panel.add( calLabel );
                                
        // set button commands and add to panel
        prevBtn.setActionCommand( "prevBtn" );
        nextBtn.setActionCommand( "nextBtn" );
        closeCalendarBtn.setActionCommand( "close" );          
                        
        panel.add( prevBtn );
        panel.add( nextBtn );
        panel.add( closeCalendarBtn );                                                                                        
        
        return panel;
    }

    /*
     *  Register all required listeners with appropriate
     *  components
     */
    private void registerListeners(){
        
        ButtonActionListener btnListener = new ButtonActionListener();
                
        // 'Combo-box' listeners
        input.addKeyListener( new InputListener() );
        comboBtn.addActionListener( btnListener );  
        
        //  Calendar (table) selection listener
        //    Must be added to both the table selection model
        //    and the column selection model; otherwise, new
        //    column selections on the same row are not recognized
        CalendarSelectionListener listener = new CalendarSelectionListener();
        table.getSelectionModel().addListSelectionListener( listener );
        table.getColumnModel().getSelectionModel()
                                    .addListSelectionListener( listener );
        
        // Calendar navigation listeners
        prevBtn.addActionListener( btnListener );
        nextBtn.addActionListener( btnListener );
        closeCalendarBtn.addActionListener( btnListener );
            
    }
    
    /*
     *  Fill the table model with the days in the selected month.
     *  Rows in the table correspond to 'weeks', columns to 'days'.
     * 
     *  Strategy:
     *      1. get the first calendar day in the new month
     *      2. find it's position in the first week of the month to
     *         determine the starting column for the day numbers
     *      3. find the actual number of days in the month     
     *      4. fill the calendar with the day values, erasing any days 
     *         left over from the old month
     */
    private void updateTable( Calendar cal ){       
        
        Calendar dayOne = new GregorianCalendar( 
                cal.get( Calendar.YEAR ),
                cal.get( Calendar.MONTH ),
                1 );

        // compute the number of  days in the month and 
        // the start column for the first day in the first week             
        int actualDays = cal.getActualMaximum( Calendar.DATE );
        int startIndex = dayOne.get( Calendar.DAY_OF_WEEK ) - 1;

        // fill the calendar for the new month
        int day = 1;
        for( int row = 0; row < 6 ; row++ ){
            for( int col = 0; col < 7; col++ ){
                if( ( col < startIndex && row == 0 ) || day > actualDays ){
                    // overwrite any left over values from old month
                    display.setValueAt( "", row, col );
                }else{
                    display.setValueAt( new Integer( day ), row, col );
                    day++;
                }                
            }
        }
        
        // set the month, year label        
        calLabel.setText( months[ cal.get( Calendar.MONTH ) ] +                       
                          ", " + cal.get( Calendar.YEAR ) );
                          
        // set the calendar selection
        table.changeSelection( cal.get( Calendar.WEEK_OF_MONTH ) - 1,
                               cal.get( Calendar.DAY_OF_WEEK ) - 1,
                               false, false );                                                    
    }

    /*
     *  Gets a Popup to hold the calendar display and determines
     *  it's position on the screen.
     */
    private Popup getPopup(){       
        Point p = input.getLocationOnScreen();
        Dimension inputSize = input.getPreferredSize();
        Dimension calendarSize = calPanel.getPreferredSize();
                
        if( ( p.y + calendarSize.height ) < screenSize.height) { 
            // will fit below input panel      
            popup = factory.getPopup( input, calPanel, 
                                       p.x, p.y + (int)inputSize.height );
        } else {
            // need to fit it above input panel
            popup = factory.getPopup( input, calPanel,
                                      p.x, p.y - (int)calendarSize.height );
        }                                          
        return popup;       
    }
 
    /*
     *  Returns the currently selected date as a <code>Calendar</code> object.
     * 
     *  @return Calendar    the currently selected calendar date
     */
    public Calendar getDate(){
        return current;
    }
    
    /**
     * Sets the current date and updates the UI to reflect the new date.
     * @param newDate the new date as a <code>Date</code> object.
     * @see Date
     * @author James Waldrop
     */
    public void setDate(Date newDate) {
        current.setTime(newDate);
        input.setValue(current.getTime());
    }   
    
    /*
     *  Creates a custom model to back the table.
     */
    private class CalendarModel extends DefaultTableModel {
        
        public CalendarModel( int row, int col ){
            super( row, col );
        }
        
        /**
         *  Overrides the method to return an Integer class
         *  type for all columns. The numbers are automatically
         *  right-aligned by a default renderer that's supplied
         *  as part of JTable.
         */
        public Class getColumnClass( int column ){
            return Integer.class;           
        }
        
        /**
         *  Overrides the method to disable cell editing.
         *  The default is editable.
         */
        public boolean isCellEditable( int row, int col ){
            return false;
        }
    }

    /*
     *  Captures the 'prevBtn', 'nextBtn', 'comboBtn' and
     *  'closeCalendarBtn' actions.
     * 
     *  The combo button is disabled when the popup is shown
     *  and enabled when the popup is hidden. Failure to do
     *  so results in the popup screen area not being cleared
     *  correctly if the user clicks the button while the popup
     *  is being displayed.
     */
    private class ButtonActionListener implements ActionListener {
        public void actionPerformed( ActionEvent e ){
            String cmd = e.getActionCommand();
                        
            if( cmd.equals( "prevBtn" ) ){
                current.add( Calendar.MONTH, -1 );
                input.setValue( current.getTime() );
            }else if( cmd.equals( "nextBtn" ) ){
                current.add( Calendar.MONTH, 1 );
                input.setValue( current.getTime() );
            }else if( cmd.equals( "close" ) ){
                popup.hide();
                comboBtn.setEnabled( true );                
            }else{
                comboBtn.setEnabled( false );                               
                popup = getPopup();
                popup.show();
            }                               
                                    
            updateTable( current );
        }
    }

    /*
     *  Captures a user selection in the calendar display and
     *  changes the value in the 'combo box' to match the selected date.
     * 
     */
    private class CalendarSelectionListener implements ListSelectionListener {
        
        public void valueChanged(ListSelectionEvent e){
            if ( !e.getValueIsAdjusting() ) {
                int row = table.getSelectedRow();
                int col = table.getSelectedColumn();
                
                Object value = null;
                try{
                    value = display.getValueAt(row, col);
                }catch( ArrayIndexOutOfBoundsException ex ){
                    // ignore, happens when the calendar is
                    // displayed for the first time
                }
                
                if( value instanceof Integer ){
                    int day = ( (Integer)value ).intValue();
                    current.set( Calendar.DATE, day );
                    input.setValue( current.getTime() );                                    
                }           
            }                                    
        }
    }

    /*
     *  Captures user input in the 'combo box'
     *  If the input is a valid date and the user pressed
     *  ENTER or TAB, the calendar selection is updated
     */
    private class InputListener extends KeyAdapter {
        public void keyTyped(KeyEvent e) {

            DateFormat df = DateFormat.getDateInstance();
            Date date = null;
                
            try{
                date = df.parse( input.getText() );
            }catch( ParseException ex ){    
                // ignore invalid dates
            }
            
            // change the calendar selection if the date is valid
            // and the user hit ENTER or TAB
            char c = e.getKeyChar();
            if(  date != null &&
                ( c == KeyEvent.VK_ENTER || c == KeyEvent.VK_TAB ) ) {
                current.setTime( date );                
                updateTable( current );
            }                                   
        }       
    }
}