#include "abstract.hpp"

#include "t1p.h"
#include "box.h"

#include <ap_disjunction.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <list>
#include <Eigen/Dense>

//static Managers mans{t1p_manager_alloc(), box_manager_alloc()};

ArithExpr::ArithExpr(): expr{nullptr} {}

ArithExpr::ArithExpr(double constant):
  expr{ap_texpr0_cst_scalar_double(constant)} {}

ArithExpr::ArithExpr(int ind): expr{ap_texpr0_dim(ind)} {}

ArithExpr::ArithExpr(double lower, double upper):
  expr{ap_texpr0_cst_interval_double(lower, upper)} {}

ArithExpr::ArithExpr(ap_texpr0_t* e): expr{e} {}

ArithExpr::ArithExpr(const ArithExpr& other):
  expr{ap_texpr0_copy(other.expr)} {}

ArithExpr::ArithExpr(ArithExpr&& other): expr(other.expr) {
  other.expr = nullptr;
}

ArithExpr::~ArithExpr() {
  if (expr != nullptr) {
    ap_texpr0_free(expr);
  }
}

ArithExpr& ArithExpr::operator=(const ArithExpr& other) {
  if (expr != nullptr) {
    ap_texpr0_free(expr);
  }
  expr = ap_texpr0_copy(other.expr);
  return *this;
}

ArithExpr& ArithExpr::operator=(ArithExpr&& other) {
  if (expr != nullptr) {
    ap_texpr0_free(expr);
  }
  expr = other.expr;
  other.expr = nullptr;
  return *this;
}

ArithExpr ArithExpr::negate() const {
  return ArithExpr{ap_texpr0_unop(AP_TEXPR_NEG, ap_texpr0_copy(expr),
        AP_RTYPE_DOUBLE, AP_RDIR_NEAREST)};
}

ArithExpr ArithExpr::operator+(const ArithExpr& other) const {
  return ArithExpr{ap_texpr0_binop(AP_TEXPR_ADD,
      ap_texpr0_copy(expr),
      ap_texpr0_copy(other.expr),
      AP_RTYPE_DOUBLE, AP_RDIR_NEAREST)};
}

ArithExpr ArithExpr::operator-(const ArithExpr& other) const {
  return ArithExpr{ap_texpr0_binop(AP_TEXPR_SUB,
      ap_texpr0_copy(expr),
      ap_texpr0_copy(other.expr),
      AP_RTYPE_DOUBLE, AP_RDIR_NEAREST)};
}

ArithExpr ArithExpr::operator*(const ArithExpr& other) const {
  return ArithExpr{ap_texpr0_binop(AP_TEXPR_MUL,
      ap_texpr0_copy(expr),
      ap_texpr0_copy(other.expr),
      AP_RTYPE_DOUBLE, AP_RDIR_NEAREST)};
}

ArithExpr ArithExpr::operator/(const ArithExpr& other) const {
  return ArithExpr{ap_texpr0_binop(AP_TEXPR_DIV,
      ap_texpr0_copy(expr),
      ap_texpr0_copy(other.expr),
      AP_RTYPE_DOUBLE, AP_RDIR_NEAREST)};
}

ArithExpr ArithExpr::operator^(int power) const {
  // Apron's t1p domain does not support exponents yet, but since we only
  // allow exponents to integer powers we just expand the exponent here.
  ap_texpr0_t* ret = ap_texpr0_copy(this->expr);
  for (int i = 0; i < power - 1; i++) {
    ret = ap_texpr0_binop(AP_TEXPR_MUL,
        ap_texpr0_copy(this->expr),
        ret,
        AP_RTYPE_DOUBLE, AP_RDIR_NEAREST);
  }
  return ArithExpr{ret};
}

LinCons::LinCons(): weights{Eigen::MatrixXd(0, 0)},
  biases{Eigen::VectorXd(0)} {}

LinCons::LinCons(const Eigen::MatrixXd& ws, const Eigen::VectorXd& bs):
  weights{ws}, biases{bs} {}

double LinCons::distance_from(const Eigen::VectorXd& x) const {
  // A x <= b -> A x - b <= 0
  // distance = max(A x - b)
  // If distance is positive then it is the l_infty distance between x and
  // the constrained space. Otherwise, distance decreases as we move away
  // from the safe region.
  return (this->weights * x - this->biases).maxCoeff();
}

inline ap_manager_t* get_manager_from_domain(AbstractDomain dom, size_t size) {
  ap_manager_t* base;
  switch (dom) {
    case AbstractDomain::ZONOTOPE:
      base = t1p_manager_alloc();
      break;
    case AbstractDomain::INTERVAL:
      base = box_manager_alloc();
      break;
    case AbstractDomain::POLYHEDRA:
      base = pk_manager_alloc(false);
      break;
    default:
      throw std::runtime_error("Unrecognized domain in get_manager");
  }
  if (size <= 1) {
    return base;
  }
  return ap_disjunction_manager_alloc(base, NULL);
}

AbstractVal::AbstractVal(): man{nullptr}, value{nullptr} {}

// Note that ap_manager_copy doesn't actually create a copy of m, it only
// increments a reference count, so this copy is not expensive.
AbstractVal::AbstractVal(ap_manager_t* m, ap_abstract0_t* v):
  man{ap_manager_copy(m)}, value{v} {}

AbstractVal::AbstractVal(AbstractDomain dom,
    const std::vector<Eigen::VectorXd>& a,
    const std::vector<double>& b): domain{dom} {
  //if (dom == AbstractDomain::ZONOTOPE) {
  //  man = mans.get_t1p_manager();
  //} else {
  //  man = mans.get_box_manager();
  //}
  man = get_manager_from_domain(dom, 1);
  ap_lincons0_array_t arr = ap_lincons0_array_make(a.size());
  for (size_t i = 0; i < a.size(); i++) {
    ap_linexpr0_t* expr = ap_linexpr0_alloc(AP_LINEXPR_DENSE, a[i].size());
    for (int j = 0; j < a[i].size(); j++) {
      ap_linexpr0_set_coeff_scalar_double(expr, j, -a[i](j));
    }
    ap_linexpr0_set_cst_scalar_double(expr, b[i]);
    ap_lincons0_t cons = ap_lincons0_make(AP_CONS_SUPEQ, expr, NULL);
    arr.p[i] = cons;
  }
  value = ap_abstract0_of_lincons_array(man, 0, a[0].size(), &arr);
  ap_lincons0_array_clear(&arr);
}

AbstractVal::AbstractVal(AbstractDomain dom, const LinCons& lc) {
  man = get_manager_from_domain(dom, 1);
  ap_lincons0_array_t arr = ap_lincons0_array_make(lc.biases.size());
  for (int i = 0; i < lc.biases.size(); i++) {
    ap_linexpr0_t* expr = ap_linexpr0_alloc(AP_LINEXPR_DENSE,
        lc.weights.row(i).size());
    for (int j = 0; j < lc.weights.row(i).size(); j++) {
      ap_linexpr0_set_coeff_scalar_double(expr, j, -lc.weights(i, j));
    }
    ap_linexpr0_set_cst_scalar_double(expr, lc.biases(i));
    ap_lincons0_t cons = ap_lincons0_make(AP_CONS_SUPEQ, expr, NULL);
    arr.p[i] = cons;
  }
  value = ap_abstract0_of_lincons_array(man, 0, lc.weights.row(0).size(), &arr);
  ap_lincons0_array_clear(&arr);
}

AbstractVal::AbstractVal(AbstractDomain dom,
    const Eigen::VectorXd& lowers,
    const Eigen::VectorXd& uppers): domain{dom} {
  //if (dom == AbstractDomain::ZONOTOPE) {
  //  man = mans.get_t1p_manager();
  //} else {
  //  man = mans.get_box_manager();
  //}
  man = get_manager_from_domain(dom, 1);
  ap_interval_t** itv =
    (ap_interval_t**) malloc(lowers.size() * sizeof(ap_interval_t*));
  for (int i = 0; i < lowers.size(); i++) {
    itv[i] = ap_interval_alloc();
    ap_interval_set_double(itv[i], lowers(i), uppers(i));
  }
  value = ap_abstract0_of_box(man, 0, lowers.size(), itv);
  ap_interval_array_free(itv, lowers.size());
}

AbstractVal::AbstractVal(const AbstractVal& other) {
  man = ap_manager_copy(other.man);
  value = ap_abstract0_copy(man, other.value);
}

AbstractVal::AbstractVal(AbstractVal&& other) {
  man = other.man;
  value = other.value;
  other.man = nullptr;
  other.value = nullptr;
}

AbstractVal::~AbstractVal() {
  if (value != nullptr) {
    ap_abstract0_free(man, value);
  }
  if (man != nullptr) {
    // Internally, apron managers are reference counted. If this manager is
    // still used elsewhere (assuming it was copied with ap_manager_copy) the
    // reference count will be decremented but the manager will not be freed.
    ap_manager_free(man);
  }
}

std::unique_ptr<AbstractVal> AbstractVal::add_trailing_dimensions(
    int n) const {
  ap_dimchange_t* dimchange = ap_dimchange_alloc(0, n);
  int d = dims();
  for (int i = 0; i < n; i++) {
    dimchange->dim[i] = d;
  }
  ap_abstract0_t* res = ap_abstract0_add_dimensions(
      man, false, value, dimchange, false);
  ap_dimchange_free(dimchange);
  return this->make_new(res);
}

std::unique_ptr<AbstractVal> AbstractVal::add_leading_dimensions(
    int n) const {
  ap_dimchange_t* dimchange = ap_dimchange_alloc(0, n);
  for (int i = 0; i < n; i++) {
    dimchange->dim[i] = 0;
  }
  ap_abstract0_t* res = ap_abstract0_add_dimensions(
      man, false, value, dimchange, false);
  ap_dimchange_free(dimchange);
  return this->make_new(res);
}

std::unique_ptr<AbstractVal> AbstractVal::remove_trailing_dimensions(
    int n) const {
  // NOTE: t1p_remove_dimensions is buggy, but seems to work for removing
  // one dimension. Therefore, we remove one dimension at a time until n
  // dimensions have been removed.
  ap_abstract0_t* res = ap_abstract0_copy(man, value);
  ap_dimchange_t* dimchange = ap_dimchange_alloc(0, 1);
  for (int i = 0; i < n; i++) {
    int d = ap_abstract0_dimension(man, res).realdim;
    dimchange->dim[0] = d - 1;

    //std::cout << "remove_trailing_dimensions before" << std::endl;
    //this->print(stdout);
    res = ap_abstract0_remove_dimensions(man, true, res, dimchange);
    //std::cout << "remove_trailing_dimensions after" << std::endl;
    //ap_abstract0_fprint(stdout, man, res, NULL);
  }
  ap_dimchange_free(dimchange);
  return this->make_new(res);
}

std::unique_ptr<AbstractVal> AbstractVal::meet_linear_constraint(
    const Eigen::MatrixXd& a,
    const Eigen::VectorXd& b) const {
  int size = b.size();
  ap_lincons0_array_t arr = ap_lincons0_array_make(size);

  // a1 x1 + a2 x2 + ... + an xn <= b ==>
  // -a1 x1 - a2 x2 - ... - an xn + b >= 0
  for (int i = 0; i < size; i++) {
    ap_linexpr0_t* expr = ap_linexpr0_alloc(AP_LINEXPR_DENSE, a.cols());
    for (int j = 0; j < a.cols(); j++) {
      ap_linexpr0_set_coeff_scalar_double(expr, j, -a(i,j));
    }
    ap_linexpr0_set_cst_scalar_double(expr, b(i));
    arr.p[i] = ap_lincons0_make(AP_CONS_SUPEQ, expr, NULL);
  }

  ap_abstract0_t* v = ap_abstract0_meet_lincons_array(man, false, value, &arr);
  ap_lincons0_array_clear(&arr);

  return this->make_new(v);
}

