/*
 * $Id: BasicMonthViewUI.java 3927 2011-02-22 16:34:11Z kleopatra $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.plaf.basic;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.SortedSet;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.CellRendererPane;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.LookAndFeel;
import javax.swing.UIManager;
import javax.swing.plaf.ComponentUI;

import org.jdesktop.swingx.JXMonthView;
import org.jdesktop.swingx.SwingXUtilities;
import org.jdesktop.swingx.action.AbstractActionExt;
import org.jdesktop.swingx.calendar.CalendarUtils;
import org.jdesktop.swingx.calendar.DateSelectionModel;
import org.jdesktop.swingx.calendar.DateSelectionModel.SelectionMode;
import org.jdesktop.swingx.event.DateSelectionEvent;
import org.jdesktop.swingx.event.DateSelectionListener;
import org.jdesktop.swingx.plaf.MonthViewUI;
import org.jdesktop.swingx.plaf.UIManagerExt;

/**
 * Base implementation of the <code>JXMonthView</code> UI.<p>
 *
 * <b>Note</b>: The api changed considerably between releases 0.9.4 and 0.9.5.  
 * <p>
 * 
 * The general drift of the change was to delegate all text rendering to a dedicated
 * rendering controller (currently named RenderingHandler), similar to 
 * the collection view rendering. The UI itself keeps layout and positioning of
 * the rendering components. Plus updating on property changes received from the 
 * monthView. <p>
 * 
 * 
 * <p>   
 * Painting: coordinate systems.
 * 
 * <ul>
 * <li> Screen coordinates of months/days, accessible via the getXXBounds() methods. These
 * coordinates are absolute in the system of the monthView. 
 * <li> The grid of visible months with logical row/column coordinates. The logical 
 * coordinates are adjusted to ComponentOrientation. 
 * <li> The grid of days in a month with logical row/column coordinates. The logical 
 * coordinates are adjusted to ComponentOrientation. The columns 
 * are the (optional) week header and the days of the week. The rows are the day header  
 * and the weeks in a month. The day header shows  the localized names of the days and 
 * has the row coordinate DAY_HEADER_ROW. It is shown always.
 * The row header shows the week number in the year and has the column coordinate WEEK_HEADER_COLUMN. It
 * is shown only if the showingWeekNumber property is true.  
 * </ul>
 * 
 * On the road to "zoomable" date range views (Vista-style).<p>
 * 
 * Added support (doesn't do anything yet, zoom-logic must yet be defined) 
 * by way of an active calendar header which is added to the monthView if zoomable. 
 * It is disabled by default. In this mode, the view is always
 * traversable and shows exactly one calendar. It is orthogonal to the classic 
 * mode, that is client code should not be effected in any way as long as the mode 
 * is not explicitly enabled. <p>
 * 
 * NOTE to LAF implementors: the active calendar header is very, very, very raw and 
 * sure to change without much notice. Better not yet to support it right now.
 * 
 * @author dmouse
 * @author rbair
 * @author rah003
 * @author Jeanette Winzenburg
 */
public class BasicMonthViewUI extends MonthViewUI {
    @SuppressWarnings("all")
    private static final Logger LOG = Logger.getLogger(BasicMonthViewUI.class
            .getName());
    
    private static final int CALENDAR_SPACING = 10;
    
    /** Return value used to identify when the month down button is pressed. */
    public static final int MONTH_DOWN = 1;
    /** Return value used to identify when the month up button is pressed. */
    public static final int MONTH_UP = 2;

    // constants for day columns
    protected static final int WEEK_HEADER_COLUMN = 0;
    protected static final int DAYS_IN_WEEK = 7;
    protected static final int FIRST_DAY_COLUMN = WEEK_HEADER_COLUMN + 1;
    protected static final int LAST_DAY_COLUMN = FIRST_DAY_COLUMN + DAYS_IN_WEEK -1;

    // constants for day rows (aka: weeks)
    protected static final int DAY_HEADER_ROW = 0;
    protected static final int WEEKS_IN_MONTH = 6;
    protected static final int FIRST_WEEK_ROW = DAY_HEADER_ROW + 1;
    protected static final int LAST_WEEK_ROW = FIRST_WEEK_ROW + WEEKS_IN_MONTH - 1;


    /** the component we are installed for. */
    protected JXMonthView monthView;
    // listeners
    private PropertyChangeListener propertyChangeListener;
    private MouseListener mouseListener;
    private MouseMotionListener mouseMotionListener;
    private Handler handler;

    // fields related to visible date range
    /** end of day of the last visible month. */
    private Date lastDisplayedDate;
    /** 
    
    //---------- fields related to selection/navigation


    /** flag indicating keyboard navigation. */
    private boolean usingKeyboard = false;
    /** For interval selections we need to record the date we pivot around. */
    private Date pivotDate = null;
    /**
     * Date span used by the keyboard actions to track the original selection.
     */
    private SortedSet<Date> originalDateSpan;

    //------------------ visuals

    protected boolean isLeftToRight;
    protected Icon monthUpImage;
    protected Icon monthDownImage;
    

    /**
     * The padding for month traversal icons.
     * PENDING JW: decouple rendering and hit-detection.
     */
    private int arrowPaddingX = 3;
    private int arrowPaddingY = 3;
    
    
    /** height of month header including the monthView's box padding. */
    private int fullMonthBoxHeight;
    /** 
     * width of a "day" box including the monthView's box padding
     * this is the same for days-of-the-week, weeks-of-the-year and days
     */
    private int fullBoxWidth;
    /** 
     * height of a "day" box including the monthView's box padding
     * this is the same for days-of-the-week, weeks-of-the-year and days
     */
    private int fullBoxHeight;
    /** the width of a single month display. */
    private int calendarWidth;
    /** the height of a single month display. */
    private int calendarHeight;
    /** the height of a single month grid cell, including padding. */
    private int fullCalendarHeight;
    /** the width of a single month grid cell, including padding. */
    private int fullCalendarWidth;
    /** The number of calendars displayed vertically. */
    private int calendarRowCount = 1;
    /** The number of calendars displayed horizontally. */
    private int calendarColumnCount = 1;
    
    /**
     * The bounding box of the grid of visible months. 
     */
    protected Rectangle calendarGrid = new Rectangle();
    
    /**
     * The Strings used for the day headers. This is the fall-back for
     * the monthView if no custom strings are set. 
     * PENDING JW: delegate to RenderingHandler?
     */
    private String[] daysOfTheWeek;

    /**
     * Provider of configured components for text rendering.
     */
    private CalendarRenderingHandler renderingHandler;
    /**
     * The CellRendererPane for stamping rendering comps.
     */
    private CellRendererPane rendererPane;

    /**
     * The CalendarHeaderHandler which provides the header component if zoomable.
     */
    private CalendarHeaderHandler calendarHeaderHandler;
    

    @SuppressWarnings({"UnusedDeclaration"})
    public static ComponentUI createUI(JComponent c) {
        return new BasicMonthViewUI();
    }

    /**
     * Installs the component as appropriate for the current lf.
     * 
     * PENDING JW: clarify sequence of installXX methods. 
     */
    @Override
    public void installUI(JComponent c) {
        monthView = (JXMonthView)c;
        monthView.setLayout(createLayoutManager());
        
        // PENDING JW: move to installDefaults or installComponents?
        installRenderingHandler();
        
        installDefaults();
        installDelegate();
        installKeyboardActions();
        installComponents();
        updateLocale(false);
        updateZoomable();
        installListeners();
    }


    @Override
    public void uninstallUI(JComponent c) {
        uninstallRenderingHandler();
        uninstallListeners();
        uninstallKeyboardActions();
        uninstallDefaults();
        uninstallComponents();
        monthView.setLayout(null);
        monthView = null;
    }

    /**
     * Creates and installs the calendar header handler. 
     */
    protected void installComponents() {
        setCalendarHeaderHandler(createCalendarHeaderHandler());
        getCalendarHeaderHandler().install(monthView);
    }

    /**
     * Uninstalls the calendar header handler.
     */
    protected void uninstallComponents() {
        getCalendarHeaderHandler().uninstall(monthView);
        setCalendarHeaderHandler(null);
    }

    /**
     * Installs default values. <p>
     * 
     * This is refactored to only install default properties on the monthView.
     * Extracted install of this delegate's properties into installDelegate. 
     *  
     */
    protected void installDefaults() {
        LookAndFeel.installProperty(monthView, "opaque", Boolean.TRUE);
        
       // @KEEP JW: do not use the core install methods (might have classloader probs)
        // instead access all properties via the UIManagerExt ..
        //        BasicLookAndFeel.installColorsAndFont(monthView, 
//                "JXMonthView.background", "JXMonthView.foreground", "JXMonthView.font");
        
        if (SwingXUtilities.isUIInstallable(monthView.getBackground())) {
            monthView.setBackground(UIManagerExt.getColor("JXMonthView.background"));
        }
        if (SwingXUtilities.isUIInstallable(monthView.getForeground())) {
            monthView.setForeground(UIManagerExt.getColor("JXMonthView.foreground"));
        }
        if (SwingXUtilities.isUIInstallable(monthView.getFont())) {
            // PENDING JW: missing in managerExt? Or not applicable anyway?
            monthView.setFont(UIManager.getFont("JXMonthView.font"));
        }
        if (SwingXUtilities.isUIInstallable(monthView.getMonthStringBackground())) {
            monthView.setMonthStringBackground(UIManagerExt.getColor("JXMonthView.monthStringBackground"));
        }
        if (SwingXUtilities.isUIInstallable(monthView.getMonthStringForeground())) {
            monthView.setMonthStringForeground(UIManagerExt.getColor("JXMonthView.monthStringForeground"));
        }
        if (SwingXUtilities.isUIInstallable(monthView.getDaysOfTheWeekForeground())) {
            monthView.setDaysOfTheWeekForeground(UIManagerExt.getColor("JXMonthView.daysOfTheWeekForeground"));
        }
        if (SwingXUtilities.isUIInstallable(monthView.getSelectionBackground())) {
            monthView.setSelectionBackground(UIManagerExt.getColor("JXMonthView.selectedBackground"));
        }
        if (SwingXUtilities.isUIInstallable(monthView.getSelectionForeground())) {
            monthView.setSelectionForeground(UIManagerExt.getColor("JXMonthView.selectedForeground"));
        }
        if (SwingXUtilities.isUIInstallable(monthView.getFlaggedDayForeground())) {
            monthView.setFlaggedDayForeground(UIManagerExt.getColor("JXMonthView.flaggedDayForeground"));
        }
        
        monthView.setBoxPaddingX(UIManagerExt.getInt("JXMonthView.boxPaddingX"));
        monthView.setBoxPaddingY(UIManagerExt.getInt("JXMonthView.boxPaddingY"));
    }

