/*
 * Copyright 2014 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.impl;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.thirdparty.guava.common.base.Predicate;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;

/**
 * Tests {@link ComputePotentiallyObservableUninitializedValues}.
 */
public class ComputePotentiallyObservableUninitializedValuesTest extends OptimizerTestBase {

  private boolean runMethodInliner;
  private boolean runSpecializer;

  @Override
  public void setUp() throws Exception {
    runMethodInliner = false;
    runSpecializer = false;
  }

  public void testSimpleClass() throws Exception {
    addSnippetClassDecl(
        "static class A  { ",
        "  int i1 = 1;",
        "  Integer i2 = new Integer(1);",
        "  final int fi1 = 1;",
        "  final Integer fi2 = new Integer(1);",
        "}");
    JProgram program = compileSnippet("void", "return;");
    assertAnalysisCorrect(program, ImmutableList.<String>of(),
        ImmutableList
            .of("EntryPoint$A.i1", "EntryPoint$A.i2", "EntryPoint$A.fi1", "EntryPoint$A.fi2"));
  }

  public void testOnlyFinalLiteralUnobservable() throws Exception {
    addSnippetClassDecl(
        "static class A  { ",
        "  A() { m(); }",
        "  void m() { }",
        "}");
    addSnippetClassDecl(
        "static class B extends A { ",
        "  int i1 = 1;",
        "  Integer i2 = new Integer(1);",
        "  final int fi1 = 1;",
        "  final Integer fi2 = new Integer(1);",
        "}");
    JProgram program = compileSnippet("void", "return;");
    assertAnalysisCorrect(program,
        ImmutableList.of("EntryPoint$B.i1", "EntryPoint$B.i2", "EntryPoint$B.fi2"),
        ImmutableList.of("EntryPoint$B.fi1"));
  }

  public void testSafeCustomInitializer() throws Exception {
    addSnippetClassDecl(
        "static class A  { ",
        "  { m(); }",
        "  static void m() { }",
        "}");
    addSnippetClassDecl(
        "static class B extends A { ",
        "  int i1 = 1;",
        "  Integer i2 = new Integer(1);",
        "  final int fi1 = 1;",
        "  final Integer fi2 = new Integer(1);",
        "}");
    JProgram program = compileSnippet("void", "return;");
    assertAnalysisCorrect(program, ImmutableList.<String>of(),
        ImmutableList
            .of("EntryPoint$B.fi1", "EntryPoint$B.i1", "EntryPoint$B.i2", "EntryPoint$B.fi2"));
    MakeCallsStatic.exec(program, false);
    assertAnalysisCorrect(program, ImmutableList.<String>of(),
        ImmutableList
            .of("EntryPoint$B.fi1", "EntryPoint$B.i1", "EntryPoint$B.i2", "EntryPoint$B.fi2"));
  }

  public void testSafeStaticCall() throws Exception {
    addSnippetClassDecl(
        "static class A  { ",
        "  A() { m(new A(), 2); }",
        "  static void m(A a, int i) { }",
        "}");
    addSnippetClassDecl(
        "static class B extends A { ",
        "  int i1 = 1;",
        "  Integer i2 = new Integer(1);",
        "  final int fi1 = 1;",
        "  final Integer fi2 = new Integer(1);",
        "}");
    JProgram program = compileSnippet("void", "return;");
    assertAnalysisCorrect(program, ImmutableList.<String>of(),
        ImmutableList
            .of("EntryPoint$B.fi1", "EntryPoint$B.i1", "EntryPoint$B.i2", "EntryPoint$B.fi2"));
  }

  public void testSafePolyCall() throws Exception {
    addSnippetClassDecl(
        "static class A  { ",
        "  A() { String s = new Integer(0).toString(); }",
        "}");
    addSnippetClassDecl(
        "static class B extends A { ",
        "  int i1 = 1;",
        "  Integer i2 = new Integer(1);",
        "  final int fi1 = 1;",
        "  final Integer fi2 = new Integer(1);",
        "}");
    JProgram program = compileSnippet("void", "return;");
    assertAnalysisCorrect(program, ImmutableList.<String>of(),
        ImmutableList
            .of("EntryPoint$B.fi1", "EntryPoint$B.i1", "EntryPoint$B.i2", "EntryPoint$B.fi2"));
  }

  public void testUnSafeCustomInitializer_polymorphicDispatch() throws Exception {
    addSnippetClassDecl(
        "static class A  { ",
        "  void m() { }",
        "  { m(); }",
        "}");
    addSnippetClassDecl(
        "static class B extends A { ",
        "  int i1 = 1;",
        "  Integer i2 = new Integer(1);",
        "  final int fi1 = 1;",
        "  final Integer fi2 = new Integer(1);",
        "}");
    JProgram program = compileSnippet("void", "return;");
    assertAnalysisCorrect(program,
        ImmutableList.of("EntryPoint$B.i1", "EntryPoint$B.i2", "EntryPoint$B.fi2"),
        ImmutableList.of("EntryPoint$B.fi1"));
  }

