/*
 * WidgetListBox.java
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.core.client.widget;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.aria.client.Id;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.aria.client.SelectedValue;
import com.google.gwt.user.client.DOM;
import org.rstudio.core.client.events.HasSelectionCommitHandlers;
import org.rstudio.core.client.events.SelectionCommitEvent;
import org.rstudio.core.client.theme.res.ThemeResources;

import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DomEvent;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.DoubleClickHandler;
import com.google.gwt.event.dom.client.HasChangeHandlers;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

// A list box that can contain arbitrary GWT widgets as options.
public class WidgetListBox<T extends Widget>
   extends FocusPanel
   implements HasChangeHandlers,
              HasSelectionCommitHandlers<T>,
              CanSetControlId
{
   private class ClickableHTMLPanel extends HTMLPanel
      implements HasClickHandlers
   {
      public ClickableHTMLPanel()
      {
         super("");
      }

      public HandlerRegistration addClickHandler(ClickHandler handler)
      {
         return addDomHandler(handler, ClickEvent.getType());
      }
   }

   public interface ListStyle extends CssResource
   {
      String selectedItem();
      String anyItem();
      String listPanel();
      String scrollPanel();
      String outerPanel();
      String emptyMessage();
   }

   public interface Resources extends ClientBundle
   {
      @Source("WidgetListBox.css")
      ListStyle listStyle();
   }

   public WidgetListBox()
   {
      super();

      resources_ = GWT.create(Resources.class);
      style_ = resources_.listStyle();
      style_.ensureInjected();

      // add styles to our own widget
      addStyleName(style_.outerPanel());
      Roles.getListboxRole().set(getElement());

      // create the panel that will host the widgets
      panel_ = new VerticalPanel();
      panel_.addStyleName(style_.listPanel());

      // wrap in a scroll panel
      scrollPanel_ = new ScrollPanel();
      scrollPanel_.add(panel_);
      scrollPanel_.addStyleName(style_.scrollPanel());
      add(scrollPanel_);

      emptyTextLabel_ = new Label();
      emptyTextBox_ = new VerticalPanel();
      emptyTextBox_.addStyleName(style_.scrollPanel());
      emptyTextBox_.addStyleName(style_.emptyMessage());
      emptyTextBox_.add(emptyTextLabel_);
      emptyTextBox_.setCellHorizontalAlignment(emptyTextLabel_, VerticalPanel.ALIGN_CENTER);
      emptyTextBox_.setCellVerticalAlignment(emptyTextLabel_, VerticalPanel.ALIGN_MIDDLE);

      addKeyDownHandler(new KeyDownHandler()
      {
         @Override
         public void onKeyDown(KeyDownEvent event)
         {
            if (event.getNativeKeyCode() == KeyCodes.KEY_DOWN &&
                selectedIdx_ < (panel_.getWidgetCount() - 1))
            {
               setSelectedIndex(selectedIdx_+1, true);
            }
            else if (event.getNativeKeyCode() == KeyCodes.KEY_UP &&
                selectedIdx_ > 0)
            {
               setSelectedIndex(selectedIdx_-1, true);
            }
         }
      });
   }

   @Override
   public HandlerRegistration addChangeHandler(final ChangeHandler handler)
   {
      return handlerManager_.addHandler(ChangeEvent.getType(), handler);
   }


   @Override
   public HandlerRegistration addSelectionCommitHandler(
         SelectionCommitEvent.Handler<T> handler)
   {
      return addHandler(handler, SelectionCommitEvent.getType());
   }

   public void setAriaLabel(String label)
   {
      Roles.getListboxRole().setAriaLabelProperty(getElement(), label);
   }

   public void addItem(T item)
   {
      addItem(item, true);
   }

   public void addItem(final T item, boolean atEnd)
   {
      // wrap the widget in a panel that can receive click events, indicate
      // selection, etc.
      final ClickableHTMLPanel panel = new ClickableHTMLPanel();
      Roles.getOptionRole().set(panel.getElement());
      panel.getElement().setId(DOM.createUniqueId());

      panel.addClickHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            setSelectedIndex(panel_.getWidgetIndex(panel), true);
         }
      });

      panel.addDomHandler(new DoubleClickHandler()
      {
         @Override
         public void onDoubleClick(DoubleClickEvent event)
         {
            SelectionCommitEvent.fire(WidgetListBox.this, item);
         }
      }, DoubleClickEvent.getType());

      panel.add(item);

      // add the panel to our root layout panel
      if (!atEnd && panel_.getWidgetCount() > 0)
      {
         panel_.insert(panel, 0);
         items_.add(0, item);
         options_.add(0, panel);
         selectedIdx_++;
      }
      else
      {
         panel_.add(panel);
         items_.add(item);
         options_.add(panel);
      }

      panel.getElement().getStyle().setPadding(itemPaddingValue_,
                                               itemPaddingUnit_);

      panel.addStyleName(style_.anyItem());
      panel.addStyleName(ThemeResources.INSTANCE.themeStyles().handCursor());

      // if it's the first item, select it
      if (options_.size() == 1)
         setSelectedIndex(0);
      else if (!atEnd && getSelectedIndex() == 1 && options_.size() > 1)
         setSelectedIndex(0, true);

      updateEmptyText();
   }

   public void setSelectedIndex(int itemIdx)
   {
      setSelectedIndex(itemIdx, false);
   }

   public void setSelectedIndex(int itemIdx, boolean fireEvent)
   {
      String selectedStyle = resources_.listStyle().selectedItem();
      panel_.getWidget(selectedIdx_).removeStyleName(selectedStyle);
      Roles.getOptionRole().setAriaSelectedState(panel_.getWidget(selectedIdx_).getElement(), SelectedValue.FALSE);
      selectedIdx_ = itemIdx;
      panel_.getWidget(selectedIdx_).addStyleName(selectedStyle);
      panel_.getWidget(selectedIdx_).getElement().scrollIntoView();
      Roles.getOptionRole().setAriaSelectedState(panel_.getWidget(selectedIdx_).getElement(), SelectedValue.TRUE);
      Roles.getListboxRole().setAriaActivedescendantProperty(getElement(),
            Id.of(panel_.getWidget(selectedIdx_).getElement().getId()));
      if (fireEvent)
      {
         DomEvent.fireNativeEvent(Document.get().createChangeEvent(),
                                  handlerManager_);
      }
   }

   public int getSelectedIndex()
   {
      return selectedIdx_;
   }

   public T getItemAtIdx(int idx)
   {
      if (idx < items_.size())
      {
         return items_.get(idx);
      }
      return null;
   }

   public List<T> getItems()
   {
      return items_;
   }

   public T getSelectedItem()
   {
      return getItemAtIdx(getSelectedIndex());
   }

   public int getItemCount()
   {
      return items_.size();
   }

   public void setItemPadding(double val, Style.Unit unit)
   {
      itemPaddingValue_ = val;
      itemPaddingUnit_ = unit;
   }

   public void clearItems()
   {
      panel_.clear();
      options_.clear();
      items_.clear();
      selectedIdx_ = 0;
      updateEmptyText();
   }

   public void removeItem(int idx)
   {
      panel_.remove(idx);
      options_.remove(idx);
      items_.remove(idx);
      if (selectedIdx_ == idx)
         selectedIdx_ = 0;
      updateEmptyText();
   }

   public void setEmptyText(String text)
   {
      emptyTextLabel_.setText(text);
      updateEmptyText();
   }

   @Override
   public void setElementId(String id)
   {
      getElement().setId(id);
   }

   private void updateEmptyText()
   {
      if (emptyTextBox_.getParent() == this && items_.size() > 0)
      {
         clear();
         add(scrollPanel_);
      }
      else if (emptyTextBox_.getParent() != this && items_.size() == 0)
      {
         clear();
         add(emptyTextBox_);
      }
   }

   private int selectedIdx_ = 0;

   private ScrollPanel scrollPanel_;
   private VerticalPanel panel_;
   private VerticalPanel emptyTextBox_;
   private Label emptyTextLabel_;
   private List<HTMLPanel> options_ = new ArrayList<>();
   private List<T> items_ = new ArrayList<>();

   private Resources resources_;
   private ListStyle style_;

   private double itemPaddingValue_ = 5;
   private Style.Unit itemPaddingUnit_ = Style.Unit.PX;

   HandlerManager handlerManager_ = new HandlerManager(this);
}
