#pragma once

/**************************************************************************
 * Combination of domains:
 *
 * (1) reduced product of two arbitrary domains with only lattice
 *     operations.
 *
 * (2) reduced product of two arbitrary domains with all operations.
 *
 * The reduction in (1) and (2) is simply done by making bottom the
 * abstract state if one of them is bottom.
 *
 * (3) reduced product of two numerical domains. The reduction is done
 *     via a special push operation that must be defined by the
 *     domains. This is often more precise than (2).
 **************************************************************************/

#include <crab/domains/abstract_domain.hpp>
#include <crab/domains/abstract_domain_specialized_traits.hpp>
#include <crab/support/stats.hpp>

#include <boost/optional.hpp>
#include <algorithm>

namespace crab {
namespace domains {

// Reduced product of two arbitrary domains with only lattice
// operations.
template <typename Domain1, typename Domain2> class basic_domain_product2 {

public:
  using basic_domain_product2_t = basic_domain_product2<Domain1, Domain2>;
  using first_type = Domain1;
  using second_type = Domain2;

private:
  bool m_is_bottom;
  Domain1 m_first;
  Domain2 m_second;

  void canonicalize() {
    if (!m_is_bottom) {
      m_is_bottom = m_first.is_bottom() || m_second.is_bottom();
      if (m_is_bottom) {
        m_first.set_to_bottom();
        m_second.set_to_bottom();
      }
    }
  }

public:
  basic_domain_product2() : m_is_bottom(false) {
    m_first.set_to_top();
    m_second.set_to_top();
  }

  basic_domain_product2(Domain1 &&first, Domain2 &&second,
                        bool &&apply_reduction = true)
      : m_is_bottom(false), m_first(std::move(first)),
        m_second(std::move(second)) {
    if (apply_reduction) {
      // we don't apply normalization when widening
      canonicalize();
    }
  }

  basic_domain_product2(const basic_domain_product2_t &other) = default;
  basic_domain_product2(basic_domain_product2_t &&other) = default;
  basic_domain_product2_t &
  operator=(const basic_domain_product2_t &other) = default;
  basic_domain_product2_t &operator=(basic_domain_product2_t &&other) = default;

  basic_domain_product2_t make_top() const {
    Domain1 dom1;
    Domain2 dom2;
    dom1.set_to_top();
    dom2.set_to_top();
    return basic_domain_product2_t(std::move(dom1), std::move(dom2));
  }

  basic_domain_product2_t make_bottom() const {
    Domain1 dom1;
    Domain2 dom2;
    dom1.set_to_bottom();
    dom2.set_to_bottom();
    return basic_domain_product2_t(std::move(dom1), std::move(dom2));
  }

  void set_to_top() {
    m_is_bottom = false;
    m_first.set_to_top();
    m_second.set_to_top();
  }

  void set_to_bottom() {
    m_is_bottom = true;
    m_first.set_to_bottom();
    m_second.set_to_bottom();
  }

  bool is_bottom() const {
    // canonicalize();
    // return m_is_bottom;
    if (m_is_bottom) {
      return true;
    } else {
      return m_first.is_bottom() || m_second.is_bottom();
    }
  }

  bool is_top() const {
    return (m_first.is_top() && m_second.is_top());
  }

  Domain1 &first() {
    canonicalize();
    return m_first;
  }

  Domain2 &second() {
    canonicalize();
    return m_second;
  }

  const Domain1 &first() const { return m_first; }

  const Domain2 &second() const { return m_second; }

  bool operator<=(const basic_domain_product2_t &other) const {
    if (is_bottom()) {
      return true;
    } else if (other.is_bottom()) {
      return false;
    } else {
      return (m_first <= other.m_first) && (m_second <= other.m_second);
    }
  }

  bool operator==(const basic_domain_product2_t &other) const {
    return (operator<=(other) && other.operator<=(*this));
  }

  void operator|=(const basic_domain_product2_t &other) {
    if (is_bottom()) {
      *this = other;
    } else if (other.is_bottom()) {
      return;
    } else {
      m_first |= other.m_first;
      m_second |= other.m_second;
    }
  }

  basic_domain_product2_t
  operator|(const basic_domain_product2_t &other) const {
    if (is_bottom()) {
      return other;
    } else if (other.is_bottom()) {
      return *this;
    } else {
      return basic_domain_product2_t(m_first | other.m_first,
                                     m_second | other.m_second);
    }
  }

  basic_domain_product2_t
  operator||(const basic_domain_product2_t &other) const {
    return basic_domain_product2_t(m_first || other.m_first,
                                   m_second || other.m_second,
                                   false /* do not apply reduction */);
  }

  basic_domain_product2_t
  operator&(const basic_domain_product2_t &other) const {
    if (is_bottom() || other.is_top()) {
      return *this;
    } else if (other.is_bottom() || is_top()) {
      return other;
    } else {
      return basic_domain_product2_t(m_first & other.m_first,
                                     m_second & other.m_second);
    }
  }

  basic_domain_product2_t
  operator&&(const basic_domain_product2_t &other) const {
    if (is_bottom() || other.is_top()) {
      return *this;
    } else if (other.is_bottom() || is_top()) {
      return other;
    } else {
      return basic_domain_product2_t(m_first && other.m_first,
                                     m_second && other.m_second);
    }
  }

  void write(crab::crab_os &o) const {
    if (is_bottom()) {
      o << "_|_";
    } else {
      o << "(" << m_first << ", " << m_second << ")";
    }
  }

  friend crab::crab_os &operator<<(crab::crab_os &o,
                                   basic_domain_product2_t &dom) {
    dom.write(o);
    return o;
  }

  std::string domain_name() const {
    std::string name =
        "Product(" + m_first.domain_name() + "," + m_second.domain_name() + ")";
    return name;
  }

}; // class basic_domain_product2

// Reduced product of two arbitrary domains with all operations.
template <typename Number, typename VariableName, typename Domain1,
          typename Domain2>
class domain_product2 final
    : public abstract_domain_api<
          domain_product2<Number, VariableName, Domain1, Domain2>> {
public:
  using domain_product2_t =
      domain_product2<Number, VariableName, Domain1, Domain2>;
  using abstract_domain_t = abstract_domain_api<domain_product2_t>;
  using first_type = Domain1;
  using second_type = Domain2;

  using typename abstract_domain_t::disjunctive_linear_constraint_system_t;
  using typename abstract_domain_t::interval_t;
  using typename abstract_domain_t::linear_constraint_system_t;
  using typename abstract_domain_t::linear_constraint_t;
  using typename abstract_domain_t::linear_expression_t;
  using typename abstract_domain_t::reference_constraint_t;
  using typename abstract_domain_t::variable_or_constant_t;
  using typename abstract_domain_t::variable_t;
  using typename abstract_domain_t::variable_vector_t;
  using typename abstract_domain_t::variable_or_constant_vector_t;
  using number_t = Number;
  using varname_t = VariableName;

private:
  using basic_domain_product2_t = basic_domain_product2<Domain1, Domain2>;

  basic_domain_product2_t m_product;

  domain_product2(basic_domain_product2_t &&product)
      : m_product(std::move(product)) {}

  // Reduce operation
  void reduce() {
    if (m_product.first().is_bottom() ||
        m_product.second().is_bottom()) {
      m_product.set_to_bottom();
    }
  }

public:
  domain_product2_t make_top() const override {
    basic_domain_product2_t dom_prod;
    return domain_product2_t(dom_prod.make_top());
  }

  domain_product2_t make_bottom() const override {
    basic_domain_product2_t dom_prod;
    return domain_product2_t(dom_prod.make_bottom());
  }

  void set_to_top() override {
    basic_domain_product2_t dom_prod;
    domain_product2_t dom(dom_prod.make_top());
    std::swap(*this, dom);
  }

  void set_to_bottom() override {
    basic_domain_product2_t dom_prod;
    domain_product2_t dom(dom_prod.make_bottom());
    std::swap(*this, dom);
  }

  domain_product2() : m_product() {}

  domain_product2(const domain_product2_t &other) : m_product(other.m_product) {}

  domain_product2(const domain_product2_t &&other)
      : m_product(std::move(other.m_product)) {}

  domain_product2_t &operator=(const domain_product2_t &other) {
    if (this != &other)
      m_product = other.m_product;
    return *this;
  }

  domain_product2_t &operator=(const domain_product2_t &&other) {
    if (this != &other)
      m_product = std::move(other.m_product);
    return *this;
  }

  bool is_bottom() const override { return m_product.is_bottom(); }

  bool is_top() const override { return m_product.is_top(); }

  Domain1 &first() { return m_product.first(); }
  const Domain1 &first() const { return m_product.first(); }

  Domain2 &second() { return m_product.second(); }
  const Domain2 &second() const { return m_product.second(); }

  bool operator<=(const domain_product2_t &other) const override {
    return (m_product <= other.m_product);
  }

  bool operator==(const domain_product2_t &other) const {
    return (m_product == other.m_product);
  }

  void operator|=(const domain_product2_t &other) override {
    m_product |= other.m_product;
  }

  domain_product2_t operator|(const domain_product2_t &other) const override {
    return domain_product2_t(m_product | other.m_product);
  }

  domain_product2_t operator&(const domain_product2_t &other) const override {
    return domain_product2_t(m_product & other.m_product);
  }

  domain_product2_t operator||(const domain_product2_t &other) const override {
    return domain_product2_t(m_product || other.m_product);
  }

  domain_product2_t widening_thresholds(
      const domain_product2_t &other,
      const iterators::thresholds<number_t> &ts) const override {
    bool apply_reduction = false;
    return domain_product2_t(basic_domain_product2_t(
        std::move(m_product.first().widening_thresholds(
            other.m_product.first(), ts)),
        std::move(m_product.second().widening_thresholds(
            other.m_product.second(), ts)),
        std::move(apply_reduction)));
  }

  domain_product2_t operator&&(const domain_product2_t &other) const override {
    return domain_product2_t(m_product && other.m_product);
  }

  void assign(const variable_t &x, const linear_expression_t &e) override {
    m_product.first().assign(x, e);
    m_product.second().assign(x, e);
  }

  void apply(arith_operation_t op, const variable_t &x, const variable_t &y,
             const variable_t &z) override {
    m_product.first().apply(op, x, y, z);
    m_product.second().apply(op, x, y, z);
    reduce();
  }

  void apply(arith_operation_t op, const variable_t &x, const variable_t &y,
             Number k) override {
    m_product.first().apply(op, x, y, k);
    m_product.second().apply(op, x, y, k);
    reduce();
  }

  void select(const variable_t &lhs, const linear_constraint_t &cond,
	      const linear_expression_t &e1,  const linear_expression_t &e2) override {
    m_product.first().select(lhs, cond, e1, e2);
    m_product.second().select(lhs, cond, e1, e2);
    reduce();
  }

  void backward_assign(const variable_t &x, const linear_expression_t &e,
                       const domain_product2_t &invariant) override {
    m_product.first().backward_assign(x, e, invariant.first());
    m_product.second().backward_assign(x, e, invariant.second());
    reduce();
  }

  void backward_apply(arith_operation_t op, const variable_t &x,
                      const variable_t &y, Number k,
                      const domain_product2_t &invariant) override {
    m_product.first().backward_apply(op, x, y, k, invariant.first());
    m_product.second().backward_apply(op, x, y, k, invariant.second());
    reduce();
  }

  void backward_apply(arith_operation_t op, const variable_t &x,
                      const variable_t &y, const variable_t &z,
                      const domain_product2_t &invariant) override {
    m_product.first().backward_apply(op, x, y, z, invariant.first());
    m_product.second().backward_apply(op, x, y, z, invariant.second());
    reduce();
  }

  void operator+=(const linear_constraint_system_t &csts) override {
    m_product.first() += csts;
    m_product.second() += csts;
    reduce();
  }

  void operator-=(const variable_t &v) override {
    m_product.first() -= v;
    m_product.second() -= v;
  }

  // cast operators

  void apply(int_conv_operation_t op, const variable_t &dst,
             const variable_t &src) override {
    m_product.first().apply(op, dst, src);
    m_product.second().apply(op, dst, src);
    reduce();
  }

  // bitwise operators

  void apply(bitwise_operation_t op, const variable_t &x, const variable_t &y,
             const variable_t &z) override {
    m_product.first().apply(op, x, y, z);
    m_product.second().apply(op, x, y, z);
    reduce();
  }

  void apply(bitwise_operation_t op, const variable_t &x, const variable_t &y,
             Number k) override {
    m_product.first().apply(op, x, y, k);
    m_product.second().apply(op, x, y, k);
    reduce();
  }

  // array operators

  virtual void array_init(const variable_t &a,
                          const linear_expression_t &elem_size,
                          const linear_expression_t &lb_idx,
                          const linear_expression_t &ub_idx,
                          const linear_expression_t &val) override {
    m_product.first().array_init(a, elem_size, lb_idx, ub_idx, val);
    m_product.second().array_init(a, elem_size, lb_idx, ub_idx, val);
    reduce();
  }

  virtual void array_load(const variable_t &lhs, const variable_t &a,
                          const linear_expression_t &elem_size,
                          const linear_expression_t &i) override {

    m_product.first().array_load(lhs, a, elem_size, i);
    m_product.second().array_load(lhs, a, elem_size, i);
    reduce();
  }

  virtual void array_store(const variable_t &a,
                           const linear_expression_t &elem_size,
                           const linear_expression_t &i,
                           const linear_expression_t &val,
                           bool is_strong_update) override {
    m_product.first().array_store(a, elem_size, i, val, is_strong_update);
    m_product.second().array_store(a, elem_size, i, val, is_strong_update);
    reduce();
  }

  virtual void array_store_range(const variable_t &a,
                                 const linear_expression_t &elem_size,
                                 const linear_expression_t &i,
                                 const linear_expression_t &j,
                                 const linear_expression_t &val) override {
    m_product.first().array_store_range(a, elem_size, i, j, val);
    m_product.second().array_store_range(a, elem_size, i, j, val);
    reduce();
  }

  virtual void array_assign(const variable_t &lhs,
                            const variable_t &rhs) override {
    m_product.first().array_assign(lhs, rhs);
    m_product.second().array_assign(lhs, rhs);
    reduce();
  }

  // backward array operations

  virtual void
  backward_array_init(const variable_t &a, const linear_expression_t &elem_size,
                      const linear_expression_t &lb_idx,
                      const linear_expression_t &ub_idx,
                      const linear_expression_t &val,
                      const domain_product2_t &invariant) override {
    m_product.first().backward_array_init(a, elem_size, lb_idx, ub_idx,
                                               val, invariant.first());
    m_product.second().backward_array_init(a, elem_size, lb_idx, ub_idx,
                                                val, invariant.second());
    reduce();
  }

  virtual void
  backward_array_load(const variable_t &lhs, const variable_t &a,
                      const linear_expression_t &elem_size,
                      const linear_expression_t &i,
                      const domain_product2_t &invariant) override {

    m_product.first().backward_array_load(lhs, a, elem_size, i,
                                               invariant.first());
    m_product.second().backward_array_load(lhs, a, elem_size, i,
                                                invariant.second());
    reduce();
  }

  virtual void backward_array_store(
      const variable_t &a, const linear_expression_t &elem_size,
      const linear_expression_t &i, const linear_expression_t &val,
      bool is_strong_update, const domain_product2_t &invariant) override {
    m_product.first().backward_array_store(
        a, elem_size, i, val, is_strong_update, invariant.first());
    m_product.second().backward_array_store(
        a, elem_size, i, val, is_strong_update, invariant.second());
    reduce();
  }

  virtual void backward_array_store_range(
      const variable_t &a, const linear_expression_t &elem_size,
      const linear_expression_t &i, const linear_expression_t &j,
      const linear_expression_t &val,
      const domain_product2_t &invariant) override {
    m_product.first().backward_array_store_range(a, elem_size, i, j, val,
                                                      invariant.first());
    m_product.second().backward_array_store_range(a, elem_size, i, j, val,
                                                       invariant.second());
    reduce();
  }

  virtual void
  backward_array_assign(const variable_t &lhs, const variable_t &rhs,
                        const domain_product2_t &invariant) override {
    m_product.first().backward_array_assign(lhs, rhs, invariant.first());
    m_product.second().backward_array_assign(lhs, rhs, invariant.second());
    reduce();
  }

  // region/reference operators
  virtual void region_init(const variable_t &reg) override {
    m_product.first().region_init(reg);
    m_product.second().region_init(reg);
    reduce();
  }

  virtual void region_copy(const variable_t &lhs_reg,
                           const variable_t &rhs_reg) override {
    m_product.first().region_copy(lhs_reg, rhs_reg);
    m_product.second().region_copy(lhs_reg, rhs_reg);
    reduce();
  }

  virtual void region_cast(const variable_t &src_reg,
                           const variable_t &dst_reg) override {
    m_product.first().region_cast(src_reg, dst_reg);
    m_product.second().region_cast(src_reg, dst_reg);
    reduce();
  }

  virtual void ref_make(const variable_t &ref, const variable_t &reg,
			const variable_or_constant_t &size,
			const allocation_site &as) override {
    m_product.first().ref_make(ref, reg, size, as);
    m_product.second().ref_make(ref, reg, size, as);
    reduce();
  }

  virtual void ref_free(const variable_t &reg, const variable_t &ref) override {
    m_product.first().ref_free(reg, ref);
    m_product.second().ref_free(reg, ref);
    reduce();
  }

  virtual void ref_load(const variable_t &ref, const variable_t &reg,
                        const variable_t &res) override {
    m_product.first().ref_load(ref, reg, res);
    m_product.second().ref_load(ref, reg, res);
    reduce();
  }

  virtual void ref_store(const variable_t &ref, const variable_t &reg,
                         const variable_or_constant_t &val) override {
    m_product.first().ref_store(ref, reg, val);
    m_product.second().ref_store(ref, reg, val);
    reduce();
  }

  virtual void ref_gep(const variable_t &ref1, const variable_t &reg1,
                       const variable_t &ref2, const variable_t &reg2,
                       const linear_expression_t &offset) override {
    m_product.first().ref_gep(ref1, reg1, ref2, reg2, offset);
    m_product.second().ref_gep(ref1, reg1, ref2, reg2, offset);
    reduce();
  }

  virtual void
  ref_load_from_array(const variable_t &lhs, const variable_t &ref,
                      const variable_t &region,
                      const linear_expression_t &index,
                      const linear_expression_t &elem_size) override {
    m_product.first().ref_load_from_array(lhs, ref, region, index,
                                               elem_size);
    m_product.second().ref_load_from_array(lhs, ref, region, index,
                                                elem_size);
    reduce();
  }

  virtual void ref_store_to_array(const variable_t &ref,
                                  const variable_t &region,
                                  const linear_expression_t &index,
                                  const linear_expression_t &elem_size,
                                  const linear_expression_t &val) override {
    m_product.first().ref_store_to_array(ref, region, index, elem_size,
                                              val);
    m_product.second().ref_store_to_array(ref, region, index, elem_size,
                                               val);
    reduce();
  }

  virtual void ref_assume(const reference_constraint_t &cst) override {
    m_product.first().ref_assume(cst);
    m_product.second().ref_assume(cst);
    reduce();
  }

  void ref_to_int(const variable_t &reg, const variable_t &ref_var,
                  const variable_t &int_var) override {
    m_product.first().ref_to_int(reg, ref_var, int_var);
    m_product.second().ref_to_int(reg, ref_var, int_var);
    reduce();
  }

  void int_to_ref(const variable_t &int_var, const variable_t &reg,
                  const variable_t &ref_var) override {
    m_product.first().int_to_ref(int_var, reg, ref_var);
    m_product.second().int_to_ref(int_var, reg, ref_var);
    reduce();
  }
  void select_ref(const variable_t &lhs_ref, const variable_t &lhs_rgn,
		  const variable_t &cond,
		  const variable_or_constant_t &ref1,
		  const boost::optional<variable_t> &rgn1,
		  const variable_or_constant_t &ref2,
		  const boost::optional<variable_t> &rgn2) override {
    m_product.first().select_ref(lhs_ref, lhs_rgn, cond, ref1, rgn1, ref2, rgn2);
    m_product.second().select_ref(lhs_ref, lhs_rgn, cond, ref1, rgn1, ref2, rgn2);
    reduce();
  }

  boolean_value is_null_ref(const variable_t &ref) override {
    return m_product.first().is_null_ref(ref) & m_product.second().is_null_ref(ref);
  }

  bool get_allocation_sites(const variable_t &ref,
			    std::vector<allocation_site> &out) override {
    std::vector<allocation_site> s1, s2;
    bool b1 = m_product.first().get_allocation_sites(ref, s1);
    bool b2 = m_product.first().get_allocation_sites(ref, s2);
    if (b1 && b2) {
      std::sort(s1.begin(), s1.end());
      std::sort(s2.begin(), s2.end());
      std::set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
			    std::back_inserter(out));
      return true;
    } else if (b1) {
      out.assign(s1.begin(), s1.end());
      return true;
    } else if (b2) {
      out.assign(s2.begin(), s2.end());
      return true;
    }
    return false;
  }

