// landmark.cpp
// Based on landmarks_maxmin.cpp in the 'landmark' R package by Matt Piekenbrock, Jason Cory Brunson, Yara Skaf
#include <carma>
#include <armadillo>

#include <vector>
#include <functional>
#include <numeric>
#include <algorithm>
#include <thread>

using std::size_t;
using std::vector;
using std::thread;

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>

using namespace pybind11::literals;
namespace py = pybind11;

template< typename InputIt >
inline double sq_euc_dist(InputIt x, InputIt y, const size_t d){
	double res = 0.0;
	for(size_t i = 0; i < d; ++i, ++x, ++y){
		res += ((*x) - (*y)) * ((*x) - (*y));
	}
	return(res);
}
// a Distance Function here just returns the distance between two points, given their *indices*
using DistFunction = typename std::function<double(size_t, size_t)>;

// dist_f  := distance function between two (indexed) points
// n_pts   := number of points in the data set
// eps     := distance threshold used as a stopping criterion for the maxmin procedure
// n       := cardinality threshold used as a stopping criterion for the maxmin procedure
// metric  := metric to use. If 0, uses `dist_f`, otherwise picks one of the available metrics.
// seed    := initial point (default is point at index 0)
// pick    := criterion to break ties. Possible values include 0 (first), 1 (random), or 2 (last).
// cover   := whether to report set membership for each point
void maxmin_f(DistFunction dist_f, const size_t n_pts,
              const double eps, const size_t n,
              const size_t seed, const size_t pick,
							vector< size_t >& indices,
							vector< double >& radii
							) {
  if (eps == -1.0 && n == 0){ throw std::invalid_argument("Must supply either positive 'eps' or positive 'n'."); }
  if (pick > 2){ throw std::invalid_argument("tiebreaker 'pick' choice must be in { 0, 1, 2 }."); }
  if (seed >= n_pts){ throw std::invalid_argument("Invalid seed index given."); }
	if (indices.size() == 0 || radii.size() == 0){ throw std::invalid_argument("Indices and radii must have at least one element to begin with."); }

  // Make a function that acts as a sentinel
  enum CRITERION { NUM, EPS, NUM_OR_EPS }; // These are the only ones that make sense
  const CRITERION stopping_criterion = (eps == -1.0) ? NUM : ((n == 0) ? EPS : NUM_OR_EPS);
  const auto is_finished = [stopping_criterion, eps, n](size_t n_landmarks, double c_eps) -> bool {
    switch(stopping_criterion){
      case NUM: return(n_landmarks >= n);
      case EPS: return(c_eps <= eps);
      case NUM_OR_EPS: return(n_landmarks >= n || c_eps <= eps);
			default: return(true); // should never happen, but gcc complains
    };
  };

  // Indices of possible candidate landmarks
  vector< size_t > candidate_pts(n_pts, 0);
  std::iota(begin(candidate_pts), end(candidate_pts), 0);
  candidate_pts.erase(begin(candidate_pts) + seed);

  // Preallocate distance vector for landmarks; one for each point
	double cover_radius = std::numeric_limits<double>::infinity();
  vector< double > lm_dist(n_pts, cover_radius);

  // Generate the landmarks
  bool stop_reached = false;
  while (!stop_reached){
    const size_t c_lm = indices.back(); // update current landmark

    // Update non-landmark points with distance to nearest landmark
    for (auto idx: candidate_pts){
      double c_dist = dist_f(c_lm, idx);
      if (c_dist < lm_dist[idx]){
        lm_dist[idx] = c_dist; // update minimum landmark distance
      }
    }

    // Of the remaining candidate points, find the one with the maximum landmark distance
    auto max_landmark = std::max_element(begin(candidate_pts), end(candidate_pts), [&lm_dist](size_t ii, size_t jj){
      return lm_dist[ii] < lm_dist[jj];
    });

    // If not greedily picking the first candidate point, partition the candidate points, then use corresponding strategy
    if (pick > 0 && max_landmark != end(candidate_pts)){
      double max_lm_dist = lm_dist[(*max_landmark)];
      auto it = std::partition(begin(candidate_pts), end(candidate_pts), [max_lm_dist, &lm_dist](size_t j){
        return lm_dist[j] == max_lm_dist;
      });
			max_landmark =
				pick == 1 ? (it != begin(candidate_pts) ? std::prev(it) : begin(candidate_pts)) :
				begin(candidate_pts) + (rand() % std::distance(begin(candidate_pts), it));
    }

    // If the iterator is valid, we have a new landmark, otherwise we're finished
    if (max_landmark != end(candidate_pts)){
      cover_radius = lm_dist[(*max_landmark)];
      stop_reached = is_finished(indices.size(), cover_radius);
      if (!stop_reached){
        indices.push_back(*max_landmark);
				radii.push_back(cover_radius);
				candidate_pts.erase(max_landmark);
      }
    } else {
      cover_radius = 0.0;
      stop_reached = true;
    }
  } // while(!finished())
}

// Point cloud wrapper - See 'maxmin_f' below for implementation
py::tuple maxmin_pc(
	const arma::mat& X,
	const double eps, const size_t n,
	const size_t metric = 1, const size_t seed = 0, const size_t pick = 0
){
  const size_t n_pts = X.n_cols, d = X.n_rows;
  if (seed >= n_pts){ throw std::invalid_argument("Invalid seed point."); }

	// Initial covering radius == Inf
	vector< double > cover_radii{ std::numeric_limits<double>::infinity() };

  // Choose the initial landmark
  vector< size_t > lm { seed };
  lm.reserve(n != 0 ? n : size_t(n_pts*0.15));

  // Choose the distance function
  DistFunction dist = [&X, d](size_t i, size_t j) {
		return(sq_euc_dist(X.begin_col(i), X.begin_col(j), d));
	};

  // Call the generalized procedure
  maxmin_f(dist, n_pts, eps, n, seed, pick, lm, cover_radii);

	return(py::make_tuple(lm, cover_radii));
}

// Converts (i,j) indices in the range { 0, 1, ..., n - 1 } to its 0-based position
// in a lexicographical ordering of the (n choose 2) combinations.
constexpr size_t to_nat_2(size_t i, size_t j, size_t n) noexcept {
  return i < j ? (n*i - i*(i+1)/2 + j - i - 1) : (n*j - j*(j+1)/2 + i - j - 1);
}

// X := (n_pts choose 2) pairwise distances
// n_pts := number of points in X
py::tuple maxmin_dist(
	const arma::vec& X, const size_t n_pts,
	const double eps, const size_t n,
	const size_t seed = 0, const size_t pick = 0){
  if (seed >= n_pts){ throw std::invalid_argument("Invalid seed point."); }

  // Parameterize the distance function
  DistFunction dist = [&X, n_pts](size_t i, size_t j) -> double {
    return X[to_nat_2(i,j,n_pts)];
  };

	// Initial covering radius == Inf
	vector< double > cover_radii{ std::numeric_limits<double>::infinity() };

  // Choose the initial landmark
  vector< size_t > lm { seed };
  lm.reserve(n != 0 ? n : size_t(n_pts*0.15));

  // Call the generalized procedure
  maxmin_f(dist, n_pts, eps, n, seed, pick, lm, cover_radii);
	return(py::make_tuple(lm, cover_radii));
}

// Maxmin procedure O(n^2)
// x := pairwise distances (not a distance matrix!) if pairwise = True, else (d x n) matrix representing a point cloud
// eps := radius to cover 'x' with, otherwise -1.0 to use 'n'
// n := number of landmarks requested
// pairwise_dist := whether input is a set of pairwise distances or a point cloud
py::tuple maxmin(const py::array_t<double>& x, const double eps, const size_t n, bool pairwise_dist, int seed){
	if (pairwise_dist){
		const arma::vec dx = carma::arr_to_col< double >(x);
		const size_t N = dx.size();

		// Find n such that choose(n, 2) == N
		size_t lb = std::sqrt(2*N);
		size_t n_pts = size_t(floor(lb));
		for (; n_pts <= size_t(std::ceil(lb+2)); ++n_pts){
			if (N == ((n_pts * (n_pts - 1))/2)){ break; }
		}
		return(maxmin_dist(dx, n_pts, eps, n, seed, 0));
	} else {
		const arma::mat X = carma::arr_to_mat< double >(x);
		return(maxmin_pc(X, eps, n, 1, seed, 0));
	}
}

void doSomething(int thread_id, vector< double >& output) {
	output[thread_id] = std::sqrt(static_cast< double >(thread_id));
}

// Spawns n threads
auto spawnThreads(int n) -> vector< double > {
	vector< thread > threads(n);
	vector< double > output(n, 0.0);
	for (int i = 0; i < n; i++) {
		threads[i] = thread(doSomething, i, std::ref(output));
	}
	for (auto& th : threads) { th.join(); } // each thread blocks until it's finished, sequentially
	return(output);
}

// Classical MDS
// D := distance matrix
void cmds_eig(const arma::mat& D, const size_t d, arma::vec& w, arma::mat& v){
	const size_t n = D.n_rows;
	arma::mat H(n, n, arma::fill::zeros);
	double fill_value = 1.0/double(n);
	H.fill(-fill_value);
	H.diag().fill(1.0 - fill_value);
	bool success = arma::eig_sym(w, v, -0.5 * H * D * H, "std");
	if (!success){
		throw std::invalid_argument("Eigenvalues failed to converge.");
	}
}

void cmds(const arma::mat& D, const size_t d, arma::mat& out){
	arma::mat v;
	arma::vec w;
	cmds_eig(D, d, w, v);
	out = arma::fliplr(v);
	arma::vec eigenvalues = arma::sort(w, "descend");
	out.resize(out.n_rows, d);
	for (size_t j = 0; j < d; ++j){
		if (eigenvalues[j] > 0){
			out.col(j) *= std::sqrt(eigenvalues[j]);
		} else {
			out.col(j).fill(0.0);
		}
	}
}

constexpr auto rank_comb2(size_t i, size_t j, size_t n) noexcept -> size_t {
  if (j < i){ std::swap(i,j); }
  return(size_t(n*i - i*(i+1)/2 + j - i - 1));
}

inline std::array< size_t, 2 > unrank_comb2(const size_t x, const size_t n) noexcept {
	auto i = static_cast< size_t >( (n - 2 - floor(sqrt(-8*x + 4*n*(n-1)-7)/2.0 - 0.5)) );
	auto j = static_cast< size_t >( x + i + 1 - n*(n-1)/2 + (n-i)*((n-i)-1)/2 );
	return (std::array< size_t, 2 >{ i, j });
}

// Measure all pairwise distances between columns of matrix 'x'
// template< typename OutputIt, typename Lambda >
// void dist(const arma::mat& x, OutputIt out){
// 	const size_t N = x.n_rows*(x.n_rows - 1)/2;
// 	for (size_t c = 0; c < N; ++c){
// 		std::array< size_t, 2 > p = unrank_comb2(c, x.n_rows);
// 		size_t i = p[0], j = p[1];
// 		*out++ = (double) arma::dot(x.col(i) - x.col(j));
// 	}
// }