    /**
     * Installs this ui delegate's properties.
     */
    protected void installDelegate() {
        isLeftToRight = monthView.getComponentOrientation().isLeftToRight();
        // PENDING JW: remove here if rendererHandler takes over control completely
        // as is, some properties are duplicated
        monthDownImage = UIManager.getIcon("JXMonthView.monthDownFileName");
        monthUpImage = UIManager.getIcon("JXMonthView.monthUpFileName");
        // install date related state
        setFirstDisplayedDay(monthView.getFirstDisplayedDay());
    }

    
    protected void uninstallDefaults() {}

    protected void installKeyboardActions() {
        // Setup the keyboard handler.
        // JW: changed (0.9.6) to when-ancestor just to be on the safe side
        // if the title contain active comps
        installKeyBindings(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        // JW: removed the automatic keybindings in WHEN_IN_FOCUSED
        // which caused #555-swingx (binding active if not focused)
        ActionMap actionMap = monthView.getActionMap();
        KeyboardAction acceptAction = new KeyboardAction(KeyboardAction.ACCEPT_SELECTION);
        actionMap.put("acceptSelection", acceptAction);
        KeyboardAction cancelAction = new KeyboardAction(KeyboardAction.CANCEL_SELECTION);
        actionMap.put("cancelSelection", cancelAction);

        actionMap.put("selectPreviousDay", new KeyboardAction(KeyboardAction.SELECT_PREVIOUS_DAY));
        actionMap.put("selectNextDay", new KeyboardAction(KeyboardAction.SELECT_NEXT_DAY));
        actionMap.put("selectDayInPreviousWeek", new KeyboardAction(KeyboardAction.SELECT_DAY_PREVIOUS_WEEK));
        actionMap.put("selectDayInNextWeek", new KeyboardAction(KeyboardAction.SELECT_DAY_NEXT_WEEK));

        actionMap.put("adjustSelectionPreviousDay", new KeyboardAction(KeyboardAction.ADJUST_SELECTION_PREVIOUS_DAY));
        actionMap.put("adjustSelectionNextDay", new KeyboardAction(KeyboardAction.ADJUST_SELECTION_NEXT_DAY));
        actionMap.put("adjustSelectionPreviousWeek", new KeyboardAction(KeyboardAction.ADJUST_SELECTION_PREVIOUS_WEEK));
        actionMap.put("adjustSelectionNextWeek", new KeyboardAction(KeyboardAction.ADJUST_SELECTION_NEXT_WEEK));

        
        actionMap.put(JXMonthView.COMMIT_KEY, acceptAction);
        actionMap.put(JXMonthView.CANCEL_KEY, cancelAction);
        
        // PENDING JW: complete (year-, decade-, ?? ) and consolidate with KeyboardAction
        // additional navigation actions
        AbstractActionExt prev = new AbstractActionExt() {

            @Override
            public void actionPerformed(ActionEvent e) {
                previousMonth();
            }
            
        };
        monthView.getActionMap().put("scrollToPreviousMonth", prev);
        AbstractActionExt next = new AbstractActionExt() {

            @Override
            public void actionPerformed(ActionEvent e) {
                nextMonth();
            }
            
        };
        monthView.getActionMap().put("scrollToNextMonth", next);
        
    }

    
    /**
     * @param inputMap
     */
    private void installKeyBindings(int type) {
        InputMap inputMap = monthView.getInputMap(type);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "acceptSelection");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "cancelSelection");

        // @KEEP quickly check #606-swingx: keybindings not working in internalframe
        // eaten somewhere
//        inputMap.put(KeyStroke.getKeyStroke("F1"), "selectPreviousDay");

        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "selectPreviousDay");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "selectNextDay");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "selectDayInPreviousWeek");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "selectDayInNextWeek");

        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_MASK, false), "adjustSelectionPreviousDay");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_MASK, false), "adjustSelectionNextDay");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_MASK, false), "adjustSelectionPreviousWeek");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_MASK, false), "adjustSelectionNextWeek");
    }

    /**
     * @param inputMap
     */
    private void uninstallKeyBindings(int type) {
        InputMap inputMap = monthView.getInputMap(type);
        inputMap.clear();
    }

    protected void uninstallKeyboardActions() {}

    protected void installListeners() {
        propertyChangeListener = createPropertyChangeListener();
        mouseListener = createMouseListener();
        mouseMotionListener = createMouseMotionListener();
        
        monthView.addPropertyChangeListener(propertyChangeListener);
        monthView.addMouseListener(mouseListener);
        monthView.addMouseMotionListener(mouseMotionListener);

        monthView.getSelectionModel().addDateSelectionListener(getHandler());
    }

    protected void uninstallListeners() {
        monthView.getSelectionModel().removeDateSelectionListener(getHandler());
        monthView.removeMouseMotionListener(mouseMotionListener);
        monthView.removeMouseListener(mouseListener);
        monthView.removePropertyChangeListener(propertyChangeListener);

        mouseMotionListener = null;
        mouseListener = null;
        propertyChangeListener = null;
    }

    /**
     * Creates and installs the renderingHandler and infrastructure to use it.
     */
    protected void installRenderingHandler() {
        setRenderingHandler(createRenderingHandler());
        if (getRenderingHandler() != null) {
            rendererPane = new CellRendererPane();
            monthView.add(rendererPane);
        }
    }
    
    /**
     * Uninstalls the renderingHandler and infrastructure that used it.
     */
    protected void uninstallRenderingHandler() {
        if (getRenderingHandler() == null) return;
        monthView.remove(rendererPane);
        rendererPane = null;
        setRenderingHandler(null);
    }

    /**
     * Returns the <code>CalendarRenderingHandler</code> to use. Subclasses may override to 
     * plug-in custom implementations. <p>
     * 
     * This implementation returns an instance of RenderingHandler.
     * 
     * @return the endering handler to use for painting, must not be null
     */
    protected CalendarRenderingHandler createRenderingHandler() {
        return new RenderingHandler();
    }
    
    /**
     * @param renderingHandler the renderingHandler to set
     */
    protected void setRenderingHandler(CalendarRenderingHandler renderingHandler) {
        this.renderingHandler = renderingHandler;
    }

    /**
     * @return the renderingHandler
     */
    protected CalendarRenderingHandler getRenderingHandler() {
        return renderingHandler;
    }

    /**
     * 
     * Empty subclass for backward compatibility. The original implementation was 
     * extracted as standalone class and renamed to BasicCalendarRenderingHandler. <p>
     * 
     * This will be available for extension by LAF providers until all collaborators 
     * in the new rendering pipeline are ready for public exposure.
     */
    protected static class RenderingHandler extends BasicCalendarRenderingHandler {
        
    }
    /**
     * Binds/clears the keystrokes in the component input map, 
     * based on the monthView's componentInputMap enabled property.
     * 
     * @see org.jdesktop.swingx.JXMonthView#isComponentInputMapEnabled()
     */
    protected void updateComponentInputMap() {
        if (monthView.isComponentInputMapEnabled()) {
            installKeyBindings(JComponent.WHEN_IN_FOCUSED_WINDOW);
        } else {
            uninstallKeyBindings(JComponent.WHEN_IN_FOCUSED_WINDOW);
        }
    }



    /**
     * Updates internal state according to monthView's locale. Revalidates the
     * monthView if the boolean parameter is true.
     * 
     * @param revalidate a boolean indicating whether the monthView should be 
     * revalidated after the change.
     */
    protected void updateLocale(boolean revalidate) {
        Locale locale = monthView.getLocale();
        if (getRenderingHandler() != null) {
            getRenderingHandler().setLocale(locale);
        }

        // fixed JW: respect property in UIManager if available
        // PENDING JW: what to do if weekdays had been set
        // with JXMonthView method? how to detect?
        daysOfTheWeek = (String[]) UIManager.get("JXMonthView.daysOfTheWeek");

        if (daysOfTheWeek == null) {
            daysOfTheWeek = new String[7];
            String[] dateFormatSymbols = DateFormatSymbols.getInstance(locale)
                    .getShortWeekdays();
            daysOfTheWeek = new String[JXMonthView.DAYS_IN_WEEK];
            for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
                daysOfTheWeek[i - 1] = dateFormatSymbols[i];
            }
        }
        if (revalidate) {
            monthView.invalidate();
            monthView.validate();
        }
    }

   @Override
   public String[] getDaysOfTheWeek() {
       String[] days = new String[daysOfTheWeek.length];
       System.arraycopy(daysOfTheWeek, 0, days, 0, days.length);
       return days;
   }
   

