// -*- 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.widgets.boxes.Box.BoxDescriptor;
import com.google.appinventor.common.utils.StringUtils;
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.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

import com.allen_sauer.gwt.dnd.client.DragEndEvent;
import com.allen_sauer.gwt.dnd.client.DragHandler;
import com.allen_sauer.gwt.dnd.client.DragStartEvent;
import com.allen_sauer.gwt.dnd.client.drop.IndexedDropController;

import java.util.ArrayList;
import java.util.Set;
import java.util.List;
import java.util.Map;

/**
 * Defines a column-based layout for the boxes on a work area panel.
 *
 */
public final class ColumnLayout extends Layout {

  /**
   * Drag handler for detecting changes to the layout.
   */
  public class ChangeDetector implements DragHandler {

    @Override
    public void onDragEnd(DragEndEvent event) {
      fireLayoutChange();
    }

    @Override
    public void onDragStart(DragStartEvent event) {
    }

    @Override
    public void onPreviewDragEnd(DragEndEvent event) {
    }

    @Override
    public void onPreviewDragStart(DragStartEvent event) {
    }
  }

  /**
   * Represents a column in the layout.
   */
  public static final class Column extends IndexedDropController {

    // Field names for JSON object encoding of layout
    private static final String NAME_WIDTH = "width";
    private static final String NAME_BOXES = "boxes";

    // Associated column container (this is the state of the layout when it is active)
    private final VerticalPanel columnPanel;

    // Relative column width (in percent of work area width)
    private int relativeWidth;

    // Absolute width (in pixel)
    private int absoluteWidth;

    // List of box description (this is the state of the layout when it is inactive)
    private final List<BoxDescriptor> boxes;

    /**
     * Creates new column.
     */
    private Column(int relativeWidth) {
      this(new VerticalPanel(), relativeWidth);
    }

    /**
     * Creates new column.
     */
    private Column(VerticalPanel columnPanel, int relativeWidth) {
      super(columnPanel);

      boxes = new ArrayList<BoxDescriptor>();

      this.columnPanel = columnPanel;
      this.relativeWidth = relativeWidth;

      columnPanel.setWidth(relativeWidth + "%");
      columnPanel.setSpacing(SPACING);
    }

    @Override
    protected void insert(Widget widget, int beforeIndex) {
      // columnPanel always contains at least one widget: the 'invisible' end marker. Therefore
      // beforeIndex cannot become negative.
      if (beforeIndex == columnPanel.getWidgetCount()) {
        beforeIndex--;
      }

      super.insert(widget, beforeIndex);

      if (widget instanceof Box) {
        ((Box) widget).onResize(absoluteWidth);
      }
    }

    /**
     * Invoked upon resizing of the work area panel.
     *
     * @see WorkAreaPanel#onResize(int, int)
     *
     * @param width column width in pixel
     */
    public void onResize(int width) {
      absoluteWidth = width * relativeWidth / 100;
      columnPanel.setWidth(absoluteWidth + "px");
      for (Widget w : columnPanel) {
        if (w instanceof Box) {
          ((Box) w).onResize(absoluteWidth);
        } else {
          // Top-of-column marker (otherwise invisible widget)
          w.setWidth(absoluteWidth + "px");
        }
      }
    }

    /**
     * Add a new box to the column.
     *
     * @param type  type of box
     * @param height  height of box in pixels if not minimized
     * @param minimized  indicates whether box is minimized
     */
    public void add(Class<? extends Box> type, int height, boolean minimized) {
      boxes.add(new BoxDescriptor(type, absoluteWidth, height, minimized));
    }

    /**
     * Updates the box descriptors for the boxes in the column.
     */
    private void updateBoxDescriptors() {
      boxes.clear();
      for (Widget w : columnPanel) {
        if (w instanceof Box) {
          boxes.add(((Box) w).getLayoutSettings());
        }
      }
    }

    /**
     * Returns JSON encoding for the boxes in a column.
     */
    private String toJson() {
      List<String> jsonBoxes = new ArrayList<String>();
      for (int i = 0; i < columnPanel.getWidgetCount(); i++) {
        Widget w = columnPanel.getWidget(i);
        if (w instanceof Box) {
          jsonBoxes.add(((Box) w).getLayoutSettings().toJson());
        }
      }

      return "{" +
            "\"" + NAME_WIDTH + "\":" + JSONUtil.toJson(relativeWidth) + "," +
            "\"" + NAME_BOXES + "\":[" + StringUtils.join(",", jsonBoxes) + "]" +
          "}";
    }

    /**
     * Creates a new column from a JSON encoded layout.
     *
     * @param columnIndex  index of column
     * @param object  column in JSON format
     */
    private static Column fromJson(int columnIndex, JSONObject object) {
      Column column = new Column(columnIndex);

      Map<String, JSONValue> properties = object.getProperties();
      column.relativeWidth = JSONUtil.intFromJsonValue(properties.get(NAME_WIDTH));

      for (JSONValue boxObject : properties.get(NAME_BOXES).asArray().getElements()) {
        column.boxes.add(BoxDescriptor.fromJson(boxObject.asObject()));
      }

      return column;
    }

