/*
 * Copyright 2013 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.dev.jjs.test;

import com.google.gwt.dev.jjs.test.overrides.package1.Caller;
import com.google.gwt.dev.jjs.test.overrides.package1.ClassExposingM;
import com.google.gwt.dev.jjs.test.overrides.package1.SomeParent;
import com.google.gwt.dev.jjs.test.overrides.package1.SomeParentParent;
import com.google.gwt.dev.jjs.test.overrides.package1.SomeParentParentParent;
import com.google.gwt.dev.jjs.test.overrides.package1.SubClassExposingM;
import com.google.gwt.dev.jjs.test.overrides.package2.SomeSubClassInAnotherPackage;
import com.google.gwt.dev.jjs.test.overrides.package2.SomeSubSubClassInAnotherPackage;
import com.google.gwt.dev.jjs.test.overrides.package3.SomeInterface;
import com.google.gwt.dev.jjs.test.overrides.package3.SomePackageConfusedParent;
import com.google.gwt.junit.DoNotRunWith;
import com.google.gwt.junit.Platform;
import com.google.gwt.junit.client.GWTTestCase;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javaemul.internal.annotations.DoNotInline;
import jsinterop.annotations.JsMethod;
import jsinterop.annotations.JsOverlay;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;

/**
 * Tests Miscellaneous fixes.
 */
public class CompilerMiscRegressionTest extends GWTTestCase {

  @Override
  public String getModuleName() {
    return "com.google.gwt.dev.jjs.CompilerSuite";
  }

  native double toNumber(String value) /*-{
    return +value;
  }-*/;

  native double addAndConvert(double v1, String v2) /*-{
    return v1 + +v2;
  }-*/;

  native double minusAndDecrement(double val) /*-{
    var lhs = val;
    return - --lhs;
  }-*/;

  /**
   * The array {@code map.get("one")[0]} gets normalized (by {@link ImplementCastsAndTypeChecks}) to
   * {@code Cast.dynamicCast(map.get("one"), ...)[0]}. The expression resulting from dynamicCast
   * would have type Object and that would not be a valid type for an array access operation.
   */
  public void testOverridingReturnType() {
    Map<String, String[]> map = new HashMap<>();
    map.put("one", new String[10]);

    map.get("one")[0] = "one";
    assertEquals("one", map.get("one")[0]);
  }

  /**
   * Test for issues 6373 and 3942.
   */
  public void testUnaryPlus() {
    // With the unary + operator stripped the first assertion only fails in
    // dev mode, in web mode the  comparison made by assertEquals masks
    // the error; whereas the second fails in both dev and web modes.
    assertEquals(11.0, toNumber("11"));
    assertEquals(12.0, toNumber("10") + toNumber("2"));
    assertEquals(12.0, addAndConvert(10, "2"));
    assertEquals(-10.0, minusAndDecrement(11));
  }
  private static float[] copy(float[] src, float[] dest) {
    System.arraycopy(src, 0, dest, 0, Math.min(src.length, dest.length));
    return dest;
  }

  private void throwE(String message) {
    throw new RuntimeException(message);
  }

  /**
   * Test for issue 8243.
   */
  public void testAddAllLargeNumberOfElements() {

    int dstLength = 10;
    // Some browser have a limit on the number of parameters a function can have and 130000 barely
    // exceeds Chrome limit (as of V34).
    // This limit also applies when functions are called through apply().
    int srcLength =  130000;
    List<String> original = new ArrayList<String>();
    for (int i = 0; i < dstLength; i++) {
      original.add("foo");
    }
    List<String> src = new ArrayList<String>();
    for (int i = 0; i < srcLength; i++) {
      src.add("bar");
    }

    original.addAll(src);
    final int totalLength = srcLength + dstLength;
    assertEquals(totalLength, original.size());

    // Check the result sampling as iterating through large arrays seems costly in IE.
    for (int i = 0; i < totalLength; i += 1000) {
      if (i < dstLength) {
        assertEquals("foo", original.get(i));
      } else {
        assertEquals("bar", original.get(i));
      }
    }
  }

  /**
   * Test for issue 7253.
   */
  public void testNestedTryFollowedByTry() {
    try {
      throwE("1");
      fail("Should have thrown RuntimeException");
    } catch (RuntimeException e) {
      assertEquals("1", e.getMessage());
      try {
        throwE("2");
        fail("Should have thrown RuntimeException");
      } catch (RuntimeException e2) {
        assertEquals("2", e2.getMessage());
      }
    }
    try {
      throwE("3");
      fail("Should have thrown RuntimeException");
    } catch (RuntimeException e) {
      assertEquals("3", e.getMessage());
    }
  }

  /**
   * Test for issue 6638.
   */
  public void testNewArrayInlining() {
    float[] src = new float[]{1,1,1};
    float[] dest = copy(src, new float[3]);

    assertEqualContents(src, dest);
  }

