/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * 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.facebook.jni;

import static org.assertj.core.api.Assertions.assertThat;

import com.facebook.jni.annotations.DoNotStrip;
import org.junit.Test;

public class HybridTests extends BaseFBJniTests {
  static class TestHybridClass {
    // Hybrid classes must include a member which manages the C++ object.  It
    // will be initialized from C++.  It must be declared exactly with this
    // type and name, so JNI can find it, and initialized once in the ctor.
    // The annotation is necessary to keep proguard from renaming it, or else JNI
    // won't be able to find it.
    @DoNotStrip private final HybridData mHybridData;

    // This is the method which creates the C++ instance and initializes
    // mHybridData.  Conventionally, it should be named initHybrid, and invoked
    // from the constructor.  This must be called only once.  If the C++
    // instance is referenced before this is called, a NullPointerException
    // will be thrown.
    private native HybridData initHybrid(int i, String s, boolean b);

    // You can have more than one, which may be useful if the ctor is
    // overloaded.  This will call the default C++ ctor.
    private native HybridData initHybrid();

    // Implements factory-style initialization.  You shouldn't usually
    // need both styles in one class.  Here we do it for testing and
    // demo purposes.
    private native HybridData initHybrid(String s, int i, boolean b);

    // Java ctor must invoke initHybrid().  This just passes arguments through,
    // but the ctor can do whatever work it wants, as long as it calls
    // initHybrid() before any native methods.
    public TestHybridClass(int i, String s, boolean b) {
      mHybridData = initHybrid(i, s, b);
    }

    // This behaves the same as the ctor above, I just wanted a different
    // signature to demonstrate factory-style initialization.
    public TestHybridClass(String s, int i, boolean b) {
      mHybridData = initHybrid(s, i, b);
    }

    // This is the simplest case.  Even if everything is default, initHybrid()
    // must still be called.
    public TestHybridClass() {
      mHybridData = initHybrid();
    }

    // Java ctor used by C++ newObjectCxxArgs.  Note this is private.
    private TestHybridClass(HybridData hd) {
      mHybridData = hd;
    }

    public void doneUsingIt() {
      mHybridData.resetNative();
    }

    // Some C++ methods.
    public native void setBoth(int i, String s);

    public native int getInt();

    public native String getString();

    public native String getCharString();

    public native boolean copy1(TestHybridClass other);

    public native boolean copy2(TestHybridClass other);

    public native void oops();

    public native void setGlobal(String s);

    public native String getGlobal1();

    public native String getGlobal2();

    public static native TestHybridClass makeWithTwo();

    public static native TestHybridClass makeWithThree();

    public static native void autoconvertMany();
  }

  @Test
  public void testHybridClass() {
    TestHybridClass thc1 = new TestHybridClass();
    assertThat(thc1.getInt()).isEqualTo(0);
    assertThat(thc1.getString()).isEqualTo("");

    thc1.setBoth(1, "one");
    assertThat(thc1.getInt()).isEqualTo(1);
    assertThat(thc1.getString()).isEqualTo("one");

    TestHybridClass thc2 = TestHybridClass.makeWithTwo();
    assertThat(thc2.getInt()).isEqualTo(2);
    assertThat(thc2.getString()).isEqualTo("two");

    thc2.doneUsingIt();

    thrown.expect(NullPointerException.class);
    thc2.getInt();
  }

  @Test
  public void testHybridAutoconversion() {
    TestHybridClass thc3 = TestHybridClass.makeWithThree();
    assertThat(thc3.copy1(new TestHybridClass(3, "three", false))).isTrue();
    assertThat(thc3.getInt()).isEqualTo(3);
    assertThat(thc3.getString()).isEqualTo("three");

    TestHybridClass thc4 = new TestHybridClass();
    thc4.copy1(new TestHybridClass("four", 4, false));
    assertThat(thc4.getInt()).isEqualTo(4);
    assertThat(thc4.getString()).isEqualTo("four");
    assertThat(thc4.getCharString()).isEqualTo("four");

    TestHybridClass thc5 = new TestHybridClass();
    assertThat(thc5.copy2(new TestHybridClass(5, "five", false))).isTrue();
    assertThat(thc5.getInt()).isEqualTo(5);
    assertThat(thc5.getString()).isEqualTo("five");
  }

  @Test
  public void testReturnGlobalRef() {
    TestHybridClass thc = new TestHybridClass();
    thc.setGlobal("global_ref");
    assertThat(thc.getGlobal1()).isEqualTo("global_ref");
    assertThat(thc.getGlobal2()).isEqualTo("global_ref");
  }