std::unique_ptr<AbstractVal> AbstractVal::scalar_affine(
    const Eigen::MatrixXd& w,
    const Eigen::VectorXd& b) const {
  int in_size = w.cols();
  int out_size = w.rows();

  std::unique_ptr<AbstractVal> v{};
  if (out_size > in_size) {
    v = this->add_trailing_dimensions(out_size - in_size);
  } else {
    v = this->clone();
  }

  ap_dim_t* dims = (ap_dim_t*) malloc(out_size * sizeof(ap_dim_t));
  ap_linexpr0_t** update = (ap_linexpr0_t**) malloc(out_size *
      sizeof(ap_linexpr0_t*));
  for (int j = 0; j < out_size; j++) {
    dims[j] = j;
    update[j] = ap_linexpr0_alloc(AP_LINEXPR_DENSE, in_size);
    for (int k = 0; k < in_size; k++) {
      ap_linexpr0_set_coeff_scalar_double(update[j], k, w(j,k));
    }
    ap_linexpr0_set_cst_scalar_double(update[j], b(j));
  }

  ap_abstract0_t* res = ap_abstract0_assign_linexpr_array(
      man, false, v->get_value(), dims, update, out_size, NULL);

  free(dims);
  for (int j = 0; j < out_size; j++) {
    ap_linexpr0_free(update[j]);
  }
  free(update);

  std::unique_ptr<AbstractVal> ret = this->make_new(res);
  if (in_size > out_size) {
    ret = ret->remove_trailing_dimensions(in_size - out_size);
  }
  return ret;
}

std::unique_ptr<AbstractVal> AbstractVal::interval_affine(
    const Eigen::MatrixXd& wl,
    const Eigen::MatrixXd& wu,
    const Eigen::VectorXd& bl,
    const Eigen::VectorXd& bu) const {
  int in_size = wl.cols();
  int out_size = wl.rows();

  std::unique_ptr<AbstractVal> v{};
  if (out_size > in_size) {
    v = this->add_trailing_dimensions(out_size - in_size);
  } else {
    v = this->clone();
  }

  ap_dim_t* dims = (ap_dim_t*) malloc(out_size * sizeof(ap_dim_t));
  ap_linexpr0_t** update = (ap_linexpr0_t**) malloc(out_size *
      sizeof(ap_linexpr0_t*));
  for (int j = 0; j < out_size; j++) {
    dims[j] = j;
    update[j] = ap_linexpr0_alloc(AP_LINEXPR_DENSE, in_size);
    for (int k = 0; k < in_size; k++) {
      ap_linexpr0_set_coeff_interval_double(update[j], k, wl(j,k), wu(j, k));
    }
    ap_linexpr0_set_cst_interval_double(update[j], bl(j), bu(j));
  }

  ap_abstract0_t* res = ap_abstract0_assign_linexpr_array(
      man, false, v->get_value(), dims, update, out_size, NULL);

  free(dims);
  for (int j = 0; j < out_size; j++) {
    ap_linexpr0_free(update[j]);
  }
  free(update);

  std::unique_ptr<AbstractVal> ret = this->make_new(res);
  if (in_size > out_size) {
    ret = ret->remove_trailing_dimensions(in_size - out_size);
  }
  return ret;

  /*
  // Create an abstract value for the coefficients.
  ap_interval_t** arr = (ap_interval_t**) malloc((in_size + 1) * out_size *
      sizeof(ap_interval_t*));
  for (int i = 0; i < in_size * out_size; i++) {
    arr[i] = ap_interval_alloc();
    int r = i / in_size;
    int c = i % in_size;
    ap_interval_set_double(arr[i], wl(r, c), wu(r, c));
  }
  for (int i = 0; i < out_size; i++) {
    int ind = i + in_size * out_size;
    arr[ind] = ap_interval_alloc();
    ap_interval_set_double(arr[ind], bl(i), bu(i));
  }
  std::unique_ptr<AbstractVal> coeffs = this->make_new(
      ap_abstract0_of_box(
          this->man, 0, (in_size + 1) * out_size, arr));

  std::unique_ptr<AbstractVal> input = this->append(*coeffs);
  // NOTE: the size of input is in_size * out_size (for the coefficients
  // + out_size (for the biases) + in_size (for the input)

  // Construct an ArithExpr for this assignment
  std::vector<ArithExpr> exprs;
  for (int i = 0; i < out_size; i++) {
    // Start with the bias term
    ArithExpr row(in_size * (out_size + 1) + i);
    for (int j = 0; j < in_size; j++) {
      // r = r + coeff(i, j) * x(j)
      row = row + ArithExpr((i + 1) * in_size + j) * ArithExpr(j);
    }
    exprs.push_back(row);
  }

  // Perform computation
  std::unique_ptr<AbstractVal> output = input->arith_computation(exprs);
  return output->remove_trailing_dimensions(in_size * out_size + in_size);
  */
}

std::unique_ptr<AbstractVal> AbstractVal::relu() const {
  std::unique_ptr<AbstractVal> z = this->clone();
  size_t num_dims = this->dims();
  Eigen::VectorXd b{Eigen::VectorXd::Zero(1)};
  Eigen::MatrixXd relu_w = Eigen::MatrixXd::Identity(num_dims, num_dims);
  Eigen::VectorXd relu_b = Eigen::VectorXd::Zero(num_dims);
  for (size_t i = 0; i < num_dims; i++) {
    Eigen::MatrixXd alt = Eigen::MatrixXd::Zero(1, num_dims);
    Eigen::MatrixXd agt = alt;
    alt(0,i) = 1.0;
    agt(0,i) = -1.0;

    std::unique_ptr<AbstractVal> zlt = z->meet_linear_constraint(alt, b);
    z = z->meet_linear_constraint(agt, b);

    relu_w(i, i) = 0.0;
    zlt = zlt->scalar_affine(relu_w, relu_b);
    relu_w(i, i) = 1.0;

    z = z->join(*zlt);
  }

  return z;
}

// NOTE: by convention, the this and other should point to the same manager.
// The manager of this is used, so if it is not compatible with the manager of
// other I'm not sure what happens.
std::unique_ptr<AbstractVal> AbstractVal::join(const AbstractVal& other) const {
  ap_abstract0_t* res = ap_abstract0_join(man, false, value, other.get_value());
  return this->make_new(res);
}

std::unique_ptr<AbstractVal> AbstractVal::meet(const AbstractVal& other) const {
  ap_abstract0_t* res = ap_abstract0_meet(man, false, value, other.get_value());
  return this->make_new(res);
}

std::unique_ptr<AbstractVal> AbstractVal::widen(
    const AbstractVal& other) const {
  ap_abstract0_t* res = ap_abstract0_widening(man, value, other.get_value());
  return this->make_new(res);
}

bool AbstractVal::operator==(const AbstractVal& other) const {
  return ap_abstract0_is_eq(man, value, other.get_value());
}

std::unique_ptr<AbstractVal> AbstractVal::append(const AbstractVal& b) const {
  int n1 = b.dims();
  int n2 = this->dims();
  std::unique_ptr<AbstractVal> p1 = this->add_trailing_dimensions(n1);
  std::unique_ptr<AbstractVal> p2 = b.add_leading_dimensions(n2);
  // At this point p1 has n1 extra unconstrained dimensions at the end and
  // p2 has n2 extra unconstrained dimensions at the beginning. Meeting
  // these two gives the desired result.

  // Apron seems to have a problem doing this meet when the zonotopes have some
  // unconstrained dimensions.
  // return p1->meet(*p2);
  //
  // Our second attempt was to convert both abstract values to arrays of linear
  // constraints, append the two arrays, then create a new value which
  // satisfies all of the resulting constraints. This runs into a very strange
  // use-after-free bug which I haven't been able to track down.
  //
  // The current strategy is to convert only b to an array of linear
  // constraints and to meet this with the resulting array.

  ap_lincons0_array_t lc2 = ap_abstract0_to_lincons_array(
      p2->get_manager(), p2->get_value());

  ap_abstract0_t* v = ap_abstract0_meet_lincons_array(
      man, false, p1->get_value(), &lc2);

  ap_lincons0_array_clear(&lc2);

  return this->make_new(v);
}

std::unique_ptr<AbstractVal> AbstractVal::arith_computation(
    const std::vector<ArithExpr>& exprs) const {
  int in_size = this->dims();
  int out_size = exprs.size();
  std::unique_ptr<AbstractVal> inp;
  if (out_size > in_size) {
    inp = this->add_trailing_dimensions(out_size - in_size);
  } else {
    inp = this->clone();
  }
  ap_texpr0_t** arr =
    (ap_texpr0_t**) malloc(exprs.size() * sizeof(ap_texpr0_t*));
  ap_dim_t* dims = (ap_dim_t*) malloc(exprs.size() * sizeof(dims));
  for (size_t i = 0; i < exprs.size(); i++) {
    arr[i] = ap_texpr0_copy(exprs[i].get_texpr());
    dims[i] = i;
  }
  ap_abstract0_t* v = ap_abstract0_assign_texpr_array(
      man, false, inp->get_value(), dims, arr, exprs.size(), NULL);
  free(dims);
  for (size_t i = 0; i < exprs.size(); i++) {
    ap_texpr0_free(arr[i]);
  }
  free(arr);
  std::unique_ptr<AbstractVal> ret = this->make_new(v);
  if (in_size > out_size) {
    ret = ret->remove_trailing_dimensions(in_size - out_size);
  }
  return ret;
}

bool AbstractVal::contains_point(const Eigen::VectorXd& x) const {
  ap_interval_t** itv = (ap_interval_t**) malloc(
      x.size() * sizeof(ap_interval_t*));
  for (int i = 0; i < x.size(); i++) {
    itv[i] = ap_interval_alloc();
    ap_interval_set_double(itv[i], x(i), x(i));
  }
  ap_abstract0_t* point = ap_abstract0_of_box(man, 0, x.size(), itv);
  ap_interval_array_free(itv, x.size());
  ap_abstract0_t* meet = ap_abstract0_meet(man, false, value, point);
  ap_abstract0_free(man, point);
  bool bottom = ap_abstract0_is_bottom(man, meet);
  ap_abstract0_free(man, meet);
  return !bottom;
}

bool AbstractVal::contains(const AbstractVal& x) const {
  // Convert this to an array of linear constraints
  ap_lincons0_array_t arr = ap_abstract0_to_lincons_array(man, value);

  // Negate each constraint
  for (size_t i = 0; i < arr.size; i++) {
    ap_linexpr0_t* expr = arr.p[i].linexpr0;
    for (size_t j = 0; j < x.dims(); j++) {
      ap_coeff_t* c = ap_linexpr0_coeffref(expr, j);
      ap_coeff_neg(c, c);
    }
    ap_coeff_t* c = ap_linexpr0_cstref(expr);
    ap_coeff_neg(c, c);
  }

  // Meet the new constraints with x
  ap_abstract0_t* v = ap_abstract0_meet_lincons_array(man, false, value, &arr);
  ap_lincons0_array_clear(&arr);

  // Since the new constraints contain everything NOT in this, if the meet is
  // not empty, then x is not entirely contained within this.
  bool bottom = ap_abstract0_is_bottom(man, v);
  ap_abstract0_free(man, v);
  return bottom;
}

Eigen::VectorXd AbstractVal::get_center() const {
  ap_interval_t** bbox = ap_abstract0_to_box(man, value);
  Eigen::VectorXd center(dims());
  for (size_t i = 0; i < dims(); i++) {
    double l, u;
    ap_double_set_scalar(&l, bbox[i]->inf, MPFR_RNDN);
    ap_double_set_scalar(&u, bbox[i]->sup, MPFR_RNDN);
    center(i) = (l + u) / 2.0;
  }
  return center;
}

