// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.client.widgets.boxes;

import com.google.appinventor.client.Images;
import com.google.appinventor.client.Ode;
import static com.google.appinventor.client.Ode.MESSAGES;
import com.google.appinventor.client.widgets.ContextMenu;
import com.google.appinventor.client.widgets.TextButton;
import com.google.appinventor.shared.properties.json.JSONObject;
import com.google.appinventor.shared.properties.json.JSONUtil;
import com.google.appinventor.shared.properties.json.JSONValue;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.PushButton;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

import java.util.Map;

/**
 * Abstract superclass for all boxes.
 *
 * <p>A box is a container widget. It automatically handles scrolling for
 * embedded widgets. Boxes can be resized, minimized and restored.
 *
 */
public abstract class Box extends HandlerPanel {

  /**
   * Describes a box in the context of a layout.
   */
  public static final class BoxDescriptor {
    // Field names for JSON encoding of box descriptors
    private static final String NAME_TYPE = "type";
    private static final String NAME_WIDTH = "width";
    private static final String NAME_HEIGHT = "height";
    private static final String NAME_MINIMIZED = "minimized";

    // Information needed to create a box in a layout
    private final String type;
    private final int width;
    private final int height;
    private final boolean minimized;

    /**
     * Creates a new box description.
     *
     * @param type  type of box
     * @param width  width of box in pixels
     * @param height  height of box in pixels if not minimized
     * @param minimized  indicates whether box is minimized
     */
    private BoxDescriptor(String type, int width, int height, boolean minimized) {
      this.type = type;
      this.width = width;
      this.height = height;
      this.minimized = minimized;
    }

    /**
     * Creates a new box description.
     *
     * @param type  type of box
     * @param width  width of box in pixels
     * @param height  height of box in pixels if not minimized
     * @param minimized  indicates whether box is minimized
     */
    public BoxDescriptor(Class<? extends Box> type, int width, int height, boolean minimized) {
      this(type.getName(), width, height, minimized);
    }

    /**
     * Returns the box type (for use by a {@link BoxRegistry}).
     *
     * @return  box type
     */
    public String getType() {
      return type;
    }

    /**
     * Encodes the box information into JSON format.
     */
    public String toJson() {
      return "{" +
            "\"" + NAME_TYPE + "\":" + JSONUtil.toJson(type) + "," +
            "\"" + NAME_WIDTH + "\":" + JSONUtil.toJson(width) + "," +
            "\"" + NAME_HEIGHT + "\":" + JSONUtil.toJson(height) + "," +
            "\"" + NAME_MINIMIZED + "\":" + JSONUtil.toJson(minimized) +
          "}";
    }

    /**
     * Creates a new box descriptor from a JSON object.
     *
     * @param object  box descriptor in JSON format
     */
    public static BoxDescriptor fromJson(JSONObject object) {
      Map<String, JSONValue> properties = object.getProperties();

      return new BoxDescriptor(JSONUtil.stringFromJsonValue(properties.get(NAME_TYPE)),
          JSONUtil.intFromJsonValue(properties.get(NAME_WIDTH)),
          JSONUtil.intFromJsonValue(properties.get(NAME_HEIGHT)),
          JSONUtil.booleanFromJsonValue(properties.get(NAME_MINIMIZED)));
    }

    /**
     * Returns the type of box from a JSON object.
     *
     * @param object  box descriptor in JSON format
     */
    public static String boxTypeFromJson(JSONObject object) {
      Map<String, JSONValue> properties = object.getProperties();

      return JSONUtil.stringFromJsonValue(properties.get(NAME_TYPE));
    }
  }

  /**
   * Control for resizing boxes.
   */
  private final class ResizeControl extends PopupPanel {