  @Test
  public void testLocalLeak() {
    TestHybridClass.autoconvertMany();
  }

  @Test
  public void testExceptionMapping() {
    TestHybridClass thc1 = new TestHybridClass();
    thrown.expect(ArrayStoreException.class);
    thc1.oops();
  }

  abstract static class AbstractTestHybrid {
    @DoNotStrip private final HybridData mHybridData;

    private int mAbstractNum;

    protected AbstractTestHybrid(HybridData hybridData, int an) {
      mHybridData = hybridData;
      mAbstractNum = an;
    }

    public int abstractNum() {
      return mAbstractNum;
    }

    public native int nativeNum();

    public abstract int concreteNum();

    public abstract int sum();
  }

  static class ConcreteTestHybrid extends AbstractTestHybrid {
    public ConcreteTestHybrid(int an, int nn, int cn) {
      super(initHybrid(nn, cn), an);
    }

    private static native HybridData initHybrid(int nn, int cn);

    // overrides can be native
    @Override
    public native int concreteNum();

    // overrides can be java
    @Override
    public int sum() {
      return nativeNum() + abstractNum() + concreteNum();
    }
  }

  @Test
  public void testHybridInheritance() {
    AbstractTestHybrid ath = new ConcreteTestHybrid(1, 2, 3);
    assertThat(ath.abstractNum()).isEqualTo(1);
    assertThat(ath.nativeNum()).isEqualTo(2);
    assertThat(ath.concreteNum()).isEqualTo(3);
    assertThat(ath.sum()).isEqualTo(6);
  }

  public static native boolean cxxTestInheritance(AbstractTestHybrid ath);

  public static native AbstractTestHybrid makeAbstractHybrid();

  @Test
  public void testHybridCxx() {
    AbstractTestHybrid ath = new ConcreteTestHybrid(4, 5, 6);
    assertThat(cxxTestInheritance(ath)).isTrue();

    AbstractTestHybrid ath2 = makeAbstractHybrid();
    assertThat(ath2 instanceof ConcreteTestHybrid).isTrue();
    assertThat(ath2.abstractNum()).isEqualTo(7);
    assertThat(ath2.nativeNum()).isEqualTo(8);
    assertThat(ath2.concreteNum()).isEqualTo(9);
    assertThat(ath2.sum()).isEqualTo(24);
  }

  static class Base {}

  static class Derived extends Base {
    @DoNotStrip private final HybridData mHybridData;

    private Derived(HybridData hybridData) {
      mHybridData = hybridData;
    }
  }

  public static native boolean cxxTestDerivedJavaClass();

  @Test
  public void testDerivedJavaClassCxx() {
    assertThat(cxxTestDerivedJavaClass()).isTrue();
  }

  static class TestHybridClassBase extends HybridClassBase {
    protected native void initHybrid();

    private native void initHybrid(int i);

    protected TestHybridClassBase() {
      // No initHybrid() here!
      // Otherwise factory construction will set native pointer twice and process will crash.
    }

    public TestHybridClassBase(int i) {
      initHybrid(i);
    }

    // Some C++ methods.
    public native void setInt(int i);

    public native int getInt();

    public static native TestHybridClassBase makeWithThree();
  }

  static class TestHybridClassBaseDefaultCtor extends TestHybridClassBase {
    public TestHybridClassBaseDefaultCtor() {
      initHybrid();
    }
  }

  @Test
  public void testHybridBaseDefaultCtor() {
    TestHybridClassBaseDefaultCtor base = new TestHybridClassBaseDefaultCtor();
    assertThat(base.getInt()).isZero();

    base.setInt(58);
    assertThat(base.getInt()).isEqualTo(58);
  }

  @Test
  public void testHybridBaseConstructorArgs() {
    TestHybridClassBase base = new TestHybridClassBase(42);
    assertThat(base.getInt()).isEqualTo(42);
  }

  @Test
  public void testHybridBaseFactoryConstruction() {
    TestHybridClassBase base = TestHybridClassBase.makeWithThree();
    assertThat(base.getInt()).isEqualTo(3);
  }

  static class Destroyable {
    @DoNotStrip private final HybridData mHybridData;

    private Destroyable(HybridData hybridData) {
      mHybridData = hybridData;
    }
  }

  public static native boolean cxxTestHybridDestruction();

  @Test
  public void testHybridDestuction() {
    assertThat(cxxTestHybridDestruction()).isTrue();
  }
}