// Measure all pairwise distances between columns of matrix 'x'
void dist_matrix(const arma::mat& x, arma::mat& D){
	const size_t N = x.n_cols*(x.n_cols - 1)/2;
	// py::print("n = ", N);
	for (size_t c = 0; c < N; ++c){
		std::array< size_t, 2 > p = unrank_comb2(c, x.n_cols);
		size_t i = p[0], j = p[1];
		arma::vec diff = x.col(i) - x.col(j);
		D(i,j) = (double) std::pow(arma::norm(diff), 2.0);
		D(j,i) = D(i,j);
	}
}

// Each C++11 thread should be running in their function with an infinite loop, constantly waiting for new tasks to grab and run.
#include "threadpool.h"
void parallel_mds_threadpool(const arma::mat& X, const vector< arma::uvec >& cover_sets, const size_t d, const size_t n_threads, vector< arma::mat >& out){
	// Allocate max number of threads
	ctpl::thread_pool p(n_threads);

	// Prepare the models
	const size_t n_opens = cover_sets.size();
	auto models = vector< arma::mat >(n_opens, arma::mat());

	// Launch the threads
	std::vector<std::future<void>> results(n_opens);

	for (size_t j = 0; j < n_opens; ++j) {
		results[j] = p.push([&models, &X, &cover_sets, j, d](int thread_id){
			const size_t n = cover_sets.at(j).size();
			const arma::mat X_j = X.cols(cover_sets.at(j));
			arma::mat D = arma::mat(n, n, arma::fill::zeros);
			dist_matrix(X_j, D);
			cmds(D, d, models.at(j));
		});
	}

	// Join them
	for (size_t j = 0; j < n_opens; ++j) { results[j].get(); }

	// Copy the local euclidean models (transposed)
	for (size_t j = 0; j < models.size(); ++j){
		out[j] = models[j];
	}
};

auto parallel_mds(const py::array_t< double >& x, const py::list& cover_sets, const size_t d, const size_t n_threads) -> py::list {

	// Conversions
	const arma::mat X = carma::arr_to_mat< double >(x).t();
	auto indices = vector< arma::uvec >(cover_sets.size());
	for (size_t j = 0; j < cover_sets.size(); ++j){
		indices[j] = cover_sets[j].cast< arma::uvec >();
	}

	// Do the parallel mds
	std::cout << "The GIL state is " << PyGILState_Check() <<std::endl;
	py::gil_scoped_release release;
	std::cout << "The GIL state is " << PyGILState_Check() <<std::endl;
	vector< arma::mat > results(indices.size(), arma::mat());
	parallel_mds_threadpool(X, indices, d, n_threads, results);
	py::gil_scoped_acquire acquire;

	// py::list output(indices.size());
	vector< py::array_t< double > > output(indices.size());
	for (size_t j = 0; j < indices.size(); ++j){
		output[j] = carma::mat_to_arr(results[j]);
	}
	return(py::cast(output));
} // parallel_mds

// auto parallel_mds_blocks(const py::array_t< double >& x, const py::list& cover_sets, const size_t d, 	const vector< size_t >& blocks, const size_t n_threads) -> py::list {
// 	// Conversions
// 	const arma::mat X = carma::arr_to_mat< double >(x).t();
// 	auto indices = vector< arma::uvec >(cover_sets.size());
// 	for (size_t j = 0; j < cover_sets.size(); ++j){
// 		indices[j] = cover_sets[j].cast< arma::uvec >();
// 	}

// 	// Do the parallel mds
// 	py::gil_scoped_release release;
// 	vector< arma::mat > results(indices.size(), arma::mat());
// 	parallel_mds_simple(X, indices, d, n_threads, results);
// 	py::gil_scoped_acquire acquire;

// 	// py::list output(indices.size());
// 	vector< py::array_t< double > > output(indices.size());
// 	for (size_t j = 0; j < indices.size(); ++j){
// 		output[j] = carma::mat_to_arr(results[j]);
// 	}
// 	return(py::cast(output));
// }

// Simple parallelization of MDS on each open of the cover
// Assumes n_threads > 1
// X := (d x n) column major matrix of points
// blocks := (n_threads+1) vector of offsets such that indicating a range [blocks[i], blocks[i+1]) of open for thread i to handle
void parallel_mds_simple(
	const arma::mat& X,
	const vector< vector< arma::uword > >& cover_sets,
	const size_t d,
	const size_t n_threads,
	const vector< size_t >& blocks,
	vector< arma::mat >& out
){
	if (blocks.size() != (n_threads+1)){ throw std::invalid_argument("Block vector size must match number of threads."); }

	// Prepare the lambda to do the work
	const auto do_mds = [&out, &X, &cover_sets, d](int i, const int j) -> void {
		for (; i < j; ++i){
			// py::print("subset: ", i, "/", cover_sets.size());
			const size_t n = cover_sets.at(i).size();
			//vector< arma::uword >& c_open = cover_sets.at(i);
			arma::uvec ind(cover_sets.at(i));// unfortunately this copy is required
			const arma::mat X_i = X.cols(ind);
			arma::mat D = arma::mat(n, n, arma::fill::zeros);
			dist_matrix(X_i, D);
			cmds(D, d, out.at(i));
		}
	};

	// Do sequential computation
	// for (size_t j = 0; j < n_threads; ++j){
	// 	// py::print("Starting thread: ", j);
	// 	do_mds(blocks.at(j), blocks.at(j+1));
	// }

	// Launch the threads
	auto tt = vector< thread >(n_threads - 1);
	for (size_t j = 0; j < (n_threads-1); ++j){
		tt.at(j) = thread(do_mds, blocks.at(j), blocks.at(j+1));
	}
	// Have the main thread do some work as well
	do_mds(blocks.at(n_threads-1), blocks.at(n_threads));

	// Join the threads
	for (size_t j = 0; j < (n_threads-1); ++j) { tt.at(j).join(); }
}

// When threads are created using the dedicated Python APIs (such as the threading module), a thread state is automatically associated to
// them and the code showed above is therefore correct. However, when threads are created from C (for example by a third-party library
// with its own thread management), they don’t hold the GIL, nor is there a thread state structure for them.

// If you need to call Python code from these threads (often this will be part of a callback API provided by the aforementioned third-party
// library), you must first register these threads with the interpreter by creating a thread state data structure, then acquiring the GIL,
// and finally storing their thread state pointer, before you can start using the Python/C API. When you are done, you should reset the
// thread state pointer, release the GIL, and finally free the thread state data structure.

// trust pybind11/carma to do the automatic conversions here
// Parallelization notes:
// (1) DO NOT PASS AN ARMA::MAT DIRECTLY AS A PARAMETER; convert manually w/ carma
// (2) The GIL lock seems to require spawning threads, if unlocked and reacquired. Only unlock if threads are spawned.
// (3) Can actually return vector< arma::mat > fine, surprisingly
auto parallel_mds_blocks(
	const py::array_t< double >& X,
	const vector< vector< arma::uword >  >& cover_sets,
	const size_t d,
	const size_t n_threads,
	const vector< size_t >& blocks
) -> vector< arma::mat > {

	// Allocate the output
	vector< arma::mat > results(cover_sets.size(), arma::mat(1,1, arma::fill::zeros));

	// Do the parallel mds
	// std::cout << "INIT: The GIL state is " << PyGILState_Check() <<std::endl;
	const arma::mat points = carma::arr_to_mat(X);
	py::gil_scoped_release release;
	vector< double > res = spawnThreads(n_threads);
	// std::cout << "BEGIN: The GIL state is " << PyGILState_Check() <<std::endl;
	parallel_mds_simple(points, cover_sets, d, n_threads, blocks, results);
	// PyGILState_Ensure();
	// std::cout << "FINISHED: The GIL state is " << PyGILState_Check() <<std::endl;
	py::gil_scoped_acquire acquire;
	// std::cout << "END: The GIL state is " << PyGILState_Check() <<std::endl;
	// py::cast(results)

	// vector< py::array_t< double > > output(cover_sets.size());
	// for (size_t j = 0; j < cover_sets.size(); ++j){
	// 	output[j] = carma::mat_to_arr(results[j], true);
	// }
	// return(output);
	return(results);
	// return(py::list(1));
}

PYBIND11_MODULE(landmark, m) {
	m.def("maxmin", &maxmin, "finds maxmin landmarks");
	m.def("do_parallel", [](size_t n_threads) -> py::array_t< double > {
		/* Release GIL before calling into (potentially long-running) C++ code */
		py::gil_scoped_release release;
		vector< double > res = spawnThreads(n_threads);
		py::gil_scoped_acquire acquire;
		return(carma::col_to_arr(arma::vec(res)));
	});
	m.def("cmds", [](const py::array_t< double >& X, const size_t d) -> py::array_t< double > {
		const arma::mat D = carma::arr_to_mat< double >(X);
		arma::mat emb;
		cmds(D, d, emb);
		return carma::mat_to_arr(emb);
	});
	// void parallel_mds(const py::array_t< double >& x, const py::list& cover_sets, const size_t d){
	m.def("parallel_cmds", parallel_mds);
	m.def("parallel_mds_blocks", parallel_mds_blocks);
	m.def("dist_matrix", [](const py::array_t< double >& x) -> py::array_t< double > {
		const arma::mat X = carma::arr_to_mat(x).t();
		arma::mat D = arma::mat(X.n_cols, X.n_cols, arma::fill::zeros);
		dist_matrix(X, D);
		return(carma::mat_to_arr(D));
	});
};


/*******************************************************************************
 * Array expansion domain
 *
 * For a given array, map sequences of consecutive bytes to cells
 * consisting of a triple <offset, size, var> where:
 *
 * - offset is an unsigned number
 * - size  is an unsigned number
 * - var is a scalar variable that represents the content of
 *   a[offset,...,offset+size-1]
 *
 * The domain is general enough to represent any possible sequence of
 * consecutive bytes including sequences of bytes starting at the same
 * offsets but different sizes, overlapping sequences starting at
 * different offsets, etc. However, there are some cases that have
 * been implemented an imprecise manner:
 *
 * (1) array store/load with a non-constant index are conservatively ignored.
 * (2) array load from a cell that overlaps with other cells return top.
 ******************************************************************************/

#pragma once

#include <crab/common/debug.hpp>
#include <crab/common/stats.hpp>
#include <crab/common/types.hpp>
#include <crab/domains/abstract_domain.hpp>
#include <crab/domains/abstract_domain_specialized_traits.hpp>

#include <crab/domains/interval.hpp>
#include <crab/domains/patricia_trees.hpp>

#include <algorithm>
#include <boost/optional.hpp>
#include <set>
#include <unordered_map>
#include <vector>

namespace crab {
namespace domains {

// forward declarations
template <typename Variable> class offset_map;
template <typename Domain> class array_expansion_domain;

// wrapper for using ikos::index_t as patricia_tree keys
class offset_t {
  ikos::index_t _val;

public:
  explicit offset_t(ikos::index_t v) : _val(v) {}

  ikos::index_t index() const { return _val; }

  bool operator<(const offset_t &o) const { return _val < o._val; }

  bool operator==(const offset_t &o) const { return _val == o._val; }

  bool operator!=(const offset_t &o) const { return !(*this == o); }

  void write(crab::crab_os &o) const { o << _val; }