  bool get_tags(const variable_t &rgn, const variable_t &ref,
		std::vector<uint64_t> &out) override {
    std::vector<uint64_t> s1, s2;
    bool b1 = m_product.first().get_tags(rgn, ref, s1);
    bool b2 = m_product.first().get_tags(rgn, ref, s2);
    if (b1 && b2) {
      std::sort(s1.begin(), s1.end());
      std::sort(s2.begin(), s2.end());
      std::set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
			    std::back_inserter(out));
      return true;
    } else if (b1) {
      out.assign(s1.begin(), s1.end());
      return true;
    } else if (b2) {
      out.assign(s2.begin(), s2.end());
      return true;
    }
    return false;
  }

  // boolean operators
  virtual void assign_bool_cst(const variable_t &lhs,
                               const linear_constraint_t &rhs) override {
    m_product.first().assign_bool_cst(lhs, rhs);
    m_product.second().assign_bool_cst(lhs, rhs);
    reduce();
  }

  virtual void assign_bool_ref_cst(const variable_t &lhs,
                                   const reference_constraint_t &rhs) override {
    m_product.first().assign_bool_ref_cst(lhs, rhs);
    m_product.second().assign_bool_ref_cst(lhs, rhs);
    reduce();
  }

  virtual void assign_bool_var(const variable_t &lhs, const variable_t &rhs,
                               bool is_not_rhs) override {
    m_product.first().assign_bool_var(lhs, rhs, is_not_rhs);
    m_product.second().assign_bool_var(lhs, rhs, is_not_rhs);
    reduce();
  }

