swingx-master/swingx-core/src/main/java/org/jdesktop/swingx/plaf/basic/BasicCalendarRenderingHandler.java
package org.jdesktop.swingx.plaf.basic;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JLabel;

import org.jdesktop.swingx.JXMonthView;
import org.jdesktop.swingx.decorator.AbstractHighlighter;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.CompoundHighlighter;
import org.jdesktop.swingx.decorator.HighlightPredicate;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.decorator.PainterHighlighter;
import org.jdesktop.swingx.plaf.UIManagerExt;
import org.jdesktop.swingx.renderer.CellContext;
import org.jdesktop.swingx.renderer.ComponentProvider;
import org.jdesktop.swingx.renderer.FormatStringValue;
import org.jdesktop.swingx.renderer.LabelProvider;
import org.jdesktop.swingx.renderer.StringValue;
import org.jdesktop.swingx.renderer.StringValues;

/**
 * The RenderingHandler responsible for text rendering. It provides 
 * and configures a rendering component for the given cell of
 * a JXMonthView. <p>
 * 
 * Note: exposing the createXXStringValue methods is an emergency workaround for
 * Issue #1062-swingx (core doesn't use arabic digits where appropriate) to allow
 * subclasses to do better than core. So beware of future changes!
 * 
 */
class BasicCalendarRenderingHandler implements CalendarRenderingHandler {
    /** The CellContext for content and default visual config. */
    private CalendarCellContext cellContext;
    /** The providers to use per DayState. */
    private Map<CalendarState, ComponentProvider<?>> providers;
    //-------- Highlight properties
    /** The Painter used for highlighting unselectable dates. */
    private TextCrossingPainter<?> textCross;
    /** The foreground color for unselectable date highlight. */
    private Color unselectableDayForeground;

    /**
     * Instantiates a RenderingHandler and installs default state.
     */
    public BasicCalendarRenderingHandler() {
        install();
    }
    
    private void install() {
        unselectableDayForeground = UIManagerExt.getColor("JXMonthView.unselectableDayForeground");
        textCross = new TextCrossingPainter<JLabel>();
        cellContext = new CalendarCellContext();
        installProviders();
    }

    /**
     * Creates and stores ComponentProviders for all DayStates.
     */
    private void installProviders() {
        providers = new HashMap<CalendarState, ComponentProvider<?>>();

        StringValue sv = createDayStringValue(null);
        ComponentProvider<?> provider = new LabelProvider(sv, JLabel.RIGHT);
        providers.put(CalendarState.IN_MONTH, provider);
        providers.put(CalendarState.TODAY, provider);
        providers.put(CalendarState.TRAILING, provider);
        providers.put(CalendarState.LEADING, provider);

        StringValue wsv = createWeekOfYearStringValue(null);
        ComponentProvider<?> weekOfYearProvider = new LabelProvider(wsv,
                JLabel.RIGHT);
        providers.put(CalendarState.WEEK_OF_YEAR, weekOfYearProvider);

        ComponentProvider<?> dayOfWeekProvider = new LabelProvider(JLabel.CENTER) {

            @Override
            protected String getValueAsString(CellContext context) {
                Object value = context.getValue();
                // PENDING JW: this is breaking provider's contract in its
                // role as StringValue! Don't in the general case.
                if (value instanceof Calendar) {
                    int day = ((Calendar) value).get(Calendar.DAY_OF_WEEK);
                    return ((JXMonthView) context.getComponent()).getDayOfTheWeek(day);
                }
                return super.getValueAsString(context);
            }
            
        };
        providers.put(CalendarState.DAY_OF_WEEK, dayOfWeekProvider);

        StringValue tsv = createMonthHeaderStringValue(null);
        ComponentProvider<?> titleProvider = new LabelProvider(tsv,
                JLabel.CENTER);
        providers.put(CalendarState.TITLE, titleProvider);
    }

    /**
     * Creates and returns a StringValue used for rendering the title of a month box.
     * The input they are assumed to handle is a Calendar configured to a day of
     * the month to render.
     * 
     * @param locale the Locale to use, might be null to indicate usage of the default
     *   Locale
     * @return a StringValue appropriate for rendering month title.
     */
    protected StringValue createMonthHeaderStringValue(Locale locale) {
        if (locale == null) {
            locale = Locale.getDefault();
        }
        final String[] monthNames = DateFormatSymbols.getInstance(locale).getMonths();
        StringValue tsv = new StringValue() {

            @Override
            public String getString(Object value) {
                if (value instanceof Calendar) {
                    String month = monthNames[((Calendar) value)
                            .get(Calendar.MONTH)];
                    return month + " "
                            + ((Calendar) value).get(Calendar.YEAR); 
                }
                return StringValues.TO_STRING.getString(value);
            }

        };
        return tsv;
    }

    /**
     * Creates and returns a StringValue used for rendering the week of year.
     * The input they are assumed to handle is a Calendar configured to a day of
     * the week to render.
     * 
     * @param locale the Locale to use, might be null to indicate usage of the default
     *   Locale
     * @return a StringValue appropriate for rendering week of year.
     */
    protected StringValue createWeekOfYearStringValue(Locale locale) {
        StringValue wsv = new StringValue() {

            @Override
            public String getString(Object value) {
                if (value instanceof Calendar) {
                    value = ((Calendar) value).get(Calendar.WEEK_OF_YEAR);
                }
                return StringValues.TO_STRING.getString(value);
            }

        };
        return wsv;
    }

