import TestLib;

srand(4282308941601638229);

StartTest("BTree_as_SortedSet");

struct wrapped_int {
  restricted int t;
  void operator init(int t) {
    this.t = t;
  }
  autounravel bool operator ==(wrapped_int a, wrapped_int b) {
    if (alias(a, null)) return alias(b, null);
    if (alias(b, null)) return false;
    return a.t == b.t;
  }
  autounravel bool operator !=(wrapped_int a, wrapped_int b) {
    return !(a == b);
  }
  autounravel bool operator <(wrapped_int a, wrapped_int b) {
    return a.t < b.t;
  }
  int hash() { return t.hash(); }
}

wrapped_int wrap(int t) = wrapped_int;  // `wrap` is alias for constructor

from collections.iter(T=wrapped_int) access
    Iterable_T as Iterable_wrapped_int;
from collections.set(T=wrapped_int) access
    Set_T as Set_wrapped_int;
from collections.hashset(T=wrapped_int) access
    HashSet_T as HashSet_wrapped_int;
from collections.sortedset(T=wrapped_int) access
    SortedSet_T as SortedSet_wrapped_int,
    Naive_T as NaiveSortedSet_wrapped_int;
from collections.btree(T=wrapped_int) access
    BTreeSet_T as BTreeSet_wrapped_int;
from collections.splaytree(T=wrapped_int) access
    SplayTree_T as SplayTree_wrapped_int;

struct ActionEnum {
  static restricted int n = 0;
  static private int next() {
    return ++n - 1;
  }
  static restricted int CONTAINS = next();
  static restricted int AFTER = next();
  static restricted int BEFORE = next();
  static restricted int AT_OR_AFTER = next();
  static restricted int AT_OR_BEFORE = next();
  static restricted int MIN = next();
  static restricted int POP_MIN = next();
  static restricted int MAX = next();
  static restricted int POP_MAX = next();
  static restricted int ADD = next();
  static restricted int PUSH = next();
  static restricted int GET = next();
  static restricted int DELETE = next();
  static restricted int DELETE_CONTAINS = next();
  // no dedicated action for: size(), empty(), operator iter(), getRandom(),
  // addAll, removeAll, operator <=, operator >=, operator ==, operator !=,
  // sameElementsInOrder, operator+, operator-.
}

from mapArray(Src=wrapped_int, Dst=int) access map;

int[] operator cast(wrapped_int[] a) {
  for (wrapped_int x : a) {
    assert(!alias(x, null), 'Null element in array');
  }
  static int get(wrapped_int w) { return w.t; }
  return map(get, a);
}

from collections.zip(T=wrapped_int) access zip;

string differences(SortedSet_wrapped_int a, SortedSet_wrapped_int b) {
  if (a.size() != b.size()) {
    return 'Different sizes: ' + string(a.size()) + ' vs ' + string(b.size());
  }
  string arrayValues = '[\n';
  bool diff = false;
  for (wrapped_int[] ab : zip(default=null, a, b)) {
    wrapped_int aItem = ab[0]; assert(!alias(aItem, null));
    wrapped_int bItem = ab[1]; assert(!alias(bItem, null));
    arrayValues += '  [' + format('%5d', aItem.t) + ',' + format('%5d', bItem.t)
                         + ']';
    if (!alias(aItem, bItem)) {
      arrayValues += '  <---';
      diff = true;
    }
    arrayValues += '\n';
  }
  arrayValues += ']';
  if (diff) {
    return arrayValues;
  }
  return '';
}

bool different(SortedSet_wrapped_int a, SortedSet_wrapped_int b) {
  if (a.size() != b.size()) {
    return true;
  }
  for (wrapped_int[] ab : zip(default=null, a, b)) {
    wrapped_int aItem = ab[0];
    wrapped_int bItem = ab[1];
    if (!alias(aItem, bItem)) {
      return true;
    }
  }
  return false;
}

string string(int[] a) {
  string result = '[';
  for (int i = 0; i < a.length; ++i) {
    if (i > 0) {
      result += ', ';
    }
    result += string(a[i]);
  }
  result += ']';
  return result;
}

string string(bool[] a) {
  string result = '[';
  for (int i = 0; i < a.length; ++i) {
    if (i > 0) {
      result += ', ';
    }
    result += a[i] ? 'true' : 'false';
  }
  result += ']';
  return result;
}


