/*
 * Copyright 2009 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.layout.client;

import static com.google.gwt.dom.client.Style.Unit.CM;
import static com.google.gwt.dom.client.Style.Unit.EM;
import static com.google.gwt.dom.client.Style.Unit.EX;
import static com.google.gwt.dom.client.Style.Unit.IN;
import static com.google.gwt.dom.client.Style.Unit.MM;
import static com.google.gwt.dom.client.Style.Unit.PC;
import static com.google.gwt.dom.client.Style.Unit.PCT;
import static com.google.gwt.dom.client.Style.Unit.PT;
import static com.google.gwt.dom.client.Style.Unit.PX;

import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.junit.DoNotRunWith;
import com.google.gwt.junit.Platform;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.layout.client.Layout.Alignment;
import com.google.gwt.layout.client.Layout.Layer;
import com.google.gwt.user.client.ResizeHelper;
import com.google.gwt.user.client.Window;

import java.util.HashMap;
import java.util.Map;

/**
 * Tests for the {@link Layout} class.
 */
public class LayoutTest extends GWTTestCase {

  /**
   * The amount of time to wait for asynchronous tests to finish.
   */
  private static final int TEST_DELAY = 2000;

  private static interface LayerInitializer {
    void setupLayers(Layer l0, Layer l1);
  }

  private DivElement parent, child0, child1;
  private Element wrapper0, wrapper1;
  private Layout layout;
  private Layer layer0, layer1;

  @Override
  public String getModuleName() {
    return "com.google.gwt.layout.LayoutTest";
  }