    /**
     * Collects box types encoded in the JSON.
     *
     * @param object    column in JSON format
     * @param boxTypes  set of box types encountered so far
     */
    private static void boxTypesFromJson(JSONObject object,
                                         Set<String> boxTypes) {
      Map<String, JSONValue> properties = object.getProperties();

      for (JSONValue boxObject : properties.get(NAME_BOXES).asArray().getElements()) {
        boxTypes.add(BoxDescriptor.boxTypeFromJson(boxObject.asObject()));
      }
    }

  }

  // Spacing between columns in pixels
  private static final int SPACING = 5;

  // Field names for JSON object encoding of layout
  private static final String NAME_NAME = "name";
  private static final String NAME_COLUMNS = "columns";

  // List of columns
  private final List<Column> columns;

  // Drag handler for detecting changes to the layout
  private final DragHandler changeDetector;

  /**
   * Creates a new layout.
   */
  public ColumnLayout(String name) {
    super(name);

    columns = new ArrayList<Column>();
    changeDetector = new ChangeDetector();
  }

  /**
   * Clears the layout (removes all existing columns etc).
   */
  private void clear(WorkAreaPanel workArea) {
    for (Column column : columns) {
      workArea.getWidgetDragController().unregisterDropController(column);
    }
    workArea.getWidgetDragController().removeDragHandler(changeDetector);
    columns.clear();
  }

  /**
   * Adds a new column to the layout.
   *
   * @param relativeWidth  relative width of column (width of all columns
   *                        should add up to 100)
   * @return  new layout column
   */
  public Column addColumn(int relativeWidth) {
    Column column = new Column(relativeWidth);
    columns.add(column);
    return column;
  }

  @Override
  public void apply(WorkAreaPanel workArea) {

    // Clear base panel
    workArea.clear();

    // Horizontal panel to hold columns
    HorizontalPanel horizontalPanel = new HorizontalPanel();
    horizontalPanel.setSize("100%", "100%");
    workArea.add(horizontalPanel);

    // Initialize columns
    for (Column column : columns) {
      horizontalPanel.add(column.columnPanel);
      workArea.getWidgetDragController().registerDropController(column);

      // Add invisible dummy widget to prevent column from collapsing when it contains no boxes
      column.columnPanel.add(new Label());

      // Add boxes from layout
      List<BoxDescriptor> boxes = column.boxes;
      for (int index = 0; index < boxes.size(); index++) {
        BoxDescriptor bd = boxes.get(index);
        Box box = workArea.createBox(bd);
        if (box != null) {
          column.insert(box, index);
          box.restoreLayoutSettings(bd);
        }
      }
    }

    workArea.getWidgetDragController().addDragHandler(changeDetector);
  }

  @Override
  public void onResize(int width, int height) {
    // Calculate the usable width for the columns (which is the width of the browser client area
    // minus the spacing on each side of the boxes).
    int usableWidth = (width - ((columns.size() + 1) * SPACING));

    // On startup it can happen that we receive a window resize event before the boxes are attached
    // to the DOM. In that case, width and height are 0, we can safely ignore because there will
    // soon be another resize event after the boxes are attached to the DOM.
    if (width > 0) {
      for (Column column : columns) {
        column.onResize(usableWidth);
      }
    }
  }

  @Override
  public String toJson() {
    List<String> jsonColumns = new ArrayList<String>(columns.size());
    for (Column column : columns) {
      jsonColumns.add(column.toJson());
    }

    return "{" +
          "\"" + NAME_NAME + "\":" + JSONUtil.toJson(getName()) + "," +
          "\"" + NAME_COLUMNS + "\":[" + StringUtils.join(",", jsonColumns) + "]" +
        "}";
  }

  /**
   * Creates a new layout from a JSON encoded layout.
   *
   * @param object  layout in JSON format
   */
  public static Layout fromJson(JSONObject object, WorkAreaPanel workArea) {

    Map<String, JSONValue> properties = object.getProperties();

    String name = properties.get(NAME_NAME).asString().getString();
    ColumnLayout layout = (ColumnLayout) workArea.getLayouts().get(name);
    if (layout == null) {
      layout = new ColumnLayout(name);
    }

    layout.clear(workArea);
    for (JSONValue columnObject : properties.get(NAME_COLUMNS).asArray().getElements()) {
      layout.columns.add(Column.fromJson(layout.columns.size(), columnObject.asObject()));
    }

    return layout;
  }

  /**
   * Collects box types encoded in the JSON.
   *
   * @param object    layout in JSON format
   * @param boxTypes  box types encountered so far
   */
  public static void boxTypesFromJson(JSONObject object, Set<String> boxTypes) {

    Map<String, JSONValue> properties = object.getProperties();

    for (JSONValue columnObject : properties.get(NAME_COLUMNS).asArray().getElements()) {
      Column.boxTypesFromJson(columnObject.asObject(), boxTypes);
    }

  }

  @Override
  protected void fireLayoutChange() {
    // Need to update box descriptors before firing change event.
    // It is easier (maintenance-wise) to do this here instead of doing this in multiple places.
    for (Column column : columns) {
      column.updateBoxDescriptors();
    }

    super.fireLayoutChange();
  }
}