Eigen::VectorXd AbstractVal::get_contained_point() const {
  ap_interval_t** bbox = ap_abstract0_to_box(man, value);
  Eigen::VectorXd lower(this->dims());
  Eigen::VectorXd upper(this->dims());
  for (size_t i = 0; i < dims(); i++) {
    double l, u;
    ap_double_set_scalar(&l, bbox[i]->inf, MPFR_RNDN);
    ap_double_set_scalar(&u, bbox[i]->sup, MPFR_RNDN);
    lower(i) = l;
    upper(i) = u;
  }
  while (true) {
    // Choose a random point inside the bounding box of this abstract value,
    // then check to see if it is contained in this value. If it is we return
    // it, otherwise try another point.
    Eigen::VectorXd rand = Eigen::VectorXd::Random(dims());
    for (size_t i = 0; i < this->dims(); i++) {
      rand(i) = (1 + rand(i)) * (upper(i) - lower(i)) / 2.0 + lower(i);
    }
    if (this->contains_point(rand)) {
      return rand;
    }
  }
}

std::unique_ptr<AbstractVal> AbstractVal::clone() const {
  return std::make_unique<AbstractVal>(man, ap_abstract0_copy(man, value));
}

std::unique_ptr<AbstractVal> AbstractVal::bottom() const {
  return std::make_unique<AbstractVal>(man, ap_abstract0_bottom(man,
        0, this->dims()));
}

LinCons AbstractVal::get_lincons() const {
  ap_lincons0_array_t res = ap_abstract0_to_lincons_array(man, value);
  int d = dims();
  std::vector<Eigen::VectorXd> ws;
  std::vector<double> bs;
  for (int i = 0; i < res.size; i++) {
    Eigen::VectorXd r(d);
    ap_linexpr0_t* exp = res.p[i].linexpr0;
    for (int j = 0; j < d; j++) {
      ap_coeff_t* c = ap_linexpr0_coeffref(exp, j);
      if (c->discr != AP_COEFF_SCALAR) {
        throw std::runtime_error("Non-scalar coefficient in get_lincons");
      }
      double d;
      ap_double_set_scalar(&d, c->val.scalar, MPFR_RNDN);
      r(j) = d;
    }
    ap_coeff_t* c = ap_linexpr0_cstref(exp);
    if (c->discr != AP_COEFF_SCALAR) {
      throw std::runtime_error("Non-scalar coefficient in get_lincons");
    }
    double d;
    ap_double_set_scalar(&d, c->val.scalar, MPFR_RNDN);
    switch (res.p[i].constyp) {
      case AP_CONS_EQ:
        ws.push_back(r);
        bs.push_back(-d);
        ws.push_back(-r);
        bs.push_back(d);
        break;
      case AP_CONS_SUPEQ:
      case AP_CONS_SUP:
        ws.push_back(-r);
        bs.push_back(d);
        break;
      default:
        throw std::runtime_error("Unknown constraint type in get_lincons");
    }
  }
  ap_lincons0_array_clear(&res);
  Eigen::MatrixXd weights(ws.size(), d);
  Eigen::VectorXd biases(bs.size());
  for (int i = 0; i < ws.size(); i++) {
    weights.row(i) = ws[i];
    biases(i) = bs[i];
  }
  return LinCons(weights, biases);
}

std::unique_ptr<AbstractVal> AbstractVal::make_new(ap_abstract0_t* a) const {
  return std::make_unique<AbstractVal>(man, a);
}

//double distance_to_abstract0(ap_manager_t* man, ap_abstract0_t* a,
//    const Eigen::VectorXd& x) {
//  // Convert the abstract value to an array of linear constraints
//  ap_lincons0_array_t arr = ap_abstract0_to_lincons_array(man, a);
//  // Determine whether each constraint is satisfied and find the constraint
//  // which is closest to x
//  bool satisfies_all = true;
//  for (int i = 0; i < arr.size; i++) {
//    // TODO: I'm not sure what the semantics of arr.p[i].scalar are. It seems
//    // to only be used for EQMOD constraints.
//    ap_linexpr0_t* expr = arr.p[i].linexpr0;
//    Eigen::VectorXd coeffs(x.size());
//    for (int j = 0; j < x.size(); j++) {
//      ap_coeff_t* c = ap_linexpr0_coeffref(expr, j);
//      if (c->discr == AP_COEFF_SCALAR) {
//        // TODO
//      } else {
//        // TODO
//      }
//    }
//  }
//  return 0;
//}
//
//double AbstractVal::distance_to_point(const Eigen::VectorXd& x) const {
//  return distance_to_abstract0(this->man, this->value, x);
//}

typedef struct {
  ap_abstract0_t* abs;
  Eigen::VectorXd center;
} abstract_value;

Eigen::VectorXd compute_center(ap_manager_t* man, ap_abstract0_t* a) {
  ap_interval_t** itv = ap_abstract0_to_box(man, a);
  int d = ap_abstract0_dimension(man, a).realdim;
  Eigen::VectorXd center(d);
  for (int i = 0; i < d; i++) {
    double l, u;
    ap_double_set_scalar(&l, itv[i]->inf, MPFR_RNDN);
    ap_double_set_scalar(&u, itv[i]->sup, MPFR_RNDN);
    center(d) = (l + u) / 2.0;
  }
  ap_interval_array_free(itv, d);
  return center;
}

// Given a manager (this is a manager for the disjunction domain) and a
// disjunctive element, reduce the number of disjuncts to size. This is done
// in place so that after this call, the value pointed to by a is a disjunctive
// value with an appropriate number of disjuncts. Note that this function does
// not perform any checks and assumes a is a disjunction. This function
// typically returns a reference to a, but not always.
ap_abstract0_t* merge_disjuncts(ap_manager_t* man, ap_abstract0_t* a,
    size_t size) {
  ap_disjunction_t* ad = (ap_disjunction_t*) a->value;
  size_t s = ad->size;
  // Get the manager for the underlying abstract domain
  ap_disjunction_internal_t* in = (ap_disjunction_internal_t*) man->internal;
  ap_manager_t* under = in->manager;

  // Compute the center of the bounding box of each disjunct
  std::list<abstract_value> vals{};
  bool is_top = false;
  for (size_t i = 0; i < s; i++) {
    abstract_value v;
    ap_abstract0_t* abs = (ap_abstract0_t*) ad->p[i];
    if (ap_abstract0_is_top(under, abs)) {
      is_top = true;
      break;
    } else if (ap_abstract0_is_bottom(under, abs)) {
      // Don't add bottom elements to vals
      continue;
    }
    v.abs = ap_abstract0_copy(under, (ap_abstract0_t*) ad->p[i]);
    v.center = compute_center(under, v.abs);
    vals.push_back(v);
  }
  if (is_top) {
    int d = ap_abstract0_dimension(man, a).realdim;
    ap_abstract0_free(man, a);
    return ap_abstract0_top(man, 0, d);
  } else if (vals.size() == 0) {
    int d = ap_abstract0_dimension(man, a).realdim;
    ap_abstract0_free(man, a);
    return ap_abstract0_bottom(man, 0, d);
  }

  if (s <= size) {
    return a;
  }

  while (vals.size() > size) {
    // Find the two elements whose centers (computed by bounding box) are
    // closest to each other.
    std::list<abstract_value>::iterator it1;
    std::list<abstract_value>::iterator it2;
    double best_dist = std::numeric_limits<double>::max();
    for (auto i = vals.begin(); i != vals.end(); i++) {
      for (auto j = vals.begin(); j != vals.end(); j++) {
        double dist = (i->center - j->center).norm();
        if (dist < best_dist) {
          best_dist = dist;
          it1 = i;
          it2 = j;
        }
      }
    }
    // Join these two elements.
    ap_abstract0_t* a1 = it1->abs;
    ap_abstract0_t* a2 = it2->abs;
    ap_abstract0_t* join = ap_abstract0_join(under, false, a1, a2);
    abstract_value av;
    av.abs = join;
    av.center = compute_center(under, join);
    // Remove the two elements from vals and add the new one.
    vals.erase(it1);
    vals.erase(it2);
    vals.push_back(av);
  }

  // Finally, we replace the values in a with those in vals.
  for (size_t i = 0; i < s; i++) {
    ap_abstract0_free(under, (ap_abstract0_t*) ad->p[i]);
  }
  free(ad->p);
  ad->size = size;
  ad->p = (void**) malloc(size * sizeof(void*));
  int ind = 0;
  for (auto it = vals.begin(); it != vals.end(); it++) {
    ad->p[ind] = it->abs;
    ind++;
  }
  return a;
}

void AbstractVal::print(FILE* out) const {
  ap_abstract0_fprint(out, man, value, NULL);
}

Powerset::Powerset(ap_manager_t* m, ap_abstract0_t* v, size_t s):
  AbstractVal{m, v}, size{s} {}

Powerset::Powerset(const Powerset& p): AbstractVal{p}, size{p.size} {}

Powerset::Powerset(AbstractDomain dom, size_t s,
    const std::vector<Eigen::VectorXd>& a, const std::vector<double>& b) {
  //ap_manager_t* base_manager;
  //if (dom == AbstractDomain::ZONOTOPE) {
  //  base_manager = mans.get_t1p_manager();
  //} else {
  //  base_manager = mans.get_box_manager();
  //}
  //man = mans.get_disj_manager(base_manager);
  man = get_manager_from_domain(dom, s);
  size = s;
  ap_lincons0_array_t arr = ap_lincons0_array_make(a.size());
  for (size_t i = 0; i < a.size(); i++) {
    ap_linexpr0_t* expr = ap_linexpr0_alloc(AP_LINEXPR_DENSE, a[i].size());
    for (int j = 0; j < a[i].size(); j++) {
      ap_linexpr0_set_coeff_scalar_double(expr, j, -a[i](j));
    }
    ap_linexpr0_set_cst_scalar_double(expr, b[i]);
    ap_lincons0_t cons = ap_lincons0_make(AP_CONS_SUPEQ, expr, NULL);
    arr.p[i] = cons;
  }
  value = ap_abstract0_of_lincons_array(man, 0, a[0].size(), &arr);
  ap_lincons0_array_clear(&arr);
}

Powerset::Powerset(AbstractDomain dom, size_t s, const LinCons& lc) {
  man = get_manager_from_domain(dom, s);
  ap_lincons0_array_t arr = ap_lincons0_array_make(lc.biases.size());
  for (int i = 0; i < lc.biases.size(); i++) {
    ap_linexpr0_t* expr = ap_linexpr0_alloc(AP_LINEXPR_DENSE,
        lc.weights.row(i).size());
    for (int j = 0; j < lc.weights.row(i).size(); j++) {
      ap_linexpr0_set_coeff_scalar_double(expr, j, -lc.weights(i, j));
    }
    ap_linexpr0_set_cst_scalar_double(expr, lc.biases(i));
    ap_lincons0_t cons = ap_lincons0_make(AP_CONS_SUPEQ, expr, NULL);
    arr.p[i] = cons;
  }
  value = ap_abstract0_of_lincons_array(man, 0, lc.weights.row(0).size(), &arr);
  ap_lincons0_array_clear(&arr);
}