  friend crab::crab_os &operator<<(crab::crab_os &o, const offset_t &v) {
    v.write(o);
    return o;
  }
};

/*
 *  A synthetic cell is used to give a symbolic name to the byte
 *  contents of some array segment. The symbolic name is m_scalar
 *  while the array segment is represented by
 *        [m_offset, m_offset+1,...,m_offset+m_size-1]
 */
template <typename Variable> class cell {
private:
  friend class offset_map<Variable>;
  typedef cell<Variable> cell_t;
  typedef ikos::interval<typename Variable::number_t> interval_t;

  offset_t _offset;
  uint64_t _size;
  boost::optional<Variable> _scalar;

  // Only offset_map<Variable> can create cells
  cell() : _offset(0), _size(0), _scalar(boost::optional<Variable>()) {}
  cell(offset_t offset, Variable scalar)
      : _offset(offset), _size(scalar.get_bitwidth()), _scalar(scalar) {}

  cell(offset_t offset, uint64_t size)
      : _offset(offset), _size(size), _scalar(boost::optional<Variable>()) {}

  static interval_t to_interval(const offset_t o, uint64_t size) {
    interval_t i(o.index(), o.index() + size - 1);
    return i;
  }

  interval_t to_interval() const {
    return to_interval(get_offset(), get_size());
  }

public:
  bool is_null() const { return (_offset.index() == 0 && _size == 0); }

  offset_t get_offset() const { return _offset; }

  size_t get_size() const { return _size; }

  bool has_scalar() const { return (bool)_scalar; }

  Variable get_scalar() const {
    if (!has_scalar()) {
      CRAB_ERROR("cannot get undefined scalar variable");
    }
    return *_scalar;
  }

  // inclusion test
  bool operator<=(const cell_t &o) const {
    interval_t x = to_interval();
    interval_t y = o.to_interval();
    return x <= y;
  }

  // ignore the scalar variable
  bool operator==(const cell_t &o) const {
    return (get_offset() == o.get_offset() && get_size() == o.get_size());
  }

  // ignore the scalar variable
  bool operator<(const cell_t &o) const {
    if (get_offset() < o.get_offset()) {
      return true;
    } else if (get_offset() == o.get_offset()) {
      return get_size() < o.get_size();
    } else {
      return false;
    }
  }

  // Return true if [o, o+size) definitely overlaps with the cell,
  // where o is a constant expression.
  bool overlap(const offset_t &o, uint64_t size) const {
    interval_t x = to_interval();
    interval_t y = to_interval(o, size);
    bool res = (!(x & y).is_bottom());
    CRAB_LOG("array-expansion-overlap", crab::outs() << "**Checking if " << x
                                                     << " overlaps with " << y
                                                     << "=" << res << "\n";);
    return res;
  }

  // Return true if [symb_lb, symb_ub] may overlap with the cell,
  // where symb_lb and symb_ub are not constant expressions.
  template <typename Dom>
  bool symbolic_overlap(const typename Dom::linear_expression_t &symb_lb,
                        const typename Dom::linear_expression_t &symb_ub,
                        const Dom &dom) const {
    typedef typename Dom::linear_expression_t linear_expression_t;
    typedef typename Dom::number_t number_t;

    interval_t x = to_interval();
    assert(x.lb().is_finite());
    assert(x.ub().is_finite());
    linear_expression_t lb(*(x.lb().number()));
    linear_expression_t ub(*(x.ub().number()));

    CRAB_LOG("array-expansion-overlap", Dom tmp(dom);
             linear_expression_t tmp_symb_lb(symb_lb);
             linear_expression_t tmp_symb_ub(symb_ub);
             crab::outs() << "**Checking if " << *this
                          << " overlaps with symbolic "
                          << "[" << tmp_symb_lb << "," << tmp_symb_ub << "]"
                          << " with abstract state=" << tmp << "\n";);

    Dom tmp1(dom);
    tmp1 += (lb >= symb_lb);
    tmp1 += (lb <= symb_ub);
    if (!tmp1.is_bottom()) {
      CRAB_LOG("array-expansion-overlap", crab::outs() << "\tyes.\n";);
      return true;
    }

    Dom tmp2(dom);
    tmp2 += (ub >= symb_lb);
    tmp2 += (ub <= symb_ub);
    if (!tmp2.is_bottom()) {
      CRAB_LOG("array-expansion-overlap", crab::outs() << "\tyes.\n";);
      return true;
    }

    CRAB_LOG("array-expansion-overlap", crab::outs() << "\tno.\n";);
    return false;
  }

  void write(crab::crab_os &o) const {
    o << to_interval() << " -> ";
    if (has_scalar()) {
      o << get_scalar();
    } else {
      o << "_";
    }
  }

  friend crab::crab_os &operator<<(crab::crab_os &o, const cell_t &c) {
    c.write(o);
    return o;
  }
};

namespace cell_set_impl {
template <typename Set> inline Set set_intersection(Set &s1, Set &s2) {
  Set s3;
  std::set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
                        std::inserter(s3, s3.end()));
  return s3;
}

template <typename Set> inline Set set_union(Set &s1, Set &s2) {
  Set s3;
  std::set_union(s1.begin(), s1.end(), s2.begin(), s2.end(),
                 std::inserter(s3, s3.end()));
  return s3;
}

template <typename Set> inline bool set_inclusion(Set &s1, Set &s2) {
  Set s3;
  std::set_difference(s1.begin(), s1.end(), s2.begin(), s2.end(),
                      std::inserter(s3, s3.end()));
  return s3.empty();
}
} // namespace cell_set_impl

// Map offsets to cells
template <typename Variable> class offset_map {
public:
  typedef cell<Variable> cell_t;

private:
  template <typename Dom> friend class array_expansion_domain;

  typedef offset_map<Variable> offset_map_t;
  typedef std::set<cell_t> cell_set_t;
  typedef crab::variable_type type_t;

  /*
    The keys in the patricia tree are processing in big-endian
    order. This means that the keys are sorted. Sortedeness is
    very important to perform efficiently operations such as
    checking for overlap cells. Since keys are treated as bit
    patterns, negative offsets can be used but they are treated
    as large unsigned numbers.
  */
  typedef patricia_tree<offset_t, cell_set_t> patricia_tree_t;
  typedef typename patricia_tree_t::binary_op_t binary_op_t;
  typedef typename patricia_tree_t::partial_order_t partial_order_t;

  patricia_tree_t _map;

  // global state to map the same triple of array, offset and size
  // to same index
  static std::map<std::pair<ikos::index_t, std::pair<offset_t, uint64_t>>,
                  ikos::index_t>
      s_index_map;

  // for algorithm::lower_bound and algorithm::upper_bound
  struct compare_binding_t {
    bool operator()(const typename patricia_tree_t::binding_t &kv,
                    const offset_t &o) const {
      return kv.first < o;
    }
    bool operator()(const offset_t &o,
                    const typename patricia_tree_t::binding_t &kv) const {
      return o < kv.first;
    }
    bool operator()(const typename patricia_tree_t::binding_t &kv1,
                    const typename patricia_tree_t::binding_t &kv2) const {
      return kv1.first < kv2.first;
    }
  };

  patricia_tree_t apply_operation(binary_op_t &o, patricia_tree_t t1,
                                  patricia_tree_t t2) {
    t1.merge_with(t2, o);
    return t1;
  }

  class join_op : public binary_op_t {
    // apply is called when two bindings (one each from a
    // different map) have the same key(i.e., offset).
    std::pair<bool, boost::optional<cell_set_t>> apply(cell_set_t x,
                                                       cell_set_t y) {
      return {false, cell_set_impl::set_union(x, y)};
    }
    // if one map does not have a key in the other map we add it.
    bool default_is_absorbing() { return false; }
  };

  class meet_op : public binary_op_t {
    std::pair<bool, boost::optional<cell_set_t>> apply(cell_set_t x,
                                                       cell_set_t y) {
      return {false, cell_set_impl::set_intersection(x, y)};
    }
    // if one map does not have a key in the other map we ignore
    // it.
    bool default_is_absorbing() { return true; }
  };

  class domain_po : public partial_order_t {
    bool leq(cell_set_t x, cell_set_t y) {
      return cell_set_impl::set_inclusion(x, y);
    }
    // default value is bottom (i.e., empty map)
    bool default_is_top() { return false; }
  }; // class domain_po

  void remove_cell(const cell_t &c) {
    if (boost::optional<cell_set_t> cells = _map.lookup(c.get_offset())) {
      if ((*cells).erase(c) > 0) {
        _map.remove(c.get_offset());
        if (!(*cells).empty()) {
          // a bit of a waste ...
          _map.insert(c.get_offset(), *cells);
        }
      }
    }
  }

  void insert_cell(const cell_t &c, bool sanity_check = true) {
    if (sanity_check && !c.has_scalar()) {
      CRAB_ERROR(
          "array expansion cannot insert a cell without scalar variable");
    }
    if (boost::optional<cell_set_t> cells = _map.lookup(c.get_offset())) {
      if ((*cells).insert(c).second) {
        // a bit of a waste ...
        _map.remove(c.get_offset());
        _map.insert(c.get_offset(), *cells);
      }
    } else {
      cell_set_t new_cells;
      new_cells.insert(c);
      _map.insert(c.get_offset(), new_cells);
    }
  }

  cell_t get_cell(offset_t o, uint64_t size) const {
    if (boost::optional<cell_set_t> cells = _map.lookup(o)) {
      cell_t tmp(o, size);
      auto it = (*cells).find(tmp);
      if (it != (*cells).end()) {
        return *it;
      }
    }
    // not found
    return cell_t();
  }

  static std::string mk_scalar_name(Variable a, offset_t o, uint64_t size) {
    crab::crab_string_os os;
    os << a << "[";
    if (size == 1) {
      os << o;
    } else {
      os << o << "..." << o.index() + size - 1;
    }
    os << "]";
    return os.str();
  }

  static type_t get_array_element_type(type_t array_type) {
    if (array_type == ARR_BOOL_TYPE) {
      return BOOL_TYPE;
    } else if (array_type == ARR_INT_TYPE) {
      return INT_TYPE;
    } else if (array_type == ARR_REAL_TYPE) {
      return REAL_TYPE;
    } else {
      assert(array_type == ARR_PTR_TYPE);
      return PTR_TYPE;
    }
  }

  ikos::index_t get_index(Variable a, offset_t o, uint64_t size) {
    auto it = s_index_map.find({a.index(), {o, size}});
    if (it != s_index_map.end()) {
      return it->second;
    } else {
      ikos::index_t res = s_index_map.size();
      s_index_map.insert({{a.index(), {o, size}}, res});
      return res;
    }
  }

  cell_t mk_cell(Variable array, offset_t o, uint64_t size) {
    // TODO: check array is the array associated to this offset map

    cell_t c = get_cell(o, size);
    if (c.is_null()) {
      auto &vfac = array.name().get_var_factory();
      std::string vname = mk_scalar_name(array, o, size);
      type_t vtype = get_array_element_type(array.get_type());
      // create a new scalar variable for representing the contents
      // of bytes array[o,o+1,..., o+size-1]
      ikos::index_t vindex = get_index(array, o, size);
      Variable scalar_var(vfac.get(vindex, vname), vtype, size);
      c = cell_t(o, scalar_var);
      insert_cell(c);
      CRAB_LOG("array-expansion", crab::outs()
                                      << "**Created cell " << c << "\n";);
    }
    // sanity check
    if (!c.has_scalar()) {
      CRAB_ERROR("array expansion created a new cell without a scalar");
    }
    return c;
  }

  offset_map(patricia_tree_t &&m) : _map(std::move(m)) {}

public:
  offset_map() {}

  bool empty() const { return _map.empty(); }

  std::size_t size() const { return _map.size(); }

  // leq operator
  bool operator<=(const offset_map_t &o) const {
    domain_po po;
    return _map.leq(o._map, po);
  }

  // set union: if two cells with same offset do not agree on
  // size then they are ignored.
  offset_map_t operator|(const offset_map_t &o) {
    join_op op;
    return offset_map_t(apply_operation(op, _map, o._map));
  }

  // set intersection: if two cells with same offset do not agree
  // on size then they are ignored.
  offset_map_t operator&(const offset_map_t &o) {
    meet_op op;
    return offset_map_t(apply_operation(op, _map, o._map));
  }

  void operator-=(const cell_t &c) { remove_cell(c); }

  void operator-=(const std::vector<cell_t> &cells) {
    for (unsigned i = 0, e = cells.size(); i < e; ++i) {
      this->operator-=(cells[i]);
    }
  }

  std::vector<cell_t> get_all_cells() const {
    std::vector<cell_t> res;
    for (auto it = _map.begin(), et = _map.end(); it != et; ++it) {
      auto const &o_cells = it->second;
      for (auto &c : o_cells) {
        res.push_back(c);
      }
    }
    return res;
  }

  // Return in out all cells that might overlap with (o, size).
  void get_overlap_cells(offset_t o, uint64_t size, std::vector<cell_t> &out) {
    compare_binding_t comp;

    bool added = false;
    cell_t c = get_cell(o, size);
    if (c.is_null()) {
      // we need to add a temporary cell for (o, size)
      c = cell_t(o, size);
      insert_cell(c, false /*disable sanity check*/);
      added = true;
    }

    auto lb_it = std::lower_bound(_map.begin(), _map.end(), o, comp);
    if (lb_it != _map.end()) {
      // Store _map[begin,...,lb_it] into a vector so that we can
      // go backwards from lb_it.
      //
      // TODO: give support for reverse iterator in patricia_tree.
      std::vector<cell_set_t> upto_lb;
      upto_lb.reserve(std::distance(_map.begin(), lb_it));
      for (auto it = _map.begin(), et = lb_it; it != et; ++it) {
        upto_lb.push_back(it->second);
      }
      upto_lb.push_back(lb_it->second);

      for (int i = upto_lb.size() - 1; i >= 0; --i) {
        ///////
        // All the cells in upto_lb[i] have the same offset. They
        // just differ in the size.
        //
        // If none of the cells in upto_lb[i] overlap with (o, size)
        // we can stop.
        ////////
        bool continue_outer_loop = false;
        for (const cell_t &x : upto_lb[i]) {
          if (x.overlap(o, size)) {
            if (!(x == c)) {
              // FIXME: we might have some duplicates. this is a very drastic
              // solution.
              if (std::find(out.begin(), out.end(), x) == out.end()) {
                out.push_back(x);
              }
            }
            continue_outer_loop = true;
          }
        }
        if (!continue_outer_loop) {
          break;
        }
      }
    }

    // search for overlapping cells > o
    auto ub_it = std::upper_bound(_map.begin(), _map.end(), o, comp);
    for (; ub_it != _map.end(); ++ub_it) {
      bool continue_outer_loop = false;
      for (const cell_t &x : ub_it->second) {
        if (x.overlap(o, size)) {
          // FIXME: we might have some duplicates. this is a very drastic
          // solution.
          if (std::find(out.begin(), out.end(), x) == out.end()) {
            out.push_back(x);
          }
          continue_outer_loop = true;
        }
      }
      if (!continue_outer_loop) {
        break;
      }
    }

    // do not forget the rest of overlapping cells == o
    for (auto it = ++lb_it, et = ub_it; it != et; ++it) {
      bool continue_outer_loop = false;
      for (const cell_t &x : it->second) {
        if (x == c) { // we dont put it in out
          continue;
        }
        if (x.overlap(o, size)) {
          if (!(x == c)) {
            if (std::find(out.begin(), out.end(), x) == out.end()) {
              out.push_back(x);
            }
          }
          continue_outer_loop = true;
        }
      }
      if (!continue_outer_loop) {
        break;
      }
    }

    if (added) {
      // remove the temporary cell for (o, size)
      assert(!c.is_null());
      remove_cell(c);
    }

    CRAB_LOG(
        "array-expansion-overlap", crab::outs()
                                       << "**Overlap set between \n"
                                       << *this << "\nand "
                                       << "(" << o << "," << size << ")={";
        for (unsigned i = 0, e = out.size(); i < e;) {
          crab::outs() << out[i];
          ++i;
          if (i < e) {
            crab::outs() << ",";
          }
        } crab::outs()
        << "}\n";);
  }

  template <typename Dom>
  void get_overlap_cells_symbolic_offset(
      const Dom &dom, const typename Dom::linear_expression_t &symb_lb,
      const typename Dom::linear_expression_t &symb_ub,
      std::vector<cell_t> &out) const {

    for (auto it = _map.begin(), et = _map.end(); it != et; ++it) {
      const cell_set_t &o_cells = it->second;
      // All cells in o_cells have the same offset. They only differ
      // in the size. If the largest cell overlaps with [offset,
      // offset + size) then the rest of cells are considered to
      // overlap. This is an over-approximation because [offset,
      // offset+size) can overlap with the largest cell but it
      // doesn't necessarily overlap with smaller cells. For
      // efficiency, we assume it overlaps with all.
      cell_t largest_cell;
      for (auto &c : o_cells) {
        if (largest_cell.is_null()) {
          largest_cell = c;
        } else {
          assert(c.get_offset() == largest_cell.get_offset());
          if (largest_cell < c) {
            largest_cell = c;
          }
        }
      }
      if (!largest_cell.is_null()) {
        if (largest_cell.symbolic_overlap(symb_lb, symb_ub, dom)) {
          for (auto &c : o_cells) {
            out.push_back(c);
          }
        }
      }
    }
  }

  void write(crab::crab_os &o) const {
    if (_map.empty()) {
      o << "empty";
    } else {
      for (auto it = _map.begin(), et = _map.end(); it != et; ++it) {
        const cell_set_t &cells = it->second;
        o << "{";
        for (auto cit = cells.begin(), cet = cells.end(); cit != cet;) {
          o << *cit;
          ++cit;
          if (cit != cet) {
            o << ",";
          }
        }
        o << "}\n";
      }
    }
  }

  friend crab::crab_os &operator<<(crab::crab_os &o, const offset_map_t &m) {
    m.write(o);
    return o;
  }

  /* Operations needed if used as value in a patricia tree */
  bool operator==(const offset_map_t &o) const {
    return *this <= o && o <= *this;
  }
  bool is_top() const { return empty(); }
  bool is_bottom() const { return false; }
  /*
     a patricia tree only calls bottom if operator[] is called over
     a bottom state. Thus, we will make sure that we don't call
     operator[] in that case.
  */
  static offset_map_t bottom() {
    CRAB_ERROR("offset_map::bottom() cannot be called");
  }
  static offset_map_t top() { return offset_map_t(); }
};

template <typename Var>
std::map<std::pair<ikos::index_t, std::pair<offset_t, uint64_t>>, ikos::index_t>
    offset_map<Var>::s_index_map;

template <typename NumDomain>
class array_expansion_domain final
    : public abstract_domain<array_expansion_domain<NumDomain>> {

public:
  typedef typename NumDomain::number_t number_t;
  typedef typename NumDomain::varname_t varname_t;

private:
  typedef array_expansion_domain<NumDomain> array_expansion_domain_t;
  typedef abstract_domain<array_expansion_domain_t> abstract_domain_t;

public:
  using typename abstract_domain_t::disjunctive_linear_constraint_system_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::variable_t;
  using typename abstract_domain_t::variable_vector_t;
  typedef crab::pointer_constraint<variable_t> ptr_cst_t;
  typedef NumDomain content_domain_t;
  typedef interval<number_t> interval_t;

private:
  typedef bound<number_t> bound_t;
  typedef crab::variable_type type_t;
  typedef offset_map<variable_t> offset_map_t;
  typedef cell<variable_t> cell_t;
  typedef std::unordered_map<variable_t, offset_map_t> array_map_t;

  // scalar domain
  NumDomain _inv;

  // We use a global array map
  static array_map_t &get_array_map() {
    static array_map_t *array_map = new array_map_t();
    return *array_map;
  }

public:
  /**
      Ugly this needs to be fixed: needed if multiple analyses are
      run so we can clear the array map from one run to another.
  **/
  static void clear_global_state() {
    array_map_t &map = get_array_map();
    if (!map.empty()) {
      if (::crab::CrabSanityCheckFlag) {
        CRAB_WARN("array_expansion static variable map is being cleared");
      }
      map.clear();
    }
  }

private:
  void remove_array_map(const variable_t &v) {
    /// We keep the array map as global so we don't remove any entry.
    // array_map_t& map = get_array_map();
    // map.erase(v);
  }

  offset_map_t &lookup_array_map(const variable_t &v) {
    array_map_t &map = get_array_map();
    return map[v];
  }

  array_expansion_domain(NumDomain inv) : _inv(inv) {}

  interval_t to_interval(linear_expression_t expr, NumDomain inv) {
    interval_t r(expr.constant());
    for (typename linear_expression_t::iterator it = expr.begin();
         it != expr.end(); ++it) {
      interval_t c(it->first);
      r += c * inv[it->second];
    }
    return r;
  }

  interval_t to_interval(linear_expression_t expr) {
    return to_interval(expr, _inv);
  }

  void kill_cells(const std::vector<cell_t> &cells, offset_map_t &offset_map) {
    if (!cells.empty()) {
      // Forget the scalars from the numerical domain
      for (unsigned i = 0, e = cells.size(); i < e; ++i) {
        const cell_t &c = cells[i];
        if (c.has_scalar()) {
          _inv -= c.get_scalar();
        } else {
          CRAB_ERROR(
              "array expansion: cell without scalar variable in array store");
        }
      }
      // Remove the cells. If needed again they they will be re-created.
      offset_map -= cells;
    }
  }

  // Helper that assign rhs to lhs by switching to the version with
  // the right type.
  void do_assign(variable_t lhs, variable_t rhs) {
    if (lhs.get_type() != rhs.get_type()) {
      CRAB_ERROR("array_adaptive assignment with different types");
    }
    switch (lhs.get_type()) {
    case BOOL_TYPE:
      _inv.assign_bool_var(lhs, rhs, false);
      break;
    case INT_TYPE:
    case REAL_TYPE:
      _inv.assign(lhs, rhs);
      break;
    case PTR_TYPE:
      _inv.pointer_assign(lhs, rhs, number_t(0));
      break;
    default:;
      CRAB_ERROR("array_adaptive assignment with unexpected type");
    }
  }

  // helper to assign a cell into a variable
  void do_assign(variable_t lhs, cell_t rhs_c) {
    if (!rhs_c.has_scalar()) {
      CRAB_ERROR("array_adaptive cell without scalar");
    }
    variable_t rhs = rhs_c.get_scalar();
    do_assign(lhs, rhs);
  }

  // helper to assign a linear expression into a cell
  void do_assign(cell_t lhs_c, linear_expression_t v) {
    if (!lhs_c.has_scalar()) {
      CRAB_ERROR("array_adaptive cell without scalar");
    }
    variable_t lhs = lhs_c.get_scalar();
    switch (lhs.get_type()) {
    case BOOL_TYPE:
      if (v.is_constant()) {
        if (v.constant() >= number_t(1)) {
          _inv.assign_bool_cst(lhs, linear_constraint_t::get_true());
        } else {
          _inv.assign_bool_cst(lhs, linear_constraint_t::get_false());
        }
      } else if (auto var = v.get_variable()) {
        _inv.assign_bool_var(lhs, (*var), false);
      }
      break;
    case INT_TYPE:
    case REAL_TYPE:
      _inv.assign(lhs, v);
      break;
    case PTR_TYPE:
      if (v.is_constant() && v.constant() == number_t(0)) {
        _inv.pointer_mk_null(lhs);
      } else if (auto var = v.get_variable()) {
        _inv.pointer_assign(lhs, (*var), number_t(0));
      }
      break;
    default:;
      CRAB_ERROR("array_adaptive assignment with unexpected type");
    }
  }

  // Helper that assign backward rhs to lhs by switching to the
  // version with the right type.
  void do_backward_assign(variable_t lhs, variable_t rhs,
                          content_domain_t &dom) {
    if (lhs.get_type() != rhs.get_type()) {
      CRAB_ERROR("array_adaptive backward assignment with different types");
    }
    switch (lhs.get_type()) {
    case BOOL_TYPE:
      _inv.backward_assign_bool_var(lhs, rhs, false, dom);
      break;
    case INT_TYPE:
    case REAL_TYPE:
      _inv.backward_assign(lhs, rhs, dom);
      break;
    case PTR_TYPE:
      CRAB_WARN("array_adaptive backward pointer assignment not implemented");
      break;
    default:;
      CRAB_ERROR("array_adaptive backward_assignment with unexpected type");
    }
  }

  // helper to assign backward a cell into a variable
  void do_backward_assign(variable_t lhs, cell_t rhs_c, content_domain_t &dom) {
    if (!rhs_c.has_scalar()) {
      CRAB_ERROR("array_adaptive cell without scalar");
    }
    variable_t rhs = rhs_c.get_scalar();
    do_backward_assign(lhs, rhs, dom);
  }

  // helper to assign backward a linear expression into a cell
  void do_backward_assign(cell_t lhs_c, linear_expression_t v,
                          content_domain_t &dom) {
    if (!lhs_c.has_scalar()) {
      CRAB_ERROR("array_adaptive cell without scalar");
    }
    variable_t lhs = lhs_c.get_scalar();
    switch (lhs.get_type()) {
    case BOOL_TYPE:
      if (v.is_constant()) {
        if (v.constant() >= number_t(1)) {
          _inv.backward_assign_bool_cst(lhs, linear_constraint_t::get_true(),
                                        dom);
        } else {
          _inv.backward_assign_bool_cst(lhs, linear_constraint_t::get_false(),
                                        dom);
        }
      } else if (auto var = v.get_variable()) {
        _inv.backward_assign_bool_var(lhs, (*var), false, dom);
      }
      break;
    case INT_TYPE:
    case REAL_TYPE:
      _inv.backward_assign(lhs, v, dom);
      break;
    case PTR_TYPE:
      CRAB_WARN("array_adaptive backward pointer assignment not implemented");
      break;
    default:;
      CRAB_ERROR("array_adaptive backward assignment with unexpected type");
    }
  }

public:
  array_expansion_domain() : _inv(NumDomain::top()) {}

  void set_to_top() {
    array_expansion_domain abs(NumDomain::top());
    std::swap(*this, abs);
  }

  void set_to_bottom() {
    array_expansion_domain abs(NumDomain::bottom());
    std::swap(*this, abs);
  }

  array_expansion_domain(const array_expansion_domain_t &other)
      : _inv(other._inv) {
    crab::CrabStats::count(getDomainName() + ".count.copy");
    crab::ScopedCrabStats __st__(getDomainName() + ".copy");
  }

  array_expansion_domain(const array_expansion_domain_t &&other)
      : _inv(std::move(other._inv)) {
    crab::CrabStats::count(getDomainName() + ".count.copy");
    crab::ScopedCrabStats __st__(getDomainName() + ".copy");
  }

  array_expansion_domain_t &operator=(const array_expansion_domain_t &other) {
    crab::CrabStats::count(getDomainName() + ".count.copy");
    crab::ScopedCrabStats __st__(getDomainName() + ".copy");
    if (this != &other) {
      _inv = other._inv;
    }
    return *this;
  }

  array_expansion_domain_t &operator=(const array_expansion_domain_t &&other) {
    crab::CrabStats::count(getDomainName() + ".count.copy");
    crab::ScopedCrabStats __st__(getDomainName() + ".copy");
    if (this != &other) {
      _inv = std::move(other._inv);
    }
    return *this;
  }

  bool is_bottom() { return (_inv.is_bottom()); }

  bool is_top() { return (_inv.is_top()); }

  bool operator<=(array_expansion_domain_t other) {
    crab::CrabStats::count(getDomainName() + ".count.leq");
    crab::ScopedCrabStats __st__(getDomainName() + ".leq");

    return (_inv <= other._inv);
  }

  bool operator==(array_expansion_domain_t other) {
    return (_inv <= other._inv && other._inv <= _inv);
  }

  void operator|=(array_expansion_domain_t other) {
    crab::CrabStats::count(getDomainName() + ".count.join");
    crab::ScopedCrabStats __st__(getDomainName() + ".join");
    _inv |= other._inv;
  }

  array_expansion_domain_t operator|(array_expansion_domain_t other) {
    crab::CrabStats::count(getDomainName() + ".count.join");
    crab::ScopedCrabStats __st__(getDomainName() + ".join");
    return array_expansion_domain_t(_inv | other._inv);
  }

  array_expansion_domain_t operator&(array_expansion_domain_t other) {
    crab::CrabStats::count(getDomainName() + ".count.meet");
    crab::ScopedCrabStats __st__(getDomainName() + ".meet");

    return array_expansion_domain_t(_inv & other._inv);
  }

  array_expansion_domain_t operator||(array_expansion_domain_t other) {
    crab::CrabStats::count(getDomainName() + ".count.widening");
    crab::ScopedCrabStats __st__(getDomainName() + ".widening");

    return array_expansion_domain_t(_inv || other._inv);
  }

  array_expansion_domain_t
  widening_thresholds(array_expansion_domain_t other,
                      const iterators::thresholds<number_t> &ts) {
    crab::CrabStats::count(getDomainName() + ".count.widening");
    crab::ScopedCrabStats __st__(getDomainName() + ".widening");
    return array_expansion_domain_t(_inv.widening_thresholds(other._inv, ts));
  }

  array_expansion_domain_t operator&&(array_expansion_domain_t other) {
    crab::CrabStats::count(getDomainName() + ".count.narrowing");
    crab::ScopedCrabStats __st__(getDomainName() + ".narrowing");
    return array_expansion_domain_t(_inv && other._inv);
  }

  void forget(const variable_vector_t &variables) {
    crab::CrabStats::count(getDomainName() + ".count.forget");
    crab::ScopedCrabStats __st__(getDomainName() + ".forget");

    if (is_bottom() || is_top()) {
      return;
    }

    _inv.forget(variables);

    for (variable_t v : variables) {
      if (v.is_array_type()) {
        remove_array_map(v);
      }
    }
  }

  void project(const variable_vector_t &variables) {
    CRAB_WARN("array expansion project not implemented");
  }

  void expand(variable_t var, variable_t new_var) {
    CRAB_WARN("array expansion expand not implemented");
  }

  void normalize() { CRAB_WARN("array expansion normalize not implemented"); }

  void minimize() { _inv.minimize(); }

  void operator+=(linear_constraint_system_t csts) {
    crab::CrabStats::count(getDomainName() + ".count.add_constraints");
    crab::ScopedCrabStats __st__(getDomainName() + ".add_constraints");

    _inv += csts;

    CRAB_LOG("array-expansion",
             crab::outs() << "assume(" << csts << ")  " << *this << "\n";);
  }

  void operator-=(variable_t var) {
    crab::CrabStats::count(getDomainName() + ".count.forget");
    crab::ScopedCrabStats __st__(getDomainName() + ".forget");

    if (var.is_array_type()) {
      remove_array_map(var);
    } else {
      _inv -= var;
    }
  }

  void assign(variable_t x, linear_expression_t e) {
    crab::CrabStats::count(getDomainName() + ".count.assign");
    crab::ScopedCrabStats __st__(getDomainName() + ".assign");

    _inv.assign(x, e);

    CRAB_LOG("array-expansion", crab::outs() << "apply " << x << " := " << e
                                             << " " << *this << "\n";);
  }

  void apply(operation_t op, variable_t x, variable_t y, number_t z) {
    crab::CrabStats::count(getDomainName() + ".count.apply");
    crab::ScopedCrabStats __st__(getDomainName() + ".apply");

    _inv.apply(op, x, y, z);

    CRAB_LOG("array-expansion", crab::outs()
                                    << "apply " << x << " := " << y << " " << op
                                    << " " << z << " " << *this << "\n";);
  }

  void apply(operation_t op, variable_t x, variable_t y, variable_t z) {
    crab::CrabStats::count(getDomainName() + ".count.apply");
    crab::ScopedCrabStats __st__(getDomainName() + ".apply");

    _inv.apply(op, x, y, z);

    CRAB_LOG("array-expansion", crab::outs()
                                    << "apply " << x << " := " << y << " " << op
                                    << " " << z << " " << *this << "\n";);
  }

  void backward_assign(variable_t x, linear_expression_t e,
                       array_expansion_domain_t inv) {
    _inv.backward_assign(x, e, inv.get_content_domain());
  }

  void backward_apply(operation_t op, variable_t x, variable_t y, number_t z,
                      array_expansion_domain_t inv) {
    _inv.backward_apply(op, x, y, z, inv.get_content_domain());
  }

  void backward_apply(operation_t op, variable_t x, variable_t y, variable_t z,
                      array_expansion_domain_t inv) {
    _inv.backward_apply(op, x, y, z, inv.get_content_domain());
  }

  void apply(int_conv_operation_t op, variable_t dst, variable_t src) {
    crab::CrabStats::count(getDomainName() + ".count.apply");
    crab::ScopedCrabStats __st__(getDomainName() + ".apply");

    _inv.apply(op, dst, src);
  }

  void apply(bitwise_operation_t op, variable_t x, variable_t y, variable_t z) {
    crab::CrabStats::count(getDomainName() + ".count.apply");
    crab::ScopedCrabStats __st__(getDomainName() + ".apply");

    _inv.apply(op, x, y, z);

    CRAB_LOG("array-expansion", crab::outs()
                                    << "apply " << x << " := " << y << " " << op
                                    << " " << z << " " << *this << "\n";);
  }

  void apply(bitwise_operation_t op, variable_t x, variable_t y, number_t k) {
    crab::CrabStats::count(getDomainName() + ".count.apply");
    crab::ScopedCrabStats __st__(getDomainName() + ".apply");

    _inv.apply(op, x, y, k);

    CRAB_LOG("array-expansion", crab::outs()
                                    << "apply " << x << " := " << y << " " << op
                                    << " " << k << " " << *this << "\n";);
  }

  // boolean operators
  virtual void assign_bool_cst(variable_t lhs,
                               linear_constraint_t rhs) override {
    _inv.assign_bool_cst(lhs, rhs);
  }

  virtual void assign_bool_var(variable_t lhs, variable_t rhs,
                               bool is_not_rhs) override {
    _inv.assign_bool_var(lhs, rhs, is_not_rhs);
  }

  virtual void apply_binary_bool(bool_operation_t op, variable_t x,
                                 variable_t y, variable_t z) override {
    _inv.apply_binary_bool(op, x, y, z);
  }

  virtual void assume_bool(variable_t v, bool is_negated) override {
    _inv.assume_bool(v, is_negated);
  }

  // backward boolean operators
  virtual void backward_assign_bool_cst(variable_t lhs, linear_constraint_t rhs,
                                        array_expansion_domain_t inv) {
    _inv.backward_assign_bool_cst(lhs, rhs, inv.get_content_domain());
  }

  virtual void backward_assign_bool_var(variable_t lhs, variable_t rhs,
                                        bool is_not_rhs,
                                        array_expansion_domain_t inv) {
    _inv.backward_assign_bool_var(lhs, rhs, is_not_rhs,
                                  inv.get_content_domain());
  }

  virtual void backward_apply_binary_bool(bool_operation_t op, variable_t x,
                                          variable_t y, variable_t z,
                                          array_expansion_domain_t inv) {
    _inv.backward_apply_binary_bool(op, x, y, z, inv.get_content_domain());
  }

  // pointer_operators_api
  virtual void pointer_load(variable_t lhs, variable_t rhs, linear_expression_t elem_size) override {
    _inv.pointer_load(lhs, rhs, elem_size);
  }

  virtual void pointer_store(variable_t lhs, variable_t rhs, linear_expression_t elem_size) override {
    _inv.pointer_store(lhs, rhs, elem_size);
  }

  virtual void pointer_assign(variable_t lhs, variable_t rhs,
                              linear_expression_t offset) override {
    _inv.pointer_assign(lhs, rhs, offset);
  }

  virtual void pointer_mk_obj(variable_t lhs, ikos::index_t address) override {
    _inv.pointer_mk_obj(lhs, address);
  }

  virtual void pointer_function(variable_t lhs, varname_t func) override {
    _inv.pointer_function(lhs, func);
  }

  virtual void pointer_mk_null(variable_t lhs) override {
    _inv.pointer_mk_null(lhs);
  }

  virtual void pointer_assume(ptr_cst_t cst) override {
    _inv.pointer_assume(cst);
  }

  virtual void pointer_assert(ptr_cst_t cst) override {
    _inv.pointer_assert(cst);
  }

  // array_operators_api

  // array_init returns a fresh array where all elements between
  // lb_idx and ub_idx are initialized to val. Thus, the first thing
  // we need to do is to kill existing cells.
  virtual void array_init(variable_t a, linear_expression_t elem_size,
                          linear_expression_t lb_idx,
                          linear_expression_t ub_idx,
                          linear_expression_t val) override {
    crab::CrabStats::count(getDomainName() + ".count.array_init");
    crab::ScopedCrabStats __st__(getDomainName() + ".array_init");

    if (is_bottom())
      return;

    offset_map_t &offset_map = lookup_array_map(a);
    std::vector<cell_t> old_cells = offset_map.get_all_cells();
    if (!old_cells.empty()) {
      kill_cells(old_cells, offset_map);
    }

    array_store_range(a, elem_size, lb_idx, ub_idx, val);
  }

  virtual void array_load(variable_t lhs, variable_t a,
                          linear_expression_t elem_size,
                          linear_expression_t i) override {
    crab::CrabStats::count(getDomainName() + ".count.array_load");
    crab::ScopedCrabStats __st__(getDomainName() + ".array_load");

    if (is_bottom())
      return;

    interval_t ii = to_interval(i);
    if (boost::optional<number_t> n = ii.singleton()) {
      offset_map_t &offset_map = lookup_array_map(a);
      offset_t o(static_cast<int64_t>(*n));
      interval_t i_elem_size = to_interval(elem_size);
      if (boost::optional<number_t> n_bytes = i_elem_size.singleton()) {
        assert(static_cast<int64_t>(*n_bytes) > 0 &&
               static_cast<int64_t>(*n_bytes) <=
                   std::numeric_limits<uint64_t>::max());
        uint64_t size = static_cast<int64_t>(*n_bytes);
        std::vector<cell_t> cells;
        offset_map.get_overlap_cells(o, size, cells);
        if (!cells.empty()) {
          CRAB_WARN("Ignored read from cell ", a, "[", o, "...",
                    o.index() + size - 1, "]", " because it overlaps with ",
                    cells.size(), " cells");
          /*
             TODO: we can apply here "Value Recomposition" 'a la'
             Mine'06 to construct values of some type from a sequence
             of bytes. It can be endian-independent but it would more
             precise if we choose between little- and big-endian.
          */
        } else {
          cell_t c = offset_map.mk_cell(a, o, size);
          assert(c.has_scalar());
          // Here it's ok to do assignment (instead of expand)
          // because c is not a summarized variable. Otherwise, it
          // would be unsound.
          do_assign(lhs, c);
          goto array_load_end;
        }
      } else {
        CRAB_ERROR(
            "array expansion domain expects constant array element sizes");
      }
    } else {
      // TODO: we can be more precise here
      CRAB_WARN("array expansion: ignored array load because of non-constant "
                "array index ",
                i);
    }

    _inv -= lhs;

  array_load_end:
    CRAB_LOG("array-expansion", linear_expression_t ub = i + elem_size - 1;
             crab::outs() << lhs << ":=" << a << "[" << i << "..." << ub
                          << "]  -- " << *this << "\n";);
  }

  virtual void array_store(variable_t a, linear_expression_t elem_size,
                           linear_expression_t i, linear_expression_t val,
                           bool /*is_strong_update*/) override {
    crab::CrabStats::count(getDomainName() + ".count.array_store");
    crab::ScopedCrabStats __st__(getDomainName() + ".array_store");

    if (is_bottom())
      return;

    interval_t i_elem_size = to_interval(elem_size);
    boost::optional<number_t> n_bytes = i_elem_size.singleton();
    if (!n_bytes) {
      CRAB_ERROR("array expansion domain expects constant array element sizes");
    }

    assert(static_cast<int64_t>(*n_bytes) > 0 &&
           static_cast<int64_t>(*n_bytes) <=
               std::numeric_limits<uint64_t>::max());
    uint64_t size = static_cast<int64_t>(*n_bytes);
    offset_map_t &offset_map = lookup_array_map(a);
    interval_t ii = to_interval(i);
    if (boost::optional<number_t> n = ii.singleton()) {
      // -- Constant index: kill overlapping cells + perform strong update
      std::vector<cell_t> cells;
      offset_t o(static_cast<int64_t>(*n));
      offset_map.get_overlap_cells(o, size, cells);
      if (cells.size() > 0) {
        CRAB_LOG("array-expansion",
                 CRAB_WARN("Killed ", cells.size(), " overlapping cells with ",
                           "[", o, "...", o.index() + size - 1, "]",
                           " before writing."));

        kill_cells(cells, offset_map);
      }
      // Perform scalar update
      // -- create a new cell it there is no one already
      cell_t c = offset_map.mk_cell(a, o, size);
      // -- strong update
      do_assign(c, val);
    } else {
      // -- Non-constant index: kill overlapping cells
      CRAB_WARN("array expansion ignored array write with non-constant index ",
                i);
      linear_expression_t symb_lb(i);
      linear_expression_t symb_ub(i + number_t(size - 1));
      std::vector<cell_t> cells;
      offset_map.get_overlap_cells_symbolic_offset(_inv, symb_lb, symb_ub,
                                                   cells);
      CRAB_LOG(
          "array-expansion", crab::outs() << "Killed cells: {";
          for (unsigned j = 0; j < cells.size();) {
            crab::outs() << cells[j];
            ++j;
            if (j < cells.size()) {
              crab::outs() << ",";
            }
          } crab::outs()
          << "}\n";);
      kill_cells(cells, offset_map);
    }

    CRAB_LOG("array-expansion", linear_expression_t ub = i + elem_size - 1;
             crab::outs() << a << "[" << i << "..." << ub << "]:=" << val
                          << " -- " << *this << "\n";);
  }

  virtual void array_store(variable_t a_new, variable_t a_old,
                           linear_expression_t elem_size, linear_expression_t i,
                           linear_expression_t val,
                           bool /*is_strong_update*/) override {
    CRAB_WARN("array_store in the array expansion domain not implemented");
  }

  // Perform array stores over an array segment [lb_idx, ub_idx]
  virtual void array_store_range(variable_t a, linear_expression_t elem_size,
                                 linear_expression_t lb_idx,
                                 linear_expression_t ub_idx,
                                 linear_expression_t val) override {
    crab::CrabStats::count(getDomainName() + ".count.array_store_range");
    crab::ScopedCrabStats __st__(getDomainName() + ".array_store_range");

    // TODO: this should be an user parameter.
    const number_t max_num_elems = 512;

    if (is_bottom())
      return;

    interval_t n_i = to_interval(elem_size);
    auto n = n_i.singleton();
    if (!n) {
      CRAB_ERROR("array expansion domain expects constant array element sizes");
    }

    interval_t lb_i = to_interval(lb_idx);
    auto lb = lb_i.singleton();
    if (!lb) {
      CRAB_WARN("array expansion store range ignored because ",
                "lower bound is not constant");
      return;
    }

    interval_t ub_i = to_interval(ub_idx);
    auto ub = ub_i.singleton();
    if (!ub) {
      CRAB_WARN("array expansion store range ignored because ",
                "upper bound is not constant");
      return;
    }

    z_number sz = (*ub - *lb) + 1;
    if (sz > max_num_elems) {
      CRAB_WARN("array expansion store range ignored because ",
                "the number of elements is larger than default limit of ",
                max_num_elems);
      return;
    }

    for (number_t i = *lb, e = *ub; i <= e;) {
      array_store(a, elem_size, i, val, false);
      i = i + *n;
    }
  }

  virtual void array_store_range(variable_t a_new, variable_t a_old,
                                 linear_expression_t elem_size,
                                 linear_expression_t lb_idx,
                                 linear_expression_t ub_idx,
                                 linear_expression_t val) override {
    CRAB_WARN(
        "array_store_range in the array expansion domain not implemented");
  }

  virtual void array_assign(variable_t lhs, variable_t rhs) override {
    CRAB_WARN("array_assign in array_expansion domain not implemented");
  }

  // backward array operations

  virtual void
  backward_array_init(variable_t a, linear_expression_t elem_size,
                      linear_expression_t lb_idx, linear_expression_t ub_idx,
                      linear_expression_t val,
                      array_expansion_domain_t invariant) override {
    crab::CrabStats::count(getDomainName() + ".count.backward_array_init");
    crab::ScopedCrabStats __st__(getDomainName() + ".backward_array_init");

    if (is_bottom())
      return;

    // make all array cells uninitialized
    offset_map_t &offset_map = lookup_array_map(a);
    std::vector<cell_t> old_cells = offset_map.get_all_cells();
    if (!old_cells.empty()) {
      kill_cells(old_cells, offset_map);
    }

    // meet with forward invariant
    *this = *this & invariant;
  }

  virtual void
  backward_array_load(variable_t lhs, variable_t a,
                      linear_expression_t elem_size, linear_expression_t i,
                      array_expansion_domain_t invariant) override {
    crab::CrabStats::count(getDomainName() + ".count.backward_array_load");
    crab::ScopedCrabStats __st__(getDomainName() + ".backward_array_load");

    if (is_bottom())
      return;

    // XXX: we use the forward invariant to extract the array index
    interval_t ii = to_interval(i, invariant.get_content_domain());
    if (boost::optional<number_t> n = ii.singleton()) {
      offset_map_t &offset_map = lookup_array_map(a);
      offset_t o(static_cast<int64_t>(*n));
      interval_t i_elem_size =
          to_interval(elem_size, invariant.get_content_domain());
      if (boost::optional<number_t> n_bytes = i_elem_size.singleton()) {
        assert(static_cast<int64_t>(*n_bytes) > 0 &&
               static_cast<int64_t>(*n_bytes) <=
                   std::numeric_limits<uint64_t>::max());
        uint64_t size = static_cast<int64_t>(*n_bytes);
        cell_t c = offset_map.mk_cell(a, o, size);
        assert(c.has_scalar());
        do_backward_assign(lhs, c, invariant.get_content_domain());
      } else {
        CRAB_ERROR(
            "array expansion domain expects constant array element sizes");
      }
    } else {
      CRAB_LOG("array-expansion",
               CRAB_WARN("array index is not a constant value"););
      // -- Forget lhs
      _inv -= lhs;
      // -- Meet with forward invariant
      *this = *this & invariant;
    }

    CRAB_LOG("array-expansion", linear_expression_t ub = i + elem_size - 1;
             crab::outs() << "BACKWARD " << lhs << ":=" << a << "[" << i
                          << "..." << ub << "]  -- " << *this << "\n";);
  }

  virtual void
  backward_array_store(variable_t a, linear_expression_t elem_size,
                       linear_expression_t i, linear_expression_t val,
                       bool /*is_strong_update*/,
                       array_expansion_domain_t invariant) override {
    crab::CrabStats::count(getDomainName() + ".count.backward_array_store");
    crab::ScopedCrabStats __st__(getDomainName() + ".backward_array_store");

    if (is_bottom())
      return;

    // XXX: we use the forward invariant to extract the array index
    interval_t i_elem_size =
        to_interval(elem_size, invariant.get_content_domain());
    boost::optional<number_t> n_bytes = i_elem_size.singleton();
    if (!n_bytes) {
      CRAB_ERROR("array expansion domain expects constant array element sizes");
    }

    assert(static_cast<int64_t>(*n_bytes) > 0 &&
           static_cast<int64_t>(*n_bytes) <=
               std::numeric_limits<uint64_t>::max());
    uint64_t size = static_cast<int64_t>(*n_bytes);
    offset_map_t &offset_map = lookup_array_map(a);
    // XXX: we use the forward invariant to extract the array index
    interval_t ii = to_interval(i, invariant.get_content_domain());
    if (boost::optional<number_t> n = ii.singleton()) {
      // -- Constant index and the store updated one single cell:
      // -- backward assign in the base domain.
      offset_t o(static_cast<int64_t>(*n));
      std::vector<cell_t> cells;
      offset_map.get_overlap_cells(o, size, cells);
      // post: forall c \in cells:: c != [o,size)
      // that is, get_overlap_cells returns cells different from [o, size)
      if (cells.size() >= 1) {
        kill_cells(cells, offset_map);
        *this = *this & invariant;
      } else {
        // c might be in _inv or not.
        cell_t c = offset_map.mk_cell(a, o, size);
        do_backward_assign(c, val, invariant.get_content_domain());
      }
    } else {
      // -- Non-constant index or multiple overlapping cells: kill
      // -- overlapping cells and meet with forward invariant.
      linear_expression_t symb_lb(i);
      linear_expression_t symb_ub(i + number_t(size - 1));
      std::vector<cell_t> cells;
      offset_map.get_overlap_cells_symbolic_offset(
          invariant.get_content_domain(), symb_lb, symb_ub, cells);
      kill_cells(cells, offset_map);
      *this = *this & invariant;
    }

    CRAB_LOG("array-expansion", linear_expression_t ub = i + elem_size - 1;
             crab::outs() << "BACKWARD " << a << "[" << i << "..." << ub
                          << "]:=" << val << " -- " << *this << "\n";);
  }

  virtual void
  backward_array_store(variable_t a_new, variable_t a_old,
                       linear_expression_t elem_size, linear_expression_t i,
                       linear_expression_t val, bool /*is_strong_update*/,
                       array_expansion_domain_t invariant) override {
    CRAB_WARN("backward_array_store in array_expansion domain not implemented");
  }

  virtual void backward_array_store_range(
      variable_t a, linear_expression_t elem_size, linear_expression_t lb_idx,
      linear_expression_t ub_idx, linear_expression_t val,
      array_expansion_domain_t invariant) override {
    crab::CrabStats::count(getDomainName() +
                           ".count.backward_array_store_range");
    crab::ScopedCrabStats __st__(getDomainName() +
                                 ".count.backward_array_store_range");

    // TODO: this should be an user parameter.
    const number_t max_num_elems = 512;

    if (is_bottom())
      return;

    interval_t n_i = to_interval(elem_size, invariant.get_content_domain());
    auto n = n_i.singleton();
    if (!n) {
      CRAB_ERROR("array expansion domain expects constant array element sizes");
    }

    interval_t lb_i = to_interval(lb_idx, invariant.get_content_domain());
    auto lb = lb_i.singleton();
    if (!lb) {
      return;
    }

    interval_t ub_i = to_interval(ub_idx, invariant.get_content_domain());
    auto ub = ub_i.singleton();
    if (!ub || ((*ub - *lb) + 1) > max_num_elems) {
      return;
    }

    for (number_t i = *lb, e = *ub; i <= e;) {
      backward_array_store(a, elem_size, i, val, false, invariant);
      i = i + *n;
    }
  }

  virtual void backward_array_store_range(
      variable_t a_new, variable_t a_old, linear_expression_t elem_size,
      linear_expression_t lb_idx, linear_expression_t ub_idx,
      linear_expression_t val, array_expansion_domain_t invariant) override {
    CRAB_WARN(
        "backward_array_store_range in array_expansion domain not implemented");
  }

  virtual void
  backward_array_assign(variable_t lhs, variable_t rhs,
                        array_expansion_domain_t invariant) override {
    CRAB_WARN(
        "backward_array_assign in array_expansion domain not implemented");
  }

  linear_constraint_system_t to_linear_constraint_system() {
    crab::CrabStats::count(getDomainName() +
                           ".count.to_linear_constraint_system");
    crab::ScopedCrabStats __st__(getDomainName() +
                                 ".to_linear_constraint_system");

    return _inv.to_linear_constraint_system();
  }

  disjunctive_linear_constraint_system_t
  to_disjunctive_linear_constraint_system() {
    return _inv.to_disjunctive_linear_constraint_system();
  }

  NumDomain get_content_domain() const { return _inv; }

  NumDomain &get_content_domain() { return _inv; }

  /* begin intrinsics operations */
  void intrinsic(std::string name,
		 const variable_vector_t &inputs,
		 const variable_vector_t &outputs) override {
    CRAB_WARN("Intrinsics ", name, " not implemented by ", getDomainName());
  }

  void backward_intrinsic(std::string name,
			  const variable_vector_t &inputs,
			  const variable_vector_t &outputs,
			  array_expansion_domain_t invariant) override {
    CRAB_WARN("Intrinsics ", name, " not implemented by ", getDomainName());
  }
  /* end intrinsics operations */

  void write(crab_os &o) { o << _inv; }

  static std::string getDomainName() {
    std::string name("ArrayExpansion(" + NumDomain::getDomainName() + ")");
    return name;
  }

  void rename(const variable_vector_t &from, const variable_vector_t &to) {
    CRAB_WARN("TODO: array_expansion rename");
  }

}; // end array_expansion_domain

template <typename BaseDomain>
struct abstract_domain_traits<array_expansion_domain<BaseDomain>> {
  typedef typename BaseDomain::number_t number_t;
  typedef typename BaseDomain::varname_t varname_t;
};

template <typename BaseDom>
class checker_domain_traits<array_expansion_domain<BaseDom>> {
public:
  typedef array_expansion_domain<BaseDom> this_type;
  typedef typename this_type::linear_constraint_t linear_constraint_t;
  typedef typename this_type::disjunctive_linear_constraint_system_t
      disjunctive_linear_constraint_system_t;

