swingx-master/swingx-core/src/main/java/org/jdesktop/swingx/calendar/SingleDaySelectionModel.java
/*
 * $Id$
 *
 * Copyright 2006 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.calendar;

import java.util.Date;
import java.util.Locale;
import java.util.SortedSet;
import java.util.TreeSet;

import org.jdesktop.swingx.event.DateSelectionEvent.EventType;
import org.jdesktop.swingx.util.Contract;

/**
 * DateSelectionModel which allows a single selection only. <p>
 * 
 * Temporary quick & dirty class to explore requirements as needed by
 * a DatePicker. Need to define the states more exactly. Currently 
 * 
 * <li> takes all Dates as-are (that is the normalized is the same as the given):
 * selected, unselectable, lower/upper bounds
 * <li> interprets any Date between the start/end of day of the selected as selected
 * <li> interprets any Date between the start/end of an unselectable date as unselectable
 * <li> interprets the lower/upper bounds as being the start/end of the given
 * dates, that is any Date after the start of day of the lower and before the end of
 * day of the upper is selectable.
 * 
 * 
 * @author Jeanette Winzenburg
 */
public class SingleDaySelectionModel extends AbstractDateSelectionModel {

    private SortedSet<Date> selectedDates;
    private SortedSet<Date> unselectableDates;
    
    /**
     * Instantiates a SingleDaySelectionModel with default locale.
     */
    public SingleDaySelectionModel() {
        this(null);
    }

    /**
     * Instantiates a SingleSelectionModel with the given locale. If the locale is
     * null, the Locale's default is used.
     * 
     * PENDING JW: fall back to JComponent.getDefaultLocale instead? We use this
     *   with components anyway?
     * 
     * @param locale the Locale to use with this model, defaults to Locale.default()
     *    if null.
     */
    public SingleDaySelectionModel(Locale locale) {
        super(locale);
        this.selectedDates = new TreeSet<Date>();
        this.unselectableDates = new TreeSet<Date>();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SelectionMode getSelectionMode() {
        return SelectionMode.SINGLE_SELECTION;
    }

    /**
     * {@inheritDoc}<p>
     * 
     * Implemented to do nothing.
     * 
     */
    @Override
    public void setSelectionMode(final SelectionMode selectionMode) {
    }


    //---------------------- selection ops    
    /**
     * {@inheritDoc} <p>
     * 
     * Implemented to call setSelectionInterval with startDate for both 
     * parameters.
     */
    @Override
    public void addSelectionInterval(Date startDate, Date endDate) {
        setSelection(startDate);
    }

    /**
     * {@inheritDoc}<p>
     * 
     * PENDING JW: define what happens if we have a selection but the interval
     * isn't selectable. 
     */
    @Override
    public void setSelectionInterval(Date startDate, Date endDate) {
        setSelection(startDate);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeSelectionInterval(Date startDate, Date endDate) {
        Contract.asNotNull(startDate, "date must not be null");
        if (isSelectionEmpty()) return;
        if (isSelectionInInterval(startDate, endDate)) {
            selectedDates.clear();
            fireValueChanged(EventType.DATES_REMOVED);
        }
    }
    
    /**
     * Checks and returns whether the selected date is contained in the interval
     * given by startDate/endDate. The selection must not be empty when 
     * calling this method. <p>
     * 
     * This implementation interprets the interval between the start of the day
     * of startDay to the end of the day of endDate. 
     * 
     * @param startDate the start of the interval, must not be null
     * @param endDate  the end of the interval, must not be null
     * @return true if the selected date is contained in the interval
     */
    protected boolean isSelectionInInterval(Date startDate, Date endDate) {
        if (selectedDates.first().before(startOfDay(startDate)) 
                || (selectedDates.first().after(endOfDay(endDate)))) return false;
        return true;
    }

    /**
     * Selects the given date if it is selectable and not yet selected. 
     * Does nothing otherwise.
     * If this operation changes the current selection, it will fire a 
     * DateSelectionEvent of type DATES_SET.
     * 
     * @param date the Date to select, must not be null. 
     */
    protected void setSelection(Date date) {
        Contract.asNotNull(date, "date must not be null");
        if (isSelectedStrict(date)) return;
        if (isSelectable(date)) {
            selectedDates.clear();
            // PENDING JW: use normalized
            selectedDates.add(date);
            fireValueChanged(EventType.DATES_SET);
        }
    }
    
    /**
     * Checks and returns whether the given date is contained in the selection.
     * This differs from isSelected in that it tests for the exact (normalized)
     * Date instead of for the same day.
     * 
     * @param date the Date to test.
     * @return true if the given date is contained in the selection, 
     *    false otherwise
     * 
     */
    private boolean isSelectedStrict(Date date) {
        if (!isSelectionEmpty()) {
            // PENDING JW: use normalized
            return selectedDates.first().equals(date);
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Date getFirstSelectionDate() {
        return isSelectionEmpty() ? null : selectedDates.first();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Date getLastSelectionDate() {
        return isSelectionEmpty() ? null : selectedDates.last();
    }

    /**
     * Returns a boolean indicating whether the given date is selectable.
     * 
     * @param date the date to check for selectable, must not be null.
     * @return true if the given date is selectable, false if not.
     */
    public  boolean isSelectable(Date date) {
        if (outOfBounds(date)) return false;
        return !inUnselectables(date);
    }

    /**
     * @param date
     * @return
     */
    private boolean inUnselectables(Date date) {
        for (Date unselectable : unselectableDates) {
            if (isSameDay(unselectable, date)) return true;
        }
        return false;
    }

    /**
     * Returns a boolean indication whether the given date is below
     * the lower or above the upper bound. 
     * 
     * @param date
     * @return
     */
    private boolean outOfBounds(Date date) {
        if (belowLowerBound(date)) return true;
        if (aboveUpperBound(date)) return true;
        return false;
    }

    /**
     * @param date
     * @return
     */
    private boolean aboveUpperBound(Date date) {
        if (upperBound != null) {
            return endOfDay(upperBound).before(date);
        }
        return false;
    }

    /**
     * @param date
     * @return
     */
    private boolean belowLowerBound(Date date) {
        if (lowerBound != null) {
           return startOfDay(lowerBound).after(date);
        }
        return false;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void clearSelection() {
        if (isSelectionEmpty()) return;
        selectedDates.clear();
        fireValueChanged(EventType.SELECTION_CLEARED);
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public SortedSet<Date> getSelection() {
        return new TreeSet<Date>(selectedDates);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSelected(Date date) {
        Contract.asNotNull(date, "date must not be null");
        if (isSelectionEmpty()) return false;
        return isSameDay(selectedDates.first(), date);
    }

    

    /**
     * {@inheritDoc}<p>
     * 
     * Implemented to return the date itself.
     */
    @Override
    public Date getNormalizedDate(Date date) {
        return new Date(date.getTime());
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSelectionEmpty() {
        return selectedDates.isEmpty();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public SortedSet<Date> getUnselectableDates() {
        return new TreeSet<Date>(unselectableDates);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setUnselectableDates(SortedSet<Date> unselectables) {
        Contract.asNotNull(unselectables, "unselectable dates must not be null");
        this.unselectableDates.clear();
        for (Date unselectableDate : unselectables) {
            removeSelectionInterval(unselectableDate, unselectableDate);
            unselectableDates.add(unselectableDate);
        }
        fireValueChanged(EventType.UNSELECTED_DATES_CHANGED);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isUnselectableDate(Date date) {
        return !isSelectable(date);
    }





}