  public void testUnSafeCustomInitializer_escapingThis() throws Exception {
    addSnippetClassDecl(
        "static class A  { ",
        "  static void m(A a) { }",
        "  { m(this); }",
        "}");
    addSnippetClassDecl(
        "static class B extends A { ",
        "  int i1 = 1;",
        "  Integer i2 = new Integer(1);",
        "  final int fi1 = 1;",
        "  final Integer fi2 = new Integer(1);",
        "}");
    JProgram program = compileSnippet("void", "return;");
    assertAnalysisCorrect(program,
        ImmutableList.of("EntryPoint$B.i1", "EntryPoint$B.i2", "EntryPoint$B.fi2"),
        ImmutableList.of("EntryPoint$B.fi1"));
  }

  public void testUnSafeCustomInitializer_escapingThisThroughArrayInitializer() throws Exception {
    addSnippetClassDecl(
        "static class A  { ",
        "  { A[] a = new A[] {this}; }",
        "}");
    addSnippetClassDecl(
        "static class B extends A { ",
        "  int i1 = 1;",
        "  Integer i2 = new Integer(1);",
        "  final int fi1 = 1;",
        "  final Integer fi2 = new Integer(1);",
        "}");
    JProgram program = compileSnippet("void", "return;");
    assertAnalysisCorrect(program,
        ImmutableList.of("EntryPoint$B.i1", "EntryPoint$B.i2", "EntryPoint$B.fi2"),
        ImmutableList.of("EntryPoint$B.fi1"));
  }

  public void testUnSafeCustomInitializer_escapingDeepReference() throws Exception {
    addSnippetClassDecl(
        "static class A  { ",
        "  A() {String s = (new Integer(0).toString() + this.toString()).substring(0, 1); }",
        "}");
    addSnippetClassDecl(
        "static class B extends A { ",
        "  int i1 = 1;",
        "  Integer i2 = new Integer(1);",
        "  final int fi1 = 1;",
        "  final Integer fi2 = new Integer(1);",
        "}");
    JProgram program = compileSnippet("void", "return;");
    assertAnalysisCorrect(program,
        ImmutableList.of("EntryPoint$B.i1", "EntryPoint$B.i2", "EntryPoint$B.fi2"),
        ImmutableList.of("EntryPoint$B.fi1"));
    MakeCallsStatic.exec(program, false); // required so that method is static
    assertAnalysisCorrect(program,
        ImmutableList.of("EntryPoint$B.i1", "EntryPoint$B.i2", "EntryPoint$B.fi2"),
        ImmutableList.of("EntryPoint$B.fi1"));
  }

  @Override
  protected boolean doOptimizeMethod(TreeLogger logger, JProgram program, JMethod method) {
    if (runMethodInliner) {
      MethodInliner.exec(program);
    }
    if (runSpecializer) {
      // required so that method is marked final
      Finalizer.exec(program, OptimizerContext.NULL_OPTIMIZATION_CONTEXT);
      MakeCallsStatic.exec(program, false); // required so that method is static
      // required so that the parameter types are tightened
      TypeTightener.exec(program, new FullOptimizerContext(program));
      MethodCallSpecializer.exec(program, OptimizerContext.NULL_OPTIMIZATION_CONTEXT);
    }

    int mods = DeadCodeElimination.exec(program, method);
    if (mods > 0) {
      // Make sure we converge in one pass.
      //
      // TODO(rluble): It does not appear to be true in general unless we iterate until a
      // fixpoint in exec().
      //
      // Example:
      //
      //     Constructor( ) { deadcode }
      //     m( new Constructor(); }
      //
      // If m is processed first, it will see the constructor as having side effects.
      // Then the constructor will become empty enabling m() become empty in the next round.
      //
      assertEquals(0, DeadCodeElimination.exec(program, method));
    }
    return mods > 0;
  }

  private void assertAnalysisCorrect(JProgram program,
      Iterable<String> fieldsThatCanBeObservedUninitialized,
      Iterable<String> fieldsThatCannotBeObservedUninitialized) {
    Predicate<JField> uninitializedValuesCanBeObserved =
        ComputePotentiallyObservableUninitializedValues.analyze(program);

    for (String fieldName : fieldsThatCanBeObservedUninitialized) {
      JField field = findField(program, fieldName);
      assertNotNull(field);
      assertTrue("Field " + fieldName + " was erroneously determined to uninitialized unobservable",
          uninitializedValuesCanBeObserved.apply(field));
    }
    for (String fieldName : fieldsThatCannotBeObservedUninitialized) {
      JField field = findField(program, fieldName);
      assertNotNull(field);
      assertFalse("Field " + fieldName + " was erroneously determined to uninitialized observable",
          uninitializedValuesCanBeObserved.apply(field));
    }
  }
}