  static bool entail(this_type &lhs,
                     const disjunctive_linear_constraint_system_t &rhs) {
    BaseDom &lhs_dom = lhs.get_content_domain();
    return checker_domain_traits<BaseDom>::entail(lhs_dom, rhs);
  }

  static bool entail(const disjunctive_linear_constraint_system_t &lhs,
                     this_type &rhs) {
    BaseDom &rhs_dom = rhs.get_content_domain();
    return checker_domain_traits<BaseDom>::entail(lhs, rhs_dom);
  }

  static bool entail(this_type &lhs, const linear_constraint_t &rhs) {
    BaseDom &lhs_dom = lhs.get_content_domain();
    return checker_domain_traits<BaseDom>::entail(lhs_dom, rhs);
  }

  static bool intersect(this_type &inv, const linear_constraint_t &cst) {
    BaseDom &dom = inv.get_content_domain();
    return checker_domain_traits<BaseDom>::intersect(dom, cst);
  }
};

template <typename BaseDom>
class special_domain_traits<array_expansion_domain<BaseDom>> {
public:
  static void clear_global_state(void) {
    array_expansion_domain<BaseDom>::clear_global_state();
  }
};

} // namespace domains
} // namespace crab


//
// OpenTissue, A toolbox for physical based simulation and animation.
// Copyright (C) 2007 Department of Computer Science, University of Copenhagen
//
#include <OpenTissue/configuration.h>