  virtual void apply_binary_bool(bool_operation_t op, const variable_t &x,
                                 const variable_t &y,
                                 const variable_t &z) override {
    m_product.first().apply_binary_bool(op, x, y, z);
    m_product.second().apply_binary_bool(op, x, y, z);
    reduce();
  }

  virtual void assume_bool(const variable_t &v, bool is_negated) override {
    m_product.first().assume_bool(v, is_negated);
    m_product.second().assume_bool(v, is_negated);
    reduce();
  }

  virtual void select_bool(const variable_t &lhs, const variable_t &cond,
			   const variable_t &b1, const variable_t &b2) override {
    m_product.first().select_bool(lhs, cond, b1, b2);
    m_product.second().select_bool(lhs, cond, b1, b2);
    reduce();
  }

  // backward boolean operators
  virtual void backward_assign_bool_cst(const variable_t &lhs,
                                        const linear_constraint_t &rhs,
                                        const domain_product2_t &inv) override {
    m_product.first().backward_assign_bool_cst(lhs, rhs, inv.first());
    m_product.second().backward_assign_bool_cst(lhs, rhs, inv.second());
    reduce();
  }

  virtual void
  backward_assign_bool_ref_cst(const variable_t &lhs,
                               const reference_constraint_t &rhs,
                               const domain_product2_t &inv) override {
    m_product.first().backward_assign_bool_ref_cst(lhs, rhs, inv.first());
    m_product.second().backward_assign_bool_ref_cst(lhs, rhs,
                                                         inv.second());
    reduce();
  }

  virtual void backward_assign_bool_var(const variable_t &lhs,
                                        const variable_t &rhs, bool is_not_rhs,
                                        const domain_product2_t &inv) override {
    m_product.first().backward_assign_bool_var(lhs, rhs, is_not_rhs,
                                                    inv.first());
    m_product.second().backward_assign_bool_var(lhs, rhs, is_not_rhs,
                                                     inv.second());
    reduce();
  }

  virtual void
  backward_apply_binary_bool(bool_operation_t op, const variable_t &x,
                             const variable_t &y, const variable_t &z,
                             const domain_product2_t &inv) override {
    m_product.first().backward_apply_binary_bool(op, x, y, z, inv.first());
    m_product.second().backward_apply_binary_bool(op, x, y, z,
                                                       inv.second());
    reduce();
  }

  virtual void forget(const variable_vector_t &variables) override {
    m_product.first().forget(variables);
    m_product.second().forget(variables);
  }

  virtual void project(const variable_vector_t &variables) override {
    m_product.first().project(variables);
    m_product.second().project(variables);
  }

  virtual void expand(const variable_t &var,
                      const variable_t &new_var) override {
    m_product.first().expand(var, new_var);
    m_product.second().expand(var, new_var);
  }

  virtual void normalize() override {
    m_product.first().normalize();
    m_product.second().normalize();
  }

  virtual void minimize() override {
    m_product.first().minimize();
    m_product.second().minimize();
  }

  virtual interval_t operator[](const variable_t &v) override {
    return m_product.first()[v] & m_product.second()[v];
  }

  virtual linear_constraint_system_t
  to_linear_constraint_system() const override {
    linear_constraint_system_t csts;
    // XXX: We might add redundant constraints.
    csts += m_product.first().to_linear_constraint_system();
    csts += m_product.second().to_linear_constraint_system();
    return csts;
  }

  virtual disjunctive_linear_constraint_system_t
  to_disjunctive_linear_constraint_system() const override {
    disjunctive_linear_constraint_system_t csts;
    // XXX: We might add redundant constraints.
    csts += m_product.first().to_disjunctive_linear_constraint_system();
    csts += m_product.second().to_disjunctive_linear_constraint_system();
    return csts;
  }

  virtual void rename(const variable_vector_t &from,
                      const variable_vector_t &to) override {
    m_product.first().rename(from, to);
    m_product.second().rename(from, to);
  }

  /* begin intrinsics operations */
  void intrinsic(std::string name,
		 const variable_or_constant_vector_t &inputs,
                 const variable_vector_t &outputs) override {
    m_product.first().intrinsic(name, inputs, outputs);
    m_product.second().intrinsic(name, inputs, outputs);
  }

  void backward_intrinsic(std::string name,
			  const variable_or_constant_vector_t &inputs,
                          const variable_vector_t &outputs,
                          const domain_product2_t &invariant) override {
    m_product.first().backward_intrinsic(name, inputs, outputs,
					 invariant.first());
    m_product.second().backward_intrinsic(name, inputs, outputs,
					  invariant.second());
  }
  /* end intrinsics operations */

  void write(crab::crab_os &o) const override { m_product.write(o); }

  std::string domain_name() const override {
    return m_product.domain_name();
  }

}; // class domain_product2

namespace reduced_product_impl {
class default_params {
public:
  enum { left_propagate_equalities = 1 };
  enum { right_propagate_equalities = 1 };
  enum { left_propagate_inequalities = 1 };
  enum { right_propagate_inequalities = 1 };
  enum { left_propagate_intervals = 1 };
  enum { right_propagate_intervals = 1 };
  enum { disable_reduction = 0 };
  enum { apply_reduction_only_add_constraint = 0 };
};

class term_dbm_params {
public:
  enum { left_propagate_equalities = 1 };
  enum { right_propagate_equalities = 1 };
  enum { left_propagate_inequalities = 0 };
  enum { right_propagate_inequalities = 0 };
  enum { left_propagate_intervals = 0 };
  enum { right_propagate_intervals = 0 };
  enum { disable_reduction = 0 };
  enum { apply_reduction_only_add_constraint = 1 };
};
} // namespace reduced_product_impl

// This domain is similar to domain_product2 but it combines two
// numerical domains and it defines a more precise, customizable
// reduction operation.
template <typename Domain1, typename Domain2,
          class Params = reduced_product_impl::default_params>
