/*
 * RHQ Management Platform
 * Copyright (C) 2005-2011 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.gui.coregui.client.components.carousel;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.gwt.event.dom.client.KeyCodes;
import com.smartgwt.client.data.Criteria;
import com.smartgwt.client.types.Overflow;
import com.smartgwt.client.types.SelectionStyle;
import com.smartgwt.client.types.VerticalAlignment;
import com.smartgwt.client.util.BooleanCallback;
import com.smartgwt.client.util.SC;
import com.smartgwt.client.widgets.Canvas;
import com.smartgwt.client.widgets.HTMLFlow;
import com.smartgwt.client.widgets.IButton;
import com.smartgwt.client.widgets.Img;
import com.smartgwt.client.widgets.Label;
import com.smartgwt.client.widgets.events.ClickEvent;
import com.smartgwt.client.widgets.events.ClickHandler;
import com.smartgwt.client.widgets.events.DoubleClickHandler;
import com.smartgwt.client.widgets.form.fields.FormItem;
import com.smartgwt.client.widgets.form.fields.HiddenItem;
import com.smartgwt.client.widgets.form.fields.SelectItem;
import com.smartgwt.client.widgets.form.fields.SpinnerItem;
import com.smartgwt.client.widgets.form.fields.TextItem;
import com.smartgwt.client.widgets.form.fields.events.ChangedEvent;
import com.smartgwt.client.widgets.form.fields.events.ChangedHandler;
import com.smartgwt.client.widgets.form.fields.events.KeyPressEvent;
import com.smartgwt.client.widgets.form.fields.events.KeyPressHandler;
import com.smartgwt.client.widgets.layout.HLayout;
import com.smartgwt.client.widgets.layout.LayoutSpacer;
import com.smartgwt.client.widgets.menu.IMenuButton;
import com.smartgwt.client.widgets.menu.MenuItem;
import com.smartgwt.client.widgets.menu.events.MenuItemClickEvent;

import org.rhq.core.domain.search.SearchSubsystem;
import org.rhq.enterprise.gui.coregui.client.CoreGUI;
import org.rhq.enterprise.gui.coregui.client.RefreshableView;
import org.rhq.enterprise.gui.coregui.client.components.buttons.BackButton;
import org.rhq.enterprise.gui.coregui.client.components.form.SearchBarItem;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableDynamicForm;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableHLayout;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableIButton;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableIMenuButton;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableMenu;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableToolStrip;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableVLayout;
import org.rhq.enterprise.gui.coregui.client.util.selenium.SeleniumUtility;

/**
 * Similar to (i.e. originally a copy of) Table but instead of encapsulating a ListGrid, it manages a list of 
 * {@link CarouselMember}s, offering horizontal presentation of the member canvases, high level filtering, and
 * other member-wide handling. See {@link BookmarkableCarousel} for a subclass providing master-detail support.
 * 
 * @author Jay Shaughnessy
 */
@SuppressWarnings("unchecked")
public abstract class Carousel extends LocatableHLayout implements RefreshableView {

    private static final String FILTER_CAROUSEL_START = "CarouselStart";
    private static final String FILTER_CAROUSEL_SIZE = "CarouselSize";

    private LocatableVLayout contents;

    private HLayout titleLayout;
    private Canvas titleComponent;
    private HTMLFlow titleCanvas;
    private String titleString;
    private List<String> titleIcons = new ArrayList<String>();
    private BackButton titleBackButton;

    private Canvas carouselDetails;

    private CarouselFilter filterForm;

    private LocatableHLayout carouselHolder;

    private Label carouselInfo;

    private boolean showTitle = true;
    private boolean showFooter = true;
    private boolean showFooterRefresh = true;
    private boolean showFilterForm = true;

    private Criteria initialCriteria;
    private boolean initialCriteriaFixed = true;
    private boolean hideSearchBar = false;
    private String initialSearchBarSearchText = null;

    private List<CarouselActionInfo> carouselActions = new ArrayList<CarouselActionInfo>();
    private boolean carouselActionDisableOverride = false;
    protected List<Canvas> extraWidgetsAboveFooter = new ArrayList<Canvas>();
    protected List<Canvas> extraWidgetsInMainFooter = new ArrayList<Canvas>();
    private LocatableToolStrip footer;
    private LocatableToolStrip footerExtraWidgets;
    private LocatableIButton refreshButton;
    private LocatableIButton nextButton;
    private LocatableIButton previousButton;
    private LocatableIButton widthButton;

    // null means no start filter, start with highest 
    private Integer carouselStartFilter = null;
    // null means no end filter, end with lowest 
    private Integer carouselEndFilter = null;