//---------------------- listener creation    
    protected PropertyChangeListener createPropertyChangeListener() {
        return getHandler();
    }

    protected LayoutManager createLayoutManager() {
        return getHandler();
    }

    protected MouseListener createMouseListener() {
        return getHandler();
    }

    protected MouseMotionListener createMouseMotionListener() {
        return getHandler();
    }

    private Handler getHandler() {
        if (handler == null) {
            handler = new Handler();
        }

        return handler;
    }

    public boolean isUsingKeyboard() {
        return usingKeyboard;
    }

    public void setUsingKeyboard(boolean val) {
        usingKeyboard = val;
    }



    // ----------------------- mapping day coordinates

    /**
     * Returns the bounds of the day in the grid of days which contains the
     * given location. The bounds are in monthView screen coordinate system.
     * <p>
     * 
     * Note: this is a pure geometric mapping. The returned rectangle need not
     * necessarily map to a date in the month which contains the location, it
     * can represent a week-number/column header or a leading/trailing date.
     * 
     * @param x the x position of the location in pixel
     * @param y the y position of the location in pixel
     * @return the bounds of the day which contains the location, or null if
     *         outside
     */
    protected Rectangle getDayBoundsAtLocation(int x, int y) {
        Rectangle monthDetails = getMonthDetailsBoundsAtLocation(x, y);
        if ((monthDetails == null) || (!monthDetails.contains(x, y)))
            return null;
        // calculate row/column in absolute grid coordinates
        int row = (y - monthDetails.y) / fullBoxHeight;
        int column = (x - monthDetails.x) / fullBoxWidth;
        return new Rectangle(monthDetails.x + column * fullBoxWidth, monthDetails.y
                + row * fullBoxHeight, fullBoxWidth, fullBoxHeight);
    }

    /**
     * Returns the bounds of the day box at logical coordinates in the given month.
     * The row's range is from DAY_HEADER_ROW to LAST_WEEK_ROW. Column's range is from
     * WEEK_HEADER_COLUMN to LAST_DAY_COLUMN.
     * 
     * @param month the month containing the day box  
     * @param row the logical row (== week) coordinate in the day grid 
     * @param column the logical column (== day) coordinate in the day grid
     * @return the bounds of the daybox or null if not showing
     * @throws IllegalArgumentException if row or column are out off range.
     * 
     * @see #getDayGridPositionAtLocation(int, int)
     */
    protected Rectangle getDayBoundsInMonth(Date month, int row, final int column) {
        checkValidRow(row, column);
        if ((WEEK_HEADER_COLUMN == column) && !monthView.isShowingWeekNumber()) return null;
        Rectangle monthBounds = getMonthBounds(month);
        if (monthBounds == null) return null;
        // dayOfWeek header is shown always
        monthBounds.y += getMonthHeaderHeight() + (row - DAY_HEADER_ROW) * fullBoxHeight;
        // PENDING JW: still looks fishy ... 
        int absoluteColumn = column - FIRST_DAY_COLUMN;
        if (monthView.isShowingWeekNumber()) {
            absoluteColumn++;
        }
        if (isLeftToRight) {
           monthBounds.x += absoluteColumn * fullBoxWidth; 
        } else {
            int leading = monthBounds.x + monthBounds.width - fullBoxWidth; 
            monthBounds.x = leading - absoluteColumn * fullBoxWidth;
        }
        monthBounds.width = fullBoxWidth;
        monthBounds.height = fullBoxHeight;
        return monthBounds;
    }
    

    /**
     * Returns the logical coordinates of the day which contains the given
     * location. The p.x of the returned value represents the week header or the 
     * day of week, ranging from WEEK_HEADER_COLUMN to LAST_DAY_COLUMN. The
     * p.y represents the day header or week of the month, ranging from DAY_HEADER_ROW
     * to LAST_WEEK_ROW. The transformation takes care of
     * ComponentOrientation.
     * <p>
     * 
     * Note: The returned grid position need not
     * necessarily map to a date in the month which contains the location, it
     * can represent a week-number/column header or a leading/trailing date.
     * 
     * @param x the x position of the location in pixel
     * @param y the y position of the location in pixel
     * @return the logical coordinates of the day in the grid of days in a month
     *         or null if outside.
     *         
     * @see #getDayBoundsInMonth(Date, int, int)     
     */
    protected Point getDayGridPositionAtLocation(int x, int y) {
        Rectangle monthDetailsBounds = getMonthDetailsBoundsAtLocation(x, y);
        if ((monthDetailsBounds == null) ||(!monthDetailsBounds.contains(x, y))) return null;
        int calendarRow = (y - monthDetailsBounds.y) / fullBoxHeight + DAY_HEADER_ROW; 
        int absoluteColumn = (x - monthDetailsBounds.x) / fullBoxWidth;
        int calendarColumn = absoluteColumn + FIRST_DAY_COLUMN;
        if (!isLeftToRight) {
            int leading = monthDetailsBounds.x + monthDetailsBounds.width;
            calendarColumn = (leading - x) / fullBoxWidth + FIRST_DAY_COLUMN;
        }
        if (monthView.isShowingWeekNumber()) {
            calendarColumn -= 1;
        }
        return new Point(calendarColumn, calendarRow);
    }

    /**
     * Returns the Date defined by the logical 
     * grid coordinates relative to the given month. May be null if the
     * logical coordinates represent a header in the day grid or is outside of the
     * given month.
     * 
     * Mapping logical day grid coordinates to Date.<p>
     * 
     * PENDING JW: relax the startOfMonth pre? Why did I require it?
     * 
     * @param month a calendar representing the first day of the month, must not
     *   be null.
     * @param row the logical row index in the day grid of the month
     * @param column the logical column index in the day grid of the month
     * @return the day at the logical grid coordinates in the given month or null
     *    if the coordinates are day/week header or leading/trailing dates 
     * @throws IllegalStateException if the month is not the start of the month. 
     * 
     * @see #getDayGridPosition(Date)  
     */
    protected Date getDayInMonth(Date month, int row, int column) {
        if ((row == DAY_HEADER_ROW) || (column == WEEK_HEADER_COLUMN) || month == null) return null;
        Calendar calendar = getCalendar(month);
        int monthField = calendar.get(Calendar.MONTH);
        if (!CalendarUtils.isStartOfMonth(calendar))
            throw new IllegalStateException("calendar must be start of month but was: " + month.getTime());
        CalendarUtils.startOfWeek(calendar);
        // PENDING JW: correctly mapped now?
        calendar.add(Calendar.DAY_OF_MONTH, 
                (row - FIRST_WEEK_ROW) * DAYS_IN_WEEK + (column - FIRST_DAY_COLUMN));
        if (calendar.get(Calendar.MONTH) == monthField) {
            return calendar.getTime();
        } 
        return null;
        
    }
    
    /**
     * Returns the given date's position in the grid of the month it is contained in.
     * 
     * @param date the Date to get the logical position for, must not be null.
     * @return the logical coordinates of the day in the grid of days in a
     *   month or null if the Date is not visible. 
     *   
     *  @see #getDayInMonth(Date, int, int)  
     */
    protected Point getDayGridPosition(Date date) {
        if (!isVisible(date)) return null;
        Calendar calendar = getCalendar(date);
        Date startOfDay = CalendarUtils.startOfDay(calendar, date);
        // there must be a less ugly way?
        // columns
        CalendarUtils.startOfWeek(calendar);
        int column = FIRST_DAY_COLUMN;
        while (calendar.getTime().before(startOfDay)) {
            column++;
            calendar.add(Calendar.DAY_OF_MONTH, 1);
        }
        
        Date startOfWeek = CalendarUtils.startOfWeek(calendar, date);
        calendar.setTime(date);
        CalendarUtils.startOfMonth(calendar);
        int row = FIRST_WEEK_ROW;
        while (calendar.getTime().before(startOfWeek)) {
            row++;
            calendar.add(Calendar.WEEK_OF_YEAR, 1);
        }
        return new Point(column, row);
    }
    

    /**
     * Returns the Date at the given location. May be null if the
     * coordinates don't map to a day in the month which contains the 
     * coordinates. Specifically: hitting leading/trailing dates returns null.
     * 
     * Mapping pixel to calendar day.
     *
     * @param x the x position of the location in pixel
     * @param y the y position of the location in pixel
     * @return the day at the given location or null if the location
     *   doesn't map to a day in the month which contains the coordinates.
     *   
     * @see #getDayBounds(Date)  
     */ 
    @Override
    public Date getDayAtLocation(int x, int y) {
        Point dayInGrid = getDayGridPositionAtLocation(x, y);
        if ((dayInGrid == null) 
                || (dayInGrid.x == WEEK_HEADER_COLUMN) || (dayInGrid.y == DAY_HEADER_ROW)) return null;
        Date month = getMonthAtLocation(x, y);
        return getDayInMonth(month, dayInGrid.y, dayInGrid.x);
    }
    
    /**
     * Returns the bounds of the given day.
     * The bounds are in monthView coordinate system.<p>
     * 
     * PENDING JW: this most probably should be public as it is the logical
     * reverse of getDayAtLocation <p>
     * 
     * @param date the Date to return the bounds for. Must not be null.
     * @return the bounds of the given date or null if not visible.
     * 
     * @see #getDayAtLocation(int, int)
     */
    protected Rectangle getDayBounds(Date date) {
        if (!isVisible(date)) return null;
        Point position = getDayGridPosition(date);
        if (position == null)
            throw new IllegalArgumentException("Cannot find grid postion for date "+date);
        Rectangle monthBounds = getMonthBounds(date);
        if (monthBounds == null)
            throw new IllegalArgumentException("Cannot find month bounds for date "+date);

        monthBounds.y += getMonthHeaderHeight() + (position.y - DAY_HEADER_ROW) * fullBoxHeight;
        if (monthView.isShowingWeekNumber()) {
            position.x++;
        }
        position.x -= FIRST_DAY_COLUMN;
        if (isLeftToRight) {
           monthBounds.x += position.x * fullBoxWidth; 
        } else {
            int start = monthBounds.x + monthBounds.width - fullBoxWidth; 
            monthBounds.x = start - position.x * fullBoxWidth;
        }
        monthBounds.width = fullBoxWidth;
        monthBounds.height = fullBoxHeight;
        return monthBounds;
    }
    
    /**
     * @param row
     */
    private void checkValidRow(int row, int column) {
        if ((column < WEEK_HEADER_COLUMN) || (column > LAST_DAY_COLUMN)) 
            throw new IllegalArgumentException("illegal column in day grid " + column);
        if ((row < DAY_HEADER_ROW) || (row > LAST_WEEK_ROW)) 
            throw new IllegalArgumentException("illegal row in day grid" + row);
    }

    /**
     * Returns a boolean indicating if the given Date is visible. Trailing/leading
     * dates of the last/first displayed month are considered to be invisible.
     * 
     * @param date the Date to check for visibility. Must not be null.
     * @return true if the date is visible, false otherwise.
     */
    private boolean isVisible(Date date) {
        if (getFirstDisplayedDay().after(date) || getLastDisplayedDay().before(date)) return false;
        return true;
    }

    
    // ------------------- mapping month parts 
 

    /**
     * Mapping pixel to bounds.<p>
     * 
     * PENDING JW: define the "action grid". Currently this replaces the old
     * version to remove all internal usage of deprecated methods.
     *  
     * @param x the x position of the location in pixel
     * @param y the y position of the location in pixel
     * @return the bounds of the active header area in containing the location
     *   or null if outside.
     */
    protected int getTraversableGridPositionAtLocation(int x, int y) {
        Rectangle headerBounds = getMonthHeaderBoundsAtLocation(x, y);
        if (headerBounds == null) return -1;
        if (y < headerBounds.y + arrowPaddingY) return -1;
        if (y > headerBounds.y + headerBounds.height - arrowPaddingY) return -1;
        headerBounds.setBounds(headerBounds.x + arrowPaddingX, y, 
                headerBounds.width - 2 * arrowPaddingX, headerBounds.height);
        if (!headerBounds.contains(x, y)) return -1;
        Rectangle hitArea = new Rectangle(headerBounds.x, headerBounds.y, monthUpImage.getIconWidth(), monthUpImage.getIconHeight());
        if (hitArea.contains(x, y)) {
            return isLeftToRight ? MONTH_DOWN : MONTH_UP;
        }
        hitArea.translate(headerBounds.width - monthUpImage.getIconWidth(), 0);
        if (hitArea.contains(x, y)) {
            return isLeftToRight ? MONTH_UP : MONTH_DOWN;
        } 
        return -1;
    }
    
    /**
     * Returns the bounds of the month header which contains the 
     * given location. The bounds are in monthView coordinate system.
     * 
     * <p>
     * 
     * @param x the x position of the location in pixel
     * @param y the y position of the location in pixel
     * @return the bounds of the month which contains the location, 
     *   or null if outside
     */
    protected Rectangle getMonthHeaderBoundsAtLocation(int x, int y) {
        Rectangle header = getMonthBoundsAtLocation(x, y);
        if (header == null) return null;
        header.height = getMonthHeaderHeight();
        return header;
    }
    
    /**
     * Returns the bounds of the month details which contains the 
     * given location. The bounds are in monthView coordinate system.
     * 
     * @param x the x position of the location in pixel
     * @param y the y position of the location in pixel
     * @return the bounds of the details grid in the month at
     *   location or null if outside.
     */
    protected Rectangle getMonthDetailsBoundsAtLocation(int x, int y) {
        Rectangle month = getMonthBoundsAtLocation(x, y);
        if (month == null) return null;
        int startOfDaysY = month.y + getMonthHeaderHeight();
        if (y < startOfDaysY) return null;
        month.y = startOfDaysY;
        month.height = month.height - getMonthHeaderHeight();
        return month;
    }

    
    // ---------------------- mapping month coordinates    

    /**
      * Returns the bounds of the month which contains the 
     * given location. The bounds are in monthView coordinate system.
     * 
     * <p>
     * 
     * Mapping pixel to bounds.
     * 
     * @param x the x position of the location in pixel
     * @param y the y position of the location in pixel
     * @return the bounds of the month which contains the location, 
     *   or null if outside
     */
    protected Rectangle getMonthBoundsAtLocation(int x, int y) {
        if (!calendarGrid.contains(x, y)) return null;
        int calendarRow = (y - calendarGrid.y) / fullCalendarHeight;
        int calendarColumn = (x - calendarGrid.x) / fullCalendarWidth;
        return new Rectangle( 
                calendarGrid.x + calendarColumn * fullCalendarWidth,
                calendarGrid.y + calendarRow * fullCalendarHeight,
                calendarWidth, calendarHeight);
    }
    
    
    /**
     * 
     * Returns the logical coordinates of the month which contains
     * the given location. The p.x of the returned value represents the column, the
     * p.y represents the row the month is shown in. The transformation takes
     * care of ComponentOrientation. <p>
     * 
     * Mapping pixel to logical grid coordinates.
     * 
     * @param x the x position of the location in pixel
     * @param y the y position of the location in pixel
     * @return the logical coordinates of the month in the grid of month shown by
     *   this monthView or null if outside. 
     */
    protected Point getMonthGridPositionAtLocation(int x, int y) {
        if (!calendarGrid.contains(x, y)) return null;
        int calendarRow = (y - calendarGrid.y) / fullCalendarHeight;
        int calendarColumn = (x - calendarGrid.x) / fullCalendarWidth;
        if (!isLeftToRight) {
            int start = calendarGrid.x + calendarGrid.width;
            calendarColumn = (start - x) / fullCalendarWidth;
              
        }
        return new Point(calendarColumn, calendarRow);
    }

    /**
     * Returns the Date representing the start of the month which 
     * contains the given location.<p>
     * 
     * Mapping pixel to calendar day.
     *
     * @param x the x position of the location in pixel
     * @param y the y position of the location in pixel
     * @return the start of the month which contains the given location or 
     *    null if the location is outside the grid of months.
     */
    protected Date getMonthAtLocation(int x, int y) {
        Point month = getMonthGridPositionAtLocation(x, y);
        if (month ==  null) return null;
        return getMonth(month.y, month.x);
    }
    
    /**
     * Returns the Date representing the start of the month at the given 
     * logical position in the grid of months. <p>
     * 
     * Mapping logical grid coordinates to Calendar.
     * 
     * @param row the rowIndex in the grid of months.
     * @param column the columnIndex in the grid months.
     * @return a Date representing the start of the month at the given
     *   logical coordinates.
     *   
     * @see #getMonthGridPosition(Date)  
     */
    protected Date getMonth(int row, int column) {
        Calendar calendar = getCalendar();
        calendar.add(Calendar.MONTH, 
                row * calendarColumnCount + column);
        return calendar.getTime();
        
    }

    /**
     * Returns the logical grid position of the month containing the given date.
     * The Point's x value is the column in the grid of months, the y value
     * is the row in the grid of months.
     * 
     * Mapping Date to logical grid position, this is the reverse of getMonth(int, int).
     * 
     * @param date the Date to return the bounds for. Must not be null.
     * @return the postion of the month that contains the given date or null if not visible.
     * 
     * @see #getMonth(int, int)
     * @see #getMonthBounds(int, int)
     */
    protected Point getMonthGridPosition(Date date) {
        if (!isVisible(date)) return null;
        // start of grid
        Calendar calendar = getCalendar();
        int firstMonth = calendar.get(Calendar.MONTH);
        int firstYear = calendar.get(Calendar.YEAR);
        
        // 
        calendar.setTime(date);
        int month = calendar.get(Calendar.MONTH);
        int year = calendar.get(Calendar.YEAR);
        
        int diffMonths = month - firstMonth
            + ((year - firstYear) * JXMonthView.MONTHS_IN_YEAR);
        
        int row = diffMonths / calendarColumnCount;
        int column = diffMonths % calendarColumnCount;

        return new Point(column, row);
    }

    /**
     * Returns the bounds of the month at the given logical coordinates
     * in the grid of visible months.<p>
     * 
     * Mapping logical grip position to pixel.
     * 
     * @param row the rowIndex in the grid of months.
     * @param column the columnIndex in the grid months.
     * @return the bounds of the month at the given logical logical position.
     * 
     * @see #getMonthGridPositionAtLocation(int, int)
     * @see #getMonthBoundsAtLocation(int, int)
     */
    protected Rectangle getMonthBounds(int row, int column) {
        int startY = calendarGrid.y + row * fullCalendarHeight;
        int startX = calendarGrid.x + column * fullCalendarWidth;
        if (!isLeftToRight) {
            startX = calendarGrid.x + (calendarColumnCount - 1 - column) * fullCalendarWidth;
        }
        return new Rectangle(startX, startY, calendarWidth, calendarHeight);
    }

    /**
     * Returns the bounds of the month containing the given date.
     * The bounds are in monthView coordinate system.<p>
     * 
     * Mapping Date to pixel.
     * 
     * @param date the Date to return the bounds for. Must not be null.
     * @return the bounds of the month that contains the given date or null if not visible.
     * 
     * @see #getMonthAtLocation(int, int)
     */
    protected Rectangle getMonthBounds(Date date) {
        Point position = getMonthGridPosition(date);
        return position != null ? getMonthBounds(position.y, position.x) : null;
    }
    
    /**
     * Returns the bounds of the month containing the given date.
     * The bounds are in monthView coordinate system.<p>
     * 
     * Mapping Date to pixel.
     * 
     * @param date the Date to return the bounds for. Must not be null.
     * @return the bounds of the month that contains the given date or null if not visible.
     * 
     * @see #getMonthAtLocation(int, int)
     */
    protected Rectangle getMonthHeaderBounds(Date date, boolean includeInsets) {
        Point position = getMonthGridPosition(date);
        if (position == null) return null;
        Rectangle bounds = getMonthBounds(position.y, position.x);
        bounds.height = getMonthHeaderHeight();
        if (!includeInsets) {
            
        }
        return bounds;
    }


    //---------------- accessors for sizes
    
    /**
     * Returns the size of a month.
     * @return the size of a month.
     */
    protected Dimension getMonthSize() {
        return new Dimension(calendarWidth, calendarHeight);
    }
    
    /**
     * Returns the size of a day including the padding.
     * @return the size of a month.
     */
    protected Dimension getDaySize() {
        return new Dimension(fullBoxWidth, fullBoxHeight);
    }
    /**
     * Returns the height of the month header.
     * 
     * @return the height of the month header.
     */
    protected int getMonthHeaderHeight() {
        return fullMonthBoxHeight;
    }

    

    //-------------------  layout    
    
    /**
     * Called from layout: calculates properties
     * of grid of months.
     */
    private void calculateMonthGridLayoutProperties() {
        calculateMonthGridRowColumnCount();
        calculateMonthGridBounds();
    }
    
    /**
     * Calculates the bounds of the grid of months. 
     * 
     * CalendarRow/ColumnCount and calendarWidth/Height must be
     * initialized before calling this. 
     */
    private void calculateMonthGridBounds() {
        calendarGrid.setBounds(calculateCalendarGridX(), 
                calculateCalendarGridY(), 
                calculateCalendarGridWidth(), 
                calculateCalendarGridHeight());
    }


    private int calculateCalendarGridY() {
        return (monthView.getHeight() - calculateCalendarGridHeight()) / 2;
    }

    private int calculateCalendarGridX() {
        return (monthView.getWidth() - calculateCalendarGridWidth()) / 2; 
    }
    
    private int calculateCalendarGridHeight() {
        return ((calendarHeight * calendarRowCount) +
                (CALENDAR_SPACING * (calendarRowCount - 1 )));
    }

    private int calculateCalendarGridWidth() {
        return ((calendarWidth * calendarColumnCount) +
                (CALENDAR_SPACING * (calendarColumnCount - 1)));
    }

    /**
     * Calculates and updates the numCalCols/numCalRows that determine the
     * number of calendars that can be displayed. Updates the last displayed
     * date if appropriate.
     * 
     */
    private void calculateMonthGridRowColumnCount() {
        int oldNumCalCols = calendarColumnCount;
        int oldNumCalRows = calendarRowCount;

        calendarRowCount = 1;
        calendarColumnCount = 1;
        if (!isZoomable()) {
            // Determine how many columns of calendars we want to paint.
            int addColumns = (monthView.getWidth() - calendarWidth)
                    / (calendarWidth + CALENDAR_SPACING);
            // happens if used as renderer in a tree.. don't know yet why
            if (addColumns > 0) {
                calendarColumnCount += addColumns;
            }

            // Determine how many rows of calendars we want to paint.
            int addRows = (monthView.getHeight() - calendarHeight)
                    / (calendarHeight + CALENDAR_SPACING);
            if (addRows > 0) {
                calendarRowCount += addRows;
            }
        }
        if (oldNumCalCols != calendarColumnCount
                || oldNumCalRows != calendarRowCount) {
            updateLastDisplayedDay(getFirstDisplayedDay());
        }
    }

    /**
     * @return true if the month view can be zoomed, false otherwise
     */
    protected boolean isZoomable() {
        return monthView.isZoomable();
    }


    