class reduced_numerical_domain_product2 final
    : public abstract_domain_api<
          reduced_numerical_domain_product2<Domain1, Domain2, Params>> {

public:
  using reduced_numerical_domain_product2_t =
      reduced_numerical_domain_product2<Domain1, Domain2, Params>;
  using abstract_domain_t =
      abstract_domain_api<reduced_numerical_domain_product2_t>;
  using typename abstract_domain_t::disjunctive_linear_constraint_system_t;
  using typename abstract_domain_t::interval_t;
  using typename abstract_domain_t::linear_constraint_system_t;
  using typename abstract_domain_t::linear_constraint_t;
  using typename abstract_domain_t::linear_expression_t;
  using typename abstract_domain_t::reference_constraint_t;
  using typename abstract_domain_t::variable_or_constant_t;
  using typename abstract_domain_t::variable_t;
  using typename abstract_domain_t::variable_vector_t;
  using typename abstract_domain_t::variable_or_constant_vector_t;
  using number_t = typename Domain1::number_t;
  using varname_t = typename Domain1::varname_t;

  static_assert(std::is_same<number_t, typename Domain2::number_t>::value,
                "Domain1 and Domain2 must have same type for number_t");
  static_assert(std::is_same<varname_t, typename Domain2::varname_t>::value,
                "Domain1 and Domain2 must have same type for varname_t");

private:
  using domain_product2_t =
      domain_product2<number_t, varname_t, Domain1, Domain2>;

  domain_product2_t m_product;

  reduced_numerical_domain_product2(const domain_product2_t &product)
      : m_product(product) {}

  linear_constraint_system_t to_linear_constraints(const variable_t &v,
                                                   interval_t i) const {
    linear_constraint_system_t csts;
    if (i.lb().is_finite() && i.ub().is_finite()) {
      auto lb = *(i.lb().number());
      auto ub = *(i.ub().number());
      if (lb == ub) {
        csts += (v == lb);
      } else {
        csts += (v >= lb);
        csts += (v <= ub);
      }
    } else if (i.lb().is_finite()) {
      auto lb = *(i.lb().number());
      csts += (v >= lb);
    } else if (i.ub().is_finite()) {
      auto ub = *(i.ub().number());
      csts += (v <= ub);
    }
    return csts;
  }

  void reduce_variable(const variable_t &v) {
    crab::CrabStats::count(domain_name() + ".count.reduce");
    crab::ScopedCrabStats __st__(domain_name() + ".reduce");

    if (!is_bottom() && !Params::disable_reduction) {

      // We just propagate from one domain to another.  We could
      // propagate in the other direction ... and repeat it
      // computing a fixpoint+narrowing of descending iterations.

      Domain1 &inv1 = m_product.first();
      Domain2 &inv2 = m_product.second();

      //////
      // propagate interval constraints between domains
      //////
      if (Params::left_propagate_intervals) {
        interval_t i1 = inv1[v];
        if (!i1.is_top())
          inv2 += to_linear_constraints(v, i1);
      }
      if (Params::right_propagate_intervals) {
        interval_t i2 = inv2[v];
        if (!i2.is_top())
          inv1 += to_linear_constraints(v, i2);
      }

      //////
      // propagate other constraints expressed by the domains
      //////
      if ((Params::left_propagate_equalities ||
           Params::left_propagate_inequalities) &&
          (Params::right_propagate_equalities ||
           Params::right_propagate_inequalities)) {
        linear_constraint_system_t csts1, csts2, filtered_csts1, filtered_csts2;
        bool propagate_only_equalities;

        propagate_only_equalities = !Params::left_propagate_inequalities;
        crab::domains::reduced_domain_traits<Domain1>::extract(
            inv1, v, csts1, propagate_only_equalities);

        propagate_only_equalities = !Params::right_propagate_inequalities;
        crab::domains::reduced_domain_traits<Domain2>::extract(
            inv2, v, csts2, propagate_only_equalities);

        // filter out those redundant constraints (i.e.,
        // constraints that the other domain already knows about)

        for (auto &c1 : csts1) {
          if (std::find_if(csts2.begin(), csts2.end(),
                           [c1](const linear_constraint_t &c2) {
                             return c2.equal(c1);
                           }) == csts2.end()) {
            filtered_csts1 += c1;
          }
        }

        {
          std::string k(domain_name() + ".count.reduce.equalities_from_" +
                        m_product.first().domain_name());
          crab::CrabStats::uset(k, crab::CrabStats::get(k) +
                                       filtered_csts1.size());
        }
        inv2 += filtered_csts1;

        for (auto &c2 : csts2) {
          if (std::find_if(csts1.begin(), csts1.end(),
                           [c2](const linear_constraint_t &c1) {
                             return c1.equal(c2);
                           }) == csts1.end()) {
            filtered_csts2 += c2;
          }
        }
        {
          std::string k(domain_name() + ".count.reduce.equalities_from_" +
                        m_product.second().domain_name());
          crab::CrabStats::uset(k, crab::CrabStats::get(k) + csts2.size());
        }
        inv1 += filtered_csts2;
      } else if (Params::left_propagate_equalities ||
                 Params::left_propagate_inequalities) {
        linear_constraint_system_t csts1;
        const bool propagate_only_equalities =
            !Params::left_propagate_inequalities;
        crab::domains::reduced_domain_traits<Domain1>::extract(
            inv1, v, csts1, propagate_only_equalities);
        std::string k(domain_name() + ".count.reduce.equalities_from_" +
                      m_product.first().domain_name());
        crab::CrabStats::uset(k, crab::CrabStats::get(k) + csts1.size());
        inv2 += csts1;
      } else if (Params::right_propagate_equalities ||
                 Params::right_propagate_inequalities) {
        linear_constraint_system_t csts2;
        const bool propagate_only_equalities =
            !Params::right_propagate_inequalities;
        crab::domains::reduced_domain_traits<Domain2>::extract(
            inv2, v, csts2, propagate_only_equalities);
        std::string k(domain_name() + ".count.reduce.equalities_from_" +
                      m_product.second().domain_name());
        crab::CrabStats::uset(k, crab::CrabStats::get(k) + csts2.size());
        inv1 += csts2;
      }
    }
  }

public:
  reduced_numerical_domain_product2_t make_top() const override {
    domain_product2_t dom_prod;
    return reduced_numerical_domain_product2_t(dom_prod.make_top());
  }

  reduced_numerical_domain_product2_t make_bottom() const override {
    domain_product2_t dom_prod;
    return reduced_numerical_domain_product2_t(dom_prod.make_bottom());
  }

  void set_to_top() override {
    domain_product2_t dom_prod;
    reduced_numerical_domain_product2_t abs(dom_prod.make_top());
    std::swap(*this, abs);
  }

  void set_to_bottom() override {
    domain_product2_t dom_prod;
    reduced_numerical_domain_product2_t abs(dom_prod.make_bottom());
    std::swap(*this, abs);
  }

  reduced_numerical_domain_product2() : m_product() {}

  reduced_numerical_domain_product2(
      const reduced_numerical_domain_product2_t &other)
    : m_product(other.m_product) {}

  reduced_numerical_domain_product2(
      reduced_numerical_domain_product2_t &&other)
    : m_product(std::move(other.m_product)) {}

  reduced_numerical_domain_product2_t &
  operator=(const reduced_numerical_domain_product2_t &other) {
    if (this != &other) {
      m_product = other.m_product;
    }
    return *this;
  }

  reduced_numerical_domain_product2_t &
  operator=(reduced_numerical_domain_product2_t &&other) {
    if (this != &other) {
      m_product = std::move(other.m_product);
    }
    return *this;
  }

  bool is_bottom() const override { return m_product.is_bottom(); }

  bool is_top() const override { return m_product.is_top(); }

  bool
  operator<=(const reduced_numerical_domain_product2_t &other) const override {
    return m_product <= other.m_product;
  }

  void operator|=(const reduced_numerical_domain_product2_t &other) override {
    CRAB_LOG("combined-domain", crab::outs()
                                    << "============ JOIN ==================";
             crab::outs() << *this << "\n----------------";
             crab::outs() << other << "\n----------------";);
    m_product |= other.m_product;
    CRAB_LOG("reduced-dom", crab::outs() << *this << "\n----------------\n";);
  }

  reduced_numerical_domain_product2_t
  operator|(const reduced_numerical_domain_product2_t &other) const override {
    reduced_numerical_domain_product2_t res(m_product | other.m_product);
    CRAB_LOG("combined-domain", crab::outs()
                                    << "============ JOIN ==================";
             crab::outs() << *this << "\n----------------";
             crab::outs() << other << "\n----------------";
             crab::outs() << res << "\n================\n");
    return res;
  }

  reduced_numerical_domain_product2_t
  operator&(const reduced_numerical_domain_product2_t &other) const override {
    reduced_numerical_domain_product2_t res(m_product & other.m_product);
    CRAB_LOG("combined-domain", crab::outs()
                                    << "============ MEET ==================";
             crab::outs() << *this << "\n----------------";
             crab::outs() << other << "\n----------------\n";);
    return res;
  }

  reduced_numerical_domain_product2_t
  operator||(const reduced_numerical_domain_product2_t &other) const override {
    reduced_numerical_domain_product2_t res(m_product || other.m_product);
    CRAB_LOG("combined-domain",
             crab::outs() << "============ WIDENING ==================";
             crab::outs() << *this << "\n----------------";
             crab::outs() << other << "\n----------------\n";);
    return res;
  }

  reduced_numerical_domain_product2_t widening_thresholds(
      const reduced_numerical_domain_product2_t &other,
      const iterators::thresholds<number_t> &ts) const override {
    reduced_numerical_domain_product2_t res(
        m_product.widening_thresholds(other.m_product, ts));
    CRAB_LOG("combined-domain",
             crab::outs() << "============ WIDENING ==================";
             crab::outs() << *this << "\n----------------";
             crab::outs() << other << "\n----------------\n";);
    return res;
  }

  reduced_numerical_domain_product2_t
  operator&&(const reduced_numerical_domain_product2_t &other) const override {
    reduced_numerical_domain_product2_t res(m_product && other.m_product);
    CRAB_LOG("combined-domain",
             crab::outs() << "============ NARROWING ==================";
             crab::outs() << *this << "\n----------------";
             crab::outs() << other << "\n----------------\n";);
    return res;
  }

  void set(const variable_t &v, interval_t x) {
    m_product.first().set(v, x);
    m_product.second().set(v, x);
  }

  interval_t operator[](const variable_t &v) override {
    return m_product.first()[v] & m_product.second()[v];
  }

  void operator+=(const linear_constraint_system_t &csts) override {
    m_product += csts;
    if (!is_bottom()) {
      for (auto const &cst : csts) {
	for (auto const &v : cst.variables()) {
	  reduce_variable(v);
	  if (is_bottom()) {
	    return;
	  }
	}
      }
    }
    CRAB_LOG("combined-domain", crab::outs() << "Added constraints " << csts
                                             << "=" << *this << "\n");
  }

  void operator-=(const variable_t &v) override { m_product -= v; }

  void assign(const variable_t &x, const linear_expression_t &e) override {
    m_product.assign(x, e);
    if (!Params::apply_reduction_only_add_constraint) {
      reduce_variable(x);
    }
    CRAB_LOG("combined-domain", crab::outs()
                                    << x << ":=" << e << "=" << *this << "\n");
  }

  void apply(arith_operation_t op, const variable_t &x, const variable_t &y,
             const variable_t &z) override {
    m_product.apply(op, x, y, z);
    if (!Params::apply_reduction_only_add_constraint) {
      reduce_variable(x);
    }
    CRAB_LOG("combined-domain",
             crab::outs() << x << ":=" << y << op << z << "=" << *this << "\n");
  }

  void apply(arith_operation_t op, const variable_t &x, const variable_t &y,
             number_t k) override {
    m_product.apply(op, x, y, k);
    if (!Params::apply_reduction_only_add_constraint) {
      reduce_variable(x);
    }
    CRAB_LOG("combined-domain",
             crab::outs() << x << ":=" << y << op << k << "=" << *this << "\n");
  }

  void backward_assign(
      const variable_t &x, const linear_expression_t &e,
      const reduced_numerical_domain_product2_t &invariant) override {
    m_product.backward_assign(x, e, invariant.m_product);
    if (!Params::apply_reduction_only_add_constraint) {
      // reduce the variables in the right-hand side
      for (auto const &v : e.variables())
        reduce_variable(v);
    }
  }

  void backward_apply(
      arith_operation_t op, const variable_t &x, const variable_t &y,
      number_t k,
      const reduced_numerical_domain_product2_t &invariant) override {
    m_product.backward_apply(op, x, y, k, invariant.m_product);
    if (!Params::apply_reduction_only_add_constraint) {
      // reduce the variables in the right-hand side
      reduce_variable(y);
    }
  }

  void backward_apply(
      arith_operation_t op, const variable_t &x, const variable_t &y,
      const variable_t &z,
      const reduced_numerical_domain_product2_t &invariant) override {
    m_product.backward_apply(op, x, y, z, invariant.m_product);
    if (!Params::apply_reduction_only_add_constraint) {
      // reduce the variables in the right-hand side
      reduce_variable(y);
      reduce_variable(z);
    }
  }

  // cast operators

  void apply(int_conv_operation_t op, const variable_t &dst,
             const variable_t &src) override {
    m_product.apply(op, dst, src);
    if (!Params::apply_reduction_only_add_constraint) {
      reduce_variable(dst);
    }
  }

  // bitwise operators

  void apply(bitwise_operation_t op, const variable_t &x, const variable_t &y,
             const variable_t &z) override {
    m_product.apply(op, x, y, z);
    if (!Params::apply_reduction_only_add_constraint) {
      reduce_variable(x);
    }
  }

  void apply(bitwise_operation_t op, const variable_t &x, const variable_t &y,
             number_t k) override {
    m_product.apply(op, x, y, k);
    if (!Params::apply_reduction_only_add_constraint) {
      reduce_variable(x);
    }
  }

  void select(const variable_t &lhs, const linear_constraint_t &cond,
	      const linear_expression_t &e1,  const linear_expression_t &e2) override {
    m_product.select(lhs, cond, e1, e2);
    if (!Params::apply_reduction_only_add_constraint) {
      reduce_variable(lhs);
    }
  }

  /// reduced_numerical_domain_product2 implements only standard
  /// abstract operations of a numerical domain so it is intended to be
  /// used as a leaf domain in the hierarchy of domains.
  BOOL_OPERATIONS_NOT_IMPLEMENTED(reduced_numerical_domain_product2_t)
  ARRAY_OPERATIONS_NOT_IMPLEMENTED(reduced_numerical_domain_product2_t)
  REGION_AND_REFERENCE_OPERATIONS_NOT_IMPLEMENTED(
      reduced_numerical_domain_product2_t)

  void rename(const variable_vector_t &from,
              const variable_vector_t &to) override {
    m_product.rename(from, to);
  }

  /* begin intrinsics operations */
  void intrinsic(std::string name,
		 const variable_or_constant_vector_t &inputs,
                 const variable_vector_t &outputs) override {
    m_product.intrinsic(name, inputs, outputs);
  }

  void backward_intrinsic(
      std::string name,
      const variable_or_constant_vector_t &inputs,
      const variable_vector_t &outputs,
      const reduced_numerical_domain_product2_t &invariant) override {
    m_product.backward_intrinsic(name, inputs, outputs,
				 invariant.m_product);
  }
  /* end intrinsics operations */

  void forget(const variable_vector_t &variables) override {
    m_product.forget(variables);
  }

  void project(const variable_vector_t &variables) override {
    m_product.project(variables);
  }

  void expand(const variable_t &var, const variable_t &new_var) override {
    m_product.expand(var, new_var);
  }

  void normalize() override { m_product.normalize(); }

  void minimize() override { m_product.minimize(); }

  void write(crab_os &o) const override { m_product.write(o); }

  linear_constraint_system_t to_linear_constraint_system() const override {
    return m_product.to_linear_constraint_system();
  }

  disjunctive_linear_constraint_system_t
  to_disjunctive_linear_constraint_system() const override {
    return m_product.to_disjunctive_linear_constraint_system();
  }

  std::string domain_name() const override {
    const Domain1 &dom1 = m_product.first();
    const Domain2 &dom2 = m_product.second();

    std::string name =
        "ReducedProduct(" + dom1.domain_name() + "," + dom2.domain_name() + ")";
    return name;
  }

}; // class reduced_numerical_domain_product2

