/*
 * Copyright 2011 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.cell.client;

import static com.google.gwt.dom.client.BrowserEvents.CLICK;
import static com.google.gwt.dom.client.BrowserEvents.KEYDOWN;
import static com.google.gwt.dom.client.BrowserEvents.MOUSEDOWN;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CommonResources;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.safecss.shared.SafeStyles;
import com.google.gwt.safecss.shared.SafeStylesBuilder;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.text.shared.SafeHtmlRenderer;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.HasEnabled;

/**
 * Base class for button Cells.
 * 
 * @param <C> the type that this Cell represents
 */
public class ButtonCellBase<C> extends AbstractCell<C> implements IsCollapsible, HasEnabled {

  /**
   * The appearance used to render this Cell.
   * 
   * @param <C> the type that this Cell represents
   */
  public interface Appearance<C> {

    /**
     * Called when the user pushes the button down.
     * 
     * @param parent the parent Element
     */
    void onPush(Element parent);

    /**
     * Called when the user releases the button from being pushed.
     * 
     * @param parent the parent Element
     */
    void onUnpush(Element parent);

    /**
     * Render the button and its contents.
     * 
     * @param cell the cell that is being rendered
     * @param context the {@link Context} of the cell
     * @param value the value that generated the content
     * @param sb the {@link SafeHtmlBuilder} to render into
     */
    void render(ButtonCellBase<C> cell, Context context, C value, SafeHtmlBuilder sb);

    /**
     * Explicitly focus/unfocus this cell.
     * 
     * @param parent the parent element
     * @param focused whether this cell should take focus or release it
     */
    void setFocus(Element parent, boolean focused);
  }

  /**
   * The decoration applied to the button.
   * 
   * <dl>
   * <dt>DEFAULT</dt>
   * <dd>A general button used in any context.</dd>
   * <dt>PRIMARY</dt>
   * <dd>A primary button that stands out against other buttons.</dd>
   * <dt>NEGATIVE</dt>
   * <dd>A button that results in a negative action, such as delete or cancel</dd>
   * </dl>
   */
  public static enum Decoration {
    DEFAULT, PRIMARY, NEGATIVE;
  }

  /**
   * The default implementation of the {@link ButtonCellBase.Appearance}.
   * 
   * @param <C> the type that this Cell represents
   */
  public static class DefaultAppearance<C> implements Appearance<C> {

    /**
     * The resources used by this appearance.
     */
    public interface Resources extends ClientBundle {

      /**
       * The background image applied to the button.
       */
      @ImageOptions(repeatStyle = RepeatStyle.Horizontal, flipRtl = true)
      ImageResource buttonCellBaseBackground();

      @Source(Style.DEFAULT_CSS)
      Style buttonCellBaseStyle();
    }

    /**
     * The Styles used by this appearance.
     */
    @ImportedWithPrefix("gwt-ButtonCellBase")
    public interface Style extends CssResource {
      String DEFAULT_CSS = "com/google/gwt/cell/client/ButtonCellBase.css";

      /**
       * Applied to the button.
       */
      String buttonCellBase();

      /**
       * Applied to the button when it has a collapsed left side.
       */
      String buttonCellBaseCollapseLeft();

      /**
       * Applied to the button when it has a collapsed right side.
       */
      String buttonCellBaseCollapseRight();

      /**
       * Applied to default buttons.
       */
      String buttonCellBaseDefault();

      /**
       * Applied to negative buttons.
       */
      String buttonCellBaseNegative();

      /**
       * Applied to primary buttons.
       */
      String buttonCellBasePrimary();

      /**
       * Applied to the button when being pushed.
       */
      String buttonCellBasePushing();
    }

    /**
     * The templates used by this appearance.
     */
    interface Template extends SafeHtmlTemplates {
      /**
       * Positions the icon next to the text.
       * 
       * NOTE: zoom:0 is a workaround for an IE7 bug where the button contents
       * wrap even when they do not need to.
       */
      @SafeHtmlTemplates.Template("<div class=\"{0}\""
          + " style=\"{1}position:relative;zoom:0;\">{2}{3}</div>")
      SafeHtml iconContentLayout(
          String classes, SafeStyles styles, SafeHtml icon, SafeHtml cellContents);