  // All testAnimationTransitions_* tests are disabled because they are flaky
  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_LTWH_LTRB_PX_CM() {
    testAnimationTransitions_LTWH_LTRB(PX, CM);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_LTWH_LTRB_PX_EM() {
    testAnimationTransitions_LTWH_LTRB(PX, EM);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_LTWH_LTRB_PX_EX() {
    testAnimationTransitions_LTWH_LTRB(PX, EX);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_LTWH_LTRB_PX_PCT() {
    testAnimationTransitions_LTWH_LTRB(PX, PCT);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_LTWH_RBWH_PX_CM() {
    testAnimationTransitions_LTWH_RBWH(PX, CM);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_LTWH_RBWH_PX_EM() {
    testAnimationTransitions_LTWH_RBWH(PX, EM);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_LTWH_RBWH_PX_EX() {
    testAnimationTransitions_LTWH_RBWH(PX, EX);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_LTWH_RBWH_PX_PCT() {
    testAnimationTransitions_LTWH_RBWH(PX, PCT);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_RBWH_LTRB_PX_CM() {
    testAnimationTransitions_RBWH_LTRB(PX, CM);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_RBWH_LTRB_PX_EM() {
    testAnimationTransitions_RBWH_LTRB(PX, EM);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_RBWH_LTRB_PX_EX() {
    testAnimationTransitions_RBWH_LTRB(PX, EX);
  }

  /**
   * Tests animation constraint- and unit-transitions.
   */
  public void notestAnimationTransitions_RBWH_LTRB_PX_PCT() {
    testAnimationTransitions_RBWH_LTRB(PX, PCT);
  }

  /**
   * Tests child alignment within a layer.
   */
  @DoNotRunWith(Platform.HtmlUnitLayout)
  public void testChildAlignment() {
    layer0.setLeftWidth(0, PX, 128, PX);
    layer0.setTopHeight(0, PX, 256, PX);

    layer0.setChildHorizontalPosition(Alignment.STRETCH);
    layer0.setChildVerticalPosition(Alignment.STRETCH);
    layout.layout();
    assertEquals(0, child0.getOffsetLeft());
    assertEquals(0, child0.getOffsetTop());
    assertEquals(128, child0.getOffsetWidth());
    assertEquals(256, child0.getOffsetHeight());

    child0.getStyle().setWidth(64, PX);
    child0.getStyle().setHeight(128, PX);

    layer0.setChildHorizontalPosition(Alignment.BEGIN);
    layer0.setChildVerticalPosition(Alignment.BEGIN);
    layout.layout();
    assertEquals(0, child0.getOffsetLeft());
    assertEquals(0, child0.getOffsetTop());
    assertEquals(64, child0.getOffsetWidth());
    assertEquals(128, child0.getOffsetHeight());

    layer0.setChildHorizontalPosition(Alignment.END);
    layer0.setChildVerticalPosition(Alignment.END);
    layout.layout();
    assertEquals(64, child0.getOffsetLeft());
    assertEquals(128, child0.getOffsetTop());
    assertEquals(64, child0.getOffsetWidth());
    assertEquals(128, child0.getOffsetHeight());
  }

  /**
   * Test that fillParent() works properly when the outer div is a child of
   * another div, and that it correctly follows that div's size.
   */
  @DoNotRunWith(Platform.HtmlUnitLayout)
  public void testFillParent() {
    // We don't use the default elements created in gwtSetUp() because we need
    // to test the behavior when the layout is contained by an element other
    // than the <body>.
    Document doc = Document.get();
    DivElement container = doc.createDivElement();
    DivElement parent = doc.createDivElement();
    DivElement child = doc.createDivElement();
    child.setInnerHTML("&nbsp;");
    doc.getBody().appendChild(container);
    container.appendChild(parent);

    // The container has to be position:relative so that it serves as an offset
    // parent.
    container.getStyle().setPosition(Position.RELATIVE);
    container.getStyle().setWidth(128, PX);
    container.getStyle().setHeight(256, PX);

    Layout layout = new Layout(parent);
    layout.onAttach();
    Layer layer = layout.attachChild(child);
    layer.setTopBottom(0, PX, 0, PX);
    layer.setLeftRight(0, PX, 0, PX);

    layout.fillParent();
    layout.layout();

    // Test 128x256.
    assertEquals(128, container.getOffsetWidth());
    assertEquals(256, container.getOffsetHeight());
    assertEquals(128, parent.getOffsetWidth());
    assertEquals(256, parent.getOffsetHeight());
    assertEquals(128, child.getOffsetWidth());
    assertEquals(256, child.getOffsetHeight());

    // Expand to 256x256. The layout should react automatically.
    container.getStyle().setWidth(256, PX);
    container.getStyle().setHeight(128, PX);
    assertEquals(256, container.getOffsetWidth());
    assertEquals(256, parent.getOffsetWidth());
    assertEquals(256, child.getOffsetWidth());

    layout.onDetach();
  }

  /**
   * Test that fillParent() works properly when the outer div is a child of the
   * document body.
   */
  @DoNotRunWith(Platform.HtmlUnitLayout)
  public void testFillWindow() {
    layer0.setTopBottom(0, PX, 0, PX);
    layer0.setLeftRight(0, PX, 0, PX);
    layout.layout();

    int w = Window.getClientWidth();
    int h = Window.getClientHeight();
    assertEquals(w, parent.getOffsetWidth());
    assertEquals(h, parent.getOffsetHeight());
    assertEquals(w, child0.getOffsetWidth());
    assertEquals(h, child0.getOffsetHeight());
  }

  /**
   * Tests that the layout reacts to font-size changes.
   */
  @DoNotRunWith(Platform.HtmlUnitLayout)
  public void testFontSizeChange() {
    layer0.setLeftWidth(0, PX, 1, EM);
    layer0.setTopHeight(0, PX, 1, EM);
    layout.layout();

    parent.getStyle().setFontSize(12, PT);
    int cw = child0.getOffsetWidth();
    int ch = child0.getOffsetHeight();

    parent.getStyle().setFontSize(24, PT);
    int nw = child0.getOffsetWidth();
    int nh = child0.getOffsetHeight();
    assertTrue(nw > cw);
    assertTrue(nh > ch);

    parent.getStyle().clearFontSize();
  }

  /**
   * Make sure to call onLayout on the AnimationCallback, when there is
   * no animation (duration =0). This will make sure that onResize will be
   * called on the child panels.
   */
  public void testOnLayoutWhenNoAnimation() {
    final Map<Layer,Double> called = new HashMap();
    layout.layout(0, new Layout.AnimationCallback() {
      @Override
      public void onAnimationComplete() {
      }

      @Override
      public void onLayout(Layer layer, double progress) {
        called.put(layer,progress);
      }
    });
    assertEquals(2,called.size());
    assertEquals(1.0,called.get(layer0));
    assertEquals(1.0,called.get(layer1));
  }

  /**
   * Ensures that two children laid out using various units in such a way that
   * they should abut one another actually do so.
   */
  public void testLayoutStructure() {
    testHorizontalSplit(CM);
    testHorizontalSplit(EM);
    testHorizontalSplit(EX);
    testHorizontalSplit(IN);
    testHorizontalSplit(MM);
    testHorizontalSplit(PC);
    testHorizontalSplit(PT);
    testHorizontalSplit(PX);

    testVerticalSplit(CM);
    testVerticalSplit(EM);
    testVerticalSplit(EX);
    testVerticalSplit(IN);
    testVerticalSplit(MM);
    testVerticalSplit(PC);
    testVerticalSplit(PT);
    testVerticalSplit(PX);
  }

  /**
   * Tests (left-right, left-width, right-width) x (top-bottom, top-height,
   * bottom-height). Ok, so we don't test the *entire* cross-product, but enough
   * to be comfortable.
   */
  public void testStaticConstraints() {
    // This test assumes enough size. Ignore it if size cannot be guaranteed.
    if (!ResizeHelper.isResizeSupported()) {
      return;
    }

    // left-right, top-bottom
    layer0.setTopBottom(32, PX, 32, PX);
    layer0.setLeftRight(32, PX, 32, PX);
    layout.layout();

    int w = parent.getClientWidth();
    int h = parent.getClientHeight();
    assertEquals(32, wrapper0.getOffsetLeft());
    assertEquals(32, wrapper0.getOffsetTop());
    assertEquals(w - 64, wrapper0.getOffsetWidth());
    assertEquals(h - 64, wrapper0.getOffsetHeight());

    // left-width, top-height
    layer0.setTopHeight(16, PX, 128, PX);
    layer0.setLeftWidth(16, PX, 128, PX);
    layout.layout();

    assertEquals(16, wrapper0.getOffsetLeft());
    assertEquals(16, wrapper0.getOffsetTop());
    assertEquals(128, wrapper0.getOffsetWidth());
    assertEquals(128, wrapper0.getOffsetHeight());

    // right-width, bottom-height
    layer0.setBottomHeight(16, PX, 128, PX);
    layer0.setRightWidth(16, PX, 128, PX);
    layout.layout();

    assertEquals(w - (16 + 128), wrapper0.getOffsetLeft());
    assertEquals(h - (16 + 128), wrapper0.getOffsetTop());
    assertEquals(128, wrapper0.getOffsetWidth());
    assertEquals(128, wrapper0.getOffsetHeight());
  }

  /**
   * Tests all unit types.
   */
  public void testUnits() {
    // This test assumes enough size. Ignore it if size cannot be guaranteed.
    if (!ResizeHelper.isResizeSupported()) {
      return;
    }

    // CM
    layer0.setTopBottom(1, CM, 1, CM);
    layer0.setLeftRight(1, CM, 1, CM);
    layout.layout();
    assertLeftRightTopBottomUnitsMakeSense(wrapper0);

    // MM
    layer0.setTopBottom(1, MM, 1, MM);
    layer0.setLeftRight(1, MM, 1, MM);
    layout.layout();
    assertLeftRightTopBottomUnitsMakeSense(wrapper0);

    // IN
    layer0.setTopBottom(1, IN, 1, IN);
    layer0.setLeftRight(1, IN, 1, IN);
    layout.layout();
    assertLeftRightTopBottomUnitsMakeSense(wrapper0);

    // EM
    layer0.setTopBottom(1, EM, 1, EM);
    layer0.setLeftRight(1, EM, 1, EM);
    layout.layout();
    assertLeftRightTopBottomUnitsMakeSense(wrapper0);

    // EX
    layer0.setTopBottom(1, EX, 1, EX);
    layer0.setLeftRight(1, EX, 1, EX);
    layout.layout();
    assertLeftRightTopBottomUnitsMakeSense(wrapper0);

    // PC
    layer0.setTopBottom(1, PC, 1, PC);
    layer0.setLeftRight(1, PC, 1, PC);
    layout.layout();
    assertLeftRightTopBottomUnitsMakeSense(wrapper0);

    // PT
    layer0.setTopBottom(10, PT, 10, PT);
    layer0.setLeftRight(10, PT, 10, PT);
    layout.layout();
    assertLeftRightTopBottomUnitsMakeSense(wrapper0);

    // PCT
    layer0.setTopBottom(10, PCT, 10, PCT);
    layer0.setLeftRight(10, PCT, 10, PCT);
    layout.layout();
    assertLeftRightTopBottomUnitsMakeSense(wrapper0);
  }

  /**
   * Tests layout in the presence of decorations on the parent and child
   * elements.
   */
  @DoNotRunWith(Platform.HtmlUnitLayout)
  public void testWithDecorations() {
    layer0.setTopBottom(0, PX, 0, PX);
    layer0.setLeftRight(0, PX, 0, PX);
    layout.layout();

    // Give each of the parent and child 1px margin and 1px border.
    parent.getStyle().setMargin(1, PX);
    parent.getStyle().setProperty("border", "1px solid black");

    child0.getStyle().setMargin(1, PX);
    child0.getStyle().setProperty("border", "1px solid black");
    layout.layout();

    int w = Window.getClientWidth();
    int h = Window.getClientHeight();
    int pw = parent.getOffsetWidth();
    int ph = parent.getOffsetHeight();

    // The parent's offsetSize should be 2px smaller than the window's client
    // area, because of the margin (the border is *included* in the offsetSize).
    assertEquals(w - 2, pw);
    assertEquals(h - 2, ph);

    // The child's offsetSize (actually that of its wrapper element), should be
    // 2px smaller than the parent, for precisely the same reason.
    assertEquals(pw - 2, wrapper0.getOffsetWidth());
    assertEquals(ph - 2, wrapper0.getOffsetHeight());
  }

  @Override
  protected void gwtSetUp() throws Exception {
    // ensure enough sizes for this test
    ResizeHelper.resizeTo(800, 600);

    Window.enableScrolling(false);

    Document doc = Document.get();
    parent = doc.createDivElement();
    child0 = doc.createDivElement();
    child1 = doc.createDivElement();
    doc.getBody().appendChild(parent);

    layout = new Layout(parent);
    layout.onAttach();
    layout.fillParent();

    layer0 = layout.attachChild(child0);
    layer1 = layout.attachChild(child1);

    wrapper0 = child0.getParentElement();
    wrapper1 = child1.getParentElement();
  }

  @Override
  protected void gwtTearDown() throws Exception {
    Window.enableScrolling(true);
    Document.get().getBody().removeChild(parent);
    layout.onDetach();
  }

  private void assertLeftRightTopBottomUnitsMakeSense(Element elem) {
    // Assume that the element has been laid out to (l, t, r, b) = (1u, 1u, 1u,
    // 1u). Assert that the element's clientLeft/Top are non-zero, and that the
    // clientLeft/Top/Width/Height are consistent with the parent's size.
    int w = parent.getClientWidth();
    int h = parent.getClientHeight();
    int cl = elem.getOffsetLeft();
    int ct = elem.getOffsetTop();
    int cw = elem.getOffsetWidth();
    int ch = elem.getOffsetHeight();

    // Assert that the left-top unit came out at least non-zero size.
    assertTrue(cl > 0);
    assertTrue(ct > 0);

    // Assert that the right-bottom also came out non-zero. We should be able
    // to assert that it came out the same size as the top-left, but it turns
    // out that this isn't quite reliable because of rounding errors.
    assertTrue(w - (cl + cw) > 0);
    assertTrue(h - (ct + ch) > 0);
  }

  // This method may only be called once per test, as it uses delayTestFinish()
  // internally.
  private void testAnimationTransitions_LTWH_LTRB(final Unit unit0,
      final Unit unit1) {
    testAnimationTransitionsHelper(new LayerInitializer() {
      @Override
      public void setupLayers(Layer l0, Layer l1) {
        l0.setLeftWidth(0, unit0, 10, unit0);
        l0.setTopHeight(0, unit0, 10, unit0);
        l1.setLeftRight(1, unit1, 1, unit1);
        l1.setTopBottom(1, unit1, 1, unit1);
      }
    }, new LayerInitializer() {
      @Override
      public void setupLayers(Layer l0, Layer l1) {
        l1.setLeftWidth(0, unit0, 10, unit0);
        l1.setTopHeight(0, unit0, 10, unit0);
        l0.setLeftRight(1, unit1, 1, unit1);
        l0.setTopBottom(1, unit1, 1, unit1);
      }
    });
  }

  // This method may only be called once per test, as it uses delayTestFinish()
  // internally.
  private void testAnimationTransitions_LTWH_RBWH(final Unit unit0,
      final Unit unit1) {
    testAnimationTransitionsHelper(new LayerInitializer() {
      @Override
      public void setupLayers(Layer l0, Layer l1) {
        l0.setLeftWidth(0, unit0, 10, unit0);
        l0.setTopHeight(0, unit0, 10, unit0);
        l1.setRightWidth(0, unit1, 10, unit1);
        l1.setBottomHeight(0, unit1, 10, unit1);
      }
    }, new LayerInitializer() {
      @Override
      public void setupLayers(Layer l0, Layer l1) {
        l1.setLeftWidth(0, unit0, 10, unit0);
        l1.setTopHeight(0, unit0, 10, unit0);
        l0.setRightWidth(0, unit1, 10, unit1);
        l0.setBottomHeight(0, unit1, 10, unit1);
      }
    });
  }

  // This method may only be called once per test, as it uses delayTestFinish()
  // internally.
  private void testAnimationTransitions_RBWH_LTRB(final Unit unit0,
      final Unit unit1) {
    testAnimationTransitionsHelper(new LayerInitializer() {
      @Override
      public void setupLayers(Layer l0, Layer l1) {
        l0.setRightWidth(0, unit0, 10, unit0);
        l0.setBottomHeight(0, unit0, 10, unit0);
        l1.setLeftRight(1, unit1, 1, unit1);
        l1.setTopBottom(1, unit1, 1, unit1);
      }
    }, new LayerInitializer() {
      @Override
      public void setupLayers(Layer l0, Layer l1) {
        l1.setRightWidth(0, unit0, 10, unit0);
        l1.setBottomHeight(0, unit0, 10, unit0);
        l0.setLeftRight(1, unit1, 1, unit1);
        l0.setTopBottom(1, unit1, 1, unit1);
      }
    });
  }

  // This method may only be called once per test, as it uses delayTestFinish()
  // internally.
  private void testAnimationTransitionsHelper(LayerInitializer before,
      LayerInitializer after) {
    before.setupLayers(layer0, layer1);
    layout.layout();

    final int l0 = wrapper0.getOffsetLeft();
    final int t0 = wrapper0.getOffsetTop();
    final int w0 = wrapper0.getOffsetWidth();
    final int h0 = wrapper0.getOffsetHeight();

    final int l1 = wrapper1.getOffsetLeft();
    final int t1 = wrapper1.getOffsetTop();
    final int w1 = wrapper1.getOffsetWidth();
    final int h1 = wrapper1.getOffsetHeight();

    after.setupLayers(layer0, layer1);
    delayTestFinish(TEST_DELAY);
    layout.layout(100, new Layout.AnimationCallback() {
      @Override
      public void onAnimationComplete() {
        // Assert that the two layers have swapped positions.
        assertEquals(l0, wrapper1.getOffsetLeft());
        assertEquals(t0, wrapper1.getOffsetTop());
        assertEquals(w0, wrapper1.getOffsetWidth());
        assertEquals(h0, wrapper1.getOffsetHeight());

        assertEquals(l1, wrapper0.getOffsetLeft());
        assertEquals(t1, wrapper0.getOffsetTop());
        assertEquals(w1, wrapper0.getOffsetWidth());
        assertEquals(h1, wrapper0.getOffsetHeight());

        finishTest();
      }

      @Override
      public void onLayout(Layer layer, double progress) {
      }
    });
  }

  private void testHorizontalSplit(Unit unit) {
    // Line them up horizontally, split at 5 units.
    layer0.setTopBottom(0, PX, 0, PX);
    layer0.setLeftWidth(0, PX, 5, unit);

    layer1.setTopBottom(0, PX, 0, PX);
    layer1.setLeftRight(5, unit, 0, PX);
    layout.layout();

    int child0Right = wrapper0.getOffsetWidth();
    int child1Left = wrapper1.getOffsetLeft();
    assertEquals(child0Right, child1Left);
  }

  private void testVerticalSplit(Unit unit) {
    // Line them up vertically, split at 5em.
    layer0.setTopHeight(0, PX, 5, unit);
    layer0.setLeftRight(0, PX, 0, PX);

    layer1.setTopBottom(5, unit, 0, PX);
    layer1.setLeftRight(0, PX, 0, PX);
    layout.layout();

    int child0Bottom = wrapper0.getOffsetHeight();
    int child1Top = wrapper1.getOffsetTop();
    assertEquals(child0Bottom, child1Top);
  }
}