template <typename Number, typename VariableName, typename Domain1,
          typename Domain2>
struct abstract_domain_traits<
    domain_product2<Number, VariableName, Domain1, Domain2>> {
  using number_t = Number;
  using varname_t = VariableName;
};

template <typename Domain1, typename Domain2, class Params>
struct abstract_domain_traits<
    reduced_numerical_domain_product2<Domain1, Domain2, Params>> {
  using number_t = typename Domain1::number_t;
  using varname_t = typename Domain1::varname_t;
};

} // end namespace domains
} // namespace crab


// MIT License
//
// Copyright (c) 2018 Lennart Braun
//
// 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.

#include <algorithm>
#include <array>
#include <cassert>
#include <cstdint>
#include <boost/asio.hpp>
#include <botan/blake2b.h>
#include <botan/hex.h>
#include "ot_co15.hpp"
#include "util/threading.hpp"

OT_CO15::OT_CO15(Connection& connection) : connection_(connection)
{
}

void OT_CO15::send_0(Sender_SharedState& state,
                     std::array<uint8_t, curve25519_ge_byte_size>& message_out)
{
    // sample y <- Zp
    curve25519::sc_random(state.y);

    // S = y*G
    curve25519::x25519_ge_scalarmult_base(&state.S, state.y);

    curve25519::ge_p3_tobytes(message_out.data(), &state.S);
}

std::pair<bytes_t, bytes_t> OT_CO15::send_1(const Sender_SharedState& state,
                                            const std::array<uint8_t, curve25519_ge_byte_size>& message_in)
{
    curve25519::ge_p3 R;
    // assert R in GG
    if (!x25519_ge_frombytes_vartime(&R, message_in.data()))
        std::terminate();

    auto hash(Botan::Blake2b(128));

    auto output = std::make_pair<>(bytes_t(16), bytes_t(16));
    assert(output.first.size() == hash.output_length());
    assert(output.second.size() == hash.output_length());

    std::array<uint8_t, 3*curve25519_ge_byte_size> hash_input;
    curve25519::ge_p3_tobytes(hash_input.data(), &state.S);
    curve25519::ge_p3_tobytes(hash_input.data() + 32, &R);

    // j = 0:
    // y*R
    curve25519::ge_p2 a_times_R_p2;
    curve25519::x25519_ge_scalarmult(&a_times_R_p2, state.y, &R);
    curve25519::x25519_ge_tobytes(hash_input.data() + 64, &a_times_R_p2);

    // H(S, R, y*R)
    hash.update(hash_input.data(), hash_input.size());
    hash.final(output.first.data());

    // j = 1:
    // y*(R - S)
    {
        curve25519::ge_cached S_cached;
        curve25519::x25519_ge_p3_to_cached(&S_cached, &state.S);

        curve25519::ge_p1p1 R_minus_S_p1p1;
        curve25519::x25519_ge_sub(&R_minus_S_p1p1, &R, &S_cached);

        curve25519::ge_p3 R_minus_S_p3;
        curve25519::x25519_ge_p1p1_to_p3(&R_minus_S_p3, &R_minus_S_p1p1);

        curve25519::ge_p2 y_times_R_minus_S_p2;
        curve25519::x25519_ge_scalarmult(&y_times_R_minus_S_p2, state.y, &R_minus_S_p3);
        curve25519::x25519_ge_tobytes(hash_input.data() + 64, &y_times_R_minus_S_p2);

    }

    // H(S, R, y*(R - S))
    hash.update(hash_input.data(), hash_input.size());
    hash.final(output.second.data());

    return output;
}

void OT_CO15::recv_0(Receiver_State& state, bool choice)
{
    state.choice = choice;
    // sample b <- Zp
    curve25519::sc_random(state.x);
}

void OT_CO15::recv_1(Receiver_SharedState& sstate,
                     const std::array<uint8_t, curve25519_ge_byte_size>& message_in)
{
    // recv S
    auto res = curve25519::x25519_ge_frombytes_vartime(&sstate.S, message_in.data());
    // assert S in GG
    if (res == 0)
        std::terminate();
}