    /**
     * Creates a control to resize the box.
     */
    private ResizeControl() {
      super(false); // no autohide

      VerticalPanel buttonPanel = new VerticalPanel();
      buttonPanel.setSpacing(10);
      addControlButton(buttonPanel, "-", new Command() {
        @Override
        public void execute() {
          height = Math.max(100, height - 20);
          restoreHeight = height;
          onResize(width, height);
        }
      });
      addControlButton(buttonPanel, "+", new Command() {
        @Override
        public void execute() {
          height = height + 20;
          restoreHeight = height;
          onResize(width, height);
        }
      });
      addControlButton(buttonPanel, MESSAGES.done(), new Command() {
        @Override
        public void execute() {
          hide();
        }
      });
      add(buttonPanel);

      setModal(true);
      setStylePrimaryName("ode-BoxResizeControl");
    }

    /**
     * Creates a button with a click handler which will execute the given command.
     */
    private void addControlButton(VerticalPanel panel, String caption, final Command command) {
      TextButton button = new TextButton(caption);
      button.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
          command.execute();
        }
      });
      panel.add(button);
      panel.setCellHorizontalAlignment(button, VerticalPanel.ALIGN_CENTER);
    }
  }

  // Height of minimized box
  private static final int MINIMIZED_HEIGHT = 31;

  // Padding between header controls
  private static final int HEADER_CONTROL_PADDING = 2;

  // Constants for box decorations (note that these constants correspond to the box's style
  // definition)
  private static final int BOX_PADDING = 5;
  private static final int BOX_BORDER = 1;

  // UI elements
  private final SimplePanel body;
  private final Label captionLabel;
  private final HandlerPanel header;
  private final DockPanel headerContainer;
  private final ScrollPanel scrollPanel;
  private final PushButton minimizeButton;
  private final PushButton menuButton;

  // Indicates that the box height is changed through resize operations of the layout
  private boolean variableHeightBoxes;

  // Box dimensions
  private int width;
  private int height;

  // Height of non-minimized box
  private int restoreHeight;

  // Whether box should always begin minimized
  private boolean startMinimized;

  // Whether new captions should be highlighted
  private boolean highlightCaption;

  // Whether user has seen/acknowledged the new caption yet
  private boolean captionAlreadySeen = false;

  /**
   * Creates a new box.
   *
   * @param caption  box caption
   * @param height  box initial height in pixel
   * @param minimizable  indicates whether box can be minimized
   * @param removable  indicates whether box can be closed/removed
   * @param startMinimized indicates whether box should always start minimized
   * @param bodyPadding indicates whether box should have padding
   * @param highlightCaption indicates whether caption should be highlighted
   *                         until user has "seen" it (interacts with the box)
   */
  protected Box(String caption, int height, boolean minimizable, boolean removable,
      boolean startMinimized, boolean bodyPadding, boolean highlightCaption) {
    this.height = height;
    this.restoreHeight = height;
    this.startMinimized = startMinimized;
    this.highlightCaption = highlightCaption;

    captionLabel = new Label(caption, false);
    captionAlreadySeen = false;
    if (highlightCaption) {
      captionLabel.setStylePrimaryName("ode-Box-header-caption-highlighted");
    } else {
      captionLabel.setStylePrimaryName("ode-Box-header-caption");
    }
    header = new HandlerPanel();
    header.add(captionLabel);
    header.setWidth("100%");

    headerContainer = new DockPanel();
    headerContainer.setStylePrimaryName("ode-Box-header");
    headerContainer.setWidth("100%");
    headerContainer.add(header, DockPanel.LINE_START);

    Images images = Ode.getImageBundle();

    if (removable) {
      PushButton closeButton = Ode.createPushButton(images.boxClose(), MESSAGES.hdrClose(),
          new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
              // TODO(user) - remove the box
              Window.alert("Not implemented yet!");
            }
          });
      headerContainer.add(closeButton, DockPanel.LINE_END);
      headerContainer.setCellWidth(closeButton,
          (closeButton.getOffsetWidth() + HEADER_CONTROL_PADDING) + "px");
    }

    if (!minimizable) {
      minimizeButton = null;
    } else {
      minimizeButton = Ode.createPushButton(images.boxMinimize(), MESSAGES.hdrMinimize(),
          new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
              if (isMinimized()) {
                restore();
              } else {
                minimize();
              }
            }
          });
      headerContainer.add(minimizeButton, DockPanel.LINE_END);
      headerContainer.setCellWidth(minimizeButton,
          (minimizeButton.getOffsetWidth() + HEADER_CONTROL_PADDING) + "px");
    }

    if (minimizable || removable) {
      menuButton = Ode.createPushButton(images.boxMenu(), MESSAGES.hdrSettings(),
          new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
              final ContextMenu contextMenu = new ContextMenu();
              contextMenu.addItem(MESSAGES.cmMinimize(), new Command() {
                @Override
                public void execute() {
                  if (! isMinimized()) {
                    minimize();
                  }
                }
              });
              contextMenu.addItem(MESSAGES.cmRestore(), new Command() {
                @Override
                public void execute() {
                  if (isMinimized()) {
                    restore();
                  }
                }
              });
              if (!variableHeightBoxes) {
                contextMenu.addItem(MESSAGES.cmResize(), new Command() {
                  @Override
                  public void execute() {
                    restore();
                    final ResizeControl resizeControl = new ResizeControl();
                    resizeControl.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
                      @Override
                      public void setPosition(int offsetWidth, int offsetHeight) {
                        // SouthEast
                        int left = menuButton.getAbsoluteLeft() + menuButton.getOffsetWidth()
                            - offsetWidth;
                        int top = menuButton.getAbsoluteTop() + menuButton.getOffsetHeight();
                        resizeControl.setPopupPosition(left, top);
                      }
                    });
                  }
                });
              }
              contextMenu.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
                @Override
                public void setPosition(int offsetWidth, int offsetHeight) {
                  // SouthEast
                  int left = menuButton.getAbsoluteLeft() + menuButton.getOffsetWidth()
                      - offsetWidth;
                  int top = menuButton.getAbsoluteTop() + menuButton.getOffsetHeight();
                  contextMenu.setPopupPosition(left, top);
                }
              });
            }
          });
      headerContainer.add(menuButton, DockPanel.LINE_END);
      headerContainer.setCellWidth(menuButton,
          (menuButton.getOffsetWidth() + HEADER_CONTROL_PADDING) + "px");
    } else {
      menuButton = null;
    }

    body = new SimplePanel();
    body.setSize("100%", "100%");

    scrollPanel = new ScrollPanel();
    scrollPanel.setStylePrimaryName("ode-Box-body");
    if (bodyPadding) {
      scrollPanel.addStyleName("ode-Box-body-padding");
    }
    scrollPanel.add(body);

    FlowPanel boxContainer = new FlowPanel();
    boxContainer.setStyleName("ode-Box-content");
    boxContainer.add(headerContainer);
    boxContainer.add(scrollPanel);

    setStylePrimaryName("ode-Box");
    setWidget(boxContainer);
  }

  protected Box(String caption, int height, boolean minimizable, boolean removable,
      boolean startMinimized, boolean highlightCaption) {
    this(caption, height, minimizable, removable, startMinimized, true, highlightCaption);
  }

  protected Box(String caption, int height, boolean minimizable, boolean removable,
      boolean startMinimized) {
    this(caption, height, minimizable, removable, startMinimized, true, false);
  }

  protected Box(String caption, int height, boolean minimizable, boolean removable) {
    this(caption, height, minimizable, removable, false, true, false);
  }

  @Override
  public void clear() {
    body.clear();
  }

  /**
   * Sets the resizing behavior of the box.
   *
   * @param variableHeightBoxes  indicates whether the box height will be
   *                             updated upon layout resize operations
   */
  public void setVariableHeightBoxes(boolean variableHeightBoxes) {
    this.variableHeightBoxes = variableHeightBoxes;
  }

  /**
   * Shows the given widget in the box.
   *
   * @param w  widget to show
   */
  public void setContent(Widget w) {
    body.setWidget(w);
  }

  /**
   * Sets the given caption for the box.
   *
   * @param caption  box caption to show
   */
  public void setCaption(String caption) {
    if (highlightCaption) {
      captionLabel.setStylePrimaryName("ode-Box-header-caption-highlighted");
      captionAlreadySeen = false;
    }
    captionLabel.setText(caption);
  }

  /**
   * Returns the box header.
   *
   * @return  box header widget
   */
  Widget getHeader() {
    return header;
  }

  /**
   * Invoked upon resizing of the box by the layout. Box height will remain
   * unmodified.
   *
   * @see Layout#onResize(int, int)
   *
   * @param width  new column width for box in pixel
   */
  protected void onResize(int width) {
    onResize(width, height);
  }

  /**
   * Invoked upon resizing of the box by the layout.
   *
   * @see Layout#onResize(int, int)
   *
   * @param width  new column width for box in pixel
   * @param height  new column height for box in pixel
   */
  protected void onResize(int width, int height) {
    this.width = width;
    this.height = height;

    if (!isMinimized()) {
      restoreHeight = height;
    }

    setSize(this.width + "px", this.height + "px");

    // In order to get the correct size for the scroll panel we need to subtract the dimensions
    // of all decorations such as padding, borders, margin etc. It is also important to set the size
    // for the scroll panel in pixels, as this seems to be the only reliably working unit.
    // We subtract padding and border sizes from top and bottom as well as the height of the box
    // header.
    int w = getOffsetWidth() - 2 * (BOX_PADDING + BOX_BORDER);
    int h = getOffsetHeight() - 2 * (BOX_PADDING + BOX_BORDER) - headerContainer.getOffsetHeight();

    // On startup it can happen that we receive a window resize event before the boxes are attached
    // to the DOM. In that case, offset width and height are 0, we can safely abort because there
    // will soon be another resize event after the boxes are attached to the DOM.
    if (w > 0 && h > 0) {
      scrollPanel.setSize(w + "px", h + "px");
    }
  }

  /**
   * Restores the box layout.
   *
   * @param bd  box descriptor with layout settings of box
   */
  public void restoreLayoutSettings(BoxDescriptor bd) {
    restoreHeight = bd.height;
    height = bd.height;

    if (bd.minimized || startMinimized) {
      minimize();
    } else {
      restore();
    }
  }

  /**
   * Returns box layout settings.
   *
   * @return  box layout settings
   */
  public BoxDescriptor getLayoutSettings() {
    return new BoxDescriptor(getClass().getName(), width, restoreHeight, isMinimized());
  }

  /**
   * Indicates whether the box is minimized.
   */
  private boolean isMinimized() {
    return height != restoreHeight;
  }

  /**
   * Minimizes a box.
   */
  private void minimize() {
    scrollPanel.setVisible(false);
    minimizeButton.getUpFace().setImage(new Image(Ode.getImageBundle().boxRestore()));
    minimizeButton.setTitle(MESSAGES.hdrRestore());

    if (highlightCaption && captionAlreadySeen) {
      captionLabel.setStylePrimaryName("ode-Box-header-caption");
    }
    captionAlreadySeen = true;

    restoreHeight = height;
    height = MINIMIZED_HEIGHT;
    onResize(width, height);
  }

  /**
   * Restores a minimized box to its previous height.
   */
  private void restore() {
    minimizeButton.getUpFace().setImage(new Image(Ode.getImageBundle().boxMinimize()));
    minimizeButton.setTitle(MESSAGES.hdrMinimize());
    scrollPanel.setVisible(true);

    if (highlightCaption && captionAlreadySeen) {
      captionLabel.setStylePrimaryName("ode-Box-header-caption");
    }
    captionAlreadySeen = true;

    height = restoreHeight;
    onResize(width, height);
  }

  /**
   * Helper method for adding style elements (in particular the rounded corners).
   */
  private void appendDecorationElement(String styleClass) {
    Element element = DOM.createDiv();
    element.setClassName(styleClass);
    getElement().appendChild(element);
  }
}