Powerset::Powerset(AbstractDomain dom, size_t s,
    const Eigen::VectorXd& lowers, const Eigen::VectorXd& uppers) {
  domain = dom;
  //ap_manager_t* base_manager;
  //if (dom == AbstractDomain::ZONOTOPE) {
  //  base_manager = mans.get_t1p_manager();
  //} else {
  //  base_manager = mans.get_box_manager();
  //}
  //man = mans.get_disj_manager(base_manager);
  man = get_manager_from_domain(dom, s);
  ap_interval_t** itv =
    (ap_interval_t**) malloc(lowers.size() * sizeof(ap_interval_t*));
  for (int i = 0; i < lowers.size(); i++) {
    itv[i] = ap_interval_alloc();
    ap_interval_set_double(itv[i], lowers(i), uppers(i));
  }
  value = ap_abstract0_of_box(man, 0, lowers.size(), itv);
  ap_interval_array_free(itv, lowers.size());
}

Powerset& Powerset::operator=(const Powerset& other) {
  size = other.size;
  man = other.man;
  value = ap_abstract0_copy(man, other.value);
  return *this;
}

std::unique_ptr<AbstractVal> Powerset::join(const AbstractVal& other) const {
  std::unique_ptr<AbstractVal> res = this->AbstractVal::join(other);
  ap_manager_t* m = res->get_manager();
  ap_abstract0_t* a = res->get_value();
  a = merge_disjuncts(m, a, size);
  ap_abstract0_t* ra = (ap_abstract0_t*) malloc(sizeof(ap_abstract0_t));
  ra->value = a;
  ra->man = m;
  return this->make_new(ra);
}

std::unique_ptr<AbstractVal> Powerset::meet(const AbstractVal& other) const {
  std::unique_ptr<AbstractVal> res = this->AbstractVal::meet(other);
  ap_manager_t* m = res->get_manager();
  ap_abstract0_t* a = res->get_value();
  a = merge_disjuncts(m, a, size);
  ap_abstract0_t* ra = (ap_abstract0_t*) malloc(sizeof(ap_abstract0_t));
  ra->value = a;
  ra->man = m;
  return this->make_new(ra);
}

std::unique_ptr<AbstractVal> Powerset::arith_computation(
    const std::vector<ArithExpr>& exprs) const {
  std::unique_ptr<AbstractVal> res =
    this->AbstractVal::arith_computation(exprs);
  ap_manager_t* m = res->get_manager();
  ap_abstract0_t* a = res->get_value();
  a = merge_disjuncts(m, a, size);
  ap_abstract0_t* ra = (ap_abstract0_t*) malloc(sizeof(ap_abstract0_t));
  ra->value = a;
  ra->man = m;
  return this->make_new(ra);
}

Eigen::VectorXd Powerset::get_contained_point() const {
  ap_disjunction_t* ad = (ap_disjunction_t*) value->value;
  size_t s = ad->size;
  // Get the manager for the underlying abstract domain
  ap_disjunction_internal_t* in = (ap_disjunction_internal_t*) man->internal;
  ap_manager_t* under = in->manager;
  if (s == 0) {
    return Eigen::VectorXd(0);
  }
  ap_abstract0_t* disjunct = (ap_abstract0_t*) ad->p[0];
  return compute_center(under, disjunct);
}

std::unique_ptr<AbstractVal> Powerset::clone() const {
  return std::make_unique<Powerset>(man, ap_abstract0_copy(man, value), size);
}

std::unique_ptr<AbstractVal> Powerset::bottom() const {
  return std::make_unique<Powerset>(man, ap_abstract0_bottom(man,
        0, this->dims()), this->size);
}

std::unique_ptr<AbstractVal> Powerset::make_new(ap_abstract0_t* a) const {
  return std::make_unique<Powerset>(man, a, size);
}


//---------------------------------------------------------------------------//
// Copyright (c) 2018-2021 Mikhail Komarov <nemo@nil.foundation>
// Copyright (c) 2020-2021 Nikita Kaskov <nbering@nil.foundation>
// Copyright (c) 2020-2021 Ilias Khairullin <ilias@nil.foundation>
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//---------------------------------------------------------------------------//

#ifndef CRYPTO3_R1CS_GG_PPZKSNARK_IPP2_PROVE_HPP
#define CRYPTO3_R1CS_GG_PPZKSNARK_IPP2_PROVE_HPP

#include <algorithm>
#include <vector>
#include <tuple>
#include <string>

#include <boost/iterator/zip_iterator.hpp>

#include <nil/crypto3/detail/pack_numeric.hpp>

#include <nil/crypto3/hash/algorithm/hash.hpp>
#include <nil/crypto3/hash/sha2.hpp>

#include <nil/crypto3/math/polynomial/basic_operations.hpp>

#include <nil/crypto3/algebra/multiexp/multiexp.hpp>
#include <nil/crypto3/algebra/multiexp/policies.hpp>
#include <nil/crypto3/algebra/algorithms/pair.hpp>

#include <nil/crypto3/zk/snark/schemes/ppzksnark/r1cs_gg_ppzksnark/detail/basic_policy.hpp>
#include <nil/crypto3/zk/snark/schemes/ppzksnark/r1cs_gg_ppzksnark/ipp2/proof.hpp>
#include <nil/crypto3/zk/snark/schemes/ppzksnark/r1cs_gg_ppzksnark/ipp2/srs.hpp>
#include <nil/crypto3/zk/snark/schemes/ppzksnark/r1cs_gg_ppzksnark/ipp2/transcript.hpp>
#include <nil/crypto3/zk/snark/schemes/ppzksnark/r1cs_gg_ppzksnark/proof.hpp>

namespace nil {
    namespace crypto3 {
        namespace zk {
            namespace snark {
                /// Returns the vector used for the linear combination fo the inner pairing product
                /// between A and B for the Groth16 aggregation: A^r * B. It is required as it
                /// is not enough to simply prove the ipp of A*B, we need a random linear
                /// combination of those.
                template<typename FieldType>
                std::vector<typename FieldType::value_type>
                    structured_scalar_power(std::size_t num, const typename FieldType::value_type &s) {
                    std::vector<typename FieldType::value_type> powers = {FieldType::value_type::one()};
                    for (int i = 1; i < num; i++) {
                        powers.emplace_back(powers.back() * s);
                    }
                    return powers;
                }

                /// compress is similar to commit::{V,W}KEY::compress: it modifies the `vec`
                /// vector by setting the value at index $i:0 -> split$  $vec[i] = vec[i] +
                /// vec[i+split]^scaler$. The `vec` vector is half of its size after this call.
                template<typename CurveType, typename InputRange,
                         typename ValueType = typename std::iterator_traits<typename InputRange::iterator>::value_type>
                typename std::enable_if<
                    std::is_same<typename CurveType::template g1_type<>::value_type, ValueType>::value ||
                    std::is_same<typename CurveType::template g2_type<>::value_type, ValueType>::value ||
                    std::is_same<typename CurveType::scalar_field_type::value_type, ValueType>::value>::type
                    compress(InputRange &vec, std::size_t split,
                             const typename CurveType::scalar_field_type::value_type &scalar) {
                    std::for_each(boost::make_zip_iterator(boost::make_tuple(vec.begin(), vec.begin() + split)),
                                  boost::make_zip_iterator(boost::make_tuple(vec.begin() + split, vec.end())),
                                  [&](const boost::tuple<ValueType &, ValueType &> &t) {
                                      t.template get<0>() = t.template get<0>() + t.template get<1>() * scalar;
                                  });
                    vec.resize(split);
                }

                /// It returns the evaluation of the polynomial $\prod (1 + x_{l-j}(rX)^{2j}$ at
                /// the point z, where transcript contains the reversed order of all challenges (the x).
                /// The challenges must be in reversed order for the correct evaluation of the
                /// polynomial in O(logn)
                template<typename FieldType, typename InputFieldValueIterator>
                typename std::enable_if<std::is_same<typename std::iterator_traits<InputFieldValueIterator>::value_type,
                                                     typename FieldType::value_type>::value,
                                        typename FieldType::value_type>::type
                    polynomial_evaluation_product_form_from_transcript(InputFieldValueIterator transcript_first,
                                                                       InputFieldValueIterator transcript_last,
                                                                       const typename FieldType::value_type &z,
                                                                       const typename FieldType::value_type &r_shift) {
                    // this is the term (rz) that will get squared at each step to produce the
                    // $(rz)^{2j}$ of the formula
                    typename FieldType::value_type power_zr = z;
                    power_zr = power_zr * r_shift;

                    // 0 iteration
                    InputFieldValueIterator transcript_iter = transcript_first;
                    typename FieldType::value_type res = FieldType::value_type::one() + (*transcript_iter * power_zr);
                    power_zr = power_zr * power_zr;
                    ++transcript_iter;

                    // the rest
                    while (transcript_iter != transcript_last) {
                        res = res * (FieldType::value_type::one() + (*transcript_iter * power_zr));
                        power_zr = power_zr * power_zr;
                        ++transcript_iter;
                    }

                    return res;
                }

                // Compute the coefficients of the polynomial $\prod_{j=0}^{l-1} (1 + x_{l-j}(rX)^{2j})$
                // It does this in logarithmic time directly; here is an example with 2
                // challenges:
                //
                //     We wish to compute $(1+x_1ra)(1+x_0(ra)^2) = 1 +  x_1ra + x_0(ra)^2 + x_0x_1(ra)^3$
                //     Algorithm: $c_{-1} = [1]$; $c_j = c_{i-1} \| (x_{l-j} * c_{i-1})$; $r = r*r$
                //     $c_0 = c_{-1} \| (x_1 * r * c_{-1}) = [1] \| [rx_1] = [1, rx_1]$, $r = r^2$
                //     $c_1 = c_0 \| (x_0 * r^2c_0) = [1, rx_1] \| [x_0r^2, x_0x_1r^3] = [1, x_1r, x_0r^2, x_0x_1r^3]$
                //     which is equivalent to $f(a) = 1 + x_1ra + x_0(ra)^2 + x_0x_1r^2a^3$
                //
                // This method expects the coefficients in reverse order so transcript[i] =
                // x_{l-j}.
                template<typename FieldType, typename InputFieldValueIterator>
                typename std::enable_if<std::is_same<typename std::iterator_traits<InputFieldValueIterator>::value_type,
                                                     typename FieldType::value_type>::value,
                                        std::vector<typename FieldType::value_type>>::type
                    polynomial_coefficients_from_transcript(InputFieldValueIterator transcript_first,
                                                            InputFieldValueIterator transcript_last,
                                                            const typename FieldType::value_type &r_shift) {
                    std::vector<typename FieldType::value_type> coefficients = {FieldType::value_type::one()};
                    typename FieldType::value_type power_2_r = r_shift;

                    InputFieldValueIterator transcript_iter = transcript_first;
                    while (transcript_iter != transcript_last) {
                        std::size_t n = coefficients.size();
                        for (int j = 0; j < n; j++) {
                            coefficients.emplace_back(coefficients[j] * (*transcript_iter * power_2_r));
                        }
                        power_2_r = power_2_r * power_2_r;

                        ++transcript_iter;
                    }

                    return coefficients;
                }