void OT_CO15::recv_2(Receiver_State& state, const Receiver_SharedState& sstate,
                     std::array<uint8_t, curve25519_ge_byte_size>& message_out)
{
    curve25519::x25519_ge_scalarmult_base(&state.R, state.x);
    // FIXME: not constant time
    if (state.choice == 1)
    {
        curve25519::ge_p1p1 R_p1p1;
        curve25519::ge_cached S_cached;
        curve25519::x25519_ge_p3_to_cached(&S_cached, &sstate.S);
        curve25519::x25519_ge_add(&R_p1p1, &state.R, &S_cached);
        curve25519::x25519_ge_p1p1_to_p3(&state.R, &R_p1p1);
    }

    curve25519::ge_p3_tobytes(message_out.data(), &state.R);
}

bytes_t OT_CO15::recv_3(const Receiver_State& state, const Receiver_SharedState& sstate)
{
    // k_R = H_(S, R, y*S)

    bytes_t hash_output(16);

    std::array<uint8_t, 3*curve25519_ge_byte_size> hash_input;
    curve25519::ge_p3_tobytes(hash_input.data(), &sstate.S);
    curve25519::ge_p3_tobytes(hash_input.data() + 32, &state.R);

    curve25519::ge_p2 y_times_S;
    curve25519::x25519_ge_scalarmult(&y_times_S, state.x, &sstate.S);
    curve25519::x25519_ge_tobytes(hash_input.data() + 64, &y_times_S);

    auto hash(Botan::Blake2b(128));
    assert(hash_output.size() == hash.output_length());
    hash.update(hash_input.data(), hash_input.size());
    hash.final(hash_output.data());

    return hash_output;
}

std::vector<std::pair<bytes_t, bytes_t>> OT_CO15::send(size_t number_ots)
{
    Sender_SharedState state;
    std::array<uint8_t, curve25519_ge_byte_size> msg_s0;
    std::vector<std::array<uint8_t, curve25519_ge_byte_size>> msgs_r1(number_ots);
    std::vector<std::pair<bytes_t, bytes_t>> output(number_ots);

    send_0(state, msg_s0);

    auto fut_send_msg_s0 = connection_.async_send(msg_s0.data(), msg_s0.size());
    auto fut_recv_msg_r1 = connection_.async_recv(reinterpret_cast<uint8_t*>(msgs_r1.data()), msgs_r1.size() * curve25519_ge_byte_size);

    auto msg_r1_size = fut_recv_msg_r1.get();
    assert(msg_r1_size == msgs_r1.size() * curve25519_ge_byte_size);

    for (size_t i = 0; i < number_ots; ++i)
    {
        output[i] = send_1(state, msgs_r1[i]);
    }

    auto msg_s0_size = fut_send_msg_s0.get();
    assert(msg_s0_size == msg_s0.size());

    return output;
}

std::vector<bytes_t> OT_CO15::recv(const std::vector<bool>& choices)
{
    auto number_ots = choices.size();
    std::vector<Receiver_State> states(number_ots);
    Receiver_SharedState sstate;
    std::array<uint8_t, curve25519_ge_byte_size> msg_s0;
    std::vector<std::array<uint8_t, curve25519_ge_byte_size>> msgs_r1(number_ots);
    std::vector<bytes_t> output(number_ots);

    auto fut_recv_msg_s0 = connection_.async_recv(msg_s0.data(), msg_s0.size());

    for (size_t i = 0; i < number_ots; ++i)
    {
        recv_0(states[i], choices[i]);
    }

    auto msg_s0_size = fut_recv_msg_s0.get();
    assert(msg_s0_size == msg_s0.size());

    recv_1(sstate, msg_s0);

    for (size_t i = 0; i < number_ots; ++i)
    {
        recv_2(states[i], sstate, msgs_r1[i]);
    }

    auto fut_send_msg_r1 = connection_.async_send(reinterpret_cast<uint8_t*>(msgs_r1.data()), msgs_r1.size() * curve25519_ge_byte_size);

    for (size_t i = 0; i < number_ots; ++i)
    {
        output[i] = recv_3(states[i], sstate);
    }

    auto msg_r1_size = fut_send_msg_r1.get();
    assert(msg_r1_size == msgs_r1.size() * curve25519_ge_byte_size);

    return output;
}

std::pair<bytes_t, bytes_t> OT_CO15::send()
{
    Sender_SharedState state;
    std::array<uint8_t, curve25519_ge_byte_size> msg_s0;
    std::array<uint8_t, curve25519_ge_byte_size> msg_r1;

    send_0(state, msg_s0);
    connection_.send(msg_s0.data(), msg_s0.size());
    connection_.recv(msg_r1.data(), msg_r1.size());
    return send_1(state, msg_r1);
}

bytes_t OT_CO15::recv(bool choice)
{
    Receiver_State state;
    Receiver_SharedState sstate;
    std::array<uint8_t, curve25519_ge_byte_size> msg_s0;
    std::array<uint8_t, curve25519_ge_byte_size> msg_r1;

    recv_0(state, choice);
    connection_.recv(msg_s0.data(), msg_s0.size());
    recv_1(sstate, msg_s0);
    recv_2(state, sstate, msg_r1);
    connection_.send(msg_r1.data(), msg_r1.size());
    return recv_3(state, sstate);
}

std::vector<std::pair<bytes_t, bytes_t>> OT_CO15::parallel_send(size_t number_ots, size_t number_threads, boost::asio::thread_pool& thread_pool)
{
    Sender_SharedState sstate;
    std::array<uint8_t, curve25519_ge_byte_size> msg_s0;
    std::vector<std::array<uint8_t, curve25519_ge_byte_size>> msgs_r1(number_ots);
    std::vector<std::pair<bytes_t, bytes_t>> output(number_ots);

    send_0(sstate, msg_s0);

    auto fut_send_msg_s0 = connection_.async_send(msg_s0.data(), msg_s0.size());
    auto fut_recv_msg_r1 = connection_.async_recv(reinterpret_cast<uint8_t*>(msgs_r1.data()), msgs_r1.size() * curve25519_ge_byte_size);

    auto msg_r1_size = fut_recv_msg_r1.get();
    assert(msg_r1_size == msgs_r1.size() * curve25519_ge_byte_size);

    compute(thread_pool, number_ots, number_threads, [this, &sstate, &msgs_r1, &output](size_t index){ output[index] = send_1(sstate, msgs_r1[index]); });

    auto msg_s0_size = fut_send_msg_s0.get();
    assert(msg_s0_size == msg_s0.size());

    return output;
}

std::vector<bytes_t> OT_CO15::parallel_recv(const std::vector<bool>& choices, size_t number_threads, boost::asio::thread_pool& thread_pool)
{
    auto number_ots = choices.size();
    std::vector<Receiver_State> states(number_ots);
    Receiver_SharedState sstate;
    std::array<uint8_t, curve25519_ge_byte_size> msg_s0;
    std::vector<std::array<uint8_t, curve25519_ge_byte_size>> msgs_r1(number_ots);
    std::vector<bytes_t> output(number_ots);

    auto fut_recv_msg_s0 = connection_.async_recv(msg_s0.data(), msg_s0.size());

    compute(thread_pool, number_ots, number_threads, [this, &states, &choices](size_t index){ recv_0(states[index], choices[index]); });

    auto msg_s0_size = fut_recv_msg_s0.get();
    assert(msg_s0_size == msg_s0.size());

    recv_1(sstate, msg_s0);

    compute(thread_pool, number_ots, number_threads, [this, &states, &sstate, &msgs_r1](size_t index){ recv_2(states[index], sstate, msgs_r1[index]); });

    auto fut_send_msg_r1 = connection_.async_send(reinterpret_cast<uint8_t*>(msgs_r1.data()), msgs_r1.size() * curve25519_ge_byte_size);

    compute(thread_pool, number_ots, number_threads, [this, &states, &sstate, &output](size_t index){ output[index] = recv_3(states[index], sstate); });

    auto msg_r1_size = fut_send_msg_r1.get();
    assert(msg_r1_size == msgs_r1.size() * curve25519_ge_byte_size);

    return output;
}


/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

/*
 Copyright (C) 2014 Master IMAFA - Polytech'Nice Sophia - Université de Nice Sophia Antipolis

 This file is part of QuantLib, a free-software/open-source library
 for financial quantitative analysts and developers - http://quantlib.org/

 QuantLib is free software: you can redistribute it and/or modify it
 under the terms of the QuantLib license.  You should have received a
 copy of the license along with this program; if not, please email
 <quantlib-dev@lists.sf.net>. The license is also available online at
 <http://quantlib.org/license.shtml>.

 This program is distributed in the hope that it will be useful, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 FOR A PARTICULAR PURPOSE.  See the license for more details.
*/

#include <ql/experimental/exoticoptions/analyticholderextensibleoptionengine.hpp>
#include <ql/math/distributions/bivariatenormaldistribution.hpp>
#include <ql/exercise.hpp>
#include <boost/make_shared.hpp>

namespace QuantLib {

    AnalyticHolderExtensibleOptionEngine::AnalyticHolderExtensibleOptionEngine(
             const boost::shared_ptr<GeneralizedBlackScholesProcess>& process)
    : process_(process) {
        registerWith(process_);
    }