  /**
   * Tests complex overriding patterns involving package private methods.
   * <p>
   * Test for issue 8654.
   */
  public void testOverride() {
    Caller aCaller = new Caller();
    assertEquals("SomeParentParent", aCaller.callPackagePrivatem(new SomeParentParent()));
    assertEquals("SomeParent", aCaller.callPackagePrivatem(new SomeParent()));
    assertEquals("SomeParent", aCaller.callPackagePrivatem(
        new SomeSubClassInAnotherPackage()));

    assertEquals("SomeSubClassInAnotherPackage",
        SomeSubClassInAnotherPackage.pleaseCallm(new SomeSubClassInAnotherPackage()));
    assertEquals("SomeSubSubClassInAnotherPackage",
        SomeSubClassInAnotherPackage.pleaseCallm(new SomeSubSubClassInAnotherPackage()));

    assertEquals("ClassExposingM",
        aCaller.callPackagePrivatem(new ClassExposingM()));

    SomeInterface i = new ClassExposingM();
    assertEquals("ClassExposingM", i.m());
    assertEquals("live at ClassExposingM", new ClassExposingM().f());

    // Confirm that both calling m through SomeInterface and through SomeParentParentParent
    // dispatch to the right implementation.
    SomeInterface i1 = new SubClassExposingM();
    assertEquals("SubClassExposingM", i1.m());

    assertEquals("SubClassExposingM",
        SomeParentParentParent.callSomeParentParentParentM(new SubClassExposingM()));

    assertEquals("SomeParentParentParent",
        SomeParentParentParent.callSomeParentParentParentM(new SomeParentParentParent()));
    assertEquals("SomeParentParentParent",
        SomeParentParentParent.callSomeParentParentParentM(new SomePackageConfusedParent()));
    assertEquals("SomeParentParent",
        SomeParentParentParent.callSomeParentParentParentM(new SomeParentParent()));
    assertEquals("SomeParent",
        SomeParentParentParent.callSomeParentParentParentM(new SomeParent()));
    assertEquals("SomeParent",
        SomeParentParentParent.callSomeParentParentParentM(new SomeSubClassInAnotherPackage()));
    assertEquals("SomeParent",
        SomeParentParentParent.callSomeParentParentParentM(new SomeSubSubClassInAnotherPackage()));
  }

  enum MyEnum {
    A,
    B,
    C;

    public final static MyEnum[] VALUES = values();

    public int getPriority() {
      return VALUES.length - ordinal();
    }
  }

  /**
   * Tests that enum ordinalizer does not incorrectly optimize {@code MyEnum}.
   * <p>
   * Test for issue 8846.
   */
  public void testMyEnum() {
    assertEquals(2, MyEnum.B.getPriority());
  }

  enum OrderingProblem {
    A,
    B;

    public static OrderingProblem getPriority1() {
      if (new Integer(1).toString().isEmpty()) {
        return B;
      }
      return A;
    }
  }

  /**
   * Test for regression introduced in patch https://gwt-review.googlesource.com/#/c/9083; where
   * depending on the order in which references to the enum class were encountered, some instances
   * were not correctly replaced .
   */
  public void testOrderingProblem() {
    assertEquals(OrderingProblem.A.ordinal(), OrderingProblem.getPriority1().ordinal());
  }

  /**
   * Tests that regexes are not incorrectly internalized.
   *
   * Test for issue 8865.
   */
  public native void testJavaScriptRegExps() /*-{
    // Make regexes large enough so that the will be interned (if regex interning was enabled).
    var regExp1 = /this is a string where the search/g;
    var regExp2 = /this is a string where the search/g;
    var str = "this is a string where the search occurs";
    @junit.framework.Assert::assertEquals(ZZ)(
       regExp1.test(str), regExp2.test(str));
  }-*/;

  private static final double MINUTES_IN_DAY = 24 * 60;

  @DoNotInline
  public void assertStaticEvaluationRegression(int hour, int minute) {
    // Do not inline this method so that the problematic expression reaches JsStaticEval.
    double expected = hour * 60 + minute;
    expected /= MINUTES_IN_DAY;
    expected *= 100;
    assertEquals(expected , (hour * 60 + minute) / MINUTES_IN_DAY * 100);
  }

  /**
   * Test for issue 8934.
   */
  public void testStaticEvaluationRegression() {
    // Perform two calls with different constant values to make sure the assertStaticEvaluation does
    // not get the constant parameters propagated and statically evaluated in the Java AST.
    assertStaticEvaluationRegression(10, 20);
    assertStaticEvaluationRegression(20, 10);
  }

  /**
   * Test for issue 8909.
   * <p>
   * DevMode does not conform to JS arithmetic semantics and this method tests exactly that.
   */
  @DoNotRunWith(Platform.Devel)
  public void testStaticEvaluationSematics() {
    float num = getRoundedValue(1.005f);
    assertEquals(1.00, num, 0.001);
  }

  private float getRoundedValue(float parameter) {
    float local = parameter;
    local = local * 100f;
    return Math.round(local) / 100f;
  }

  /**
   * Test for issue 9043.
   */
  public native void testMultipleClassLiteralReferences() /*-{
    var a = @com.google.gwt.dev.jjs.test.CompilerMiscRegressionTest::class;
    var b = @com.google.gwt.dev.jjs.test.CompilerMiscRegressionTest::class;
  }-*/;