                /// Returns the KZG opening proof for the given commitment key. Specifically, it
                /// returns $g^{f(alpha) - f(z) / (alpha - z)}$ for $a$ and $b$.
                template<typename GroupType, typename InputGroupIterator, typename InputScalarRange>
                typename std::enable_if<
                    std::is_same<typename GroupType::value_type,
                                 typename std::iterator_traits<InputGroupIterator>::value_type>::value &&
                        std::is_same<
                            typename GroupType::curve_type::scalar_field_type::value_type,
                            typename std::iterator_traits<typename InputScalarRange::iterator>::value_type>::value,
                    kzg_opening<GroupType>>::type
                    prove_commitment_key_kzg_opening(
                        InputGroupIterator srs_powers_alpha_first, InputGroupIterator srs_powers_alpha_last,
                        InputGroupIterator srs_powers_beta_first, InputGroupIterator srs_powers_beta_last,
                        const InputScalarRange &poly,
                        const typename GroupType::curve_type::scalar_field_type::value_type &eval_poly,
                        const typename GroupType::curve_type::scalar_field_type::value_type &kzg_challenge) {
                    typename GroupType::curve_type::scalar_field_type::value_type neg_kzg_challenge = -kzg_challenge;

                    BOOST_ASSERT(poly.size() == std::distance(srs_powers_alpha_first, srs_powers_alpha_last));
                    BOOST_ASSERT(poly.size() == std::distance(srs_powers_beta_first, srs_powers_beta_last));

                    // f_v(X) - f_v(z) / (X - z)
                    std::vector<typename GroupType::curve_type::scalar_field_type::value_type> f_vX_sub_f_vZ;
                    math::_polynomial_subtraction(f_vX_sub_f_vZ,
                                                  poly,
                                                  {{
                                                      eval_poly,
                                                  }});
                    std::vector<typename GroupType::curve_type::scalar_field_type::value_type> quotient_polynomial,
                        remainder_polynomial;
                    math::_polynomial_division<typename GroupType::curve_type::scalar_field_type>(
                        quotient_polynomial, remainder_polynomial, f_vX_sub_f_vZ,
                        {{
                            neg_kzg_challenge,
                            GroupType::curve_type::scalar_field_type::value_type::one(),
                        }});

                    if (quotient_polynomial.size() < poly.size()) {
                        quotient_polynomial.resize(poly.size(),
                                                   GroupType::curve_type::scalar_field_type::value_type::zero());
                    }
                    BOOST_ASSERT(quotient_polynomial.size() == poly.size());

                    // we do one proof over h^a and one proof over h^b (or g^a and g^b depending
                    // on the curve we are on). that's the extra cost of the commitment scheme
                    // used which is compatible with Groth16 CRS insteaf of the original paper
                    // of Bunz'19
                    return kzg_opening<GroupType> {algebra::multiexp<algebra::policies::multiexp_method_bos_coster>(
                                                       srs_powers_alpha_first, srs_powers_alpha_last,
                                                       quotient_polynomial.begin(), quotient_polynomial.end(), 1),
                                                   algebra::multiexp<algebra::policies::multiexp_method_bos_coster>(
                                                       srs_powers_beta_first, srs_powers_beta_last,
                                                       quotient_polynomial.begin(), quotient_polynomial.end(), 1)};
                }

                template<typename CurveType, typename InputG2Iterator, typename InputScalarIterator>
                typename std::enable_if<
                    std::is_same<typename CurveType::template g2_type<>::value_type,
                                 typename std::iterator_traits<InputG2Iterator>::value_type>::value &&
                        std::is_same<typename CurveType::scalar_field_type::value_type,
                                     typename std::iterator_traits<InputScalarIterator>::value_type>::value,
                    kzg_opening<typename CurveType::template g2_type<>>>::type
                    prove_commitment_v(InputG2Iterator srs_powers_alpha_first, InputG2Iterator srs_powers_alpha_last,
                                       InputG2Iterator srs_powers_beta_first, InputG2Iterator srs_powers_beta_last,
                                       InputScalarIterator transcript_first, InputScalarIterator transcript_last,
                                       const typename CurveType::scalar_field_type::value_type &kzg_challenge) {
                    std::vector<typename CurveType::scalar_field_type::value_type> vkey_poly =
                        polynomial_coefficients_from_transcript<typename CurveType::scalar_field_type>(
                            transcript_first, transcript_last, CurveType::scalar_field_type::value_type::one());
                    math::_condense(vkey_poly);
                    BOOST_ASSERT(!math::_is_zero(vkey_poly));

                    typename CurveType::scalar_field_type::value_type vkey_poly_z =
                        polynomial_evaluation_product_form_from_transcript<typename CurveType::scalar_field_type>(
                            transcript_first, transcript_last, kzg_challenge,
                            CurveType::scalar_field_type::value_type::one());

                    return prove_commitment_key_kzg_opening<typename CurveType::template g2_type<>>(
                        srs_powers_alpha_first, srs_powers_alpha_last, srs_powers_beta_first, srs_powers_beta_last,
                        vkey_poly, vkey_poly_z, kzg_challenge);
                }

                template<typename CurveType, typename InputG1Iterator, typename InputScalarIterator>
                typename std::enable_if<
                    std::is_same<typename CurveType::template g1_type<>::value_type,
                                 typename std::iterator_traits<InputG1Iterator>::value_type>::value &&
                        std::is_same<typename CurveType::scalar_field_type::value_type,
                                     typename std::iterator_traits<InputScalarIterator>::value_type>::value,
                    kzg_opening<typename CurveType::template g1_type<>>>::type
                    prove_commitment_w(InputG1Iterator srs_powers_alpha_first, InputG1Iterator srs_powers_alpha_last,
                                       InputG1Iterator srs_powers_beta_first, InputG1Iterator srs_powers_beta_last,
                                       InputScalarIterator transcript_first, InputScalarIterator transcript_last,
                                       typename CurveType::scalar_field_type::value_type r_shift,
                                       const typename CurveType::scalar_field_type::value_type &kzg_challenge) {
                    std::size_t n = std::distance(srs_powers_beta_first, srs_powers_beta_last) / 2;
                    BOOST_ASSERT(2 * n == std::distance(srs_powers_alpha_first, srs_powers_alpha_last));

                    // this computes f(X) = \prod (1 + x (rX)^{2^j})
                    std::vector<typename CurveType::scalar_field_type::value_type> fcoeffs =
                        polynomial_coefficients_from_transcript<typename CurveType::scalar_field_type>(
                            transcript_first, transcript_last, r_shift);
                    // this computes f_w(X) = X^n * f(X) - it simply shifts all coefficients to by n
                    fcoeffs.insert(fcoeffs.begin(), n, CurveType::scalar_field_type::value_type::zero());

                    // this computes f(z)
                    typename CurveType::scalar_field_type::value_type fz =
                        polynomial_evaluation_product_form_from_transcript<typename CurveType::scalar_field_type>(
                            transcript_first, transcript_last, kzg_challenge, r_shift);
                    // this computes the "shift" z^n
                    typename CurveType::scalar_field_type::value_type zn = kzg_challenge.pow(n);
                    // this computes f_w(z) by multiplying by zn
                    typename CurveType::scalar_field_type::value_type fwz = fz * zn;

                    return prove_commitment_key_kzg_opening<typename CurveType::template g1_type<>>(
                        srs_powers_alpha_first, srs_powers_alpha_last, srs_powers_beta_first, srs_powers_beta_last,
                        fcoeffs, fwz, kzg_challenge);
                }

                /// gipa_tipp_mipp peforms the recursion of the GIPA protocol for TIPP and MIPP.
                /// It returns a proof containing all intermdiate committed values, as well as
                /// the challenges generated necessary to do the polynomial commitment proof
                /// later in TIPP.
                template<typename CurveType, typename Hash = hashes::sha2<256>, typename InputG1Iterator1,
                         typename InputG2Iterator, typename InputG1Iterator2, typename InputScalarIterator>
                typename std::enable_if<
                    std::is_same<typename CurveType::template g1_type<>::value_type,
                                 typename std::iterator_traits<InputG1Iterator1>::value_type>::value &&
                        std::is_same<typename CurveType::template g2_type<>::value_type,
                                     typename std::iterator_traits<InputG2Iterator>::value_type>::value &&
                        std::is_same<typename CurveType::scalar_field_type::value_type,
                                     typename std::iterator_traits<InputScalarIterator>::value_type>::value &&
                        std::is_same<typename CurveType::template g1_type<>::value_type,
                                     typename std::iterator_traits<InputG1Iterator2>::value_type>::value,
                    std::tuple<gipa_proof<CurveType>, std::vector<typename CurveType::scalar_field_type::value_type>,
                               std::vector<typename CurveType::scalar_field_type::value_type>>>::type
                    gipa_tipp_mipp(transcript<CurveType, Hash> &tr, InputG1Iterator1 a_first, InputG1Iterator1 a_last,
                                   InputG2Iterator b_first, InputG2Iterator b_last, InputG1Iterator2 c_first,
                                   InputG1Iterator2 c_last, const r1cs_gg_ppzksnark_ipp2_vkey<CurveType> &vkey_input,
                                   const r1cs_gg_ppzksnark_ipp2_wkey<CurveType> &wkey_input,
                                   InputScalarIterator r_first, InputScalarIterator r_last) {
                    std::size_t input_len = std::distance(a_first, a_last);
                    BOOST_ASSERT(input_len >= 2);
                    BOOST_ASSERT((input_len & (input_len - 1)) == 0);
                    BOOST_ASSERT(input_len == std::distance(b_first, b_last));
                    BOOST_ASSERT(input_len == std::distance(r_first, r_last));
                    BOOST_ASSERT(input_len == std::distance(c_first, c_last));

                    // the values of vectors A and B rescaled at each step of the loop
                    // the values of vectors C and r rescaled at each step of the loop
                    std::vector<typename CurveType::template g1_type<>::value_type> m_a {a_first, a_last}, m_c {c_first, c_last};
                    std::vector<typename CurveType::template g2_type<>::value_type> m_b {b_first, b_last};
                    std::vector<typename CurveType::scalar_field_type::value_type> m_r {r_first, r_last};

                    // the values of the commitment keys rescaled at each step of the loop
                    r1cs_gg_ppzksnark_ipp2_vkey<CurveType> vkey = vkey_input;
                    r1cs_gg_ppzksnark_ipp2_wkey<CurveType> wkey = wkey_input;

                    // storing the values for including in the proof
                    std::vector<std::pair<typename r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::output_type,
                                          typename r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::output_type>>
                        comms_ab;
                    std::vector<std::pair<typename r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::output_type,
                                          typename r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::output_type>>
                        comms_c;
                    std::vector<
                        std::pair<typename CurveType::gt_type::value_type, typename CurveType::gt_type::value_type>>
                        z_ab;
                    std::vector<
                        std::pair<typename CurveType::template g1_type<>::value_type, typename CurveType::template g1_type<>::value_type>>
                        z_c;
                    std::vector<typename CurveType::scalar_field_type::value_type> challenges, challenges_inv;

                    constexpr std::array<std::uint8_t, 4> domain_separator {'g', 'i', 'p', 'a'};
                    tr.write_domain_separator(domain_separator.begin(), domain_separator.end());
                    typename CurveType::scalar_field_type::value_type _i = tr.read_challenge();

                    while (m_a.size() > 1) {
                        // recursive step
                        // Recurse with problem of half size
                        std::size_t split = m_a.size() / 2;

                        auto [vk_left, vk_right] = vkey.split(split);
                        auto [wk_left, wk_right] = wkey.split(split);

                        // TODO: parallel
                        // See section 3.3 for paper version with equivalent names
                        // TIPP part
                        typename r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::output_type tab_l =
                            r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::pair(
                                vk_left, wk_right, m_a.begin() + split, m_a.end(), m_b.begin(), m_b.begin() + split);
                        typename r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::output_type tab_r =
                            r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::pair(
                                vk_right, wk_left, m_a.begin(), m_a.begin() + split, m_b.begin() + split, m_b.end());

                        // \prod e(A_right,B_left)
                        typename CurveType::gt_type::value_type zab_l = CurveType::gt_type::value_type::one();
                        std::for_each(boost::make_zip_iterator(boost::make_tuple(m_a.begin() + split, m_b.begin())),
                                      boost::make_zip_iterator(boost::make_tuple(m_a.end(), m_b.begin() + split)),
                                      [&](const boost::tuple<const typename CurveType::template g1_type<>::value_type &,
                                                             const typename CurveType::template g2_type<>::value_type &> &t) {
                                          zab_l = zab_l *
                                                  algebra::pair<CurveType>(t.template get<0>(), t.template get<1>());
                                      });
                        zab_l = algebra::final_exponentiation<CurveType>(zab_l);
                        typename CurveType::gt_type::value_type zab_r = CurveType::gt_type::value_type::one();
                        std::for_each(boost::make_zip_iterator(boost::make_tuple(m_a.begin(), m_b.begin() + split)),
                                      boost::make_zip_iterator(boost::make_tuple(m_a.begin() + split, m_b.end())),
                                      [&](const boost::tuple<const typename CurveType::template g1_type<>::value_type &,
                                                             const typename CurveType::template g2_type<>::value_type &> &t) {
                                          zab_r = zab_r *
                                                  algebra::pair<CurveType>(t.template get<0>(), t.template get<1>());
                                      });
                        zab_r = algebra::final_exponentiation<CurveType>(zab_r);

                        // MIPP part
                        // z_l = c[n':] ^ r[:n']
                        typename CurveType::template g1_type<>::value_type zc_l =
                            algebra::multiexp<algebra::policies::multiexp_method_bos_coster>(
                                m_c.begin() + split, m_c.end(), m_r.begin(), m_r.begin() + split, 1);
                        // Z_r = c[:n'] ^ r[n':]
                        typename CurveType::template g1_type<>::value_type zc_r =
                            algebra::multiexp<algebra::policies::multiexp_method_bos_coster>(
                                m_c.begin(), m_c.begin() + split, m_r.begin() + split, m_r.end(), 1);
                        // u_l = c[n':] * v[:n']
                        typename r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::output_type tuc_l =
                            r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::single(vk_left, m_c.begin() + split,
                                                                                m_c.end());
                        // u_r = c[:n'] * v[n':]
                        typename r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::output_type tuc_r =
                            r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::single(vk_right, m_c.begin(),
                                                                                m_c.begin() + split);

                        // Fiat-Shamir challenge
                        // combine both TIPP and MIPP transcript
                        tr.template write<typename CurveType::gt_type>(zab_l);
                        tr.template write<typename CurveType::gt_type>(zab_r);
                        tr.template write<typename CurveType::template g1_type<>>(zc_l);
                        tr.template write<typename CurveType::template g1_type<>>(zc_r);
                        tr.template write<typename CurveType::gt_type>(tab_l.first);
                        tr.template write<typename CurveType::gt_type>(tab_l.second);
                        tr.template write<typename CurveType::gt_type>(tab_r.first);
                        tr.template write<typename CurveType::gt_type>(tab_r.second);
                        tr.template write<typename CurveType::gt_type>(tuc_l.first);
                        tr.template write<typename CurveType::gt_type>(tuc_l.second);
                        tr.template write<typename CurveType::gt_type>(tuc_r.first);
                        tr.template write<typename CurveType::gt_type>(tuc_r.second);
                        typename CurveType::scalar_field_type::value_type c_inv = tr.read_challenge();

                        // Optimization for multiexponentiation to rescale G2 elements with
                        // 128-bit challenge Swap 'c' and 'c_inv' since can't control bit size
                        // of c_inv
                        typename CurveType::scalar_field_type::value_type c = c_inv.inversed();

                        // Set up values for next step of recursion
                        // A[:n'] + A[n':] ^ x
                        compress<CurveType>(m_a, split, c);
                        // B[:n'] + B[n':] ^ x^-1
                        compress<CurveType>(m_b, split, c_inv);
                        // c[:n'] + c[n':]^x
                        compress<CurveType>(m_c, split, c);
                        // r[:n'] + r[n':]^x^-1
                        compress<CurveType>(m_r, split, c_inv);

                        // v_left + v_right^x^-1
                        vkey = vk_left.compress(vk_right, c_inv);
                        // w_left + w_right^x
                        wkey = wk_left.compress(wk_right, c);

                        comms_ab.emplace_back(std::make_pair(tab_l, tab_r));
                        comms_c.emplace_back(std::make_pair(tuc_l, tuc_r));
                        z_ab.emplace_back(std::make_pair(zab_l, zab_r));
                        z_c.emplace_back(std::make_pair(zc_l, zc_r));
                        challenges.emplace_back(c);
                        challenges_inv.emplace_back(c_inv);
                    }

                    BOOST_ASSERT(m_a.size() == 1 && m_b.size() == 1);
                    BOOST_ASSERT(m_c.size() == 1 && m_r.size() == 1);
                    BOOST_ASSERT(vkey.a.size() == 1 && vkey.b.size() == 1);
                    BOOST_ASSERT(wkey.a.size() == 1 && wkey.b.size() == 1);

                    return std::make_tuple(gipa_proof<CurveType> {input_len, comms_ab, comms_c, z_ab, z_c, m_a[0],
                                                                  m_b[0], m_c[0], vkey.first(), wkey.first()},
                                           challenges, challenges_inv);
                }