    // null or < 0 indicates no limit to the carousel size. A size limit is always recommended to avoid unbounded view 
    private Integer carouselSizeFilter = getDefaultCarouselSize();
    private boolean carouselUsingFixedWidths = false;

    public Carousel(String locatorId) {
        this(locatorId, null, null);
    }

    public Carousel(String locatorId, String titleString) {
        this(locatorId, titleString, null);
    }

    public Carousel(String locatorId, String titleString, Criteria criteria) {
        super(locatorId);

        setWidth100();
        setHeight100();
        setOverflow(Overflow.HIDDEN);

        this.titleString = titleString;
        this.initialCriteria = criteria;
    }

    /**
     * If this returns true, then even if a {@link #getSearchSubsystem() search subsystem}
     * is defined by the class, the search bar will not be shown.
     * 
     * @return true if the search bar is to be hidden (default is false)
     */
    public boolean getHideSearchBar() {
        return this.hideSearchBar;
    }

    public void setHideSearchBar(boolean flag) {
        this.hideSearchBar = flag;
    }

    public String getInitialSearchBarSearchText() {
        return this.initialSearchBarSearchText;
    }

    public void setInitialSearchBarSearchText(String text) {
        this.initialSearchBarSearchText = text;
    }

    @Override
    protected void onInit() {
        super.onInit();

        contents = new LocatableVLayout(extendLocatorId("Contents"));
        contents.setWidth100();
        contents.setHeight100();

        addMember(contents);

        filterForm = new CarouselFilter(this);

        /*
         * carousel filters and search bar are currently mutually exclusive
         */

        if (getSearchSubsystem() == null) {
            configureCarouselFilters();

        } else {
            if (!this.hideSearchBar) {
                final SearchBarItem searchFilter = new SearchBarItem("search", MSG.common_button_search(),
                    getSearchSubsystem(), getInitialSearchBarSearchText());
                setFilterFormItems(searchFilter);
            }
        }

        carouselHolder = new LocatableHLayout(extendLocatorId("Holder"));
        carouselHolder.setOverflow(Overflow.AUTO);
        carouselHolder.setWidth100();
        contents.addMember(carouselHolder);
    }

    protected SelectionStyle getDefaultSelectionStyle() {
        return SelectionStyle.MULTIPLE;
    }

    @Override
    protected void onDraw() {
        try {
            super.onDraw();

            for (Canvas child : contents.getMembers()) {
                contents.removeChild(child);
            }

            // Title
            this.titleCanvas = new HTMLFlow();
            setTitleString(this.titleString);

            if (showTitle) {
                titleLayout = new LocatableHLayout(contents.extendLocatorId("Title"));
                titleLayout.setAutoHeight();
                titleLayout.setAlign(VerticalAlignment.BOTTOM);
                titleLayout.setMembersMargin(4);
                contents.addMember(titleLayout, 0);
            }

            if (null != carouselDetails) {
                contents.addMember(carouselDetails);
            }

            if (filterForm.hasContent()) {
                contents.addMember(filterForm);
            }

            contents.addMember(carouselHolder);

            // Footer

            // A second toolstrip that optionally appears before the main footer - it will contain extra widgets.
            // This is hidden from view unless extra widgets are actually added to the carousel above the main footer.
            this.footerExtraWidgets = new LocatableToolStrip(contents.extendLocatorId("FooterExtraWidgets"));
            footerExtraWidgets.setPadding(5);
            footerExtraWidgets.setWidth100();
            footerExtraWidgets.setMembersMargin(15);
            footerExtraWidgets.hide();
            contents.addMember(footerExtraWidgets);

            this.footer = new LocatableToolStrip(contents.extendLocatorId("Footer"));
            footer.setPadding(5);
            footer.setWidth100();
            footer.setMembersMargin(15);
            contents.addMember(footer);

            // The ListGrid has been created and configured
            // Now give subclasses a chance to configure the carousel
            configureCarousel();

            Label carouselInfo = new Label();
            carouselInfo.setWrap(false);
            setCarouselInfo(carouselInfo);

            if (showTitle) {
                drawTitle();
            }

            if (showFooter) {
                drawFooter();
            }
        } catch (Exception e) {
            CoreGUI.getErrorHandler().handleError(MSG.view_table_drawFail(this.toString()), e);
        }
    }

    @Override
    public void destroy() {
        SeleniumUtility.destroyMembers(contents);
        super.destroy();
    }