  /**
   * Test for issue 9153.
   * <p>
   * Typetightener used to incorrectly tighten method calls marked with STATIC_DISPATCH_ONLY.
   */

  public void testIncorrectDispatch() {
    state = new int[1];
    new B().m();
    assertEquals(1, state[0]);
  }

  static int[] state;
  @JsType
  abstract static class A {
    public void m() {
      state[0] = 1;
    }
  }

  @JsType
  static class B extends A {
    public void m() {
      super.m();
    }
  }

  private static void assertEqualContents(float[] expected, float[] actual) {

    assertEquals("Array length mismatch", expected.length, actual.length);
    for (int i = 0; i < expected.length; i++) {
      assertEquals("Array mismatch at element " + i , expected[i], actual[i]);
    }
  }

  @JsType(isNative = true)
  interface SomeNativeInterface {
    @JsProperty
    String getTextContent();
  }

  @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
  static abstract class AbstractNativeType implements SomeNativeInterface {
  }

  private static native AbstractNativeType createAbstractNativeType(String value) /*-{
    return {textContent : value};
  }-*/;

  // Tests that methods that override native JsMethods are generated properly w.r.t
  // JsMethod/JsProperty annotations even if the overridden method is not live.
  // See bug #9358
  //
  // Make sure this method is part of a suite that is run with -nogenerateJsInteropExports.
  public void testNativeJsMethodDispatch_unreferencedSupertypeMethod() {
    final AbstractNativeType o = createAbstractNativeType("Hello");
    assertEquals("Hello", o.getTextContent());
  }

  @JsMethod
  private static List<String> singletonFrom(int i, String... arguments) {
    // Make the second parameter varargs and pass it as a whole to trigger the arguments copying
    // preamble.
    return Arrays.asList(arguments).subList(i,i + 1);
  }

  @JsMethod
  private static List<String> argumentsParameterClasher(int arguments, String... others) {
    // Make the second parameter varargs and pass it as a whole to trigger the arguments copying
    // preamble.
    return Arrays.asList(others).subList(0, arguments);
  }

  @JsMethod
  private static List<String> argumentsVariableClasher(int i, String... others) {
    // Make the second parameter varargs and pass it as a whole to trigger the arguments copying
    // preamble.
    {
      int arguments = 3;
    }
    return Arrays.asList(others).subList(0, i);
  }

 public void testVarargsNamedArguments() {
    assertEquals("GoodBye", singletonFrom(1, "Hello", "GoodBye").get(0));
    assertEquals("Hello", argumentsParameterClasher(1, "Hello", "GoodBye").get(0));
    assertEquals("Hello", argumentsVariableClasher(1, "Hello", "GoodBye").get(0));
  }

  @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Array")
  private static class NativeArray {
    @JsProperty(name = "length")
    public int len;
    @JsMethod(name = "push")
    public native void add(double element);
  }

  private static native Object newArray() /*-{
    return @NativeArray::new()();
  }-*/;

  private static native int arrayLength(Object array) /*-{
    return array.@NativeArray::len;
  }-*/;

  private static native void arrayAdd(Object array, double d) /*-{
    array.@NativeArray::add(D)(d);
    return array;
  }-*/;

  private static native Object newArrayThroughCtorReference() /*-{
    var ctor =  @NativeArray::new();
    return ctor();
  }-*/;

  private static native boolean jsniIsNan(double number) /*-{
    return @Global::isNan(D)(number);
  }-*/;

  private static native double jsniGetNan() /*-{
    return @Global::Nan;
  }-*/;

  @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
  private static class Global {
    @JsProperty(namespace = JsPackage.GLOBAL, name = "NaN")
    public static double Nan;
    @JsMethod(namespace = JsPackage.GLOBAL, name = "isNaN")
    public static native boolean isNan(double number);
  }

  // Regression tests for issue #9520.
  public void testNativeConstructorJSNI() {
    Object nativeArray = newArray();
    arrayAdd(nativeArray, 0);
    arrayAdd(nativeArray, 1);
    assertTrue(nativeArray instanceof NativeArray);
    assertEquals(2, arrayLength(nativeArray));
    assertTrue(newArrayThroughCtorReference() instanceof NativeArray);
    assertTrue(Double.isNaN(jsniGetNan()));
    assertTrue(jsniIsNan(Double.NaN));
  }

  @JsMethod(namespace = "<window>", name = "isNaN")
  public static native boolean isNaN(double number);

  // Regression tests for issue #9573
  public void testTopLevelNameClash() {
    boolean isNaN = isNaN(Global.Nan);
    assertTrue(isNaN);
  }

  @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
  private static class NativeObjectWithOverlay {
    public NativeObjectWithOverlay() { }
    @JsOverlay
    final NativeObjectWithOverlay getThis() {
      return this;
    }
  }

  public void testOverlayDispatchOnNull() {
    // Define a variable where the compiler can not statically determine that it is actually null.
    NativeObjectWithOverlay objectWithOverlay =
        Math.random() > 1000 ? new NativeObjectWithOverlay() : null;

    assertTrue(objectWithOverlay.getThis() == null);
  }
}