                /// Proves a TIPP relation between A and B as well as a MIPP relation with C and
                /// r. Commitment keys must be of size of A, B and C. In the context of Groth16
                /// aggregation, we have that B = B^r and wkey is scaled by r^{-1}. The
                /// commitment key v is used to commit to A and C recursively in GIPA such that
                /// only one KZG proof is needed for v. In the original paper version, since the
                /// challenges of GIPA would be different, two KZG proofs would be needed.
                template<typename CurveType, typename Hash = hashes::sha2<256>, typename InputG1Iterator1,
                         typename InputG2Iterator, typename InputG1Iterator2, typename InputScalarIterator>
                typename std::enable_if<
                    std::is_same<typename CurveType::template g1_type<>::value_type,
                                 typename std::iterator_traits<InputG1Iterator1>::value_type>::value &&
                        std::is_same<typename CurveType::template g2_type<>::value_type,
                                     typename std::iterator_traits<InputG2Iterator>::value_type>::value &&
                        std::is_same<typename CurveType::template g1_type<>::value_type,
                                     typename std::iterator_traits<InputG1Iterator2>::value_type>::value &&
                        std::is_same<typename CurveType::scalar_field_type::value_type,
                                     typename std::iterator_traits<InputScalarIterator>::value_type>::value,
                    tipp_mipp_proof<CurveType>>::type
                    prove_tipp_mipp(const r1cs_gg_ppzksnark_aggregate_proving_srs<CurveType> &srs,
                                    transcript<CurveType, Hash> &tr, InputG1Iterator1 a_first, InputG1Iterator1 a_last,
                                    InputG2Iterator b_first, InputG2Iterator b_last, InputG1Iterator2 c_first,
                                    InputG1Iterator2 c_last, const r1cs_gg_ppzksnark_ipp2_wkey<CurveType> &wkey,
                                    InputScalarIterator r_first, InputScalarIterator r_last) {
                    typename CurveType::scalar_field_type::value_type r_shift = *(r_first + 1);
                    // Run GIPA
                    auto [proof, challenges, challenges_inv] = gipa_tipp_mipp<CurveType>(
                        tr, a_first, a_last, b_first, b_last, c_first, c_last, srs.vkey, wkey, r_first, r_last);

                    // Prove final commitment keys are wellformed
                    // we reverse the transcript so the polynomial in kzg opening is constructed
                    // correctly - the formula indicates x_{l-j}. Also for deriving KZG
                    // challenge point, input must be the last challenge.
                    std::reverse(challenges.begin(), challenges.end());
                    std::reverse(challenges_inv.begin(), challenges_inv.end());
                    typename CurveType::scalar_field_type::value_type r_inverse = r_shift.inversed();

                    // KZG challenge point
                    constexpr std::array<std::uint8_t, 8> domain_separator {'r', 'a', 'n', 'd', 'o', 'm', '-', 'z'};
                    tr.write_domain_separator(domain_separator.begin(), domain_separator.end());
                    tr.template write<typename CurveType::scalar_field_type>(challenges[0]);
                    tr.template write<typename CurveType::template g2_type<>>(proof.final_vkey.first);
                    tr.template write<typename CurveType::template g2_type<>>(proof.final_vkey.second);
                    tr.template write<typename CurveType::template g1_type<>>(proof.final_wkey.first);
                    tr.template write<typename CurveType::template g1_type<>>(proof.final_wkey.second);
                    typename CurveType::scalar_field_type::value_type z = tr.read_challenge();

                    // Complete KZG proofs
                    return tipp_mipp_proof<CurveType> {
                        proof,
                        prove_commitment_v<CurveType>(srs.h_alpha_powers.begin(), srs.h_alpha_powers.end(),
                                                      srs.h_beta_powers.begin(), srs.h_beta_powers.end(),
                                                      challenges_inv.begin(), challenges_inv.end(), z),
                        prove_commitment_w<CurveType>(srs.g_alpha_powers.begin(), srs.g_alpha_powers.end(),
                                                      srs.g_beta_powers.begin(), srs.g_beta_powers.end(),
                                                      challenges.begin(), challenges.end(), r_inverse, z)};
                }