#include <OpenTissue/core/math/math_basic_types.h>
#include <OpenTissue/collision/gjk/gjk_outside_edge_face_voronoi_plane.h>

#define BOOST_AUTO_TEST_MAIN
#include <OpenTissue/utility/utility_push_boost_filter.h>
#include <boost/test/auto_unit_test.hpp>
#include <boost/test/unit_test_suite.hpp>
#include <boost/test/floating_point_comparison.hpp>
#include <boost/test/test_tools.hpp>
#include <OpenTissue/utility/utility_pop_boost_filter.h>

#include <cmath>

using namespace OpenTissue;

BOOST_AUTO_TEST_SUITE(opentissue_collision_gjk_outside_edge_face);

BOOST_AUTO_TEST_CASE(case_by_case_test)
{
  typedef OpenTissue::math::BasicMathTypes<double, size_t> math_types;
  typedef math_types::vector3_type                         V;

  V const a = V(0.0, 0.0, 0.0);
  V const b = V(1.0, 0.0, 0.0);;
  V const c = V(0.0, 1.0, 0.0);;

  // Front side of AB voronoi plane
  {
    V p = V(-0.5, -1.0,  1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, b, c);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, a, c);
    BOOST_CHECK( outside2 );
  }
  // Back side of AB voronoi plane
  {
    V p = V(-0.5, 1.0,  1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, b, c);
    BOOST_CHECK( !outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, a, c);
    BOOST_CHECK( !outside2 );
  }
  // In AB voronoi plane
  {
    V p = V(-0.5, 0.0,  1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, b, c);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, a, c);
    BOOST_CHECK( outside2 );
  }
  // Front side of AC voronoi plane
  {
    V p = V(-1.0, 0.5,  1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, c, b);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, a, b);
    BOOST_CHECK( outside2 );
  }
  // Back side of AC voronoi plane
  {
    V p = V( 1.0, 0.5,  1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, c, b);
    BOOST_CHECK( !outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, a, b);
    BOOST_CHECK( !outside2 );
  }
  // In AC voronoi plane
  {
    V p = V( 0.0, 0.5,  1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, c, b);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, a, b);
    BOOST_CHECK( outside2 );
  }
  // Front side of BC voronoi plane
  {
    V p = V( 1.0, 1.0,  1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, c, a);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, b, a);
    BOOST_CHECK( outside2 );
  }
  // Back side of BC voronoi plane
  {
    V p = V( 0.0, 0.0,  1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, c, a);
    BOOST_CHECK( !outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, b, a);
    BOOST_CHECK( !outside2 );
  }
  // In BC voronoi plane
  {
    V p = V( 0.5, 0.5,  1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, c, a);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, b, a);
    BOOST_CHECK( outside2 );
  }

  // We just used a point above the face plane, next we will try using a test point lying in the face plane

  // Front side of AB voronoi plane
  {
    V p = V(-0.5, -1.0,  0.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, b, c);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, a, c);
    BOOST_CHECK( outside2 );
  }
  // Back side of AB voronoi plane
  {
    V p = V(-0.5, 1.0,  0.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, b, c);
    BOOST_CHECK( !outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, a, c);
    BOOST_CHECK( !outside2 );
  }
  // In AB voronoi plane
  {
    V p = V(-0.5, 0.0,  0.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, b, c);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, a, c);
    BOOST_CHECK( outside2 );
  }
  // Front side of AC voronoi plane
  {
    V p = V(-1.0, 0.5,  0.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, c, b);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, a, b);
    BOOST_CHECK( outside2 );
  }
  // Back side of AC voronoi plane
  {
    V p = V( 1.0, 0.5,  0.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, c, b);
    BOOST_CHECK( !outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, a, b);
    BOOST_CHECK( !outside2 );
  }
  // In AC voronoi plane
  {
    V p = V( 0.0, 0.5,  0.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, c, b);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, a, b);
    BOOST_CHECK( outside2 );
  }
  // Front side of BC voronoi plane
  {
    V p = V( 1.0, 1.0,  0.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, c, a);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, b, a);
    BOOST_CHECK( outside2 );
  }
  // Back side of BC voronoi plane
  {
    V p = V( 0.0, 0.0,  0.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, c, a);
    BOOST_CHECK( !outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, b, a);
    BOOST_CHECK( !outside2 );
  }
  // In BC voronoi plane
  {
    V p = V( 0.5, 0.5,  0.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, c, a);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, b, a);
    BOOST_CHECK( outside2 );
  }

  // Finally we will use a test point lying below the face-plane

  // Front side of AB voronoi plane
  {
    V p = V(-0.5, -1.0,  -1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, b, c);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, a, c);
    BOOST_CHECK( outside2 );
  }
  // Back side of AB voronoi plane
  {
    V p = V(-0.5, 1.0,  -1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, b, c);
    BOOST_CHECK( !outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, a, c);
    BOOST_CHECK( !outside2 );
  }
  // In AB voronoi plane
  {
    V p = V(-0.5, 0.0,  -1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, b, c);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, a, c);
    BOOST_CHECK( outside2 );
  }
  // Front side of AC voronoi plane
  {
    V p = V(-1.0, 0.5,  -1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, c, b);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, a, b);
    BOOST_CHECK( outside2 );
  }
  // Back side of AC voronoi plane
  {
    V p = V( 1.0, 0.5,  -1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, c, b);
    BOOST_CHECK( !outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, a, b);
    BOOST_CHECK( !outside2 );
  }
  // In AC voronoi plane
  {
    V p = V( 0.0, 0.5,  -1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, a, c, b);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, a, b);
    BOOST_CHECK( outside2 );
  }
  // Front side of BC voronoi plane
  {
    V p = V( 1.0, 1.0,  -1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, c, a);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, b, a);
    BOOST_CHECK( outside2 );
  }
  // Back side of BC voronoi plane
  {
    V p = V( 0.0, 0.0, -1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, c, a);
    BOOST_CHECK( !outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, b, a);
    BOOST_CHECK( !outside2 );
  }
  // In BC voronoi plane
  {
    V p = V( 0.5, 0.5, -1.0);
    bool outside = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, b, c, a);
    BOOST_CHECK( outside );

    bool outside2 = OpenTissue::gjk::detail::outside_edge_face_voronoi_plane(p, c, b, a);
    BOOST_CHECK( outside2 );
  }

}

