import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.checkerframework.framework.testchecker.h1h2checker.quals.*;

public class InferTypeArgsPolyChecker<OUTER_SCOPE_TV> {
  // ----------------------------------------------------------
  // Test Case - A
  <A> A methodA(@H2Top A a1, @H2Top A a2) {
    return null;
  }

  void contextA(@H1S1 @H2Bot String str, @H1Bot @H2Bot List<@H1S2 String> s) {
    @H2Bot Object a = methodA(str, s);
    @H1Top @H2Bot Object aTester = a;
  }

  // ----------------------------------------------------------
  // Test Case - B
  <B> B methodB(List<@H2S2 B> b1, List<@H1S2 B> b2) {
    return null;
  }

  void contextB(List<@H1S1 @H2S2 String> l1, List<@H1S2 @H2S1 String> l2) {
    @H1S1 @H2S1 String str = methodB(l1, l2);
  }

  // ----------------------------------------------------------
  // Test Case - C
  <C extends List<? extends Object>> C methodC(C c1, C c2) {
    return null;
  }

  void contextC(List<@H1S1 @H2S2 ? extends @H1S1 @H2S2 String> l1, List<@H1S1 @H2S2 String> l2) {
    List<@H1S1 @H2S2 ?> str = methodC(l1, l2);
  }

  // ----------------------------------------------------------
  // Test Case - D

  <D extends OUTER_SCOPE_TV, DD> D methodD(D d1, D d2, DD dd) {
    return null;
  }

  <D extends OUTER_SCOPE_TV, DD> DD methodD2(D d1, D d2, DD dd) {
    return null;
  }

  void contextD(OUTER_SCOPE_TV os1, @H1S1 @H2S1 OUTER_SCOPE_TV os2) {
    OUTER_SCOPE_TV osNaked1 = methodD(os1, os1, os2);

    // So for the next failure we correctly infer that for methodD to take both os1 and os2 as
    // arguments D must be @H1Top @H2Top OUTER_SCOPE_TV.  However, the UPPER_BOUND of D is
    // <@H1Bottom @H2Bottom OUTER_SCOPE_TV extends @H1Top @H2Top Object> notice that our
    // inferred type for D is above this bound.
    //
    // A similar, more useful example in the Nullness type system would be:
    /*
       class Gen<OUTER> {
          public List<OUTER> listo;

          <T extends OUTER> void addToListo(T t1, T T2) {
              listo.add(t1);
              listo.add(t2);
          }

          void launder(@NonNull OUTER arg1, @Nullable OUTER arg2) {
              addToListo(arg1, arg2); // T is inferred to be <@Nullable OUTER>
                                      // if we did not mark this as type.argument
                                      // then we would have no idea that in the last
                                      // line of this example we are putting a null value into
                                      // a List of @NonNull Strings
          }

       }

       Gen<@NonNull String> g = ...;
       g.listo = new ArrayList<@NonNull String>();
       g.launder("", null);    // during this method call null would be added to g.listo
    */
    // :: error: (type.arguments.not.inferred)
    OUTER_SCOPE_TV osNaked2 = methodD(os1, os2, "");

    // :: error: (type.arguments.not.inferred)
    OUTER_SCOPE_TV osAnnoed = methodD(os2, os2, "");

    // :: error: (type.arguments.not.inferred)
    String str = methodD2(os2, os1, "");
    // :: error: (type.arguments.not.inferred)
    OUTER_SCOPE_TV osNaked3 = methodD2(os1, os1, os2);
  }

  // ----------------------------------------------------------
  // Test Case - E
  <E> E methodE(E e1, E[] aos2) {
    return null;
  } // pass an array to one of these to cover A2FReducer.visitArray_Typevar

  void contextE(String[] strArr, String[][] strArrArr, OUTER_SCOPE_TV os, OUTER_SCOPE_TV[] aos) {
    String[] strArrLocal = methodE(strArr, strArrArr);
    OUTER_SCOPE_TV osLocal = methodE(os, aos);
  }

  // ----------------------------------------------------------
  // Test Case - C
  <F> List<? super F> methodF(List<? extends F> lExtF, List<? super F> lSupF) {
    return null;
  }

  void contextF(
      List<@H1Bot @H2Bot ? extends @H1Top @H2S1 String> l1,
      List<? super @H1S1 @H2S2 String> l2,
      List<@H1S1 @H2S2 ? extends @H1Top @H2Top String> l3) {

    // :: error: (type.arguments.not.inferred)
    List<? super @H1Bot @H2Bot String> lstr1 = methodF(l1, l2);
    // :: error: (type.arguments.not.inferred)
    List<? super @H1Top @H2S2 String> lstr2 = methodF(l3, l2);
  }

  <G extends List<H>, H extends List<G>> List<H> methodG(G g1, G G2) {
    return null;
  }

  class NodeList extends ArrayList<@H1Top @H2Top NodeList> {}

  void contextG(NodeList l1, NodeList l2) {
    List<@H1Top @H2Top NodeList> a = methodG(l1, l2);
  }

  // add test case for Array<String> vs. String[]
  // add test case of E extends F, F extends G, H extends List<? super E> and other craziness

  <M, N extends M> Map<M, N> method() {
    return null;
  }

  void contextMN() {

    // so I am not exactly sure how to create a meaningful test for this case
    // what occurs is the SubtypeSolver.propagateGlbs forces N to be a subtype of M
    // and it works (at least while I was debugging) but I don't know how to create
    // a check that fails or succeeds because of this
    Map<? super @H1S1 @H2S2 CharSequence, @H1Top @H2Top ? super String> mnl = method();
  }

  //    class Pair<PONE,PTWO> {
  //        PONE _1;
  //        PTWO _2;
  //    }

  <O extends P, P> @H1Top @H2Top P methodOP(@H1Top @H2Top P p, O o) {
    return null;
  }

  void contextOP(@H1S1 @H2S1 String s1, @H1Bot @H2Bot String s2) {
    // This test is actually here to test that the constraint P :> O is implied on p
    // :: error: (assignment)
    @H1Bot @H2Bot String loc = methodOP(s1, s2);
  }

  //    class Triplet<L,M,N> {
  //        L l;
  //        M m;
  //        N n;
  //    }
  //
  //    <X extends Y, Y extends Z, Z> Triplet<X,Y,Z> methodXYZ() {
  //        return null;
  //    }
  //
  //    void contextXYZ() {
  //        Triplet<@H1S>
  //    }
}