      /**
       * The wrapper around the icon that aligns it vertically with the text.
       */
      @SafeHtmlTemplates.Template("<div style=\"{0}position:absolute;top:50%;line-height:0px;\">"
          + "{1}</div>")
      SafeHtml iconWrapper(SafeStyles styles, SafeHtml image);
    }

    private static final int DEFAULT_ICON_PADDING = 3;
    private static Resources defaultResources;
    private static Template template;

    private static Resources getDefaultResources() {
      if (defaultResources == null) {
        defaultResources = GWT.create(Resources.class);
      }
      return defaultResources;
    }

    private SafeHtml iconSafeHtml = SafeHtmlUtils.EMPTY_SAFE_HTML;
    private ImageResource lastIcon;
    private final SafeHtmlRenderer<C> renderer;
    private final Style style;

    /**
     * Construct a new {@link ButtonCellBase.DefaultAppearance} using the default styles.
     * 
     * @param renderer the {@link SafeHtmlRenderer} used to render the contents
     */
    public DefaultAppearance(SafeHtmlRenderer<C> renderer) {
      this(renderer, getDefaultResources());
    }

    /**
     * Construct a new {@link ButtonCellBase.DefaultAppearance} using the specified resources.
     * 
     * @param renderer the {@link SafeHtmlRenderer} used to render the contents
     * @param resources the resources and styles to apply to the button
     */
    public DefaultAppearance(SafeHtmlRenderer<C> renderer, Resources resources) {
      this.renderer = renderer;
      this.style = resources.buttonCellBaseStyle();
      this.style.ensureInjected();
      if (template == null) {
        template = GWT.create(Template.class);
      }
    }

    /**
     * Return the {@link SafeHtmlRenderer} used by this Appearance to render the
     * contents of the button.
     * 
     * @return a {@link SafeHtmlRenderer} instance
     */
    public SafeHtmlRenderer<C> getRenderer() {
      return renderer;
    }

    @Override
    public void onPush(Element parent) {
      parent.getFirstChildElement().addClassName(style.buttonCellBasePushing());
    }

    @Override
    public void onUnpush(Element parent) {
      parent.getFirstChildElement().removeClassName(style.buttonCellBasePushing());
    }

