/*
 * MultiSelectCellTable.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 com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.cellview.client.CellTable;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.MultiSelectionModel;
import com.google.gwt.view.client.ProvidesKey;
import org.rstudio.core.client.BrowseCap;
import org.rstudio.core.client.command.KeyboardShortcut;
import org.rstudio.core.client.dom.DomUtils;

public class MultiSelectCellTable<T> extends CellTable<T>
      implements HasKeyDownHandlers, HasClickHandlers, HasMouseDownHandlers,
                 HasContextMenuHandlers
{
   public MultiSelectCellTable()
   {
      commonInit();
   }

   public MultiSelectCellTable(int pageSize)
   {
      super(pageSize);
      commonInit();
   }

   public MultiSelectCellTable(ProvidesKey<T> keyProvider)
   {
      super(keyProvider);
      commonInit();
   }

   public MultiSelectCellTable(int pageSize, Resources resources)
   {
      super(pageSize, resources);
      commonInit();
   }

   public MultiSelectCellTable(int pageSize, ProvidesKey<T> keyProvider)
   {
      super(pageSize, keyProvider);
      commonInit();
   }

   public MultiSelectCellTable(int pageSize,
                               Resources resources,
                               ProvidesKey<T> keyProvider)
   {
      super(pageSize, resources, keyProvider);
      commonInit();
   }

   public MultiSelectCellTable(int pageSize,
                               Resources resources,
                               ProvidesKey<T> keyProvider,
                               Widget loadingIndicator)
   {
      super(pageSize, resources, keyProvider, loadingIndicator);
      commonInit();
   }

   @Override
   public void setFocus(boolean focused)
   {
      if (focused)
         DomUtils.focus(getElement(), false);
   }

   private void commonInit()
   {
      setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
      getElement().setTabIndex(-1);

      addDomHandler(new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent event)
         {
            // Note: this formerly used DomUtils.focus, but the implementation
            // of DomUtils used on IE11 focuses the window afterwards, which 
            // blurs the element. Since we don't need to drive selection, we
            // now just use native focus here.
            getElement().focus();
         }
      }, ClickEvent.getType());

      addKeyDownHandler(new KeyDownHandler()
      {
         @Override
         public void onKeyDown(KeyDownEvent event)
         {
            MultiSelectCellTable.this.handleKeyDown(event);
         }
      });
      
      addDomHandler(new ContextMenuHandler() {
         @Override
         public void onContextMenu(ContextMenuEvent event)
         {
            MultiSelectCellTable.this.handleContextMenu(event);
         }
      }, ContextMenuEvent.getType());
   }

   @Override
   protected boolean isKeyboardNavigationSuppressed()
   {
      return true;
   }

   @SuppressWarnings("rawtypes")
   private void clearSelection()
   {
      if (getSelectionModel() instanceof MultiSelectionModel)
         ((MultiSelectionModel)getSelectionModel()).clear();
   }

   private void handleKeyDown(KeyDownEvent event)
   {
      int modifiers = KeyboardShortcut.getModifierValue(event.getNativeEvent());
      switch (event.getNativeKeyCode())
      {
         // TODO: Handle home/end, pageup/pagedown
         case KeyCodes.KEY_UP:
         case KeyCodes.KEY_DOWN:
            event.preventDefault();
            event.stopPropagation();

            switch (modifiers)
            {
               case 0:
               case KeyboardShortcut.SHIFT:
                  break;
               default:
                  return;
            }

            moveSelection(event.getNativeKeyCode() == KeyCodes.KEY_UP,
                          modifiers == KeyboardShortcut.SHIFT);

            break;
         case 'A':
            if (modifiers == (BrowseCap.hasMetaKey() ? KeyboardShortcut.META
                                                     : KeyboardShortcut.CTRL))
            {
               if (getSelectionModel() instanceof MultiSelectionModel)
               {
                  event.preventDefault();
                  event.stopPropagation();

                  for (T item : getVisibleItems())
                     getSelectionModel().setSelected(item, true);
               }
            }
            break;
      }

   }

   
   // handle context-menu clicks. this implementation (specifically the 
   // detection of table rows from dom events) is based on the code in
   // AbstractCellTable.onBrowserEvent2. we first determine if the click
   // applies to a row in the table -- if it does then we squelch the 
   // standard handling of the event and update the selection and then 
   // forward the event on to any external listeners
   private void handleContextMenu(ContextMenuEvent cmEvent)
   {
      // bail if there are no context menu handlers
      if (handlerManager_.getHandlerCount(ContextMenuEvent.getType()) == 0)
         return;
       
      // Get the event target.
      NativeEvent event = cmEvent.getNativeEvent();
      EventTarget eventTarget = event.getEventTarget();
      if (!Element.is(eventTarget))
        return;
      final Element target = event.getEventTarget().cast();

      // always squelch default handling (when there is a handler)
      event.stopPropagation();
      event.preventDefault();
      
      // find the table cell element then get its parent and cast to row
      TableCellElement tableCell = findNearestParentCell(target);
      if (tableCell == null)
         return;         
      Element trElem = tableCell.getParentElement();
      if (trElem == null) 
        return;
      TableRowElement tr = TableRowElement.as(trElem);
      
      // get the section of the row and confirm it is a tbody (as opposed
      // to a thead or tfoot)
      Element sectionElem = tr.getParentElement();
      if (sectionElem == null)
        return;
      TableSectionElement section = TableSectionElement.as(sectionElem);
      if (section != getTableBodyElement())
         return; 
      
      // determine the row/item target
      int row = tr.getSectionRowIndex();
      T item = getVisibleItem(row);
      
      // if this row isn't already selected then clear the existing selection
      if (!getSelectionModel().isSelected(item))
         clearSelection();
     
      // select the clicked on item
      getSelectionModel().setSelected(item, true);
      
      // forward the event
      DomEvent.fireNativeEvent(event, handlerManager_);
   }
   
   // forked from private AbstractCellTable.findNearestParentCell
   private TableCellElement findNearestParentCell(Element elem) {
      while ((elem != null) && (elem != getElement())) {
        // TODO: We need is() implementations in all Element subclasses.
        // This would allow us to use TableCellElement.is() -- much cleaner.
        String tagName = elem.getTagName();
        if ("td".equalsIgnoreCase(tagName) || "th".equalsIgnoreCase(tagName)) {
          return elem.cast();
        }
        elem = elem.getParentElement();
      }
      return null;
   }
  
   @Override
   public HandlerRegistration addKeyDownHandler(KeyDownHandler handler)
   {
      return addDomHandler(handler, KeyDownEvent.getType());
   }

   public void moveSelection(boolean up, boolean extend)
   {
      if (getVisibleItemCount() == 0)
         return;

      int min = getVisibleItemCount();
      int max = -1;

      for (int i = 0; i < getVisibleItemCount(); i++)
      {
         if (getSelectionModel().isSelected(getVisibleItem(i)))
         {
            max = i;
            if (min > i)
               min = i;
         }
      }

      if (up)
      {
         int row = Math.max(0, min - 1);
         if (!canSelectVisibleRow(row))
            row = min;

         if (!extend)
            clearSelection();
         getSelectionModel().setSelected(getVisibleItem(row), true);
         ensureRowVisible(row, true);
      }
      else
      {
         int row = Math.min(getVisibleItemCount()-1, max + 1);
         if (!canSelectVisibleRow(row))
            row = max;

         if (!extend)
            clearSelection();
         getSelectionModel().setSelected(getVisibleItem(row), true);
         ensureRowVisible(row, false);
      }
   }

   private void ensureRowVisible(int row, boolean alignWithTop)
   {
      Element el;
      if (row == 0 && alignWithTop)
         el = (getElement().<TableElement>cast()).getRows().getItem(0);
      else
         el = getRowElement(row);

      if (el != null)
         DomUtils.scrollIntoViewVert(el);
   }

   protected boolean canSelectVisibleRow(int visibleRow)
   {
      return true;
   }

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

   @Override
   public HandlerRegistration addMouseDownHandler(MouseDownHandler handler)
   {
      return addDomHandler(handler, MouseDownEvent.getType());
   }
   
   @Override
   public HandlerRegistration addContextMenuHandler(ContextMenuHandler handler)
   {
      return handlerManager_.addHandler(ContextMenuEvent.getType(), handler);
   }
   
   // we have our own HandlerManager so that we can fire the ContextMenuEvent
   // to listeners after we have done our own handling of it (specifically
   // we need to nix the browser context menu and update the selection). 
   private final HandlerManager handlerManager_ = new HandlerManager(this);
}