                /// Aggregate `n` zkSnark proofs, where `n` must be a power of two.
                template<typename CurveType, typename Hash = hashes::sha2<256>, typename InputTranscriptIncludeIterator,
                         typename InputProofIterator>
                typename std::enable_if<
                    std::is_same<std::uint8_t,
                                 typename std::iterator_traits<InputTranscriptIncludeIterator>::value_type>::value &&
                        std::is_same<typename std::iterator_traits<InputProofIterator>::value_type,
                                     r1cs_gg_ppzksnark_proof<CurveType>>::value,
                    r1cs_gg_ppzksnark_aggregate_proof<CurveType>>::type
                    aggregate_proofs(const r1cs_gg_ppzksnark_aggregate_proving_srs<CurveType> &srs,
                                     InputTranscriptIncludeIterator tr_include_first,
                                     InputTranscriptIncludeIterator tr_include_last, InputProofIterator proofs_first,
                                     InputProofIterator proofs_last) {
                    std::size_t nproofs = std::distance(proofs_first, proofs_last);
                    BOOST_ASSERT(nproofs >= 2);
                    BOOST_ASSERT((nproofs & (nproofs - 1)) == 0);
                    BOOST_ASSERT(srs.has_correct_len(nproofs));

                    // TODO: parallel
                    // We first commit to A B and C - these commitments are what the verifier
                    // will use later to verify the TIPP and MIPP proofs
                    std::vector<typename CurveType::template g1_type<>::value_type> a, c;
                    std::vector<typename CurveType::template g2_type<>::value_type> b;
                    auto proofs_it = proofs_first;
                    while (proofs_it != proofs_last) {
                        a.emplace_back(proofs_it->g_A);
                        b.emplace_back(proofs_it->g_B);
                        c.emplace_back(proofs_it->g_C);
                        ++proofs_it;
                    }

                    // A and B are committed together in this scheme
                    // we need to take the reference so the macro doesn't consume the value
                    // first
                    typename r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::output_type com_ab =
                        r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::pair(srs.vkey, srs.wkey, a.begin(), a.end(),
                                                                           b.begin(), b.end());
                    typename r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::output_type com_c =
                        r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::single(srs.vkey, c.begin(), c.end());

                    // Derive a random scalar to perform a linear combination of proofs
                    constexpr std::array<std::uint8_t, 9> application_tag = {'s', 'n', 'a', 'r', 'k',
                                                                             'p', 'a', 'c', 'k'};
                    constexpr std::array<std::uint8_t, 8> domain_separator {'r', 'a', 'n', 'd', 'o', 'm', '-', 'r'};
                    transcript<CurveType, Hash> tr(application_tag.begin(), application_tag.end());
                    tr.write_domain_separator(domain_separator.begin(), domain_separator.end());
                    tr.template write<typename CurveType::gt_type>(com_ab.first);
                    tr.template write<typename CurveType::gt_type>(com_ab.second);
                    tr.template write<typename CurveType::gt_type>(com_c.first);
                    tr.template write<typename CurveType::gt_type>(com_c.second);
                    tr.write(tr_include_first, tr_include_last);
                    typename CurveType::scalar_field_type::value_type r = tr.read_challenge();

                    // 1,r, r^2, r^3, r^4 ...
                    std::vector<typename CurveType::scalar_field_type::value_type> r_vec =
                        structured_scalar_power<typename CurveType::scalar_field_type>(
                            std::distance(proofs_first, proofs_last), r);
                    // 1,r^-1, r^-2, r^-3
                    std::vector<typename CurveType::scalar_field_type::value_type> r_inv;
                    std::transform(r_vec.begin(), r_vec.end(), std::back_inserter(r_inv),
                                   [](const auto &r_i) { return r_i.inversed(); });

                    // B^{r}
                    std::vector<typename CurveType::template g2_type<>::value_type> b_r;
                    std::for_each(
                        boost::make_zip_iterator(boost::make_tuple(b.begin(), r_vec.begin())),
                        boost::make_zip_iterator(boost::make_tuple(b.end(), r_vec.end())),
                        [&](const boost::tuple<const typename CurveType::template g2_type<>::value_type &,
                                               const typename CurveType::scalar_field_type::value_type &> &t) {
                            b_r.emplace_back((t.template get<0>() * t.template get<1>()));
                        });
                    // TODO: parallel
                    // compute A * B^r for the verifier
                    // auto ip_ab = algebra::pair<CurveType>(a, b_r);
                    typename CurveType::gt_type::value_type ip_ab = CurveType::gt_type::value_type::one();
                    std::for_each(boost::make_zip_iterator(boost::make_tuple(a.begin(), b_r.begin())),
                                  boost::make_zip_iterator(boost::make_tuple(a.end(), b_r.end())),
                                  [&](const boost::tuple<const typename CurveType::template g1_type<>::value_type &,
                                                         const typename CurveType::template g2_type<>::value_type &> &t) {
                                      ip_ab =
                                          ip_ab * algebra::pair<CurveType>(t.template get<0>(), t.template get<1>());
                                  });
                    ip_ab = algebra::final_exponentiation<CurveType>(ip_ab);
                    // compute C^r for the verifier
                    typename CurveType::template g1_type<>::value_type agg_c =
                        algebra::multiexp<algebra::policies::multiexp_method_bos_coster>(c.begin(), c.end(),
                                                                                r_vec.begin(), r_vec.end(), 1);
                    tr.template write<typename CurveType::gt_type>(ip_ab);
                    tr.template write<typename CurveType::template g1_type<>>(agg_c);

                    // w^{r^{-1}}
                    r1cs_gg_ppzksnark_ipp2_commitment_key<typename CurveType::template g1_type<>> wkey_r_inv =
                        srs.wkey.scale(r_inv.begin(), r_inv.end());

                    // we prove tipp and mipp using the same recursive loop
                    tipp_mipp_proof<CurveType> proof =
                        prove_tipp_mipp(srs, tr, a.begin(), a.end(), b_r.begin(), b_r.end(), c.begin(), c.end(),
                                        wkey_r_inv, r_vec.begin(), r_vec.end());

                    // debug assert
                    BOOST_ASSERT(com_ab == r1cs_gg_ppzksnark_ipp2_commitment<CurveType>::pair(
                                               srs.vkey, wkey_r_inv, a.begin(), a.end(), b_r.begin(), b_r.end()));

                    return {com_ab, com_c, ip_ab, agg_c, proof};
                }

                template<typename CurveType, typename BasicProver>
                class r1cs_gg_ppzksnark_aggregate_prover {
                    typedef detail::r1cs_gg_ppzksnark_basic_policy<CurveType, ProvingMode::Aggregate> policy_type;

                public:

                    typedef BasicProver basic_prover;

                    typedef typename policy_type::primary_input_type primary_input_type;
                    typedef typename policy_type::auxiliary_input_type auxiliary_input_type;
                    typedef typename policy_type::proving_key_type proving_key_type;
                    typedef typename policy_type::proving_srs_type proving_srs_type;
                    typedef typename policy_type::proof_type proof_type;
                    typedef typename policy_type::aggregate_proof_type aggregate_proof_type;

                    // Aggregate prove
                    template<typename Hash, typename InputTranscriptIncludeIterator, typename InputProofIterator>
                    static inline aggregate_proof_type process(const proving_srs_type &srs,
                                                               InputTranscriptIncludeIterator transcript_include_first,
                                                               InputTranscriptIncludeIterator transcript_include_last,
                                                               InputProofIterator proofs_first,
                                                               InputProofIterator proofs_last) {
                        return aggregate_proofs<CurveType, Hash>(srs, transcript_include_first, transcript_include_last,
                                                                 proofs_first, proofs_last);
                    }

                    // Basic prove
                    static inline proof_type process(const proving_key_type &pk,
                                                     const primary_input_type &primary_input,
                                                     const auxiliary_input_type &auxiliary_input) {

                        return BasicProver::process(pk, primary_input, auxiliary_input);
                    }
                };
            }    // namespace snark
        }        // namespace zk
    }            // namespace crypto3
}    // namespace nil

#endif    // CRYPTO3_R1CS_GG_PPZKSNARK_TYPES_POLICY_HPP


// Copyright (c) 2013 Spotify AB
//
// 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.

#include "Python.h"
#include <stdio.h>
#include <string>
#include <boost/python.hpp>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <math.h>
#include <vector>
#include <queue>
#include <limits>
#include <boost/random.hpp>
#include <boost/random/normal_distribution.hpp>
#include <boost/random/uniform_01.hpp>
#include <boost/random/bernoulli_distribution.hpp>

#ifndef NO_PACKED_STRUCTS
#define PACKED_STRUCTS_EXTRA __attribute__((__packed__))
// TODO: this is turned on by default, but may not work for all architectures! Need to investigate.
#endif

using namespace std;
using namespace boost;

template<typename T>
struct Randomness {
  // Just a dummy class to avoid code repetition.
  // Owned by the AnnoyIndex, passed around to the distance metrics
  mt19937 _rng;
  normal_distribution<T> _nd;
  variate_generator<mt19937&,
		    normal_distribution<T> > _var_nor;
  uniform_01<T> _ud;
  variate_generator<mt19937&,
		    uniform_01<T> > _var_uni;
  bernoulli_distribution<T> _bd;
  variate_generator<mt19937&,
		    bernoulli_distribution<T> > _var_ber;
  Randomness() : _rng(), _nd(), _var_nor(_rng, _nd), _ud(), _var_uni(_rng, _ud), _bd(), _var_ber(_rng, _bd) {}
  inline T gaussian() {
    return _var_nor();
  }
  inline int flip() {
    return _var_ber();
  }
  inline T uniform(T min, T max) {
    return _var_uni() * (max - min) + min;
  }
};

template<typename T>
inline void normalize(T* v, int f) {
  T sq_norm = 0;
  for (int z = 0; z < f; z++)
    sq_norm += v[z] * v[z];
  T norm = sqrt(sq_norm);
  for (int z = 0; z < f; z++)
    v[z] /= norm;
}

template<typename T>
struct Angular {
  struct PACKED_STRUCTS_EXTRA node {
    /*
     * We store a binary tree where each node has two things
     * - A vector associated with it
     * - Two children
     * All nodes occupy the same amount of memory
     * All nodes with n_descendants == 1 are leaf nodes.
     * A memory optimization is that for nodes with 2 <= n_descendants <= K,
     * we skip the vector. Instead we store a list of all descendants. K is
     * determined by the number of items that fits in the same space.
     * For nodes with n_descendants == 1 or > K, there is always a
     * corresponding vector.
     * Note that we can't really do sizeof(node<T>) because we cheat and allocate
     * more memory to be able to fit the vector outside
     */
    int n_descendants;
    int children[2]; // Will possibly store more than 2
    T v[0]; // Hack. We just allocate as much memory as we need and let this array overflow
  };
  static inline T distance(const T* x, const T* y, int f) {
    // want to calculate (a/|a| - b/|b|)^2
    // = a^2 / a^2 + b^2 / b^2 - 2ab/|a||b|
    // = 2 - 2cos
    T pp = 0, qq = 0, pq = 0;
    for (int z = 0; z < f; z++) {
      pp += x[z] * x[z];
      qq += y[z] * y[z];
      pq += x[z] * y[z];
    }
    T ppqq = pp * qq;
    if (ppqq > 0) return 2.0 - 2.0 * pq / sqrt(ppqq);
    else return 2.0; // cos is 0
  }
  static inline T margin(const node* n, const T* y, int f) {
    T dot = 0;
    for (int z = 0; z < f; z++)
      dot += n->v[z] * y[z];
    return dot;
  }
  static inline bool side(const node* n, const T* y, int f, Randomness<T>* random) {
    T dot = margin(n, y, f);
    if (dot != 0)
      return (dot > 0);
    else
      return random->flip();
  }
  static inline void create_split(const vector<node*>& nodes, int f, Randomness<T>* random, node* n) {
    for (int z = 0; z < f; z++)
      n->v[z] = random->gaussian();
    normalize(n->v, f);
  }
};

template<typename T>
struct Euclidean {
  struct __attribute__((__packed__)) node {
    int n_descendants;
    T a; // need an extra constant term to determine the offset of the plane
    int children[2];
    T v[0];
  };
  static inline T distance(const T* x, const T* y, int f) {
    T d = 0.0;
    for (int i = 0; i < f; i++)
      d += (x[i] - y[i]) * (x[i] - y[i]);
    return d;
  }
  static inline T margin(const node* n, const T* y, int f) {
    T dot = n->a;
    for (int z = 0; z < f; z++)
      dot += n->v[z] * y[z];
    return dot;
  }
  static inline bool side(const node* n, const T* y, int f, Randomness<T>* random) {
    T dot = margin(n, y, f);
    if (dot != 0)
      return (dot > 0);
    else
      return random->flip();
  }
  static inline void create_split(const vector<node*>& nodes, int f, Randomness<T>* random, node* n) {
    // See http://en.wikipedia.org/wiki/Bertrand_paradox_(probability)
    // We want to sample a random hyperplane out of all hyperplanes that cut the convex hull.
    // The probability of each angle is in proportion to the extent of the projection.
    // This is good because it means we try to split in the longest direction.
    // Doing this using Metropolis-Hastings sampling using 10 steps
    T* v = (T*)malloc(sizeof(T) * f); // TODO: would be really nice to get rid of this allocation
    double max_proj = 0.0;
    for (int step = 0; step < 10; step++) {
      for (int z = 0; z < f; z++)
	v[z] = random->gaussian();
      normalize(v, f);
      // Project the nodes onto the vector and calculate max and min
      T min = INFINITY, max = -INFINITY;
      for (size_t i = 0; i < nodes.size(); i++) {
	T dot = 0;
	for (int z = 0; z < f; z++)
	  dot += nodes[i]->v[z] * v[z];
	if (dot > max)
	  max = dot;
	if (dot < min)
	  min = dot;
      }
      if (max - min > random->uniform(0, max_proj)) {
	max_proj = max - min;
	memcpy(n->v, v, sizeof(T) * f);
	n->a = -random->uniform(min, max); // Take a random split along this axis
      }
    }
    free(v);
  }
};