    @Override
    public void render(ButtonCellBase<C> cell, Context context, C value, SafeHtmlBuilder sb) {
      // Determine the classes from the state of the button.
      SafeHtmlBuilder classes = new SafeHtmlBuilder();
      classes.appendEscaped(style.buttonCellBase());
      Decoration decoration = cell.getDecoration();
      if (decoration == Decoration.PRIMARY) {
        classes.appendEscaped(" " + style.buttonCellBasePrimary());
      } else if (decoration == Decoration.NEGATIVE) {
        classes.appendEscaped(" " + style.buttonCellBaseNegative());
      } else {
        classes.appendEscaped(" " + style.buttonCellBaseDefault());
      }
      if (cell.isCollapseLeft()) {
        classes.appendEscaped(" " + style.buttonCellBaseCollapseLeft());
      }
      if (cell.isCollapseRight()) {
        classes.appendEscaped(" " + style.buttonCellBaseCollapseRight());
      }

      // Update the cached HTML string used for the icon if the icon changes.
      ImageResource icon = cell.getIcon();
      if (icon != lastIcon) {
        if (icon == null) {
          iconSafeHtml = SafeHtmlUtils.EMPTY_SAFE_HTML;
        } else {
          AbstractImagePrototype proto = AbstractImagePrototype.create(icon);
          SafeHtml iconOnly = proto.getSafeHtml();
          int halfHeight = (int) Math.round(icon.getHeight() / 2.0);
          SafeStylesBuilder styles = new SafeStylesBuilder();
          styles.marginTop(-halfHeight, Unit.PX);
          if (LocaleInfo.getCurrentLocale().isRTL()) {
            styles.right(0, Unit.PX);
          } else {
            styles.left(0, Unit.PX);
          }
          iconSafeHtml = template.iconWrapper(styles.toSafeStyles(), iconOnly);
        }
      }

      // Figure out the attributes.
      char accessKey = cell.getAccessKey();
      StringBuilder attributes = new StringBuilder();
      if (!cell.isEnabled()) {
        attributes.append("disabled=disabled ");
      }
      if (accessKey != 0) {
        attributes.append("accessKey=\"").append(SafeHtmlUtils.htmlEscape("" + accessKey));
        attributes.append("\" ");
      }

      // Create the button.
      SafeStylesBuilder styles = new SafeStylesBuilder();
      int iconWidth = (icon == null) ? 0 : icon.getWidth();
      int iconPadding = iconWidth + DEFAULT_ICON_PADDING;
      if (LocaleInfo.getCurrentLocale().isRTL()) {
        styles.paddingRight(iconPadding, Unit.PX);
      } else {
        styles.paddingLeft(iconPadding, Unit.PX);
      }
      SafeHtml safeValue = renderer.render(value);
      SafeHtml content = template.iconContentLayout(
          CommonResources.getInlineBlockStyle(), styles.toSafeStyles(), iconSafeHtml, safeValue);
      int tabIndex = cell.getTabIndex();
      StringBuilder openTag = new StringBuilder();
      openTag.append("<button type=\"button\"");
      openTag.append(" class=\"" + classes.toSafeHtml().asString() + "\"");
      openTag.append(" tabindex=\"" + tabIndex + "\" ");
      openTag.append(attributes.toString()).append(">");
      sb.append(SafeHtmlUtils.fromTrustedString(openTag.toString()));
      sb.append(content);
      sb.appendHtmlConstant("</button>");
    }

    @Override
    public void setFocus(Element parent, boolean focused) {
      Element focusable = parent.getFirstChildElement().cast();
      if (focused) {
        focusable.focus();
      } else {
        focusable.blur();
      }
    }
  }

  /**
   * The {@link NativePreviewHandler} used to unpush a button onmouseup, even if
   * the event doesn't occur over the button.
   */
  private class UnpushHandler implements NativePreviewHandler {

    private final Element parent;
    private final HandlerRegistration reg;

    public UnpushHandler(Element parent) {
      this.parent = parent;
      this.reg = Event.addNativePreviewHandler(this);
    }

    @Override
    public void onPreviewNativeEvent(NativePreviewEvent event) {
      if (BrowserEvents.MOUSEUP.equals(event.getNativeEvent().getType())) {
        // Unregister self.
        reg.removeHandler();

        // Unpush the element.
        appearance.onUnpush(parent);
      }
    }
  }

  private char accessKey;
  private final Appearance<C> appearance;
  private Decoration decoration = Decoration.DEFAULT;
  private ImageResource icon;
  private boolean isCollapsedLeft;
  private boolean isCollapsedRight;
  private boolean isEnabled = true;

  /**
   * The tab index applied to the buttons. If the button is used in a list of
   * table, a tab index of -1 should be used so it doesn't interrupt the tab
   * sequence.
   */
  private int tabIndex = -1;

  /**
   * Construct a new {@link ButtonCellBase} using the specified
   * {@link Appearance} to render the contents.
   * 
   * @param appearance the appearance of the cell
   */
  public ButtonCellBase(Appearance<C> appearance) {
    super(CLICK, KEYDOWN, MOUSEDOWN);
    this.appearance = appearance;
  }

  /**
   * Get the access key.
   * 
   * @return the access key, or 0 if one is not defined
   */
  public char getAccessKey() {
    return accessKey;
  }

  /**
   * Get the decoration style of the button.
   */
  public Decoration getDecoration() {
    return decoration;
  }

  /**
   * Get the icon displayed next to the button text.
   * 
   * @return the icon resource
   */
  public ImageResource getIcon() {
    return icon;
  }

  /**
   * Return the tab index that is given to all rendered cells.
   * 
   * @return the tab index
   */
  public int getTabIndex() {
    return tabIndex;
  }