    /**
     * Creates and returns a StringValue used for rendering days in a month.
     * The input they are assumed to handle is a Calendar configured to the day.
     * 
     * @param locale the Locale to use, might be null to indicate usage of the default
     *   Locale
     * @return a StringValue appropriate for rendering days in a month
     */
    protected StringValue createDayStringValue(Locale locale) {
        if (locale == null) {
            locale = Locale.getDefault();
        }
        FormatStringValue sv = new FormatStringValue(new SimpleDateFormat("d", locale)) {

            @Override
            public String getString(Object value) {
                if (value instanceof Calendar) {
                    ((DateFormat) getFormat()).setTimeZone(((Calendar) value).getTimeZone());
                    value = ((Calendar) value).getTime();
                }
                return super.getString(value);
            }

        };
        return sv;
    }
    

    /**
     * Updates internal state to the given Locale.
     * 
     * @param locale the new Locale.
     */
    @Override
    public void setLocale(Locale locale) {
        StringValue dayValue = createDayStringValue(locale);
        providers.get(CalendarState.IN_MONTH).setStringValue(dayValue);
        providers.get(CalendarState.TODAY).setStringValue(dayValue);
        providers.get(CalendarState.TRAILING).setStringValue(dayValue);
        providers.get(CalendarState.LEADING).setStringValue(dayValue);
        
        providers.get(CalendarState.WEEK_OF_YEAR).setStringValue(createWeekOfYearStringValue(locale));
        providers.get(CalendarState.TITLE).setStringValue(createMonthHeaderStringValue(locale));
    }

    /**
     * Configures and returns a component for rendering of the given monthView cell.
     * 
     * @param monthView the JXMonthView to render onto
     * @param calendar the cell value
     * @param dayState the DayState of the cell
     * @return a component configured for rendering the given cell
     */
    @Override
    public JComponent prepareRenderingComponent(JXMonthView monthView, Calendar calendar, CalendarState dayState) {
        cellContext.installContext(monthView, calendar, 
                isSelected(monthView, calendar, dayState), 
                isFocused(monthView, calendar, dayState),
                dayState);
        JComponent comp = providers.get(dayState).getRendererComponent(cellContext);
        return highlight(comp, monthView, calendar, dayState);
    }


    /**
     * 
     * NOTE: it's the responsibility of the CalendarCellContext to detangle
     * all "default" (that is: which could be queried from the comp and/or UIManager)
     * foreground/background colors based on the given state! Moved out off here.
     * <p>
     * PENDING JW: replace hard-coded logic by giving over to highlighters.
     * 
     * @param monthView the JXMonthView to render onto
     * @param calendar the cell value
     * @param dayState the DayState of the cell
     * @param dayState
     */
    private JComponent highlight(JComponent comp, JXMonthView monthView,
            Calendar calendar, CalendarState dayState) {
        CalendarAdapter adapter = getCalendarAdapter(monthView, calendar, dayState);
        return (JComponent) getHighlighter().highlight(comp, adapter);
    }

    /**
     * @return
     */
    private Highlighter getHighlighter() {
        if (highlighter == null) {
            highlighter = new CompoundHighlighter();
            installHighlighters();
        }
        return highlighter;
    }

    /**
     * 
     */
    private void installHighlighters() {
        HighlightPredicate boldPredicate = new HighlightPredicate() {

            @Override
            public boolean isHighlighted(Component renderer,
                    ComponentAdapter adapter) {
                if (!(adapter instanceof CalendarAdapter))
                    return false;
                CalendarAdapter ca = (CalendarAdapter) adapter;
                return CalendarState.DAY_OF_WEEK == ca.getCalendarState() || 
                    CalendarState.TITLE == ca.getCalendarState();
            }
            
        };
        Highlighter font = new AbstractHighlighter(boldPredicate) {

            @Override
            protected Component doHighlight(Component component,
                    ComponentAdapter adapter) {
                component.setFont(getDerivedFont(component.getFont()));
                return component;
            }
            
        };
        highlighter.addHighlighter(font);
        
        HighlightPredicate unselectable = new HighlightPredicate() {

            @Override
            public boolean isHighlighted(Component renderer,
                    ComponentAdapter adapter) {
                if (!(adapter instanceof CalendarAdapter)) 
                    return false;
                return ((CalendarAdapter) adapter).isUnselectable();
            }
            
        };
        textCross.setForeground(unselectableDayForeground);
        Highlighter painterHL = new PainterHighlighter(unselectable, textCross);
        highlighter.addHighlighter(painterHL);
        
    }

    /**
     * @param monthView
     * @param calendar
     * @param dayState
     * @return
     */
    private CalendarAdapter getCalendarAdapter(JXMonthView monthView,
            Calendar calendar, CalendarState dayState) {
        if (calendarAdapter == null) {
            calendarAdapter = new CalendarAdapter(monthView);
        }
        return calendarAdapter.install(calendar, dayState);
    }

    private CalendarAdapter calendarAdapter;
    private CompoundHighlighter highlighter;
    
    /**
     * @param font
     * @return
     */
    private Font getDerivedFont(Font font) {
        return font.deriveFont(Font.BOLD);
    }

    /**
     * @param monthView
     * @param calendar
     * @param dayState
     * @return
     */
    private boolean isFocused(JXMonthView monthView, Calendar calendar,
            CalendarState dayState) {
        return false;
    }
    
    /**
     * @param monthView the JXMonthView to render onto
     * @param calendar the cell value
     * @param dayState the DayState of the cell
     * @return
     */
    private boolean isSelected(JXMonthView monthView, Calendar calendar,
            CalendarState dayState) {
        if (!isSelectable(dayState)) return false;
        return monthView.isSelected(calendar.getTime());
    }

    
    /**
     * @param dayState
     * @return
     */
    private boolean isSelectable(CalendarState dayState) {
        return (CalendarState.IN_MONTH == dayState) || (CalendarState.TODAY == dayState);
    }

}