    private void drawTitle() {
        for (String headerIcon : titleIcons) {
            Img img = new Img(headerIcon, 24, 24);
            img.setPadding(4);
            titleLayout.addMember(img);
        }

        if (null != titleBackButton) {
            titleLayout.addMember(titleBackButton);
        }

        titleLayout.addMember(titleCanvas);

        if (titleComponent != null) {
            titleLayout.addMember(new LayoutSpacer());
            titleLayout.addMember(titleComponent);
        }
    }

    private void drawFooter() {
        // populate the extraWidgets toolstrip
        footerExtraWidgets.removeMembers(footerExtraWidgets.getMembers());
        if (!extraWidgetsAboveFooter.isEmpty()) {
            for (Canvas extraWidgetCanvas : extraWidgetsAboveFooter) {
                footerExtraWidgets.addMember(extraWidgetCanvas);
            }
            footerExtraWidgets.show();
        }

        footer.removeMembers(footer.getMembers());

        for (final CarouselActionInfo carouselAction : carouselActions) {

            if (null == carouselAction.getValueMap()) {
                // button action
                IButton button = new LocatableIButton(carouselAction.getLocatorId(), carouselAction.getTitle());
                button.setDisabled(true);
                button.setOverflow(Overflow.VISIBLE);
                button.addClickHandler(new ClickHandler() {
                    public void onClick(ClickEvent clickEvent) {
                        disableAllFooterControls();
                        if (carouselAction.confirmMessage != null) {
                            String message = carouselAction.confirmMessage.replaceAll("\\#", "TODO:");

                            SC.ask(message, new BooleanCallback() {
                                public void execute(Boolean confirmed) {
                                    if (confirmed) {
                                        carouselAction.action.executeAction(null);
                                    } else {
                                        refreshCarouselInfo();
                                    }
                                }
                            });
                        } else {
                            carouselAction.action.executeAction(null);
                        }
                    }
                });

                carouselAction.actionCanvas = button;
                footer.addMember(button);

            } else {
                // menu action
                LocatableMenu menu = new LocatableMenu(carouselAction.getLocatorId() + "Menu");
                final Map<String, ? extends Object> menuEntries = carouselAction.getValueMap();
                for (final String key : menuEntries.keySet()) {
                    MenuItem item = new MenuItem(key);
                    item.addClickHandler(new com.smartgwt.client.widgets.menu.events.ClickHandler() {
                        public void onClick(MenuItemClickEvent event) {
                            disableAllFooterControls();
                            carouselAction.getAction().executeAction(menuEntries.get(key));
                        }
                    });
                    menu.addItem(item);
                }

                IMenuButton menuButton = new LocatableIMenuButton(carouselAction.getLocatorId(), carouselAction
                    .getTitle());
                menuButton.setMenu(menu);
                menuButton.setDisabled(true);
                menuButton.setAutoFit(true); // this makes it pretty tight, but maybe better than the default, which is pretty wide
                menuButton.setOverflow(Overflow.VISIBLE);
                menuButton.setShowMenuBelow(false);

                carouselAction.actionCanvas = menuButton;
                footer.addMember(menuButton);
            }
        }

        for (Canvas extraWidgetCanvas : extraWidgetsInMainFooter) {
            footer.addMember(extraWidgetCanvas);
        }

        footer.addMember(new LayoutSpacer());

        widthButton = new LocatableIButton(extendLocatorId("Width"), MSG.common_button_fixedWidth());
        widthButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent clickEvent) {
                carouselUsingFixedWidths = !carouselUsingFixedWidths;
                widthButton.setTitle(carouselUsingFixedWidths ? MSG.common_button_scaleToFit() : MSG
                    .common_button_fixedWidth());
                buildCarousel(true);
            }
        });
        footer.addMember(widthButton);

        if (isShowFooterRefresh()) {
            this.refreshButton = new LocatableIButton(extendLocatorId("Refresh"), MSG.common_button_refresh());
            refreshButton.addClickHandler(new ClickHandler() {
                public void onClick(ClickEvent clickEvent) {
                    disableAllFooterControls();
                    refresh();
                }
            });
            footer.addMember(refreshButton);
        }

        previousButton = new LocatableIButton(extendLocatorId("Previous"), MSG.common_button_previous());
        previousButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent clickEvent) {
                disableAllFooterControls();
                previous();
            }
        });
        footer.addMember(previousButton);

        nextButton = new LocatableIButton(extendLocatorId("Next"), MSG.common_button_next());
        nextButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent clickEvent) {
                disableAllFooterControls();
                next();
            }
        });
        footer.addMember(nextButton);

        footer.addMember(carouselInfo);

        // Ensure buttons are initially set correctly.
        refreshCarouselInfo();
    }

    private void previous() {
        if (null == carouselSizeFilter) {
            carouselSizeFilter = getDefaultCarouselSize();
        }

        if (null != carouselStartFilter) {
            int newEnd = carouselStartFilter + 1;
            setCarouselEndFilter(newEnd);

            // it's ok if this is higher than the current max, the actual fetch will make sure the
            // values are sane.
            setCarouselStartFilter(carouselStartFilter + carouselSizeFilter);
        }

        buildCarousel(true);
    }

    private void next() {
        if (null == carouselSizeFilter) {
            carouselSizeFilter = getDefaultCarouselSize();
        }

        if (null != carouselEndFilter) {
            int newStart = carouselEndFilter - 1;
            newStart = (newStart < carouselSizeFilter) ? carouselSizeFilter : newStart;
            setCarouselStartFilter(newStart);
        }

        setCarouselEndFilter(null);

        buildCarousel(true);
    }

    protected abstract int getDefaultCarouselSize();

    protected abstract String getCarouselMemberFixedWidth();

    protected abstract String getCarouselStartFilterLabel();

    protected abstract String getCarouselSizeFilterLabel();

    private void disableAllFooterControls() {
        for (CarouselActionInfo action : carouselActions) {
            action.actionCanvas.disable();
        }
        for (Canvas extraWidget : extraWidgetsAboveFooter) {
            extraWidget.disable();
        }
        for (Canvas extraWidget : extraWidgetsInMainFooter) {
            extraWidget.disable();
        }
        if (isShowFooterRefresh() && this.refreshButton != null) {
            this.refreshButton.disable();
        }
    }

    /**
     * Subclasses can use this as a chance to configure the list grid after it has been
     * created but before it has been drawn to the DOM. This is also the proper place to add
     * actions so that they're rendered in the footer.
     */
    protected void configureCarousel() {
        return;
    }

    /**
     * If not overriden this will append the standard carousel filters to those already supplied. To ensure
     * the supplied form items end a row, call {@link FormItem#setEndRow(Boolean)} as needed. The filter form is 
     * set to use 6 columns, which allows the carousle filters to fit on one row.
     * @param formItems
     */
    public void setFilterFormItems(FormItem... formItems) {
        // since Arrays.copyOf is unsupported...
        FormItem[] carouselFormItems = new FormItem[2 + formItems.length];
        int i = 0;
        for (FormItem item : formItems) {
            carouselFormItems[i++] = item;
        }

        // drift file path filter
        SpinnerItem startFilter = new SpinnerItem(FILTER_CAROUSEL_START, getCarouselStartFilterLabel());
        startFilter.setMin(1);
        //TextItem startFilter = new TextItem(FILTER_CAROUSEL_START, getCarouselStartFilterLabel());
        TextItem numFilter = new TextItem(FILTER_CAROUSEL_SIZE, getCarouselSizeFilterLabel());
        carouselFormItems[i++] = startFilter;
        carouselFormItems[i++] = numFilter;

        this.filterForm.setNumCols(4);
        this.filterForm.setItems(carouselFormItems);
    }

    /**
     * Overriding components can use this as a chance to add {@link FormItem}s which will filter
     * the carousel members that display their data. If not overriden the standard carousel filters are applied.
     */
    protected void configureCarouselFilters() {
        setFilterFormItems();
    }

    /**
     * Set the Carousel's title string. This will subsequently call {@link #updateTitleCanvas(String)}.
     * @param titleString
     */
    public void setTitleString(String titleString) {
        this.titleString = titleString;
        if (this.titleCanvas != null) {
            updateTitleCanvas(titleString);
        }
    }

    public Canvas getTitleCanvas() {
        return this.titleCanvas;
    }

    /**
     * To set the Carousel's title, call {@link #setTitleString(String)}. This is primarily declared for purposes of
     * override.
     * @param titleString
     */
    public void updateTitleCanvas(String titleString) {
        if (titleString == null) {
            titleString = "";
        }
        if (titleString.length() > 0) {
            titleCanvas.setWidth100();
            titleCanvas.setHeight(35);
            titleCanvas.setContents(titleString);
            titleCanvas.setPadding(4);
            titleCanvas.setStyleName("HeaderLabel");
        } else {
            titleCanvas.setWidth100();
            titleCanvas.setHeight(0);
            titleCanvas.setContents(null);
            titleCanvas.setPadding(0);
            titleCanvas.setStyleName("normal");
        }

        titleCanvas.markForRedraw();
    }

    /**
     * Returns the encompassing canvas that contains all content for this component.
     * This content includes the carousel members, the buttons, etc.
     */
    public Canvas getCarouselContents() {
        return this.contents;
    }

    public boolean isShowTitle() {
        return showTitle;
    }

    public void setShowTitle(boolean showTitle) {
        this.showTitle = showTitle;
    }

    public boolean isShowFooter() {
        return showFooter;
    }

    public void setShowFooter(boolean showFooter) {
        this.showFooter = showFooter;
    }

    protected boolean isInitialCriteriaFixed() {
        return initialCriteriaFixed;
    }

    /**
     * @param initialCriteriaFixed If true initialCriteria is applied to all subsequent fetch criteria. If false
     * initialCriteria is used only for the initial autoFetch. Irrelevant if autoFetch is false. Default is true.
     */
    protected void setInitialCriteriaFixed(boolean initialCriteriaFixed) {
        this.initialCriteriaFixed = initialCriteriaFixed;
    }

    /**
     *
     * @return the current criteria, which includes any fixed criteria, as well as any user-specified filters; may be
     *         null if there are no fixed criteria or user-specified filters
     */
    protected Criteria getCurrentCriteria() {
        Criteria criteria = null;

        // If this carousel has a filter form (filters OR search bar),
        // we need to refresh it as per the filtering, combined with any fixed criteria.
        if (this.filterForm != null && this.filterForm.hasContent()) {

            criteria = this.filterForm.getValuesAsCriteria();

            if (this.initialCriteriaFixed) {
                if (criteria != null) {
                    if (this.initialCriteria != null) {
                        // There is fixed criteria - add it to the filter form criteria.
                        addCriteria(criteria, this.initialCriteria);
                    }
                } else {
                    criteria = this.initialCriteria;
                }
            }
        } else if (this.initialCriteriaFixed) {

            criteria = this.initialCriteria;
        }

        return criteria;
    }

    //TODO move to a utility
    // SmartGWT 2.4's version of Criteria.addCriteria for some reason doesn't have else clauses for the array types
    // and it doesn't handle Object types properly (seeing odd behavior because of this), so this method explicitly
    // supports adding array types and Objects.
    // This method takes the src criteria and adds it to the dest criteria.
    public static void addCriteria(Criteria dest, Criteria src) {
        Map otherMap = src.getValues();
        Set otherKeys = otherMap.keySet();
        for (Iterator i = otherKeys.iterator(); i.hasNext();) {
            String field = (String) i.next();
            Object value = otherMap.get(field);

            if (value instanceof Integer) {
                dest.addCriteria(field, (Integer) value);
            } else if (value instanceof Float) {
                dest.addCriteria(field, (Float) value);
            } else if (value instanceof String) {
                dest.addCriteria(field, (String) value);
            } else if (value instanceof Date) {
                dest.addCriteria(field, (Date) value);
            } else if (value instanceof Boolean) {
                dest.addCriteria(field, (Boolean) value);
            } else if (value instanceof Integer[]) {
                dest.addCriteria(field, (Integer[]) value);
            } else if (value instanceof Double[]) {
                dest.addCriteria(field, (Double[]) value);
            } else if (value instanceof String[]) {
                dest.addCriteria(field, (String[]) value);
            } else {
                // this is the magic piece - we need to get attrib as an object and set that value
                dest.setAttribute(field, src.getAttributeAsObject(field));
            }
        }
    }

    public void setCarouselDetails(Canvas carouselDetails) {
        this.carouselDetails = carouselDetails;
    }

    public Canvas getCarouselDetails() {
        return carouselDetails;
    }

    public void setTitleComponent(Canvas canvas) {
        this.titleComponent = canvas;
    }

    /**
     * Note: To prevent user action while a current action completes, all widgets on the footer are disabled
     * when footer actions take place, typically a button click.  It is up to the action to ensure the page
     * (via refresh() or CoreGUI.refresh()) or footer (via refreshCarouselActions) are refreshed as needed at action
     * completion. Failure to do so may leave the widgets disabled.
     */
    public void addCarouselAction(String locatorId, String title, CarouselAction action) {
        this.addCarouselAction(locatorId, title, null, null, action);
    }

    /**
     * Note: To prevent user action while a current action completes, all widgets on the footer are disabled
     * when footer actions take place, typically a button click.  It is up to the action to ensure the page
     * (via refresh() or CoreGUI.refresh()) or footer (via refreshCarouselActions) are refreshed as needed at action
     * completion. Failure to do so may leave the widgets disabled.
     */
    public void addCarouselAction(String locatorId, String title, String confirmation, CarouselAction action) {
        this.addCarouselAction(locatorId, title, confirmation, null, action);
    }

    /**
     * Note: To prevent user action while a current action completes, all widgets on the footer are disabled
     * when footer actions take place, typically a button click.  It is up to the action to ensure the page
     * (via refresh() or CoreGUI.refresh()) or footer (via refreshCarouselActions) are refreshed as needed at action
     * completion. Failure to do so may leave the widgets disabled.
     */
    public void addCarouselAction(String locatorId, String title, String confirmation,
        LinkedHashMap<String, ? extends Object> valueMap, CarouselAction action) {
        // If the specified locator ID is qualified, strip off the ancestry prefix, so we can make sure its locator ID
        // extends the footer's locator ID as it should.
        int underscoreIndex = locatorId.lastIndexOf('_');
        String unqualifiedLocatorId;
        if (underscoreIndex >= 0 && underscoreIndex != (locatorId.length() - 1)) {
            unqualifiedLocatorId = locatorId.substring(underscoreIndex + 1);
        } else {
            unqualifiedLocatorId = locatorId;
        }
        CarouselActionInfo info = new CarouselActionInfo(this.footer.extendLocatorId(unqualifiedLocatorId), title,
            confirmation, valueMap, action);
        carouselActions.add(info);
    }

    public void addCarouselMember(Canvas member) {
        member.setWidth(carouselUsingFixedWidths ? getCarouselMemberFixedWidth() : "*");
        this.carouselHolder.addMember(member);
    }

    public void setListGridDoubleClickHandler(DoubleClickHandler handler) {
        //doubleClickHandler = handler;
    }

    /**
     * Adds extra widgets to the bottom of the carousel view.
     * <br/><br/>
     * Note: To prevent user action while a current action completes, all widgets on the footer are disabled
     * when footer actions take place, typically a button click.  It is up to the action to ensure the page
     * (via refresh() or CoreGUI.refresh()) or footer (via refreshCarouselActions) are refreshed as needed at action
     * completion. Failure to do so may leave the widgets disabled.
     *
     * @param widget the new widget to add to the view
     * @param aboveFooter if true, the widget will be placed in a second toolstrip just above the main footer.
     *                    if false, the widget will be placed in the main footer toolstrip itself. This is
     *                    useful if the widget is really big and won't fit in the main footer along with the
     *                    rest of the main footer members.
     */
    public void addExtraWidget(Canvas widget, boolean aboveFooter) {
        if (aboveFooter) {
            this.extraWidgetsAboveFooter.add(widget);
        } else {
            this.extraWidgetsInMainFooter.add(widget);
        }
    }

    public void setHeaderIcon(String headerIcon) {
        if (this.titleIcons.size() > 0) {
            this.titleIcons.clear();
        }
        addHeaderIcon(headerIcon);
    }

    public void addHeaderIcon(String headerIcon) {
        this.titleIcons.add(headerIcon);
    }

    public void setTitleBackButton(BackButton backButton) {
        this.titleBackButton = backButton;
    }

    /**
     * By default, all actions have buttons that are enabled or
     * disabled based on if and how many rows are selected. There are
     * times when you don't want the user to be able to press action
     * buttons regardless of which rows are selected. This method let's
     * you set this override-disable flag.
     * 
     * Note: this also effects the double-click handler - if this disable override
     * is on, the double-click handler is not called.
     * 
     * @param disabled if true, all action buttons will be disabled
     *                 if false, action buttons will be enabled based on their predefined
     *                 selection enablement rule.
     */
    public void setCarouselActionDisableOverride(boolean disabled) {
        this.carouselActionDisableOverride = disabled;
        refreshCarouselInfo();
    }

    public boolean getCarouselActionDisableOverride() {
        return this.carouselActionDisableOverride;
    }

    /**
     * Refreshes the members, filtered by any fixed criteria, as well as any user-specified filters.
     */
    public void refresh() {
        Criteria criteria = getCurrentCriteria();

        Map<String, Object> criteriaMap = (criteria != null) ? criteria.getValues() : Collections
            .<String, Object> emptyMap();

        try {
            carouselSizeFilter = Integer.valueOf((String) criteriaMap.get(FILTER_CAROUSEL_SIZE));
        } catch (Exception e) {
            carouselSizeFilter = null;
        }

        try {
            carouselStartFilter = (Integer) criteriaMap.get(FILTER_CAROUSEL_START);
        } catch (Exception e) {
            carouselStartFilter = null;
        }

        // on refresh remove any end filter as the criteria may have changed completely
        carouselEndFilter = null;

        // Any change to filters means we have to rebuild the carousel because the set of members may change, because
        // "empty" members (i.e. members whose relevant data has been completely filtered) may be omitted completely
        // from the carousel.
        buildCarousel(true);

        // TODO: it would be best if this was actually called after the async return of the member refreshes
        refreshCarouselInfo();
    }

    protected void buildCarousel(boolean isRefresh) {
        if (null != carouselHolder) {
            carouselHolder.destroyMembers();
        }
    }

    public void refreshCarouselInfo() {
        if (this.showFooter && (this.carouselHolder != null)) {
            if (this.carouselActionDisableOverride) {
                //this.listGrid.setSelectionType(SelectionStyle.NONE);
            } else {
                //this.listGrid.setSelectionType(getDefaultSelectionStyle());
            }

            //int selectionCount = this.listGrid.getSelection().length;
            for (CarouselActionInfo carouselAction : this.carouselActions) {
                if (carouselAction.actionCanvas != null) { // if null, we haven't initialized our buttons yet, so skip this
                    boolean enabled = (!this.carouselActionDisableOverride && carouselAction.action.isEnabled());
                    carouselAction.actionCanvas.setDisabled(!enabled);
                }
            }
            for (Canvas extraWidget : this.extraWidgetsAboveFooter) {
                extraWidget.enable();
                if (extraWidget instanceof CarouselWidget) {
                    ((CarouselWidget) extraWidget).refresh(carouselHolder.getMembers());
                }
            }
            for (Canvas extraWidget : this.extraWidgetsInMainFooter) {
                extraWidget.enable();
                if (extraWidget instanceof CarouselWidget) {
                    ((CarouselWidget) extraWidget).refresh(carouselHolder.getMembers());
                }
            }

            if (isShowFooterRefresh() && this.refreshButton != null) {
                this.refreshButton.enable();
            }
        }
    }

    protected String getDataTypeName() {
        return "item";
    }

    protected String getDataTypeNamePlural() {
        return "items";
    }

    protected String getDeleteConfirmMessage() {
        return MSG.common_msg_deleteConfirm(getDataTypeNamePlural());
    }

    // -------------- Inner utility classes ------------- //

    /**
     * A subclass of SmartGWT's DynamicForm widget that provides a more convenient interface for filtering a 
     * {@link Carousel} of data.
     *
     * @author Joseph Marques 
     */
    private static class CarouselFilter extends LocatableDynamicForm implements KeyPressHandler, ChangedHandler,
        com.google.gwt.event.dom.client.KeyPressHandler {

        private Carousel carousel;
        private SearchBarItem searchBarItem;
        private HiddenItem hiddenItem;

        public CarouselFilter(Carousel carousel) {
            super(carousel.extendLocatorId("CarouselFilter"));
            setIsGroup(true);
            setGroupTitle(MSG.common_label_filters());
            setWidth100();
            setPadding(5);
            this.carousel = carousel;
        }

        @Override
        public void setItems(FormItem... items) {
            for (FormItem nextFormItem : items) {
                nextFormItem.setWrapTitle(false);
                nextFormItem.setWidth(300); // wider than default
                if (nextFormItem instanceof TextItem) {
                    nextFormItem.addKeyPressHandler(this);
                } else if (nextFormItem instanceof SelectItem) {
                    nextFormItem.addChangedHandler(this);
                } else if (nextFormItem instanceof SearchBarItem) {
                    searchBarItem = (SearchBarItem) nextFormItem;
                    searchBarItem.getSearchBar().addKeyPressHandler(this);
                    String name = searchBarItem.getName();
                    searchBarItem.setName(name + "_hidden");
                    hiddenItem = new HiddenItem(name);
                    hiddenItem.setValue(searchBarItem.getSearchBar().getValue());
                }
            }

            if (hiddenItem != null) {
                FormItem[] tmpItems = new FormItem[items.length + 1];
                System.arraycopy(items, 0, tmpItems, 0, items.length);
                tmpItems[items.length] = hiddenItem;
                items = tmpItems;
            }

            super.setItems(items);
        }

        private void fetchFilteredCarouselData() {
            carousel.refresh();
        }

        public void onKeyPress(KeyPressEvent event) {
            if (event.getKeyName().equals("Enter") == false) {
                return;
            }
            fetchFilteredCarouselData();
        }

        public void onChanged(ChangedEvent event) {
            fetchFilteredCarouselData();
        }

        public boolean hasContent() {
            return super.getFields().length != 0;
        }

        @Override
        public void onKeyPress(com.google.gwt.event.dom.client.KeyPressEvent event) {
            if (event.getCharCode() != KeyCodes.KEY_ENTER) {
                return;
            }
            // TODO: figure out why this event is being sent twice
            hiddenItem.setValue(searchBarItem.getSearchBar().getValue());
            fetchFilteredCarouselData();
        }
    }

    public static class CarouselActionInfo {
        private String locatorId;
        private String title;
        private String confirmMessage;
        private LinkedHashMap<String, ? extends Object> valueMap;
        private CarouselAction action;
        private Canvas actionCanvas;

        protected CarouselActionInfo(String locatorId, String title, String confirmMessage,
            LinkedHashMap<String, ? extends Object> valueMap, CarouselAction action) {
            this.locatorId = locatorId;
            this.title = title;
            this.confirmMessage = confirmMessage;
            this.valueMap = valueMap;
            this.action = action;
        }

        public String getLocatorId() {
            return locatorId;
        }

        public String getTitle() {
            return title;
        }

        public String getConfirmMessage() {
            return confirmMessage;
        }

        public LinkedHashMap<String, ? extends Object> getValueMap() {
            return valueMap;
        }

        public Canvas getActionCanvas() {
            return actionCanvas;
        }

        public void setActionCanvas(Canvas actionCanvas) {
            this.actionCanvas = actionCanvas;
        }

        public CarouselAction getAction() {
            return action;
        }

        public void setAction(CarouselAction action) {
            this.action = action;
        }

    }

    public boolean isShowFooterRefresh() {
        return showFooterRefresh;
    }

    public void setShowFooterRefresh(boolean showFooterRefresh) {
        this.showFooterRefresh = showFooterRefresh;
    }

    public Label getCarouselInfo() {
        return carouselInfo;
    }

    public void setCarouselInfo(Label carouselInfo) {
        this.carouselInfo = carouselInfo;
    }

    public boolean isShowFilterForm() {
        return showFilterForm;
    }

    public void setShowFilterForm(boolean showFilterForm) {
        this.showFilterForm = showFilterForm;
    }

    /*
     * by default, no search bar is shown above this.  if this represents a subsystem that is capable
     * of search, return the specific object here.
     */
    protected SearchSubsystem getSearchSubsystem() {
        return null;
    }

    protected Integer getCarouselStartFilter() {
        return carouselStartFilter;
    }

    protected Integer getCarouselStartFilterMax() {
        Double max = ((SpinnerItem) this.filterForm.getItem(FILTER_CAROUSEL_START)).getMax();
        return (null == max) ? null : max.intValue();
    }

    protected void setCarouselStartFilterMax(Integer startFilterMax) {
        SpinnerItem spinner = (SpinnerItem) this.filterForm.getItem(FILTER_CAROUSEL_START);

        spinner.setMax((Integer) ((null == startFilterMax || startFilterMax < 0) ? null : startFilterMax));
    }

    protected void setCarouselStartFilter(Integer carouselStartFilter) {
        this.carouselStartFilter = carouselStartFilter;

        if (null != carouselStartFilter) {
            this.filterForm.getItem(FILTER_CAROUSEL_START).setValue(String.valueOf(carouselStartFilter));
            this.filterForm.getItem(FILTER_CAROUSEL_START).redraw();
        }
    }

    protected Integer getCarouselEndFilter() {
        return carouselEndFilter;
    }

    protected void setCarouselEndFilter(Integer carouselEndFilter) {
        this.carouselEndFilter = carouselEndFilter;
    }

    protected Integer getCarouselSizeFilter() {
        return carouselSizeFilter;
    }

    protected void setCarouselSizeFilter(Integer carouselSizeFilter) {
        this.carouselSizeFilter = carouselSizeFilter;
        this.filterForm.getItem(FILTER_CAROUSEL_SIZE).setValue(String.valueOf(carouselSizeFilter));
        this.filterForm.getItem(FILTER_CAROUSEL_SIZE).redraw();
    }

    protected boolean isCarouselUsingFixedWidths() {
        return carouselUsingFixedWidths;
    }

    protected void setCarouselUsingFixedWidths(boolean carouselUsingFixedWidths) {
        this.carouselUsingFixedWidths = carouselUsingFixedWidths;
    }

    protected void setFilter(String name, String value) {
        FormItem item = this.filterForm.getItem(name);
        if (null != item) {
            item.setValue(value);
        }
    }

    protected interface CarouselAction {

        /**
         * Returns true if the action should be enabled based on the currently selected record(s).
         *
         * @param selection the currently selected record(s)
         *
         * @return true if the action should be enabled based on the currently selected record(s)
         */
        boolean isEnabled(); //TODO add arg

        /**
         * Execute the action with the currently selected record(s) as the target(s).
         *
         * @param selection the currently selected record(s)
         * @param actionValue a value optionally supplied by the action (for example, a menuItem action's selection) 
         */
        void executeAction(Object actionValue); //TODO add arg
    }
}