template<typename T, typename Distance>
class AnnoyIndex {
  /*
   * We use random projection to build a forest of binary trees of all items.
   * Basically just split the hyperspace into two sides by a hyperplane,
   * then recursively split each of those subtrees etc.
   * We create a tree like this q times. The default q is determined automatically
   * in such a way that we at most use 2x as much memory as the vectors take.
   */
protected:
  int _f;
  size_t _s;
  int _n_items;
  Randomness<T> _random;
  void* _nodes; // Could either be mmapped, or point to a memory buffer that we reallocate
  int _n_nodes;
  int _nodes_size;
  vector<int> _roots;
  int _K;
  bool _loaded;
public:
  AnnoyIndex(int f) : _random() {
    _f = f;
    _s = sizeof(typename Distance::node) + sizeof(T) * f; // Size of each node
    _n_items = 0;
    _n_nodes = 0;
    _nodes_size = 0;
    _nodes = NULL;
    _loaded = false;

    _K = (sizeof(T) * f + sizeof(int) * 2) / sizeof(int);
  }
  ~AnnoyIndex() {
    if (_loaded) {
      unload();
    } else if(_nodes) {
      free(_nodes);
    }
  }

  void add_item(int item, const T* w) {
    _allocate_size(item+1);
    typename Distance::node* n = _get(item);

    n->children[0] = 0;
    n->children[1] = 0;
    n->n_descendants = 1;

    for (int z = 0; z < _f; z++)
      n->v[z] = w[z];

    if (item >= _n_items)
      _n_items = item + 1;
  }

  void build(int q) {
    _n_nodes = _n_items;
    while (1) {
      if (q == -1 && _n_nodes >= _n_items * 2)
	break;
      if (q != -1 && _roots.size() >= (size_t)q)
	break;
      fprintf(stderr, "pass %zd...\n", _roots.size());

      vector<int> indices;
      for (int i = 0; i < _n_items; i++)
	indices.push_back(i);

      _roots.push_back(_make_tree(indices));
    }
    // Also, copy the roots into the last segment of the array
    // This way we can load them faster without reading the whole file
    _allocate_size(_n_nodes + _roots.size());
    for (size_t i = 0; i < _roots.size(); i++)
      memcpy(_get(_n_nodes + i), _get(_roots[i]), _s);
    _n_nodes += _roots.size();

    fprintf(stderr, "has %d nodes\n", _n_nodes);
  }

  void save(const string& filename) {
    FILE *f = fopen(filename.c_str(), "w");

    fwrite(_nodes, _s, _n_nodes, f);

    fclose(f);

    free(_nodes);
    _n_items = 0;
    _n_nodes = 0;
    _nodes_size = 0;
    _nodes = NULL;
    _roots.clear();
    load(filename);
  }

  void reinitialize() {
    _nodes = NULL;
    _loaded = false;
    _n_items = 0;
    _n_nodes = 0;
    _nodes_size = 0;
    _roots.clear();
  }

  void unload() {
    off_t size = _n_nodes * _s;
    munmap(_nodes, size);
    reinitialize();
    fprintf(stderr, "unloaded\n");
  }

  void load(const string& filename) {
    struct stat buf;
    stat(filename.c_str(), &buf);
    off_t size = buf.st_size;
    int fd = open(filename.c_str(), O_RDONLY, (mode_t)0400);
#ifdef MAP_POPULATE
    _nodes = (typename Distance::node*)mmap(
        0, size, PROT_READ, MAP_SHARED | MAP_POPULATE, fd, 0);
#else
    _nodes = (typename Distance::node*)mmap(
        0, size, PROT_READ, MAP_SHARED, fd, 0);
#endif

    _n_nodes = size / _s;

    // Find the roots by scanning the end of the file and taking the nodes with most descendants
    int m = -1;
    for (int i = _n_nodes - 1; i >= 0; i--) {
      int k = _get(i)->n_descendants;
      if (m == -1 || k == m) {
	_roots.push_back(i);
	m = k;
      } else {
	break;
      }
    }
    _loaded = true;
    _n_items = m;
    fprintf(stderr, "found %lu roots with degree %d\n", _roots.size(), m);
  }

  inline T get_distance(int i, int j) {
    const T* x = _get(i)->v;
    const T* y = _get(j)->v;
    return Distance::distance(x, y, _f);
  }

  void get_nns_by_item(int item, size_t n, vector<int>* result) {
    const typename Distance::node* m = _get(item);
    _get_all_nns(m->v, n, result);
  }

  void get_nns_by_vector(const T* w, size_t n, vector<int>* result) {
    _get_all_nns(w, n, result);
  }
  int get_n_items() {
    return _n_items;
  }
protected:
  void _allocate_size(int n) {
    if (n > _nodes_size) {
      int new_nodes_size = (_nodes_size + 1) * 2;
      if (n > new_nodes_size)
	new_nodes_size = n;
      _nodes = realloc(_nodes, _s * new_nodes_size);
      memset((char *)_nodes + (_nodes_size * _s)/sizeof(char), 0, (new_nodes_size - _nodes_size) * _s);
      _nodes_size = new_nodes_size;
    }
  }

  inline typename Distance::node* _get(int i) {
    return (typename Distance::node*)((char *)_nodes + (_s * i)/sizeof(char));
  }

  int _make_tree(const vector<int >& indices) {
    if (indices.size() == 1)
      return indices[0];

    _allocate_size(_n_nodes + 1);
    int item = _n_nodes++;
    typename Distance::node* m = _get(item);
    m->n_descendants = indices.size();

    if (indices.size() <= (size_t)_K) {
      for (size_t i = 0; i < indices.size(); i++)
	m->children[i] = indices[i];
      return item;
    }

    vector<int> children_indices[2];
    for (int attempt = 0; attempt < 20; attempt ++) {
      /*
       * Create a random hyperplane.
       * If all points end up on the same time, we try again.
       * We could in principle *construct* a plane so that we split
       * all items evenly, but I think that could violate the guarantees
       * given by just picking a hyperplane at random
       */
      vector<typename Distance::node*> children;

      for (size_t i = 0; i < indices.size(); i++) {
	// TODO: this loop isn't needed for the angular distance, because
	// we can just split by a random vector and it's fine. For Euclidean
	// distance we need it to calculate the offset
	int j = indices[i];
	typename Distance::node* n = _get(j);
	if (n)
	  children.push_back(n);
      }

      Distance::create_split(children, _f, &_random, m);

      children_indices[0].clear();
      children_indices[1].clear();

      for (size_t i = 0; i < indices.size(); i++) {
	int j = indices[i];
	typename Distance::node* n = _get(j);
	if (n) {
	  bool side = Distance::side(m, n->v, _f, &_random);
	  children_indices[side].push_back(j);
	}
      }

      if (children_indices[0].size() > 0 && children_indices[1].size() > 0) {
	break;
      }
    }

    while (children_indices[0].size() == 0 || children_indices[1].size() == 0) {
      // If we didn't find a hyperplane, just randomize sides as a last option
      if (indices.size() > 100000)
	fprintf(stderr, "Failed splitting %lu items\n", indices.size());

      children_indices[0].clear();
      children_indices[1].clear();

      // Set the vector to 0.0
      for (int z = 0; z < _f; z++)
	m->v[z] = 0.0;

      for (size_t i = 0; i < indices.size(); i++) {
	int j = indices[i];
	// Just randomize...
	children_indices[_random.flip()].push_back(j);
      }
    }

    int children_0 = _make_tree(children_indices[0]);
    int children_1 = _make_tree(children_indices[1]);

    // We need to fetch m again because it might have been reallocated
    m = _get(item);
    m->children[0] = children_0;
    m->children[1] = children_1;

    return item;
  }

  void _get_nns(const T* v, int i, vector<int>* result, int limit) {
    const typename Distance::node* n = _get(i);

    if (n->n_descendants == 0) {
      // unknown item, nothing to do...
    } else if (n->n_descendants == 1) {
      result->push_back(i);
    } else if (n->n_descendants <= _K) {
      for (int j = 0; j < n->n_descendants; j++) {
	result->push_back(n->children[j]);
      }
    } else {
      bool side = Distance::side(n, v, _f, &_random);

      _get_nns(v, n->children[side], result, limit);
      if (result->size() < (size_t)limit)
	_get_nns(v, n->children[!side], result, limit);
    }
  }

  void _get_all_nns(const T* v, size_t n, vector<int>* result) {
    std::priority_queue<pair<T, int> > q;

    for (size_t i = 0; i < _roots.size(); i++) {
      q.push(make_pair(numeric_limits<T>::infinity(), _roots[i]));
    }

    vector<int> nns;
    while (nns.size() < n * _roots.size() && !q.empty()) {
      const pair<T, int>& top = q.top();
      int i = top.second;
      const typename Distance::node* n = _get(top.second);
      q.pop();
      if (n->n_descendants == 1) {
	nns.push_back(i);
      } else if (n->n_descendants <= _K) {
	for (int x = 0; x < n->n_descendants; x++) {
	  int j = n->children[x];
	  nns.push_back(j);
	}
      } else{
	T margin = Distance::margin(n, v, _f);
	q.push(make_pair(+margin, n->children[1]));
	q.push(make_pair(-margin, n->children[0]));
      }
    }

    sort(nns.begin(), nns.end());
    vector<pair<T, int> > nns_dist;
    int last = -1;
    for (size_t i = 0; i < nns.size(); i++) {
      int j = nns[i];
      if (j == last)
	continue;
      last = j;
      nns_dist.push_back(make_pair(Distance::distance(v, _get(j)->v, _f), j));
    }

    sort(nns_dist.begin(), nns_dist.end());
    for (size_t i = 0; i < nns_dist.size() && result->size() < n; i++) {
      result->push_back(nns_dist[i].second);
    }
  }
};

template<typename T, typename Distance>
class AnnoyIndexPython : public AnnoyIndex<T, Distance > {
public:
  AnnoyIndexPython(int f): AnnoyIndex<T, Distance>(f) {}
  void add_item_py(int item, const python::list& v) {
    vector<T> w;
    for (int z = 0; z < this->_f; z++)
      w.push_back(python::extract<T>(v[z]));

    this->add_item(item, &w[0]);
  }
  python::list get_nns_by_item_py(int item, size_t n) {
    vector<int> result;
    this->get_nns_by_item(item, n, &result);
    python::list l;
    for (size_t i = 0; i < result.size(); i++)
      l.append(result[i]);
    return l;
  }
  python::list get_nns_by_vector_py(python::list v, size_t n) {
    vector<T> w(this->_f);
    for (int z = 0; z < this->_f; z++)
      w[z] = python::extract<T>(v[z]);
    vector<int> result;
    this->get_nns_by_vector(&w[0], n, &result);
    python::list l;
    for (size_t i = 0; i < result.size(); i++)
      l.append(result[i]);
    return l;
  }
  python::list get_item_vector_py(int item) {
    const typename Distance::node* m = this->_get(item);
    const T* v = m->v;
    python::list l;
    for (int z = 0; z < this->_f; z++) {
      l.append(v[z]);
    }
    return l;
  }
};

template<typename C>
void expose_methods(python::class_<C> c) {
  c.def("add_item",          &C::add_item_py)
    .def("build",             &C::build)
    .def("save",              &C::save)
    .def("load",              &C::load)
    .def("unload",            &C::unload)
    .def("get_distance",      &C::get_distance)
    .def("get_nns_by_item",   &C::get_nns_by_item_py)
    .def("get_nns_by_vector", &C::get_nns_by_vector_py)
    .def("get_item_vector",   &C::get_item_vector_py)
    .def("get_n_items",       &C::get_n_items);
}

BOOST_PYTHON_MODULE(annoylib)
{
  expose_methods(python::class_<AnnoyIndexPython<float, Angular<float> > >("AnnoyIndexAngular", python::init<int>()));
  expose_methods(python::class_<AnnoyIndexPython<float, Euclidean<float> > >("AnnoyIndexEuclidean", python::init<int>()));
}