BOOST_AUTO_TEST_SUITE_END();


// Copyright © 2016-2019 Thomas Nagler and Thibault Vatter
//
// This file is part of the vinecopulib library and licensed under the terms of
// the MIT license. For a copy, see the LICENSE file in the root directory of
// vinecopulib or https://vinecopulib.github.io/vinecopulib/.

#pragma once

#include <Eigen/Dense>
#include <vinecopulib/misc/triangular_array.hpp>

namespace vinecopulib {

//! @brief R-vine structures
//!
//! RVineStructure objects encode the tree structure of the vine, i.e. the
//! conditioned/conditioning variables of each edge. It is represented by a
//! triangular array. An exemplary array is
//! ```
//! 4 4 4 4
//! 3 3 3
//! 2 2
//! 1
//! ```
//! which encodes the following pair-copulas:
//! ```
//! | tree | edge | pair-copulas   |
//! |------|------|----------------|
//! | 0    | 0    | `(1, 4)`       |
//! |      | 1    | `(2, 4)`       |
//! |      | 2    | `(3, 4)`       |
//! | 1    | 0    | `(1, 3; 4)`    |
//! |      | 1    | `(2, 3; 4)`    |
//! | 2    | 0    | `(1, 2; 3, 4)` |
//! ```
//! Denoting by `M[i, j]` the array entry in row `i` and column `j`,
//! the pair-copula index for edge `e` in tree `t` of a `d` dimensional vine
//! is `(M[d - 1 - t, e], M[t, e]; M[t - 1, e], ..., M[0, e])`. Less
//! formally,
//! 1. Start with the counter-diagonal element of column `e` (first conditioned
//!    variable).
//! 2. Jump up to the element in row `t` (second conditioned variable).
//! 3. Gather all entries further up in column `e` (conditioning set).
//!
//! A valid R-vine array must satisfy several conditions which are checked
//! when `RVineStructure()` is called:
//! 1. It only contains numbers between 1 and d.
//! 2. The diagonal must contain the numbers 1, ..., d.
//! 3. The diagonal entry of a column must not be contained in any
//!    column further to the right.
//! 4. The entries of a column must be contained in all columns to the left.
//! 5. The proximity condition must hold: For all t = 1, ..., d - 2 and
//!    e = 0, ..., d - t - 1 there must exist an index j > d, such that
//!    `(M[t, e], {M[0, e], ..., M[t-1, e]})` equals either
//!    `(M[d-j-1, j], {M[0, j], ..., M[t-1, j]})` or
//!    `(M[t-1, j], {M[d-j-1, j], M[0, j], ..., M[t-2, j]})`.
//!
//! An R-vine array is said to be in natural order when the anti-diagonal
//! entries are \f$ 1, \dots, d \f$ (from left to right). The exemplary arrray
//! above is in natural order. Any R-vine array can be characterized by the
//! diagonal entries (called order) and the entries below the diagonal of the
//! corresponding R-vine array in natural order. Since most algorithms work
//! with the structure in natural order, this is how RVineStructure stores the
//! structure internally.
class RVineStructure {
public:
    RVineStructure() {}