    void AnalyticHolderExtensibleOptionEngine::calculate() const {
        //Spot
        Real S = process_->x0();
        Real r = riskFreeRate();
        Real b = r - dividendYield();
        Real X1 = strike();
        Real X2 = arguments_.secondStrike;
        Time T2 = secondExpiryTime();
        Time t1 = firstExpiryTime();
        Real A = arguments_.premium;

        Real z1 = this->z1();

        Real z2 = this->z2();

        Real rho = sqrt(t1 / T2);

        boost::shared_ptr<PlainVanillaPayoff> payoff =
            boost::dynamic_pointer_cast<PlainVanillaPayoff>(arguments_.payoff);

        //QuantLib requires sigma * sqrt(T) rather than just sigma/volatility
        Real vol = volatility();

        //calculate dividend discount factor assuming continuous compounding (e^-rt)
        DiscountFactor growth = dividendDiscount(t1);
        //calculate payoff discount factor assuming continuous compounding
        DiscountFactor discount = riskFreeDiscount(t1);
        Real result = 0;
        Real minusInf=-std::numeric_limits<Real>::infinity();

        Real y1 = this->y1(payoff->optionType()),
             y2 = this->y2(payoff->optionType());
        if (payoff->optionType() == Option::Call) {
            //instantiate payoff function for a call
            boost::shared_ptr<PlainVanillaPayoff> vanillaCallPayoff =
                boost::make_shared<PlainVanillaPayoff>(Option::Call, X1);
            Real BSM = BlackScholesCalculator(vanillaCallPayoff, S, growth, vol*sqrt(t1), discount).value();
            result = BSM
                + S*exp((b - r)*T2)*M2(y1, y2, minusInf, z1, rho)
                - X2*exp(-r*T2)*M2(y1 - vol*sqrt(t1), y2 - vol*sqrt(t1), minusInf, z1 - vol*sqrt(T2), rho)
                - S*exp((b - r)*t1)*N2(y1, z2) + X1*exp(-r*t1)*N2(y1 - vol*sqrt(t1), z2 - vol*sqrt(t1))
                - A*exp(-r*t1)*N2(y1 - vol*sqrt(t1), y2 - vol*sqrt(t1));
        } else {
            //instantiate payoff function for a call
            boost::shared_ptr<PlainVanillaPayoff> vanillaPutPayoff =
                boost::make_shared<PlainVanillaPayoff>(Option::Put, X1);
            result = BlackScholesCalculator(vanillaPutPayoff, S, growth, vol*sqrt(t1), discount).value()
                - S*exp((b - r)*T2)*M2(y1, y2, minusInf, -z1, rho)
                + X2*exp(-r*T2)*M2(y1 - vol*sqrt(t1), y2 - vol*sqrt(t1), minusInf, -z1 + vol*sqrt(T2), rho)
                + S*exp((b - r)*t1)*N2(z2, y2) - X1*exp(-r*t1)*N2(z2 - vol*sqrt(t1), y2 - vol*sqrt(t1))
                - A*exp(-r*t1)*N2(y1 - vol*sqrt(t1), y2 - vol*sqrt(t1));
        }
        this->results_.value = result;
    }

    Real AnalyticHolderExtensibleOptionEngine::I1Call() const {
        Real Sv = process_->x0();
        Real A = arguments_.premium;

        if(A==0)
        {
            return 0;
        }
        else
        {
            BlackScholesCalculator bs = bsCalculator(Sv, Option::Call);
            Real ci = bs.value();
            Real dc = bs.delta();

            Real yi = ci - A;
            //da/ds = 0
            Real di = dc - 0;
            Real epsilon = 0.001;

            //Newton-Raphson process
            while (std::fabs(yi) > epsilon){
                Sv = Sv - yi / di;

                bs = bsCalculator(Sv, Option::Call);
                ci = bs.value();
                dc = bs.delta();

                yi = ci - A;
                di = dc - 0;
            }
            return Sv;
        }
    }

    Real AnalyticHolderExtensibleOptionEngine::I2Call() const {
        Real Sv = process_->x0();
        Real X1 = strike();
        Real X2 = arguments_.secondStrike;
        Real A = arguments_.premium;
        Time T2 = secondExpiryTime();
        Time t1 = firstExpiryTime();
        Real r=riskFreeRate();

        Real val=X1-X2*std::exp(-r*(T2-t1));
        if(A< val){
            return std::numeric_limits<Real>::infinity();
        } else {
            BlackScholesCalculator bs = bsCalculator(Sv, Option::Call);
            Real ci = bs.value();
            Real dc = bs.delta();

            Real yi = ci - A - Sv + X1;
            //da/ds = 1
            Real di = dc - 1;
            Real epsilon = 0.001;

            //Newton-Raphson process
            while (std::fabs(yi) > epsilon){
                Sv = Sv - yi / di;

                bs = bsCalculator(Sv, Option::Call);
                ci = bs.value();
                dc = bs.delta();

                yi = ci - A - Sv + X1;
                di = dc - 1;
            }
            return Sv;
        }
    }

    Real AnalyticHolderExtensibleOptionEngine::I1Put() const {
        Real Sv = process_->x0();
        //Srtike
        Real X1 = strike();
        //Premium
        Real A = arguments_.premium;

        BlackScholesCalculator bs = bsCalculator(Sv, Option::Put);
        Real pi = bs.value();
        Real dc = bs.delta();

        Real yi = pi - A + Sv - X1;
        //da/ds = 1
        Real di = dc - 1;
        Real epsilon = 0.001;

        //Newton-Raphson prosess
        while (std::fabs(yi) > epsilon){
            Sv = Sv - yi / di;

            bs = bsCalculator(Sv, Option::Put);
            pi = bs.value();
            dc = bs.delta();

            yi = pi - A + Sv - X1;
            di = dc - 1;
        }
        return Sv;
    }

    Real AnalyticHolderExtensibleOptionEngine::I2Put() const {
        Real Sv = process_->x0();
        Real A = arguments_.premium;
        if(A==0){
            return std::numeric_limits<Real>::infinity();
        }
        else{
            BlackScholesCalculator bs = bsCalculator(Sv, Option::Put);
            Real pi = bs.value();
            Real dc = bs.delta();

            Real yi = pi - A;
            //da/ds = 0
            Real di = dc - 0;
            Real epsilon = 0.001;

            //Newton-Raphson prosess
            while (std::fabs(yi) > epsilon){
                Sv = Sv - yi / di;

                bs = bsCalculator(Sv, Option::Put);
                pi = bs.value();
                dc = bs.delta();

                yi = pi - A;
                di = dc - 0;
            }
            return Sv;
        }
    }

    BlackScholesCalculator AnalyticHolderExtensibleOptionEngine::bsCalculator(
                                    Real spot, Option::Type optionType) const {
        //Real spot = process_->x0();
        Real vol;
        DiscountFactor growth;
        DiscountFactor discount;
        Real X2 = arguments_.secondStrike;
        Time T2 = secondExpiryTime();
        Time t1 = firstExpiryTime();
        Time t = T2 - t1;

        //payoff
        boost::shared_ptr<PlainVanillaPayoff > vanillaPayoff =
            boost::make_shared<PlainVanillaPayoff>(optionType, X2);

        //QuantLib requires sigma * sqrt(T) rather than just sigma/volatility
        vol = volatility() * std::sqrt(t);
        //calculate dividend discount factor assuming continuous compounding (e^-rt)
        growth = dividendDiscount(t);
        //calculate payoff discount factor assuming continuous compounding
        discount = riskFreeDiscount(t);

        BlackScholesCalculator bs(vanillaPayoff, spot, growth, vol, discount);
        return bs;
    }

    Real AnalyticHolderExtensibleOptionEngine::M2(Real a, Real b, Real c, Real d, Real rho) const {
        BivariateCumulativeNormalDistributionDr78 CmlNormDist(rho);
        return CmlNormDist(b, d) - CmlNormDist(a, d) - CmlNormDist(b, c) + CmlNormDist(a,c);
    }

    Real AnalyticHolderExtensibleOptionEngine::N2(Real a, Real b) const {
        CumulativeNormalDistribution  NormDist;
        return NormDist(b) - NormDist(a);
    }

    Real AnalyticHolderExtensibleOptionEngine::strike() const {
        boost::shared_ptr<PlainVanillaPayoff> payoff =
            boost::dynamic_pointer_cast<PlainVanillaPayoff>(arguments_.payoff);
        QL_REQUIRE(payoff, "non-plain payoff given");
        return payoff->strike();
    }

    Time AnalyticHolderExtensibleOptionEngine::firstExpiryTime() const {
        return process_->time(arguments_.exercise->lastDate());
    }

    Time AnalyticHolderExtensibleOptionEngine::secondExpiryTime() const {
        return process_->time(arguments_.secondExpiryDate);
    }

    Volatility AnalyticHolderExtensibleOptionEngine::volatility() const {
        return process_->blackVolatility()->blackVol(firstExpiryTime(), strike());
    }
    Rate AnalyticHolderExtensibleOptionEngine::riskFreeRate() const {
        return process_->riskFreeRate()->zeroRate(firstExpiryTime(), Continuous,
            NoFrequency);
    }
    Rate AnalyticHolderExtensibleOptionEngine::dividendYield() const {
        return process_->dividendYield()->zeroRate(firstExpiryTime(),
            Continuous, NoFrequency);
    }

    DiscountFactor AnalyticHolderExtensibleOptionEngine::dividendDiscount(Time t) const {
        return process_->dividendYield()->discount(t);
    }

    DiscountFactor AnalyticHolderExtensibleOptionEngine::riskFreeDiscount(Time t) const {
        return process_->riskFreeRate()->discount(t);
    }

    Real AnalyticHolderExtensibleOptionEngine::y1(Option::Type type) const {
        Real S = process_->x0();
        Real I2 = (type == Option::Call) ? I2Call() : I2Put();

        Real b = riskFreeRate() - dividendYield();
        Real vol = volatility();
        Time t1 = firstExpiryTime();

        return (log(S / I2) + (b + pow(vol, 2) / 2)*t1) / (vol*sqrt(t1));
    }

    Real AnalyticHolderExtensibleOptionEngine::y2(Option::Type type) const {
        Real S = process_->x0();
        Real I1 = (type == Option::Call) ? I1Call() : I1Put();

        Real b = riskFreeRate() - dividendYield();
        Real vol = volatility();
        Time t1 = firstExpiryTime();

        return (log(S / I1) + (b + pow(vol, 2) / 2)*t1) / (vol*sqrt(t1));
    }

    Real AnalyticHolderExtensibleOptionEngine::z1() const {
        Real S = process_->x0();
        Real X2 = arguments_.secondStrike;
        Real b = riskFreeRate() - dividendYield();
        Real vol = volatility();
        Time T2 = secondExpiryTime();

        return (log(S / X2) + (b + pow(vol, 2) / 2)*T2) / (vol*sqrt(T2));
    }

