/*
 * Copyright 2010 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.canvas.dom.client;

import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d.Composite;
import com.google.gwt.canvas.dom.client.Context2d.LineCap;
import com.google.gwt.canvas.dom.client.Context2d.LineJoin;
import com.google.gwt.canvas.dom.client.Context2d.TextAlign;
import com.google.gwt.canvas.dom.client.Context2d.TextBaseline;
import com.google.gwt.junit.DoNotRunWith;
import com.google.gwt.junit.Platform;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.ui.RootPanel;

import java.util.Locale;

/**
 * Tests {@link Context2d}.
 * 
 * Because HtmlUnit does not support HTML5, you will need to run these tests manually in order to
 * have them run. To do that, go to "run configurations" or "debug configurations", select the test
 * you would like to run, and put this line in the VM args under the arguments tab:
 * -Dgwt.args="-runStyle Manual:1"
 */
@DoNotRunWith(Platform.HtmlUnitUnknown)
public class Context2dTest extends GWTTestCase {
  protected Canvas canvas1;
  protected Canvas canvas2;

  native boolean isGecko190OrBefore() /*-{
    return @com.google.gwt.dom.client.DOMImplMozilla::isGecko190OrBefore()();
  }-*/;

  native boolean isWebkit525OrBefore() /*-{
    return @com.google.gwt.dom.client.DOMImplWebkit::isWebkit525OrBefore()();
  }-*/;

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

  @Override
  protected void gwtSetUp() throws Exception {
    canvas1 = Canvas.createIfSupported();
    canvas2 = Canvas.createIfSupported();

    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    RootPanel.get().add(canvas1);
    RootPanel.get().add(canvas2);
  }

  @Override
  protected void gwtTearDown() throws Exception {
    RootPanel.get().remove(canvas1);
    RootPanel.get().remove(canvas2);
  }

  public void testArc() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    canvas1.setHeight("40px");
    canvas1.setWidth("60px");
    canvas1.setCoordinateSpaceHeight(80);
    canvas1.setCoordinateSpaceWidth(120);

    // get a 2d context
    Context2d context = canvas1.getContext2d();