    RVineStructure(
        const Eigen::Matrix<size_t, Eigen::Dynamic, Eigen::Dynamic>& mat,
        bool check = true);
    RVineStructure(const std::vector<size_t>& order,
                   bool check = true);
    RVineStructure(const std::vector<size_t>& order,
                   const size_t& trunc_lvl,
                   bool check = true);
    RVineStructure(const std::vector<size_t>& order,
                   const TriangularArray<size_t>& struct_array,
                   bool is_natural_order = false,
                   bool check = true);

    size_t get_dim() const;
    size_t get_trunc_lvl() const;
    std::vector<size_t> get_order() const;
    TriangularArray<size_t> get_struct_array() const;
    TriangularArray<size_t> get_min_array() const;
    TriangularArray<size_t> get_needed_hfunc1() const;
    TriangularArray<size_t> get_needed_hfunc2() const;
    Eigen::Matrix<size_t, Eigen::Dynamic, Eigen::Dynamic> get_matrix() const;

    size_t struct_array(size_t tree, size_t edge) const;
    size_t min_array(size_t tree, size_t edge) const;

    void truncate(size_t trunc_lvl);
    std::string str() const;

protected:
    size_t find_trunc_lvl(
        const Eigen::Matrix<size_t, Eigen::Dynamic, Eigen::Dynamic>& mat) const;
    std::vector<size_t> get_order(
        const Eigen::Matrix<size_t, Eigen::Dynamic, Eigen::Dynamic>& mat) const;
    TriangularArray<size_t> to_rvine_array(
        const Eigen::Matrix<size_t, Eigen::Dynamic, Eigen::Dynamic>& mat) const;

    TriangularArray<size_t> to_natural_order() const;
    TriangularArray<size_t> compute_dvine_struct_array() const;
    TriangularArray<size_t> compute_min_array() const;
    TriangularArray<size_t> compute_needed_hfunc1() const;
    TriangularArray<size_t> compute_needed_hfunc2() const;

    void check_if_quadratic(
        const Eigen::Matrix<size_t, Eigen::Dynamic, Eigen::Dynamic>& mat) const;
    void check_lower_tri(
        const Eigen::Matrix<size_t, Eigen::Dynamic, Eigen::Dynamic>& mat) const;
    void check_upper_tri() const;
    void check_columns() const;
    void check_antidiagonal() const;
    void check_proximity_condition() const;

    std::vector<size_t> order_;
    size_t d_;
    size_t trunc_lvl_;
    TriangularArray<size_t> struct_array_;
    TriangularArray<size_t> min_array_;
    TriangularArray<size_t> needed_hfunc1_;
    TriangularArray<size_t> needed_hfunc2_;
};

std::ostream& operator<<(std::ostream& os, const RVineStructure& rvs);

}

#include <vinecopulib/vinecop/implementation/rvine_structure.ipp>