    Real AnalyticHolderExtensibleOptionEngine::z2() const {
        Real S = process_->x0();
        Real X1 = strike();

        Real b = riskFreeRate() - dividendYield();
        Real vol = volatility();
        Time t1 = firstExpiryTime();

        return (log(S / X1) + (b + pow(vol, 2) / 2)*t1) / (vol*sqrt(t1));
    }

}


#include <cstdint>
#include <cmath>
#include <array>
#include <iostream>
#include <fstream>
#include <algorithm>
#include <iterator>
#include <charconv>
#include <random>
#include <boost/container/flat_map.hpp>
#include <boost/program_options.hpp>
#include <omp.h>
#include <nlohmann/json.hpp>
#include <OpenImageIO/imageio.h>

#define BRDF_SAMPLING_RES_THETA_H       90
#define BRDF_SAMPLING_RES_THETA_D       90
#define BRDF_SAMPLING_RES_PHI_D         360

#define RED_SCALE (1.0/1500.0)
#define GREEN_SCALE (1.15/1500.0)
#define BLUE_SCALE (1.66/1500.0)

// Read BRDF data
bool read_brdf(const char *filename, double* &brdf)
{
	FILE *f = fopen(filename, "rb");
	if (!f)
		return false;

	int dims[3];
	fread(dims, sizeof(int), 3, f);
	int n = dims[0] * dims[1] * dims[2];
	if (n != BRDF_SAMPLING_RES_THETA_H *
		 BRDF_SAMPLING_RES_THETA_D *
		 BRDF_SAMPLING_RES_PHI_D / 2)
	{
		fprintf(stderr, "Dimensions don't match\n");
		fclose(f);
		return false;
	}

	brdf = (double*) malloc (sizeof(double)*3*n);
	fread(brdf, sizeof(double), 3*n, f);

	fclose(f);
	return true;
}

// Lookup theta_half index
// This is a non-linear mapping!
// In:  [0 .. pi/2]
// Out: [0 .. 89]
inline int theta_half_index(double theta_half)
{
	if (theta_half <= 0.0)
		return 0;
	double theta_half_deg = ((theta_half / (M_PI/2.0))*BRDF_SAMPLING_RES_THETA_H);
	double temp = theta_half_deg*BRDF_SAMPLING_RES_THETA_H;
	temp = sqrt(temp);
	int ret_val = (int)temp;
	if (ret_val < 0) ret_val = 0;
	if (ret_val >= BRDF_SAMPLING_RES_THETA_H)
		ret_val = BRDF_SAMPLING_RES_THETA_H-1;
	return ret_val;
}

inline int theta_diff_index(double theta_diff)
{
	int tmp = int(theta_diff / (M_PI * 0.5) * BRDF_SAMPLING_RES_THETA_D);
	if (tmp < 0)
		return 0;
	else if (tmp < BRDF_SAMPLING_RES_THETA_D - 1)
		return tmp;
	else
		return BRDF_SAMPLING_RES_THETA_D - 1;
}

inline int phi_diff_index(double phi_diff)
{
	// Because of reciprocity, the BRDF is unchanged under
	// phi_diff -> phi_diff + M_PI
	if (phi_diff < 0.0)
		phi_diff += M_PI;

	// In: phi_diff in [0 .. pi]
	// Out: tmp in [0 .. 179]
	int tmp = int(phi_diff / M_PI * BRDF_SAMPLING_RES_PHI_D / 2);
	if (tmp < 0)
		return 0;
	else if (tmp < BRDF_SAMPLING_RES_PHI_D / 2 - 1)
		return tmp;
	else
		return BRDF_SAMPLING_RES_PHI_D / 2 - 1;
}

double gamma( double v ) {
  return v / (v + 0.155 ) * 1.019;
}

int main( int argc, char *argv[] ) {
  boost::program_options::options_description options("オプション");
  options.add_options()
    ("help,h",    "ヘルプを表示")
    ("input,i", boost::program_options::value<std::string>(), "入力ファイル")
    ("output,o", boost::program_options::value<std::string>(), "出力ファイル");
  boost::program_options::variables_map params;
  boost::program_options::store( boost::program_options::parse_command_line( argc, argv, options ), params );
  boost::program_options::notify( params );
  if( params.count("help") || !params.count("input") || !params.count("output") ) {
    std::cout << options << std::endl;
    return !params.count( "help" );
  }
  double* brdf;
  if ( !read_brdf( params[ "input" ].as< std::string >().c_str(), brdf ) ) {
    std::cerr <<  "Unable to read " << params[ "input" ].as< std::string >() << std::endl;
    return 1;
  }
  double max = 0.0;
  std::vector< float > v;
  v.reserve( BRDF_SAMPLING_RES_THETA_D * BRDF_SAMPLING_RES_THETA_H * 3 );
  double sum = 0.0;
  for( unsigned int i = 0; i != BRDF_SAMPLING_RES_THETA_D; ++i ) {
    double theta_diff = double( BRDF_SAMPLING_RES_THETA_D - i - 1 ) / double( BRDF_SAMPLING_RES_THETA_D ) * M_PI / 2.0;
    for( unsigned int j = 0; j != BRDF_SAMPLING_RES_THETA_H; ++j ) {
      double theta_half = double( j ) / double( BRDF_SAMPLING_RES_THETA_H ) * M_PI / 2.0;
      int ind = phi_diff_index(M_PI/2) +
        theta_diff_index(theta_diff) * BRDF_SAMPLING_RES_PHI_D / 2 +
        theta_half_index(theta_half) * BRDF_SAMPLING_RES_PHI_D / 2 *
        BRDF_SAMPLING_RES_THETA_D;
      auto red_val = gamma( std::max( brdf[ind] * RED_SCALE, 0.0 ) );
      auto green_val = gamma( std::max( brdf[ind + BRDF_SAMPLING_RES_THETA_H*BRDF_SAMPLING_RES_THETA_D*BRDF_SAMPLING_RES_PHI_D/2] * GREEN_SCALE, 0.0 ) );
      auto blue_val = gamma( std::max( brdf[ind + BRDF_SAMPLING_RES_THETA_H*BRDF_SAMPLING_RES_THETA_D*BRDF_SAMPLING_RES_PHI_D] * BLUE_SCALE, 0.0 ) );
      max = std::max( std::max( std::max( max, red_val ), green_val ), blue_val );
      sum += red_val + green_val + blue_val;
      v.push_back( red_val );
      v.push_back( green_val );
      v.push_back( blue_val );
    }
  }
  double average = sum / ( BRDF_SAMPLING_RES_THETA_D * BRDF_SAMPLING_RES_THETA_H * 3 );
  std::vector< uint8_t > image;
  image.reserve( BRDF_SAMPLING_RES_THETA_D * BRDF_SAMPLING_RES_THETA_H * 3 );
  for( unsigned int i = 0; i != BRDF_SAMPLING_RES_THETA_D; ++i ) {
    for( unsigned int j = 0; j != BRDF_SAMPLING_RES_THETA_H; ++j ) {
      int index = i * BRDF_SAMPLING_RES_THETA_H * 3 + j * 3;
      std::cout << v[ index ]/(average*2) << " " << v[ index + 1 ]/(average*2) << " " << v[ index + 2 ]/(average*2) << std::endl;
      image.push_back( uint8_t( std::min( std::max( v[ index ]/(average*2), 0.0  ), 1.0 ) * 255.0 ) );
      image.push_back( uint8_t( std::min( std::max( v[ index + 1 ]/(average*2), 0.0  ), 1.0 ) * 255.0 ) );
      image.push_back( uint8_t( std::min( std::max( v[ index + 2 ]/(average*2), 0.0  ), 1.0 ) * 255.0 ) );
    }
  }
  using namespace OIIO_NAMESPACE;
  ImageOutput *out = ImageOutput::create( params[ "output" ].as< std::string >() );
  if ( !out ) {
    std::cerr << "Unable to open output file" << std::endl;
    return -1;
  }
  ImageSpec spec ( BRDF_SAMPLING_RES_THETA_H, BRDF_SAMPLING_RES_THETA_D, 3, TypeDesc::UINT8);
  out->open( params[ "output" ].as< std::string >(), spec );
  out->write_image( TypeDesc::UINT8, image.data() );
  out->close();
  return 0;
}


/*  Copyright (c) 2013, Abdullah A. Hassan <voodooattack@hotmail.com>
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
 *     disclaimer in the documentation and/or other materials provided with the distribution.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 *  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 *  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 *  OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 *  OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

#ifndef NOISEMODULES_H
#define NOISEMODULES_H

#include "config.hpp"

#ifdef NOISE_DIR_IS_LIBNOISE
#include <libnoise/noise.h>
#else
#include <noise/noise.h>
#endif

#include <boost/multi_array.hpp>

namespace ADWIF
{
  class HeightMapModule: public noise::module::Module
  {
  public:
    inline static double cubicInterpolate (const double p[4], double x) {
      return p[1] + 0.5 * x*(p[2] - p[0] + x*(2.0*p[0] - 5.0*p[1] +
      4.0*p[2] - p[3] + x*(3.0*(p[1] - p[2]) + p[3] - p[0])));
    }

    inline static double bicubicInterpolate (const double p[4][4], double x, double y) {
      double arr[4];
      arr[0] = cubicInterpolate(p[0], y);
      arr[1] = cubicInterpolate(p[1], y);
      arr[2] = cubicInterpolate(p[2], y);
      arr[3] = cubicInterpolate(p[3], y);
      return cubicInterpolate(arr, x);
    }

    HeightMapModule(const boost::multi_array<double, 2> & heightMap,
                    int cellSizeX, int cellSizeY): Module(0), myHeightmap(heightMap),
                    myCellSizeX(cellSizeX), myCellSizeY(cellSizeY) { }

    virtual int GetSourceModuleCount() const { return 0; }

    virtual double GetValue(double x, double y, double z) const;

    int myCellSizeX, myCellSizeY;
    const boost::multi_array<double, 2> myHeightmap;
  };

}

#endif // NOISEMODULES_H