/*
 * Copyright 2012-2014 Institut National des Sciences Appliquées de Lyon (INSA-Lyon)
 *
 * 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 fr.insalyon.citi.golo.runtime;

import org.testng.annotations.Test;

import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandle;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;

import static java.lang.invoke.MethodHandles.lookup;
import static java.lang.invoke.MethodType.methodType;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;


public class MethodInvocationSupportTest {

  public static class Person {
    String name;
    String email;

    public Person(String name, String email) {
      this.name = name;
      this.email = email;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    public String getEmail() {
      return email;
    }

    public void setEmail(String email) {
      this.email = email;
    }

    public String greet(Person... people) {
      StringBuilder builder = new StringBuilder("Hello");
      for (Person p : people) {
        builder.append(" ").append(p.name).append("!");
      }
      return builder.toString();
    }

    @Override
    public String toString() {
      return "Person{" +
          "name='" + name + '\'' +
          ", email='" + email + '\'' +
          '}';
    }
  }

  public static class VarargsChecking {

    public String concat(String separator, String... values) {
      if (values.length == 0) {
        return "";
      }
      String result = values[0];
      for (int i = 1; i < values.length; i++) {
        result = result + separator + values[i];
      }
      return result;
    }

    public String defaultConcat(String... values) {
      return concat("-", values);
    }
  }

  public static class FieldAccessors {
    public Object property;
  }

  public Person julien() {
    return new Person("Julien", "julien.ponge@insa-lyon.fr");
  }

  public VarargsChecking varargsChecking() {
    return new VarargsChecking();
  }

  @Test
  public void check_to_string() throws Throwable {
    CallSite toString = MethodInvocationSupport.bootstrap(lookup(), "toString", methodType(Object.class, Object.class), 0);
    String result = (String) toString.dynamicInvoker().invokeWithArguments(julien());
    assertThat(result, notNullValue());
    assertThat(result, is("Person{name='Julien', email='julien.ponge@insa-lyon.fr'}"));
  }

  @Test
  public void check_set_name() throws Throwable {
    CallSite setName = MethodInvocationSupport.bootstrap(lookup(), "setName", methodType(Object.class, Object.class, Object.class), 0);
    Person julien = julien();
    setName.dynamicInvoker().invokeWithArguments(julien, "Julien Ponge");
    assertThat(julien.name, is("Julien Ponge"));
  }

  @Test
  public void check_equals() throws Throwable {
    CallSite equals = MethodInvocationSupport.bootstrap(lookup(), "equals", methodType(Object.class, Object.class, Object.class), 0);
    Person julien = julien();
    Boolean result = (Boolean) equals.dynamicInvoker().invokeWithArguments(julien, julien);
    assertThat(result, is(true));
  }

  @Test
  public void check_field_read() throws Throwable {
    CallSite name = MethodInvocationSupport.bootstrap(lookup(), "name", methodType(Object.class, Object.class), 0);
    String result = (String) name.dynamicInvoker().invokeWithArguments(julien());
    assertThat(result, notNullValue());
    assertThat(result, is("Julien"));
  }

  @Test(expectedExceptions = NoSuchMethodError.class)
  public void check_bogus() throws Throwable {
    CallSite bogus = MethodInvocationSupport.bootstrap(lookup(), "bogus", methodType(Object.class, Object.class), 0);
    bogus.dynamicInvoker().invokeWithArguments(julien());
  }

  @Test
  public void check_many_to_string() throws Throwable {
    CallSite toString = MethodInvocationSupport.bootstrap(lookup(), "toString", methodType(Object.class, Object.class), 0);
    MethodHandle toStringMH = toString.dynamicInvoker();

    for (int i = 0; i < 5; i++) {
      String result = (String) toStringMH.invokeWithArguments(julien());
      assertThat(result, is("Person{name='Julien', email='julien.ponge@insa-lyon.fr'}"));

      result = (String) toStringMH.invokeWithArguments("foo");
      assertThat(result, is("foo"));

      result = (String) toStringMH.invokeWithArguments(666);
      assertThat(result, is("666"));

      result = (String) toStringMH.invokeWithArguments(666L);
      assertThat(result, is("666"));

      result = (String) toStringMH.invokeWithArguments("foo");
      assertThat(result, is("foo"));

      result = (String) toStringMH.invokeWithArguments(new BigInteger("1234"));
      assertThat(result, is("1234"));

      result = (String) toStringMH.invokeWithArguments(new Object() {
        @Override
        public String toString() {
          return "Damn";
        }
      });
      assertThat(result, is("Damn"));

      result = (String) toStringMH.invokeWithArguments(new Object() {
        @Override
        public String toString() {
          return "Plop";
        }
      });
      assertThat(result, is("Plop"));

      result = (String) toStringMH.invokeWithArguments(new Object() {
        @Override
        public String toString() {
          return "Hey!";
        }
      });
      assertThat(result, is("Hey!"));
    }
  }

  @Test
  public void check_primitive_argument_allowance() throws Throwable {
    List<String> list = Arrays.asList("a", "b", "c");
    CallSite get = MethodInvocationSupport.bootstrap(lookup(), "get", methodType(Object.class, Object.class, Object.class), 0);

    MethodHandle handle = get.dynamicInvoker();
    assertThat(((String) handle.invokeWithArguments(list, 0)), is("a"));
    assertThat(((String) handle.invokeWithArguments(list, 1)), is("b"));
    assertThat(((String) handle.invokeWithArguments(list, 2)), is("c"));
  }

  @Test
  public void check_varags() throws Throwable {
    CallSite concat = MethodInvocationSupport.bootstrap(lookup(), "concat", methodType(Object.class, Object.class, Object.class, Object.class, Object.class, Object.class), 0);
    VarargsChecking receiver = varargsChecking();

    Object result = concat.dynamicInvoker().invokeWithArguments(receiver, "-", "a", "b", "c");
    assertThat(result, notNullValue());
    assertThat(result, instanceOf(String.class));
    assertThat((String) result, is("a-b-c"));

    concat = MethodInvocationSupport.bootstrap(lookup(), "concat", methodType(Object.class, Object.class, Object.class, Object.class), 0);
    result = concat.dynamicInvoker().invokeWithArguments(receiver, "-", new String[]{"a", "b", "c"});
    assertThat(result, notNullValue());
    assertThat(result, instanceOf(String.class));
    assertThat((String) result, is("a-b-c"));
  }

  @Test
  public void check_varags_only() throws Throwable {
    CallSite concat = MethodInvocationSupport.bootstrap(lookup(), "defaultConcat", methodType(Object.class, Object.class, Object.class, Object.class, Object.class), 0);
    VarargsChecking receiver = varargsChecking();

    Object result = concat.dynamicInvoker().invokeWithArguments(receiver, "a", "b", "c");
    assertThat(result, notNullValue());
    assertThat(result, instanceOf(String.class));
    assertThat((String) result, is("a-b-c"));

    concat = MethodInvocationSupport.bootstrap(lookup(), "defaultConcat", methodType(Object.class, Object.class, Object.class), 0);
    result = concat.dynamicInvoker().invokeWithArguments(receiver, new String[]{"a", "b", "c"});
    assertThat(result, notNullValue());
    assertThat(result, instanceOf(String.class));
    assertThat((String) result, is("a-b-c"));

    concat = MethodInvocationSupport.bootstrap(lookup(), "defaultConcat", methodType(Object.class, Object.class, Object.class), 0);
    receiver = varargsChecking();
    assertThat((String) concat.dynamicInvoker().invokeWithArguments(receiver, "a"), is("a"));

    concat = MethodInvocationSupport.bootstrap(lookup(), "defaultConcat", methodType(Object.class, Object.class), 0);
    receiver = varargsChecking();
    assertThat((String) concat.dynamicInvoker().invokeWithArguments(receiver), is(""));
  }

  @Test
  public void check_field_getter() throws Throwable {
    CallSite property = MethodInvocationSupport.bootstrap(lookup(), "property", methodType(Object.class, Object.class), 0);
    FieldAccessors receiver = new FieldAccessors();
    receiver.property = "foo";

    Object result = property.dynamicInvoker().invokeWithArguments(receiver);
    assertThat(result, notNullValue());
    assertThat(result, instanceOf(String.class));
    assertThat((String) result, is("foo"));
  }

  @Test
  public void check_field_setter() throws Throwable {
    CallSite property = MethodInvocationSupport.bootstrap(lookup(), "property", methodType(Object.class, Object.class, Object.class), 0);
    FieldAccessors receiver = new FieldAccessors();
    receiver.property = "undefined";

    property.dynamicInvoker().invokeWithArguments(receiver, "foo");
    assertThat((String) receiver.property, is("foo"));
  }

  @Test(expectedExceptions = NullPointerException.class)
  public void not_nullsafe_invocation() throws Throwable {
    CallSite toString = MethodInvocationSupport.bootstrap(lookup(), "toString", methodType(Object.class, Object.class), 0);
    toString.dynamicInvoker().invoke(null);
  }

  @Test
  public void nullsafe_invocation() throws Throwable {
    CallSite toString = MethodInvocationSupport.bootstrap(lookup(), "toString", methodType(Object.class, Object.class), 1);

    MethodHandle invoker = toString.dynamicInvoker();
    assertThat(invoker.invoke(null), nullValue());
    assertThat((String) invoker.invoke("a"), is("a"));
    assertThat((String) invoker.invoke("b"), is("b"));
    assertThat(invoker.invoke(null), nullValue());
  }

  @Test
  public void nullsafe_megamorphic_invocation() throws Throwable {
    CallSite toString = MethodInvocationSupport.bootstrap(lookup(), "toString", methodType(Object.class, Object.class), 1);
    MethodInvocationSupport.InlineCache pic = (MethodInvocationSupport.InlineCache) toString;
    pic.depth = MethodInvocationSupport.InlineCache.MEGAMORPHIC_THRESHOLD + 10;

    MethodHandle invoker = toString.dynamicInvoker();
    assertThat(invoker.invoke(null), nullValue());
    assertThat((String) invoker.invoke("a"), is("a"));
    assertThat((String) invoker.invoke(1), is("1"));
    assertThat((String) invoker.invoke(1L), is("1"));
    assertThat((String) invoker.invoke(Arrays.asList()), is("[]"));
    assertThat((String) invoker.invoke(new Object()), startsWith("java.lang.Object"));
    assertThat(invoker.invoke(null), nullValue());
  }
}