using Action = void(int ...SortedSet_wrapped_int[]);
Action[] actions = new Action[ActionEnum.n];
actions[ActionEnum.CONTAINS] =
    new void(int  maxItem ...SortedSet_wrapped_int[] sets) {
      int toCheck = rand() % maxItem;
      bool[] results;
      for (SortedSet_wrapped_int s : sets) {
        results.push(s.contains(wrap(toCheck)));
      }
      if (results.length > 1) {
        bool expected = results[0];
        for (bool r : results) {
          if (r != expected) {
            assert(false, 'Different results: ' + string(results));
            break;
          }
        }
      }
    };
actions[ActionEnum.AFTER] =
    new void(int maxItem ...SortedSet_wrapped_int[] sets) {
      int toCheck = rand() % maxItem;
      wrapped_int[] results;
      for (SortedSet_wrapped_int s : sets) {
        wrapped_int w = s.after(wrap(toCheck));
        results.push(w);
        if (!alias(w, null)) {
          assert(w.t > toCheck);
        }
      }
      if (results.length > 1) {
        wrapped_int expected = results[0];
        for (wrapped_int r : results) {
          if (!alias(r, expected)) {
            assert(false, 'Different results: ' + string(results));
            break;
          }
        }
      }
    };
actions[ActionEnum.BEFORE] =
    new void(int maxItem ...SortedSet_wrapped_int[] sets) {
      int toCheck = rand() % maxItem;
      wrapped_int[] results;
      for (SortedSet_wrapped_int s : sets) {
        wrapped_int w = s.before(wrap(toCheck));
        results.push(w);
        if (!alias(w, null)) {
          assert(w.t < toCheck);
        }
      }
      if (results.length > 1) {
        wrapped_int expected = results[0];
        for (wrapped_int r : results) {
          if (!alias(r, expected)) {
            assert(false, 'Different results: ' + string(results));
            break;
          }
        }
      }
    };
actions[ActionEnum.AT_OR_AFTER] =
    new void(int maxItem ...SortedSet_wrapped_int[] sets) {
      int toCheck = rand() % maxItem;
      wrapped_int[] results;
      for (SortedSet_wrapped_int s : sets) {
        wrapped_int w = s.atOrAfter(wrap(toCheck));
        results.push(w);
        if (!alias(w, null)) {
          assert(w.t >= toCheck);
        }
      }
      if (results.length > 1) {
        wrapped_int expected = results[0];
        for (wrapped_int r : results) {
          if (!alias(r, expected)) {
            assert(false, 'Different results: ' + string(results));
            break;
          }
        }
      }
    };
actions[ActionEnum.AT_OR_BEFORE] =
    new void(int maxItem ...SortedSet_wrapped_int[] sets) {
      int toCheck = rand() % maxItem;
      wrapped_int[] results;
      for (SortedSet_wrapped_int s : sets) {
        wrapped_int w = s.atOrBefore(wrap(toCheck));
        results.push(w);
        if (!alias(w, null)) {
          assert(w.t <= toCheck);
        }
      }
      if (results.length > 1) {
        wrapped_int expected = results[0];
        for (wrapped_int r : results) {
          if (!alias(r, expected)) {
            assert(false, 'Different results: ' + string(results));
            break;
          }
        }
      }
    };