    // make sure there are no issues drawing an arc
    try {
      context.beginPath();
      context.arc(50, 50, 40, 0, Math.PI);
      context.closePath();
      context.stroke();
    } catch (Exception e) {
      fail("Should not throw an exception drawing an arc: " + e.getMessage());
    }
  }

  public void testFillRect() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    // Safari 3.0 does not support getImageData(), so the following tests are disabled for
    // Safari 3.0 and before.
    if (isWebkit525OrBefore()) {
      return;
    }

    canvas1.setHeight("40px");
    canvas1.setWidth("60px");
    canvas1.setCoordinateSpaceHeight(80);
    canvas1.setCoordinateSpaceWidth(120);
    assertEquals(40, canvas1.getOffsetHeight());
    assertEquals(60, canvas1.getOffsetWidth());
    assertEquals(80, canvas1.getCoordinateSpaceHeight());
    assertEquals(120, canvas1.getCoordinateSpaceWidth());

    // get a 2d context
    Context2d context = canvas1.getContext2d();

    // draw green rectangle filling 1st half of canvas
    context.setFillStyle(CssColor.make("#00fF00"));
    context.fillRect(0, 0, 60, 80);
    // draw red rectangle filling 2nd half of canvas
    context.setFillStyle("#fF0000");
    context.fillRect(60, 0, 60, 80);

    // test that the first pixel is green and the last pixel is red
    ImageData imageData = context.getImageData(0, 0, 120, 80);
    CanvasPixelArray pixelArray = imageData.getData();
    assertEquals(000, pixelArray.get(0));
    assertEquals(255, pixelArray.get(1));
    assertEquals(000, pixelArray.get(2));
    assertEquals(255, pixelArray.get(3));
    assertEquals(255, pixelArray.get((40 * 20 - 1) * 4 + 0));
    assertEquals(000, pixelArray.get((40 * 20 - 1) * 4 + 1));
    assertEquals(000, pixelArray.get((40 * 20 - 1) * 4 + 2));
    assertEquals(255, pixelArray.get((40 * 20 - 1) * 4 + 3));
    
    // draw a blue square in the top left
    context.setFillStyle("#0000fF");
    context.fillRect(0, 0, 20, 20);
    imageData = context.getImageData(0, 0, 20, 20);
    pixelArray = imageData.getData();
    
    // test that the first pixel is blue
    assertEquals(000, pixelArray.get(0));
    assertEquals(000, pixelArray.get(1));
    assertEquals(255, pixelArray.get(2));
    assertEquals(255, pixelArray.get(3));
  }

  public void testFillStyle() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    // get the 2d contexts
    Context2d context1 = canvas1.getContext2d();
    Context2d context2 = canvas2.getContext2d();

    // test that a color can be set and is correct
    context1.setFillStyle(CssColor.make("#ffff00"));
    FillStrokeStyle fillStyleCol = context1.getFillStyle();
    assertTrue("fillStyleCol is a color", fillStyleCol.getType() == FillStrokeStyle.TYPE_CSSCOLOR);
    assertFalse("fillStyleCol is a color", fillStyleCol.getType() == FillStrokeStyle.TYPE_GRADIENT);
    assertFalse("fillStyleCol is a color", fillStyleCol.getType() == FillStrokeStyle.TYPE_PATTERN);

    // test that a gradient can be set and is correct
    CanvasGradient gradient = context1.createLinearGradient(0, 0, 10, 20);
    gradient.addColorStop(0.5f, "#ffaaff");
    context1.setFillStyle(gradient);
    FillStrokeStyle fillStyleGrad = context1.getFillStyle();
    assertFalse("fillStyleGrad is a gradient", fillStyleGrad.getType() == FillStrokeStyle.TYPE_CSSCOLOR);
    assertTrue("fillStyleGrad is a gradient", fillStyleGrad.getType() == FillStrokeStyle.TYPE_GRADIENT);
    assertFalse("fillStyleGrad is a gradient", fillStyleGrad.getType() == FillStrokeStyle.TYPE_PATTERN);

    // test that a pattern can be set and is correct
    CanvasPattern pattern = context1.createPattern(canvas1.getCanvasElement(), 
        Context2d.Repetition.REPEAT);
    context1.setFillStyle(pattern);
    FillStrokeStyle fillStylePat = context1.getFillStyle();
    assertFalse("fillStylePat is a pattern", fillStylePat.getType() == FillStrokeStyle.TYPE_CSSCOLOR);
    assertFalse("fillStylePat is a pattern", fillStylePat.getType() == FillStrokeStyle.TYPE_GRADIENT);
    assertTrue("fillStylePat is a pattern", fillStylePat.getType() == FillStrokeStyle.TYPE_PATTERN);
    
    // test that a StrokeStyle can be passed around w/o knowing what it is
    FillStrokeStyle fillStyle = context1.getFillStyle();
    context2.setFillStyle(fillStyle);
    FillStrokeStyle fillStyle2 = context2.getFillStyle();
    assertFalse("fillStyle2 is a pattern", fillStyle2.getType() == FillStrokeStyle.TYPE_CSSCOLOR);
    assertFalse("fillStyle2 is a pattern", fillStyle2.getType() == FillStrokeStyle.TYPE_GRADIENT);
    assertTrue("fillStyle2 is a pattern", fillStyle2.getType() == FillStrokeStyle.TYPE_PATTERN);
  }

  public void testFont() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    Context2d context = canvas1.getContext2d();
    context.setFont("40px \"Times New Roman\"");
    assertEquals("40px \"Times New Roman\"", context.getFont());
  }

  public void testGlobalAlpha() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    Context2d context = canvas1.getContext2d();
    context.setGlobalAlpha(0.5);
    assertEquals(0.5, context.getGlobalAlpha());
  }

  public void testGlobalComposite() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    Context2d context = canvas1.getContext2d();
    context.setGlobalCompositeOperation(Composite.SOURCE_OVER);
    assertEquals(Composite.SOURCE_OVER.getValue(), context.getGlobalCompositeOperation());
    context.setGlobalCompositeOperation(Composite.DESTINATION_OVER);
    assertEquals(Composite.DESTINATION_OVER.getValue(), context.getGlobalCompositeOperation());
  }

  public void testGradient() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    // Safari 3.0 does not support getImageData(), so the following tests are disabled for
    // Safari 3.0 and before.
    if (isWebkit525OrBefore()) {
      return;
    }

    canvas1.setHeight("40px");
    canvas1.setWidth("60px");
    canvas1.setCoordinateSpaceHeight(40);
    canvas1.setCoordinateSpaceWidth(60);
    Context2d context = canvas1.getContext2d();

    // fill the canvas with black
    context.setFillStyle("#000000");
    context.fillRect(0, 0, 60, 40);
    
    // create a linear gradient from the top-left to the bottom-right.
    CanvasGradient linearGradient = context.createLinearGradient(0, 0, 60, 40);
    linearGradient.addColorStop(0, "#ff0000");
    linearGradient.addColorStop(1, "#00ffff");
    context.setFillStyle(linearGradient);
    context.fillRect(0, 0, 60, 40);
    
    // test that the first pixel is ff0000, the last is 000fff, and the middle is something else
    // isn't exact due to rounding, give 5px approx wiggle-room
    int approx = 5;
    ImageData imageData = context.getImageData(0, 0, 60, 40);
    CanvasPixelArray pixelArray = imageData.getData();

    assertTrue(255 - pixelArray.get(0) < approx);
    assertTrue(pixelArray.get(1) - 000 < approx);
    assertTrue(pixelArray.get(2) - 000 < approx);
    assertTrue(255 - pixelArray.get(3) < approx);
    assertFalse(000 == pixelArray.get((60 * 40 / 2) * 4 + 0));
    assertFalse(255 == pixelArray.get((60 * 40 / 2) * 4 + 0));
    assertTrue(pixelArray.get((60 * 40 - 1) * 4 + 0) - 000 < approx);
    assertTrue(255 - pixelArray.get((60 * 40 - 1) * 4 + 1) < approx);
    assertTrue(255 - pixelArray.get((60 * 40 - 1) * 4 + 2) < approx);
    assertTrue(255 - pixelArray.get((60 * 40 - 1) * 4 + 3) < approx);
  }

  public void testImageData() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }
    
    // Firefox 3.0 does not support createImageData(), so the following tests are disabled
    // for FF 3.0 and before.
    if (isGecko190OrBefore()) {
      return;
    }

    // Safari 3.0 does not support getImageData(), so the following tests are disabled for
    // Safari 3.0 and before.
    if (isWebkit525OrBefore()) {
      return;
    }

    canvas1.setHeight("40px");
    canvas1.setWidth("60px");
    canvas1.setCoordinateSpaceHeight(40);
    canvas1.setCoordinateSpaceWidth(60);
    Context2d context = canvas1.getContext2d();

    // fill the canvas with ffff00
    context.setFillStyle("#ffff00");
    context.fillRect(0, 0, 60, 40);
    
    // create a 1 x 1 image
    ImageData onePx = context.createImageData(1, 1); // will fail on FF3.0 (not such method)
    assertEquals(1, onePx.getHeight());
    assertEquals(1, onePx.getWidth());
    
    // set the image to a single blue pixel
    CanvasPixelArray onePxArray = onePx.getData();
    onePxArray.set(0, 000);
    onePxArray.set(1, 000);
    onePxArray.set(2, 255);
    onePxArray.set(3, 255);
    
    // put the pixel at location 10, 10 on the canvas
    context.putImageData(onePx, 10, 10);
    ImageData verifyPx = context.getImageData(10, 10, 1, 1);
    assertEquals(1, verifyPx.getWidth());
    assertEquals(1, verifyPx.getHeight());
    CanvasPixelArray verifyPxArray = verifyPx.getData();
    assertEquals(onePxArray.get(0), verifyPxArray.get(0));
    assertEquals(onePxArray.get(1), verifyPxArray.get(1));
    assertEquals(onePxArray.get(2), verifyPxArray.get(2));
    assertEquals(onePxArray.get(3), verifyPxArray.get(3));

    // test that edge cases don't blow up: values outside the range 0...255
    ImageData clampPixels = context.createImageData(3, 3); // will fail on FF3.0 (not such method)
    CanvasPixelArray clampArraySet = clampPixels.getData();
    try {
      clampArraySet.set(2, -2);
      clampArraySet.set(3, 270);
      context.putImageData(clampPixels, 4, 4);
    } catch (Exception e) {
      fail("Should not throw exception when setting values outside the range of 0...255");
    }
    clampPixels = context.getImageData(4, 4, 3, 3);
    CanvasPixelArray clampArrayGet = clampPixels.getData();
    
    // test that edge cases don't blow up: fall off the CanvasPixelArray end
    int aPixel = clampArrayGet.get(9999);
    assertEquals("CanvasPixelArray should return 0 for values off its end", 0, aPixel);
    int bPixel = clampArrayGet.get(-9999);
    assertEquals("CanvasPixelArray should return 0 for values off its end", 0, bPixel);
  }

  public void testIsPointInPath() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    canvas1.setHeight("40px");
    canvas1.setWidth("60px");
    canvas1.setCoordinateSpaceHeight(40);
    canvas1.setCoordinateSpaceWidth(60);

    Context2d context = canvas1.getContext2d();
    context.beginPath();
    context.moveTo(10, 10);
    context.lineTo(20, 20);
    context.lineTo(20, 10);
    context.closePath();
    
    assertTrue("Point should be in path", context.isPointInPath(18, 12));
    assertFalse("Point should not be in path", context.isPointInPath(1, 1));
  }

  public void testLines() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    canvas1.setHeight("40px");
    canvas1.setWidth("60px");
    canvas1.setCoordinateSpaceHeight(40);
    canvas1.setCoordinateSpaceWidth(60);
    
    Context2d context = canvas1.getContext2d();
    context.setFillStyle("#ff00ff");
    context.setLineCap(LineCap.BUTT);
    context.setLineJoin(LineJoin.BEVEL);
    context.setLineWidth(2);

    context.beginPath();
    context.moveTo(10, 10);
    context.lineTo(20, 20);
    context.lineTo(20, 10);
    context.closePath();

    assertEquals(LineCap.BUTT.getValue(), context.getLineCap());
    assertEquals(LineJoin.BEVEL.getValue(), context.getLineJoin());
    assertEquals(2.0, context.getLineWidth());
  }

  public void testMiter() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    Context2d context = canvas1.getContext2d();
    context.setMiterLimit(3);
    assertEquals(3.0, context.getMiterLimit());
  }

  public void testPixelManipulation() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    // Safari 3.0 does not support getImageData(), so the following tests are disabled for
    // Safari 3.0 and before.
    if (isWebkit525OrBefore()) {
      return;
    }

    canvas1.setHeight("40px");
    canvas1.setWidth("60px");
    canvas1.setCoordinateSpaceHeight(40);
    canvas1.setCoordinateSpaceWidth(60);
    Context2d context = canvas1.getContext2d();

    // fill the canvas with ff0000
    context.setFillStyle("#ff0000");
    context.fillRect(0, 0, 60, 40);
    // fill the 1st and 2nd quadrants with green
    context.setFillStyle("#00ff00");
    context.fillRect(0, 0, 60, 20);
    
    ImageData imageData = context.getImageData(0, 0, 60, 40);
    assertEquals("Pixels in first quadrant should be green", 0, imageData.getRedAt(45, 10));
    assertEquals("Pixels in first quadrant should be green", 255, imageData.getGreenAt(45, 10));
    assertEquals("Pixels in third quadrant should be red", 255, imageData.getRedAt(15, 30));
    assertEquals("Pixels in third quadrant should be red", 0, imageData.getGreenAt(15, 30));
  }

  public void testShadows() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }
    
    // Firefox 3.0 returns the incorrect shadowBlur value so the following tests are disabled
    // for FF 3.0 and before.
    if (isGecko190OrBefore()) {
      return;
    }

    canvas1.setHeight("40px");
    canvas1.setWidth("60px");
    canvas1.setCoordinateSpaceHeight(40);
    canvas1.setCoordinateSpaceWidth(60);

    Context2d context = canvas1.getContext2d();
    context.setShadowBlur(3);
    context.setShadowColor("#ff00ff");
    context.setShadowOffsetX(3);
    context.setShadowOffsetY(4);
    context.lineTo(60, 40);
    assertEquals(3.0, context.getShadowBlur());
    assertEquals("#ff00ff", context.getShadowColor().toLowerCase(Locale.ROOT));
    assertEquals(3.0, context.getShadowOffsetX());
    assertEquals(4.0, context.getShadowOffsetY());
  }

  public void testStrokeStyle() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }

    // get the 2d contexts
    Context2d context1 = canvas1.getContext2d();
    Context2d context2 = canvas2.getContext2d();

    // test that a color can be set and is correct
    context1.setStrokeStyle(CssColor.make("#ffff00"));
    FillStrokeStyle strokeStyleCol = context1.getStrokeStyle();
    assertTrue("strokeStyleCol is a color", strokeStyleCol.getType() == FillStrokeStyle.TYPE_CSSCOLOR);
    assertFalse("strokeStyleCol is a color", strokeStyleCol.getType() == FillStrokeStyle.TYPE_GRADIENT);
    assertFalse("strokeStyleCol is a color", strokeStyleCol.getType() == FillStrokeStyle.TYPE_PATTERN);

    // test that a gradient can be set and is correct
    CanvasGradient gradient = context1.createLinearGradient(0, 0, 10, 20);
    gradient.addColorStop(0.5, "#ff000f");
    context1.setStrokeStyle(gradient);
    FillStrokeStyle strokeStyleGrad = context1.getStrokeStyle();
    assertFalse("strokeStyleGrad is a gradient", strokeStyleGrad.getType() == FillStrokeStyle.TYPE_CSSCOLOR);
    assertTrue("strokeStyleGrad is a gradient", strokeStyleGrad.getType() == FillStrokeStyle.TYPE_GRADIENT);
    assertFalse("strokeStyleGrad is a gradient", strokeStyleGrad.getType() == FillStrokeStyle.TYPE_PATTERN);

    // test that a pattern can be set and is correct
    CanvasPattern pattern = context1.createPattern(canvas1.getCanvasElement(), 
        Context2d.Repetition.REPEAT);
    context1.setStrokeStyle(pattern);
    FillStrokeStyle strokeStylePat = context1.getStrokeStyle();
    assertFalse("strokeStylePat is a pattern", strokeStylePat.getType() == FillStrokeStyle.TYPE_CSSCOLOR);
    assertFalse("strokeStylePat is a pattern", strokeStylePat.getType() == FillStrokeStyle.TYPE_GRADIENT);
    assertTrue("strokeStylePat is a pattern", strokeStylePat.getType() == FillStrokeStyle.TYPE_PATTERN);
    
    // test that a StrokeStyle can be passed around w/o knowing what it is
    FillStrokeStyle strokeStyle = context1.getStrokeStyle();
    context2.setStrokeStyle(strokeStyle);
    FillStrokeStyle strokeStyle2 = context2.getStrokeStyle();
    assertFalse("strokeStyle2 is a pattern", strokeStyle2.getType() == FillStrokeStyle.TYPE_CSSCOLOR);
    assertFalse("strokeStyle2 is a pattern", strokeStyle2.getType() == FillStrokeStyle.TYPE_GRADIENT);
    assertTrue("strokeStyle2 is a pattern", strokeStyle2.getType() == FillStrokeStyle.TYPE_PATTERN);
  }

  public void testText() {
    if (canvas1 == null) {
      return; // don't continue if not supported
    }
    
    canvas1.setHeight("40px");
    canvas1.setWidth("60px");
    Context2d context = canvas1.getContext2d();
    context.setFont("bold 40px sans-serif");
    context.setTextAlign(TextAlign.CENTER);
    context.setTextBaseline(TextBaseline.HANGING);
    context.fillText("GWT", 50, 60);
    assertEquals(TextAlign.CENTER.getValue(), context.getTextAlign());
    assertEquals(TextBaseline.HANGING.getValue(), context.getTextBaseline());
  }
}