//-------------------- painting

    
    /**
     * Overridden to extract the background painting for ease-of-use of 
     * subclasses.
     */
    @Override
    public void update(Graphics g, JComponent c) {
        paintBackground(g);
        paint(g, c);
    }

    /**
     * Paints the background of the component. This implementation fill the
     * monthView's area with its background color if opaque, does nothing
     * if not opaque. Subclasses can override but must comply to opaqueness
     * contract.
     * 
     * @param g the Graphics to fill.
     * 
     */
    protected void paintBackground(Graphics g) {
        if (monthView.isOpaque()) {
            g.setColor(monthView.getBackground());
            g.fillRect(0, 0, monthView.getWidth(), monthView.getHeight());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void paint(Graphics g, JComponent c) {
        Rectangle clip = g.getClipBounds();
        // Get a calender set to the first displayed date
        Calendar cal = getCalendar();
        // loop through grid of months
        for (int row = 0; row < calendarRowCount; row++) {
            for (int column = 0; column < calendarColumnCount; column++) {
                // get the bounds of the current month.
                Rectangle bounds = getMonthBounds(row, column);
                // Check if this row falls in the clip region.
                if (bounds.intersects(clip)) {
                    paintMonth(g, cal);
                }
                cal.add(Calendar.MONTH, 1);
            }
        }

    }

    /**
     * Paints the month represented by the given Calendar.
     * 
     * Note: the given calendar must not be changed.
     * @param g the graphics to paint into
     * @param month the calendar specifying the first day of the month to
     *        paint, must not be null
     */
    protected void paintMonth(Graphics g, Calendar month) {
        paintMonthHeader(g, month);
        paintDayHeader(g, month);
        paintWeekHeader(g, month);
        paintDays(g, month);
    }

    /**
     * Paints the header of a month.
     * 
     * Note: the given calendar must not be changed.
     * @param g the graphics to paint into
     * @param month the calendar specifying the first day of the month to
     *        paint, must not be null
     */
    protected void paintMonthHeader(Graphics g, Calendar month) {
        Rectangle page = getMonthHeaderBounds(month.getTime(), false);
        paintDayOfMonth(g, page, month, CalendarState.TITLE);
    }

    /**
     * Paints the day column header.
     * 
     * Note: the given calendar must not be changed.
     * @param g the graphics to paint into
     * @param month the calendar specifying the first day of the month to
     *        paint, must not be null
     */
    protected void paintDayHeader(Graphics g, Calendar month) {
        paintDaysOfWeekSeparator(g, month);
        Calendar cal = (Calendar) month.clone();
        CalendarUtils.startOfWeek(cal);
        for (int i = FIRST_DAY_COLUMN; i <= LAST_DAY_COLUMN; i++) {
            Rectangle dayBox = getDayBoundsInMonth(month.getTime(), DAY_HEADER_ROW, i);
            paintDayOfMonth(g, dayBox, cal, CalendarState.DAY_OF_WEEK);
            cal.add(Calendar.DATE, 1);
        }
    }

    /**
     * Paints the day column header.
     * 
     * Note: the given calendar must not be changed.
     * @param g the graphics to paint into
     * @param month the calendar specifying the first day of the month to
     *        paint, must not be null
     */
    protected void paintWeekHeader(Graphics g, Calendar month) {
        if (!monthView.isShowingWeekNumber())
            return;
        paintWeekOfYearSeparator(g, month);
    
        int weeks = getWeeks(month);
        // the calendar passed to the renderers
        Calendar weekCalendar = (Calendar) month.clone();
        // we loop by logical row (== week in month) coordinates 
        for (int week = FIRST_WEEK_ROW; week < FIRST_WEEK_ROW + weeks; week++) {
            // get the day bounds based on logical row/column coordinates
            Rectangle dayBox = getDayBoundsInMonth(month.getTime(), week, WEEK_HEADER_COLUMN);
            // NOTE: this can be set to any day in the week to render the weeknumber of
            // categorized by CalendarState
            paintDayOfMonth(g, dayBox, weekCalendar, CalendarState.WEEK_OF_YEAR);
            weekCalendar.add(Calendar.WEEK_OF_YEAR, 1);
        }
    }

    /**
     * Paints the days of the given month.
     * 
     * Note: the given calendar must not be changed.
     * @param g the graphics to paint into
     * @param month the calendar specifying the first day of the month to
     *        paint, must not be null
     */
    protected void paintDays(Graphics g, Calendar month) {
        Calendar clonedCal = (Calendar) month.clone();
        CalendarUtils.startOfMonth(clonedCal);
        Date startOfMonth = clonedCal.getTime();
        CalendarUtils.endOfMonth(clonedCal);
        Date endOfMonth = clonedCal.getTime();
        // reset the clone
        clonedCal.setTime(month.getTime());
        // adjust to start of week
        clonedCal.setTime(month.getTime());
        CalendarUtils.startOfWeek(clonedCal);
        for (int week = FIRST_WEEK_ROW; week <= LAST_WEEK_ROW; week++) {
            for (int day = FIRST_DAY_COLUMN; day <= LAST_DAY_COLUMN; day++) {
                CalendarState state = null;
                if (clonedCal.getTime().before(startOfMonth)) {
                    if (monthView.isShowingLeadingDays()) {
                        state = CalendarState.LEADING;
                    }
                } else if (clonedCal.getTime().after(endOfMonth)) {
                    if (monthView.isShowingTrailingDays()) {
                        state = CalendarState.TRAILING;
                    }

                } else {
                    state = isToday(clonedCal.getTime()) ? CalendarState.TODAY : CalendarState.IN_MONTH;
                }
                if (state != null) {
                    Rectangle bounds = getDayBoundsInMonth(startOfMonth, week, day);
                    paintDayOfMonth(g, bounds, clonedCal, state);
                }
                clonedCal.add(Calendar.DAY_OF_MONTH, 1);
            }
        }
    }


    /**
     * Paints a day which is of the current month with the given state.<p>
     * 
     * PENDING JW: mis-nomer - this is in fact called for rendering any day-related
     * state (including weekOfYear, dayOfWeek headers) and for rendering
     * the month header as well, that is from everywhere.
     *  Rename to paintSomethingGeneral. Think about impact for subclasses 
     *  (what do they really need? feedback please!)
     * 
     * @param g the graphics to paint into.
     * @param bounds the rectangle to paint the day into
     * @param calendar the calendar representing the day to paint
     * @param state the calendar state
     */
    protected void paintDayOfMonth(Graphics g, Rectangle bounds, Calendar calendar, CalendarState state) {
        JComponent comp = getRenderingHandler().prepareRenderingComponent(monthView, calendar, 
                state);
        if (bounds != null)
            rendererPane.paintComponent(g, comp, monthView, bounds.x, bounds.y,
                bounds.width, bounds.height, true);
    }

    /**
     * Paints the separator between row header (weeks of year) and days.
     * 
     * Note: the given calendar must not be changed.
     * @param g the graphics to paint into
     * @param month the calendar specifying the first day of the month to
     *        paint, must not be null
     */
    protected void paintWeekOfYearSeparator(Graphics g, Calendar month) {
        Rectangle r = getSeparatorBounds(month, FIRST_WEEK_ROW, WEEK_HEADER_COLUMN);
        if (r == null) return;
        g.setColor(monthView.getForeground());
        g.drawLine(r.x, r.y, r.x, r.y + r.height);
    }

    /**
     * Paints the separator between column header (days of week) and days.
     * 
     * Note: the given calendar must not be changed.
     * @param g the graphics to paint into
     * @param month the calendar specifying the the first day of the month to
     *        paint, must not be null
     */
    protected void paintDaysOfWeekSeparator(Graphics g, Calendar month) {
        Rectangle r = getSeparatorBounds(month, DAY_HEADER_ROW, FIRST_DAY_COLUMN);
        if (r == null) return;
        g.setColor(monthView.getForeground());
        g.drawLine(r.x, r.y, r.x + r.width, r.y);
    }
    
    /**
     * @param month
     * @param row
     * @param column
     * @return
     */
    private Rectangle getSeparatorBounds(Calendar month, int row, int column) {
        Rectangle separator = getDayBoundsInMonth(month.getTime(), row, column);
        if (separator == null) return null;
        if (column == WEEK_HEADER_COLUMN) {
            separator.height *= WEEKS_IN_MONTH;
            if (isLeftToRight) {
                separator.x += separator.width - 1;
            }
            separator.width = 1;
        } else if (row == DAY_HEADER_ROW) {
            int oldWidth = separator.width;
            separator.width *= DAYS_IN_WEEK;
            if (!isLeftToRight) {
                separator.x -= separator.width - oldWidth;
            }
            separator.y += separator.height - 1;
            separator.height = 1;
        }
        return separator;
    }

    /**
     * Returns the number of weeks to paint in the current month, as represented
     * by the given calendar. The calendar is expected to be set to the first
     * of the month. 
     * 
     * Note: the given calendar must not be changed.
     * 
     * @param month the calendar specifying the the first day of the month to
     *        paint, must not be null
     * @return the number of weeks of this month.
     */
    protected int getWeeks(Calendar month) {
        Calendar cloned = (Calendar) month.clone();
        // the calendar is set to the first of month, get date for last
        CalendarUtils.endOfMonth(cloned);
        // marker for end
        Date last = cloned.getTime();
        // start again
        cloned.setTime(month.getTime());
        CalendarUtils.startOfWeek(cloned);
        int weeks = 0;
        while (last.after(cloned.getTime())) {
            weeks++;
            cloned.add(Calendar.WEEK_OF_MONTH, 1);
        }
        return weeks;
    }



    private void traverseMonth(int arrowType) {
        if (arrowType == MONTH_DOWN) {
            previousMonth();
        } else if (arrowType == MONTH_UP) {
            nextMonth();
        }
    }

    private void nextMonth() {
        Date upperBound = monthView.getUpperBound();
        if (upperBound == null
                || upperBound.after(getLastDisplayedDay()) ){
            Calendar cal = getCalendar();
            cal.add(Calendar.MONTH, 1);
            monthView.setFirstDisplayedDay(cal.getTime());
        }
    }

    private void previousMonth() {
        Date lowerBound = monthView.getLowerBound();
        if (lowerBound == null
                || lowerBound.before(getFirstDisplayedDay())){
            Calendar cal = getCalendar();
            cal.add(Calendar.MONTH, -1);
            monthView.setFirstDisplayedDay(cal.getTime());
        }
    }

//--------------------------- displayed dates, calendar

    
    /**
     * Returns the monthViews calendar configured to the firstDisplayedDate.
     * 
     * NOTE: it's safe to change the calendar state without resetting because
     * it's JXMonthView's responsibility to protect itself.
     * 
     * @return the monthView's calendar, configured with the firstDisplayedDate.
     */
    protected Calendar getCalendar() {
        return getCalendar(getFirstDisplayedDay());
    }
    
    /**
     * Returns the monthViews calendar configured to the given time.
     * 
     * NOTE: it's safe to change the calendar state without resetting because
     * it's JXMonthView's responsibility to protect itself.
     * 
     * @param date the date to configure the calendar with
     * @return the monthView's calendar, configured with the given date.
     */
    protected Calendar getCalendar(Date date) {
        Calendar calendar = monthView.getCalendar();
        calendar.setTime(date);
        return calendar;
    }

    

    /**
     * Updates the lastDisplayedDate property based on the given first and 
     * visible # of months.
     * 
     * @param first the date of the first visible day.
     */
    private void updateLastDisplayedDay(Date first) {
        Calendar cal = getCalendar(first);
        cal.add(Calendar.MONTH, ((calendarColumnCount * calendarRowCount) - 1));
        CalendarUtils.endOfMonth(cal);
        lastDisplayedDate = cal.getTime();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Date getLastDisplayedDay() {
        return lastDisplayedDate;
    }

    /*-------------- refactored: encapsulate aliased fields
     */

    /**
     * Updates internal state that depends on the MonthView's firstDisplayedDay
     * property. <p>
     * 
     * Here: updates lastDisplayedDay.
     * <p>
     * 
     * 
     * @param firstDisplayedDay the firstDisplayedDate to set
     */
    protected void setFirstDisplayedDay(Date firstDisplayedDay) {
        updateLastDisplayedDay(firstDisplayedDay);
    }
    
    /**
     * Returns the first displayed day. Convenience delegate to 
     * 
     * @return the firstDisplayed
     */
    protected Date getFirstDisplayedDay() {
        return monthView.getFirstDisplayedDay();
    }

    /**
     * @return the firstDisplayedMonth
     */
    protected int getFirstDisplayedMonth() {
        return getCalendar().get(Calendar.MONTH);
    }


    /**
     * @return the firstDisplayedYear
     */
    protected int getFirstDisplayedYear() {
        return getCalendar().get(Calendar.YEAR);
    }


    /**
     * @return the selection
     */
    protected SortedSet<Date> getSelection() {
        return monthView.getSelection();
    }
    
    
    /**
     * @return the start of today.
     */
    protected Date getToday() {
        return monthView.getToday();
    }

    /**
     * Returns true if the date passed in is the same as today.
     *
     * PENDING JW: really want the exact test?
     * 
     * @param date long representing the date you want to compare to today.
     * @return true if the date passed is the same as today.
     */
    protected boolean isToday(Date date) {
        return date.equals(getToday());
    }
    

//-----------------------end encapsulation
 
    
//------------------ Handler implementation 
//  
    /**
     * temporary: removed SelectionMode.NO_SELECTION, replaced
     * all access by this method to enable easy re-adding, if we want it.
     * If not - remove.
     */
    private boolean canSelectByMode() {
        return true;
    }
    

    private class Handler implements  
        MouseListener, MouseMotionListener, LayoutManager,
            PropertyChangeListener, DateSelectionListener {
        private boolean armed;
        private Date startDate;
        private Date endDate;

        @Override
        public void mouseClicked(MouseEvent e) {}

        @Override
        public void mousePressed(MouseEvent e) {
            // If we were using the keyboard we aren't anymore.
            setUsingKeyboard(false);

            if (!monthView.isEnabled()) {
                return;
            }

            if (!monthView.hasFocus() && monthView.isFocusable()) {
                monthView.requestFocusInWindow();
            }

            // Check if one of the month traverse buttons was pushed.
            if (monthView.isTraversable()) {
                int arrowType = getTraversableGridPositionAtLocation(e.getX(), e.getY());
                if (arrowType != -1) {
                    traverseMonth(arrowType);
                    return;
                }
            }

            if (!canSelectByMode()) {
                return;
            }

            Date cal = getDayAtLocation(e.getX(), e.getY());
            if (cal == null) {
                return;
            }

            // Update the selected dates.
            startDate = cal;
            endDate = cal;

            if (monthView.getSelectionMode() == SelectionMode.SINGLE_INTERVAL_SELECTION ||
//                    selectionMode == SelectionMode.WEEK_INTERVAL_SELECTION ||
                    monthView.getSelectionMode() == SelectionMode.MULTIPLE_INTERVAL_SELECTION) {
                pivotDate = startDate;
            }

            monthView.getSelectionModel().setAdjusting(true);
            
            if (monthView.getSelectionMode() == SelectionMode.MULTIPLE_INTERVAL_SELECTION && e.isControlDown()) {
                monthView.addSelectionInterval(startDate, endDate);
            } else {
                monthView.setSelectionInterval(startDate, endDate);
            }

            // Arm so we fire action performed on mouse release.
            armed = true;
        }

        
        @Override
        public void mouseReleased(MouseEvent e) {
            // If we were using the keyboard we aren't anymore.
            setUsingKeyboard(false);

            if (!monthView.isEnabled()) {
                return;
            }

            if (!monthView.hasFocus() && monthView.isFocusable()) {
                monthView.requestFocusInWindow();
            }
            
            if (armed) {
                monthView.commitSelection();
            }
            armed = false;
        }

        @Override
        public void mouseEntered(MouseEvent e) {}

        @Override
        public void mouseExited(MouseEvent e) {}

        @Override
        public void mouseDragged(MouseEvent e) {
            // If we were using the keyboard we aren't anymore.
            setUsingKeyboard(false);
            if (!monthView.isEnabled() || !canSelectByMode()) {
                return;
            }
            Date cal = getDayAtLocation(e.getX(), e.getY());
            if (cal == null) {
                return;
            }

            Date selected = cal;
            Date oldStart = startDate;
            Date oldEnd = endDate;

            if (monthView.getSelectionMode() == SelectionMode.SINGLE_SELECTION) {
                if (selected.equals(oldStart)) {
                    return;
                }
                startDate = selected;
                endDate = selected;
            } else  if (pivotDate != null){
                if (selected.before(pivotDate)) {
                    startDate = selected;
                    endDate = pivotDate;
                } else if (selected.after(pivotDate)) {
                    startDate = pivotDate;
                    endDate = selected;
                }
            } else { // pivotDate had not yet been initialiased
                // might happen on first click into leading/trailing dates
                // JW: fix of #996-swingx: NPE when dragging 
                startDate = selected;
                endDate = selected;
                pivotDate = selected;
            }

            if (startDate.equals(oldStart) && endDate.equals(oldEnd)) {
                return;
            }

            if (monthView.getSelectionMode() == SelectionMode.MULTIPLE_INTERVAL_SELECTION && e.isControlDown()) {
                monthView.addSelectionInterval(startDate, endDate);
            } else {
                monthView.setSelectionInterval(startDate, endDate);
            }

            // Set trigger.
            armed = true;
        }

        @Override
        public void mouseMoved(MouseEvent e) {}

//------------------------ layout
        
        
        private Dimension preferredSize = new Dimension();

        @Override
        public void addLayoutComponent(String name, Component comp) {}

        @Override
        public void removeLayoutComponent(Component comp) {}

        @Override
        public Dimension preferredLayoutSize(Container parent) {
            layoutContainer(parent);
            return new Dimension(preferredSize);
        }

        @Override
        public Dimension minimumLayoutSize(Container parent) {
            return preferredLayoutSize(parent);
        }

        @Override
        public void layoutContainer(Container parent) {

            int maxMonthWidth = 0;
            int maxMonthHeight = 0;
            Calendar calendar = getCalendar();
            for (int i = calendar.getMinimum(Calendar.MONTH); i <= calendar.getMaximum(Calendar.MONTH); i++) {
                calendar.set(Calendar.MONTH, i);
                CalendarUtils.startOfMonth(calendar);
                JComponent comp = getRenderingHandler().prepareRenderingComponent(monthView, calendar, CalendarState.TITLE);
                Dimension pref = comp.getPreferredSize();
                maxMonthWidth = Math.max(maxMonthWidth, pref.width);
                maxMonthHeight = Math.max(maxMonthHeight, pref.height);
            }
            
            int maxBoxWidth = 0;
            int maxBoxHeight = 0;
            calendar = getCalendar();
            CalendarUtils.startOfWeek(calendar);
            for (int i = 0; i < JXMonthView.DAYS_IN_WEEK; i++) {
                JComponent comp = getRenderingHandler().prepareRenderingComponent(monthView, calendar, CalendarState.DAY_OF_WEEK);
                Dimension pref = comp.getPreferredSize();
                maxBoxWidth = Math.max(maxBoxWidth, pref.width);
                maxBoxHeight = Math.max(maxBoxHeight, pref.height);
                calendar.add(Calendar.DATE, 1);
            }
            
            calendar = getCalendar();
            for (int i = 0; i < calendar.getMaximum(Calendar.DAY_OF_MONTH); i++) {
                JComponent comp = getRenderingHandler().prepareRenderingComponent(monthView, calendar, CalendarState.IN_MONTH);
                Dimension pref = comp.getPreferredSize();
                maxBoxWidth = Math.max(maxBoxWidth, pref.width);
                maxBoxHeight = Math.max(maxBoxHeight, pref.height);
                calendar.add(Calendar.DATE, 1);
            }
            
            int dayColumns = JXMonthView.DAYS_IN_WEEK;
            if (monthView.isShowingWeekNumber()) {
                dayColumns++;
            }
            
            if (maxMonthWidth > maxBoxWidth * dayColumns) {
                //  monthHeader pref > sum of box widths
                // handle here: increase day box width accordingly
                double diff = maxMonthWidth - (maxBoxWidth * dayColumns);
                maxBoxWidth += Math.ceil(diff/(double) dayColumns);
                
            }
            
            fullBoxWidth = maxBoxWidth;
            fullBoxHeight = maxBoxHeight;
            // PENDING JW: huuh? what we doing here?
            int boxHeight = maxBoxHeight - 2 * monthView.getBoxPaddingY();
            fullMonthBoxHeight = Math.max(boxHeight, maxMonthHeight) ; 

            // Keep track of calendar width and height for use later.
            calendarWidth = fullBoxWidth * JXMonthView.DAYS_IN_WEEK;
            if (monthView.isShowingWeekNumber()) {
                calendarWidth += fullBoxWidth;
            }
            fullCalendarWidth = calendarWidth + CALENDAR_SPACING;
            
            calendarHeight = (fullBoxHeight * 7) + fullMonthBoxHeight;
            fullCalendarHeight = calendarHeight + CALENDAR_SPACING;
            // Calculate minimum width/height for the component.
            int prefRows = getPreferredRows();
            preferredSize.height = (calendarHeight * prefRows) +
                    (CALENDAR_SPACING * (prefRows - 1));

            int prefCols = getPreferredColumns();
            preferredSize.width = (calendarWidth * prefCols) +
                    (CALENDAR_SPACING * (prefCols - 1));

            // Add insets to the dimensions.
            Insets insets = monthView.getInsets();
            preferredSize.width += insets.left + insets.right;
            preferredSize.height += insets.top + insets.bottom;
           
            calculateMonthGridLayoutProperties();
            
            if (isZoomable()) {
                getCalendarHeaderHandler().getHeaderComponent().setBounds(getMonthHeaderBounds(monthView.getFirstDisplayedDay(), false));
            }
        }

        /**
         * @return
         */
        private int getPreferredColumns() {
            return isZoomable() ? 1 : monthView.getPreferredColumnCount();
        }

        /**
         * @return
         */
        private int getPreferredRows() {
            return isZoomable() ? 1 : monthView.getPreferredRowCount();
        }



        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            String property = evt.getPropertyName();

            if ("componentOrientation".equals(property)) {
                isLeftToRight = monthView.getComponentOrientation().isLeftToRight();
                monthView.revalidate();
                monthView.repaint();
            } else if (JXMonthView.SELECTION_MODEL.equals(property)) {
                DateSelectionModel selectionModel = (DateSelectionModel) evt.getOldValue();
                selectionModel.removeDateSelectionListener(getHandler());
                selectionModel = (DateSelectionModel) evt.getNewValue();
                selectionModel.addDateSelectionListener(getHandler());
            } else if ("firstDisplayedDay".equals(property)) {
                setFirstDisplayedDay(((Date) evt.getNewValue()));
                monthView.repaint();
            } else if (JXMonthView.BOX_PADDING_X.equals(property) 
                    || JXMonthView.BOX_PADDING_Y.equals(property) 
                    || JXMonthView.TRAVERSABLE.equals(property) 
                    || JXMonthView.DAYS_OF_THE_WEEK.equals(property) 
                    || "border".equals(property) 
                    || "showingWeekNumber".equals(property)
                    || "traversable".equals(property) 
                   
                    ) {
                monthView.revalidate();
                monthView.repaint();
            } else if ("zoomable".equals(property)) {
                updateZoomable();
//            } else if ("font".equals(property)) {
//                calendarHeaderHandler.getHeaderComponent().setFont(getAsNotUIResource(createDerivedFont()));
//                monthView.revalidate();
            } else if ("componentInputMapEnabled".equals(property)) {
                updateComponentInputMap();
            } else if ("locale".equals(property)) { // "locale" is bound property
                updateLocale(true);
            } else {
                monthView.repaint();
//                LOG.info("got propertyChange:" + property);
            }
        }

        @Override
        public void valueChanged(DateSelectionEvent ev) {
            monthView.repaint();
        }


    }

    /**
     * Class that supports keyboard traversal of the JXMonthView component.
     */
    private class KeyboardAction extends AbstractAction {
        public static final int ACCEPT_SELECTION = 0;
        public static final int CANCEL_SELECTION = 1;
        public static final int SELECT_PREVIOUS_DAY = 2;
        public static final int SELECT_NEXT_DAY = 3;
        public static final int SELECT_DAY_PREVIOUS_WEEK = 4;
        public static final int SELECT_DAY_NEXT_WEEK = 5;
        public static final int ADJUST_SELECTION_PREVIOUS_DAY = 6;
        public static final int ADJUST_SELECTION_NEXT_DAY = 7;
        public static final int ADJUST_SELECTION_PREVIOUS_WEEK = 8;
        public static final int ADJUST_SELECTION_NEXT_WEEK = 9;

        private int action;

        public KeyboardAction(int action) {
            this.action = action;
        }

        @Override
        public void actionPerformed(ActionEvent ev) {
            if (!canSelectByMode())
                return;
            if (!isUsingKeyboard()) {
                originalDateSpan = getSelection();
            }
            // JW: removed the isUsingKeyboard from the condition
            // need to fire always.
            if (action >= ACCEPT_SELECTION && action <= CANCEL_SELECTION) { 
                // refactor the logic ...
                if (action == CANCEL_SELECTION) {
                    // Restore the original selection.
                    if ((originalDateSpan != null)
                            && !originalDateSpan.isEmpty()) {
                        monthView.setSelectionInterval(
                                originalDateSpan.first(), originalDateSpan
                                        .last());
                    } else {
                        monthView.clearSelection();
                    }
                    monthView.cancelSelection();
                } else {
                    // Accept the keyboard selection.
                    monthView.commitSelection();
                }
                setUsingKeyboard(false);
            } else if (action >= SELECT_PREVIOUS_DAY
                    && action <= SELECT_DAY_NEXT_WEEK) {
                setUsingKeyboard(true);
                monthView.getSelectionModel().setAdjusting(true);
                pivotDate = null;
                traverse(action);
            } else if (isIntervalMode()
                    && action >= ADJUST_SELECTION_PREVIOUS_DAY
                    && action <= ADJUST_SELECTION_NEXT_WEEK) {
                setUsingKeyboard(true);
                monthView.getSelectionModel().setAdjusting(true);
                addToSelection(action);
            }
        }


        /**
         * @return
         */
        private boolean isIntervalMode() {
            return !(monthView.getSelectionMode() == SelectionMode.SINGLE_SELECTION);
        }

        private void traverse(int action) {
            Date oldStart = monthView.isSelectionEmpty() ? 
                    monthView.getToday() : monthView.getFirstSelectionDate();
            Calendar cal = getCalendar(oldStart);
            switch (action) {
                case SELECT_PREVIOUS_DAY:
                    cal.add(Calendar.DAY_OF_MONTH, -1);
                    break;
                case SELECT_NEXT_DAY:
                    cal.add(Calendar.DAY_OF_MONTH, 1);
                    break;
                case SELECT_DAY_PREVIOUS_WEEK:
                    cal.add(Calendar.DAY_OF_MONTH, -JXMonthView.DAYS_IN_WEEK);
                    break;
                case SELECT_DAY_NEXT_WEEK:
                    cal.add(Calendar.DAY_OF_MONTH, JXMonthView.DAYS_IN_WEEK);
                    break;
            }

            Date newStartDate = cal.getTime();
            if (!newStartDate.equals(oldStart)) {
                monthView.setSelectionInterval(newStartDate, newStartDate);
                monthView.ensureDateVisible(newStartDate);
            }
        }

        /**
         * If we are in a mode that allows for range selection this method
         * will extend the currently selected range.
         *
         * NOTE: This may not be the expected behavior for the keyboard controls
         * and we ay need to update this code to act in a way that people expect.
         *
         * @param action action for adjusting selection
         */
        private void addToSelection(int action) {
            Date newStartDate;
            Date newEndDate;
            Date selectionStart;
            Date selectionEnd;
            if (!monthView.isSelectionEmpty()) {
                newStartDate = selectionStart = monthView.getFirstSelectionDate();
                newEndDate = selectionEnd = monthView.getLastSelectionDate();
            } else {
        	Date today = monthView.getToday();
        	if (today != null) {
                    newStartDate = selectionStart = today;
                    newEndDate = selectionEnd = newStartDate;
        	} else {
        	    // no date info, cann't do anything
        	    return;
        	}
            }

            if (pivotDate == null) {
                pivotDate = newStartDate;
            }

            // want a copy to play with - each branch sets and reads the time
            // actually don't care about the pre-set time.
            Calendar cal = getCalendar();
            boolean isStartMoved;
            switch (action) {
            case ADJUST_SELECTION_PREVIOUS_DAY:
                if (newEndDate.after(pivotDate)) {
                    newEndDate = previousDay(cal, newEndDate);
                    isStartMoved = false;
                } else {
                    newStartDate = previousDay(cal, newStartDate);
                    newEndDate = pivotDate;
                    isStartMoved = true;
                }
                break;
            case ADJUST_SELECTION_NEXT_DAY:
                if (newStartDate.before(pivotDate)) {
                    newStartDate = nextDay(cal, newStartDate);
                    isStartMoved = true;
                } else {
                    newEndDate = nextDay(cal, newEndDate);
                    isStartMoved = false;
                    newStartDate = pivotDate;
                }
                break;
            case ADJUST_SELECTION_PREVIOUS_WEEK:
                if (newEndDate.after(pivotDate)) {
                    Date newTime = previousWeek(cal, newEndDate);
                    if (newTime.after(pivotDate)) {
                        newEndDate = newTime;
                        isStartMoved = false;
                    } else {
                        newStartDate = newTime;
                        newEndDate = pivotDate;
                        isStartMoved = true;
                    }
                } else {
                    newStartDate = previousWeek(cal, newStartDate);
                    isStartMoved = true;
                }
                break;
            case ADJUST_SELECTION_NEXT_WEEK:
                if (newStartDate.before(pivotDate)) {
                    Date newTime = nextWeek(cal, newStartDate);
                    if (newTime.before(pivotDate)) {
                        newStartDate = newTime;
                        isStartMoved = true;
                    } else {
                        newStartDate = pivotDate;
                        newEndDate = newTime;
                        isStartMoved = false;
                    }
                } else {
                    newEndDate = nextWeek(cal, newEndDate);
                    isStartMoved = false;
                }
                break;
            default : throw new IllegalArgumentException("invalid adjustment action: " + action);
            }
            
            if (!newStartDate.equals(selectionStart) || !newEndDate.equals(selectionEnd)) {
                monthView.setSelectionInterval(newStartDate, newEndDate);
                monthView.ensureDateVisible(isStartMoved ? newStartDate  : newEndDate);
            }

        }

        /**
         * @param cal
         * @param date
         * @return
         */
        private Date nextWeek(Calendar cal, Date date) {
            cal.setTime(date);
            cal.add(Calendar.DAY_OF_MONTH, JXMonthView.DAYS_IN_WEEK);
            return cal.getTime();
        }

        /**
         * @param cal
         * @param date
         * @return
         */
        private Date previousWeek(Calendar cal, Date date) {
            cal.setTime(date);
            cal.add(Calendar.DAY_OF_MONTH, -JXMonthView.DAYS_IN_WEEK);
            return cal.getTime();
        }

        /**
         * @param cal
         * @param date
         * @return
         */
        private Date nextDay(Calendar cal, Date date) {
            cal.setTime(date);
            cal.add(Calendar.DAY_OF_MONTH, 1);
            return cal.getTime();
        }

        /**
         * @param cal
         * @param date
         * @return
         */
        private Date previousDay(Calendar cal, Date date) {
            cal.setTime(date);
            cal.add(Calendar.DAY_OF_MONTH, -1);
            return cal.getTime();
        }
        

    }

//--------------------- zoomable    

    /**
     * Updates state after the monthView's zoomable property has been changed.
     * This implementation adds/removes the header component if zoomable is true/false
     * respectively.
     */
    protected void updateZoomable() {
        if (monthView.isZoomable()) {
            monthView.add(getCalendarHeaderHandler().getHeaderComponent());
        } else {
            monthView.remove(getCalendarHeaderHandler().getHeaderComponent());
        }
        monthView.revalidate();
        monthView.repaint();
    }

    /**
     * Creates and returns a calendar header handler which provides and configures
     * a component for use in a zoomable monthView. Subclasses may override to return
     * a custom handler.<p>
     * 
     * This implementation first queries the UIManager for class to use and returns 
     * that if available, returns a BasicCalendarHeaderHandler if not.
     * 
     * @return a calendar header handler providing a component for use in zoomable
     *   monthView.
     * 
     * @see #getHeaderFromUIManager()  
     * @see CalendarHeaderHandler
     * @see BasicCalendarHeaderHandler  
     */
    protected CalendarHeaderHandler createCalendarHeaderHandler() {
        CalendarHeaderHandler handler = getHeaderFromUIManager();
        return handler != null ? handler : new BasicCalendarHeaderHandler();
    }

    
    /**
     * Returns a CalendarHeaderHandler looked up in the UIManager. This implementation 
     * looks for a String registered with a key of CalendarHeaderHandler.uiControllerID. If
     * found it assumes that the value is the class name of the handler and tries 
     * to instantiate the handler. 
     * 
     * @return a CalendarHeaderHandler from the UIManager or null if none 
     *   available or instantiation failed.
     */
    protected CalendarHeaderHandler getHeaderFromUIManager() {
        Object handlerClass = UIManager.get(CalendarHeaderHandler.uiControllerID);
        if (handlerClass instanceof String) {
            return instantiateClass((String) handlerClass);
        }
        return null;
    }

    /**
     * @param handlerClassName
     * @return
     */
    private CalendarHeaderHandler instantiateClass(String handlerClassName) {
        Class<?> handler = null;
        try {
            handler = Class.forName(handlerClassName);
            return instantiateClass(handler);
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
         return null;
    }

    /**
     * @param handlerClass
     * @return
     */
    private CalendarHeaderHandler instantiateClass(Class<?> handlerClass) {
        Constructor<?> constructor = null; 
        try {
            constructor = handlerClass.getConstructor();
        } catch (SecurityException e) {
            LOG.finer("cant instantiate CalendarHeaderHandler (security) " + handlerClass);
        } catch (NoSuchMethodException e) {
            LOG.finer("cant instantiate CalendarHeaderHandler (missing parameterless constructo?)" + handlerClass);
        }
        if (constructor != null) {
            try {
                return (CalendarHeaderHandler) constructor.newInstance();
            } catch (IllegalArgumentException e) {
                LOG.finer("cant instantiate CalendarHeaderHandler (missing parameterless constructo?)" + handlerClass);
            } catch (InstantiationException e) {
                LOG.finer("cant instantiate CalendarHeaderHandler (not instantiable) " + handlerClass);
            } catch (IllegalAccessException e) {
                LOG.finer("cant instantiate CalendarHeaderHandler (constructor not public) " + handlerClass);
            } catch (InvocationTargetException e) {
                LOG.finer("cant instantiate CalendarHeaderHandler (Invocation target)" + handlerClass);
            }
        }
        return null;
    }

    /**
     * @param calendarHeaderHandler the calendarHeaderHandler to set
     */
    protected void setCalendarHeaderHandler(CalendarHeaderHandler calendarHeaderHandler) {
        this.calendarHeaderHandler = calendarHeaderHandler;
    }
    
    /**
     * @return the calendarHeaderHandler
     */
    protected CalendarHeaderHandler getCalendarHeaderHandler() {
        return calendarHeaderHandler;
    }
    

}