actions[ActionEnum.MIN] = new void(int ...SortedSet_wrapped_int[] sets) {
  wrapped_int[] results;
  for (SortedSet_wrapped_int s : sets) {
    results.push(s.min());
  }
  if (results.length > 1) {
    wrapped_int expected = results[0];
    for (wrapped_int r : results) {
      if (!alias(r, expected)) {
        assert(false, 'Different results: ' + string(results));
        break;
      }
    }
  }
};
actions[ActionEnum.POP_MIN] = new void(int ...SortedSet_wrapped_int[] sets) {
  wrapped_int[] results = new wrapped_int[];
  for (SortedSet_wrapped_int s : sets) {
    results.push(s.popMin());
  }
  if (results.length > 0) {
    wrapped_int expected = results[0];
    for (wrapped_int r : results) {
      if (!alias(r, expected)) {
        assert(false, 'Different results: ' + string(results));
        break;
      }
    }
  }
};
actions[ActionEnum.MAX] = new void(int ...SortedSet_wrapped_int[] sets) {
  // write('Max\n');
  wrapped_int[] results = new wrapped_int[];
  for (SortedSet_wrapped_int s : sets) {
    results.push(s.max());
  }
  if (results.length > 0) {
    wrapped_int expected = results[0];
    for (wrapped_int r : results) {
      if (!alias(r, expected)) {
        assert(false, 'Different results: ' + string(results));
        break;
      }
    }
  }
};
actions[ActionEnum.POP_MAX] = new void(int ...SortedSet_wrapped_int[] sets) {
  wrapped_int[] results = new wrapped_int[];
  for (SortedSet_wrapped_int s : sets) {
    results.push(s.popMax());
  }
  if (results.length > 0) {
    wrapped_int expected = results[0];
    for (wrapped_int r : results) {
      if (!alias(r, expected)) {
        assert(false, 'Different results: ' + string(results));
        break;
      }
    }
  }
};
actions[ActionEnum.ADD] =
    new void(int maxItem ...SortedSet_wrapped_int[] sets) {
      wrapped_int toInsert = wrap(rand() % maxItem);
      // write('Inserting ' + string(toInsert.t) + '\n');
      bool[] results;
      for (SortedSet_wrapped_int s : sets) {
        results.push(s.add(toInsert));
      }
      if (results.length > 1) {
        bool expected = results[0];
        for (bool r : results) {
          if (r != expected) {
            assert(false, 'Different results: ' + string(results));
            break;
          }
        }
      }
    };
actions[ActionEnum.PUSH] =
    new void(int maxItem ...SortedSet_wrapped_int[] sets) {
      wrapped_int toReplace = wrap(rand() % maxItem);
      // write('Replacing ' + string(toReplace.t) + '\n');
      wrapped_int[] results = new wrapped_int[];
      for (SortedSet_wrapped_int s : sets) {
        results.push(s.push(toReplace));
      }
      if (results.length > 0) {
        wrapped_int expected = results[0];
        for (wrapped_int r : results) {
          if (!alias(r, expected)) {
            assert(false, 'Different results: ' + string(results));
            break;
          }
        }
      }
    };
actions[ActionEnum.GET] = new void(int maxItem ...SortedSet_wrapped_int[] sets)
{
  wrapped_int toGet = wrap(rand() % maxItem);
  // write('Getting ' + string(toGet) + '\n');
  wrapped_int[] results = new wrapped_int[];
  for (SortedSet_wrapped_int s : sets) {
    results.push(s.get(toGet));
  }
  if (results.length > 0) {
    wrapped_int expected = results[0];
    for (wrapped_int r : results) {
      if (!alias(r, expected)) {
        assert(false, 'Different results: ' + string(results));
        break;
      }
    }
  }
};
actions[ActionEnum.DELETE] =
    new void(int maxItem ...SortedSet_wrapped_int[] sets) {
      wrapped_int toDelete = wrap(rand() % maxItem);
      // write('Deleting ' + string(toDelete.t) + '\n');
      wrapped_int[] results = new wrapped_int[];
      for (SortedSet_wrapped_int s : sets) {
        results.push(s.extract(toDelete));
      }
      if (results.length > 1) {
        wrapped_int expected = results[0];
        for (wrapped_int r : results) {
          if (!alias(r, expected)) {
            assert(false, 'Different results: ' + string(results));
            break;
          }
        }
      }
      if (results.length > 0) {
        wrapped_int r = results[0];
        if (!alias(r, null)) {
          for (SortedSet_wrapped_int s : sets) {
            assert(!s.contains(r));
          }
        }
      }
    };
actions[ActionEnum.DELETE_CONTAINS] =
    new void(int ...SortedSet_wrapped_int[] sets) {
      if (sets.length == 0 || sets[0].empty()) {
        return;
      }
      int initialsize = sets[0].size();
      wrapped_int toDelete = wrap(sets[0].getRandom().t);
      for (SortedSet_wrapped_int s : sets) {
        assert(s.extract(toDelete) == toDelete, 'Delete failed');
        assert(!s.contains(toDelete), 'Contains failed');
        assert(s.size() == initialsize - 1, 'Size failed');
      }
    };