  @Override
  public boolean isCollapseLeft() {
    return this.isCollapsedLeft;
  }

  @Override
  public boolean isCollapseRight() {
    return this.isCollapsedRight;
  }

  @Override
  public boolean isEnabled() {
    return isEnabled;
  }

  @Override
  public void onBrowserEvent(Context context, Element parent, C value, NativeEvent event,
      ValueUpdater<C> valueUpdater) {
    // Ignore all events if disabled.
    if (!isEnabled()) {
      return;
    }

    // Let AbstractCell handle the enter key.
    super.onBrowserEvent(context, parent, value, event, valueUpdater);

    // Ignore events that occur outside of the button element.
    Element target = event.getEventTarget().cast();
    if (!parent.getFirstChildElement().isOrHasChild(target)) {
      return;
    }

    String eventType = event.getType();
    if (CLICK.equals(eventType)) {
      // Click the button.
      onEnterKeyDown(context, parent, value, event, valueUpdater);
    } else if (MOUSEDOWN.equals(eventType)) {
      // Push.
      appearance.onPush(parent);

      /*
       * Add a preview handler to unpush the button onmouseup or onblur, even if
       * the event doesn't occur over the button.
       */
      new UnpushHandler(parent);

      // We will be handling all user visual feedback manually. Prevent browser
      // doing default handling (i.e. double-click highlights button text).
      event.preventDefault();
    }
  }

  @Override
  public void render(Context context, C value, SafeHtmlBuilder sb) {
    appearance.render(this, context, value, sb);
  }

  /**
   * Sets the cell's 'access key'. This key is used (in conjunction with a
   * browser-specific modifier key) to automatically focus the cell.
   * 
   * <p>
   * The change takes effect the next time the Cell is rendered.
   * 
   * @param key the cell's access key
   */
  public void setAccessKey(char key) {
    this.accessKey = key;
  }

  /**
   * {@inheritDoc}
   * 
   * <p>
   * The change takes effect the next time the Cell is rendered.
   */
  @Override
  public void setCollapseLeft(boolean isCollapsed) {
    this.isCollapsedLeft = isCollapsed;
  }

  /**
   * {@inheritDoc}
   * 
   * <p>
   * The change takes effect the next time the Cell is rendered.
   */
  @Override
  public void setCollapseRight(boolean isCollapsed) {
    this.isCollapsedRight = isCollapsed;
  }

  /**
   * Set the {@link Decoration} of the button.
   * 
   * <p>
   * This change takes effect the next time the cell is rendered.
   * 
   * @param decoration the button decoration
   */
  public void setDecoration(Decoration decoration) {
    this.decoration = decoration;
  }

  /**
   * {@inheritDoc}
   * 
   * <p>
   * The change takes effect the next time the Cell is rendered.
   */
  @Override
  public void setEnabled(boolean isEnabled) {
    this.isEnabled = isEnabled;
  }

  /**
   * Explicitly focus/unfocus this cell. Only one UI component can have focus at
   * a time, and the component that does will receive all keyboard events.
   * 
   * @param parent the parent element
   * @param focused whether this cell should take focus or release it
   */
  public void setFocus(Element parent, boolean focused) {
    appearance.setFocus(parent, focused);
  }

  /**
   * Set the icon to display next to the button text.
   * 
   * @param icon the icon resource, or null not to show an icon
   */
  public void setIcon(ImageResource icon) {
    this.icon = icon;
  }

  /**
   * Set the tab index to apply to the button. By default, the tab index is set
   * to -1 so that the button does not interrupt the tab chain when in a table
   * or list. The change takes effect the next time the Cell is rendered.
   * 
   * @param tabIndex the tab index
   */
  public void setTabIndex(int tabIndex) {
    this.tabIndex = tabIndex;
  }

  @Override
  protected void onEnterKeyDown(Context context, Element parent, C value, NativeEvent event,
      ValueUpdater<C> valueUpdater) {
    if (valueUpdater != null) {
      valueUpdater.update(value);
    }
  }
}