real[] increasingProbs = array(n=ActionEnum.n, value=0.0);
// Actions that don't modify the set:
increasingProbs[ActionEnum.CONTAINS] = 1 / 2^5;
increasingProbs[ActionEnum.AFTER] = 1 / 2^5;
increasingProbs[ActionEnum.BEFORE] = 1 / 2^5;
increasingProbs[ActionEnum.AT_OR_AFTER] = 1 / 2^5;
increasingProbs[ActionEnum.AT_OR_BEFORE] = 1 / 2^5;
increasingProbs[ActionEnum.MIN] = 1 / 2^5;
increasingProbs[ActionEnum.MAX] = 1 / 2^5;
increasingProbs[ActionEnum.GET] = 1 / 2^5;
// 1/4 probability of this sort of action:
assert(sum(increasingProbs) == 8 / 2^5);
// Actions that might add an element:
increasingProbs[ActionEnum.ADD] = 1 / 4;
increasingProbs[ActionEnum.PUSH] = 1 / 4;
assert(sum(increasingProbs) == 3/4);
// Actions that might remove an element:
increasingProbs[ActionEnum.POP_MIN] = 1 / 16;
increasingProbs[ActionEnum.POP_MAX] = 1 / 16;
increasingProbs[ActionEnum.DELETE] = 1 / 16;
increasingProbs[ActionEnum.DELETE_CONTAINS] = 1 / 16;
assert(sum(increasingProbs) == 1, 'Probabilities do not sum to 1');

real[] decreasingProbs = copy(increasingProbs);
// Actions that might add an element:
decreasingProbs[ActionEnum.ADD] = 1 / 8;
decreasingProbs[ActionEnum.PUSH] = 1 / 8;
// Actions that might remove an element:
decreasingProbs[ActionEnum.POP_MIN] = 1 / 8;
decreasingProbs[ActionEnum.POP_MAX] = 1 / 8;
decreasingProbs[ActionEnum.DELETE] = 1 / 8;
decreasingProbs[ActionEnum.DELETE_CONTAINS] = 1 / 8;
assert(sum(decreasingProbs) == 1, 'Probabilities do not sum to 1');

SortedSet_wrapped_int naive = NaiveSortedSet_wrapped_int(operator <, null);
SortedSet_wrapped_int btree1 = BTreeSet_wrapped_int(null, maxPivots=4);
SortedSet_wrapped_int btree2 = BTreeSet_wrapped_int(null, maxPivots=128);
SortedSet_wrapped_int splayTree = SplayTree_wrapped_int(operator <, null);

bool isStrictlySorted(SortedSet_wrapped_int s) {
  wrapped_int last = null;
  for (wrapped_int w : s) {
    if (!alias(last, null) && last.t >= w.t) {
      return false;
    }
    last = w;
  }
  return true;
}

int chooseAction(real[] probs) {
  real r = unitrand();
  real sum = 0;
  for (int i = 0; i < probs.length; ++i) {
    sum += probs[i];
    if (r < sum) {
      return i;
    }
  }
  return probs.length - 1;
} 

int[] counts = array(n=ActionEnum.n, value=0);
string[] names = {
  'CONTAINS', 'AFTER', 'BEFORE', 'AT_OR_AFTER', 'AT_OR_BEFORE', 'MIN',
  'POP_MIN', 'MAX', 'POP_MAX', 'ADD', 'PUSH', 'GET', 'DELETE', 'DELETE_CONTAINS'
};

int maxSize = 0;
int numActions = 2000;
for (int i : range(numActions)) {
  real[] probs = i < numActions * 2 # 5 ? increasingProbs : decreasingProbs;
  int choice = chooseAction(probs);
  ++counts[choice];
  actions[choice](100, naive, btree1, btree2, splayTree);
  for (var btree : new SortedSet_wrapped_int[] {btree1, btree2, splayTree}) {
    if (different(naive, btree)) {
      write('Different sets after action ' + names[choice]);
      assert(false, 'Naive vs btree: \n' + differences(naive, btree));
    }
    assert(isStrictlySorted(btree), 'Not sorted');
    maxSize = max(maxSize, btree.size());
  }
}

if (false) {
  write('Max size: ' + string(maxSize));
  write('Action counts: {');
  for (int i : range(ActionEnum.n)) {
    write('  ' + names[i] + ': ' + string(counts[i]) + ',');
  }
}

EndTest();

StartTest('BTree_binary_ops');

Set_wrapped_int a = BTreeSet_wrapped_int();
a.add(wrap(1));
a.add(wrap(2));
a.add(wrap(3));
Set_wrapped_int b = HashSet_wrapped_int();
b.add(wrap(2));
b.add(wrap(3));
b.add(wrap(4));

{
  Set_wrapped_int c = a + b;
  assert(c.size() == 4, 'Union failed: wrong size ' + string(c.size()));
  assert(c.contains(wrap(1)), 'Union failed: missing 1');
  assert(c.contains(wrap(2)), 'Union failed: missing 2');
  assert(c.contains(wrap(3)), 'Union failed: missing 3');
  assert(c.contains(wrap(4)), 'Union failed: missing 4');
}
{
  Set_wrapped_int c = a - b;
  assert(c.size() == 1, 'Difference failed: wrong size ' + string(c.size()));
  assert(c.contains(wrap(1)), 'Difference failed: missing 1');
}
{
  Set_wrapped_int c = a & b;
  assert(c.size() == 2, 'Intersection failed: wrong size ' + string(c.size()));
  assert(c.contains(wrap(2)), 'Intersection failed: missing 2');
  assert(c.contains(wrap(3)), 'Intersection failed: missing 3');
}
{
  Set_wrapped_int c = a ^ b;
  assert(c.size() == 2, 'Symmetric difference failed: wrong size ' +
         string(c.size()));
  assert(c.contains(wrap(1)), 'Symmetric difference failed: missing 1');
  assert(c.contains(wrap(4)), 'Symmetric difference failed: missing 4');
}
EndTest();

StartTest('BTree: atOrAfter and atOrBefore');
{
  from collections.sortedset(T=int) access SortedSet_T as SortedSet_int;
  from collections.btree(T=int) access BTreeSet_T as BTreeSet_int;

  BTreeSet_int btree = BTreeSet_int(nullT=-1, maxPivots=4);
  SortedSet_int set = btree;

  // Insert even numbers 0, 2, 4, ..., 18
  for (int i = 0; i < 20; i += 2) {
    set.add(i);
  }

  // atOrAfter for a present element returns that element.
  assert(set.atOrAfter(4) == 4);
  // atOrAfter for a missing element returns the next larger.
  assert(set.atOrAfter(5) == 6);
  // atOrAfter for something beyond max returns nullT.
  assert(set.atOrAfter(19) == -1);

  // atOrBefore for a present element returns that element.
  assert(set.atOrBefore(4) == 4);
  // atOrBefore for a missing element returns the next smaller.
  assert(set.atOrBefore(5) == 4);
  // atOrBefore for something below min returns nullT.
  assert(set.atOrBefore(-1) == -1);
}
EndTest();

StartTest('BTree: extract and push');
{
  from collections.set(T=int) access Set_T as Set_int;
  from collections.btree(T=int) access BTreeSet_T as BTreeSet_int;

  BTreeSet_int btree = BTreeSet_int(nullT=-1, maxPivots=4);
  Set_int set = btree;

  set.add(10);
  set.add(20);
  set.add(30);

  // get returns the element.
  assert(set.get(20) == 20);

  // push on a new element returns nullT.
  int replaced = set.push(25);
  assert(replaced == -1);
  assert(set.size() == 4);

  // push on an existing element returns the old element.
  replaced = set.push(20);
  assert(replaced == 20);
  assert(set.size() == 4);

  // extract removes and returns the element.
  int extracted = set.extract(25);
  assert(extracted == 25);
  assert(set.size() == 3);
  assert(!set.contains(25));

  // extract on a non-existent element returns nullT.
  extracted = set.extract(99);
  assert(extracted == -1);
  assert(set.size() == 3);
}
EndTest();

StartTest('BTree: newEmpty');
{
  from collections.set(T=int) access Set_T as Set_int;
  from collections.btree(T=int) access BTreeSet_T as BTreeSet_int;

  BTreeSet_int btree = BTreeSet_int(nullT=-1, maxPivots=4);
  Set_int set = btree;
  set.add(1);
  set.add(2);
  set.add(3);

  Set_int emptyClone = set.newEmpty();
  assert(emptyClone.size() == 0);
  assert(emptyClone.empty());
  emptyClone.add(42);
  assert(emptyClone.contains(42));
  assert(!set.contains(42));
  assert(set.size() == 3);
}
EndTest();
