/* This file is part of brille

Copyright © 2019-2022 Greg Tucker <gregory.tucker@ess.eu>

brille is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.

brille 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 GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with brille. If not, see <https://www.gnu.org/licenses/>.            */

template<class T>
Array2<T> Array2<T>::view() const{
  return Array2<T>(_data,_num,_shift,_own,_ref,_shape,_stride,false);
}
template<class T>
Array2<T> Array2<T>::view(const ind_t i) const {
  if (i<_shape[0]){
    shape_t osize{_shape};
    ind_t oshft{_shift + i*_stride[0]};
    osize[0] = 1;
    return Array2<T>(_data, _num, oshft, _own, _ref, osize, _stride, false);
  }
  throw std::runtime_error("Array2 index too large");
}

template<class T>
Array2<T> Array2<T>::view(const ind_t i, const ind_t j) const {
  if (i<j && i<_shape[0] && j<=_shape[0]){
    shape_t osize{_shape};
    ind_t oshft{_shift + i*_stride[0]};
    osize[0] = j-i;
    return Array2<T>(_data, _num, oshft, _own, _ref, osize, _stride, false);
  }
  if (i==j) {
    shape_t shape{_shape};
    shape[0] = 0;
    return Array2<T>(shape);
  }
  throw std::runtime_error("Array2 view indexing error");
}

template<class T>
Array2<T> Array2<T>::view(const shape_t& v) const {
  shape_t osize{_shape};
  ind_t oshft{_shift};
  for (size_t i=0; i<_shape.size(); ++i) if (v[i] < _shape[i]) {
    osize[i] -= v[i];
    oshft += v[i]*_stride[i];
  }
  return Array2<T>(_data, _num, oshft, _own, _ref, osize, _stride, false);
}

template<class T>
Array2<T>
Array2<T>::extract(const ind_t i) const{
  return this->view(i).decouple();
}

template<class T>
template<class I>
std::enable_if_t<std::is_integral_v<I>, Array2<T>>
Array2<T>::extract(const Array2<I>& i) const {
  if (!i.is_vector())
    throw std::runtime_error("Array2 extraction requires (N, 1) or (1, N) shape");
  for (auto x: i.valItr()) if (!(0 <= x && static_cast<ind_t>(x) < _shape[0]))
    throw std::runtime_error("Array2 extract index must be in range");
  shape_t osize{_shape};
  osize[0] = i.numel();
  Array2<T> out(osize);
  ind_t j{0};
  for (auto v: i.valItr()) out.set(j++, this->view(static_cast<ind_t>(v)));
  return out;
}

template<class T>
template<class I>
std::enable_if_t<std::is_integral_v<I>, Array2<T>>
Array2<T>::extract(const std::vector<I>& i) const {
  for (auto x: i) if (!(0 <= x && static_cast<ind_t>(x) < _shape[0]))
    throw std::runtime_error("Array2 extract index must be in range");
  shape_t osize{_shape};
  osize[0] = static_cast<ind_t>(i.size());
  Array2<T> out(osize);
  for (ind_t j=0; j<osize[0]; ++j) out.set(j, this->view(static_cast<ind_t>(i[j])));
  return out;
}

template<class T>
template<class I, size_t Nel>
std::enable_if_t<std::is_integral_v<I>, Array2<T>>
Array2<T>::extract(const std::array<I,Nel>& i) const
{
  for (auto x: i) if (!(0 <= x && static_cast<ind_t>(x) < _shape[0]))
    throw std::runtime_error("Array2 extract index must be in range");
  shape_t osize{_shape};
  osize[0] = static_cast<ind_t>(Nel);
  Array2<T> out(osize);
  for (ind_t j=0; j<osize[0]; ++j)
  {
    out.set(j, this->view(static_cast<ind_t>(i[j])));
  }
  return out;
}

// template<class T>
// template<class I, size_t Nel>
// std::enable_if_t<std::is_integral_v<I>, Array<T>>
// Array2<T>::extract(const std::vector<std::array<I,Nel>>& i) const
// {
//   for (auto a: i) for (auto x: a) if (!(0 <= x && static_cast<ind_t>(x) < _shape[0]))
//     throw std::runtime_error("Array2 extract index must be in range");
//   brille::shape_t osh{static_cast<ind_t>(i.size()), static_cast<ind_t>(Nel)};
//   for (ind_t i=1; i<this->ndim(); ++i)
//     osh.push_back(_shape[i]);
//   Array<T> out(osh);
//   shape_t xi{_shape};
//   for (ind_t a=0; a<i.size(); ++a)
//   {
//     osh[0] = a;
//     for (ind_t b=0; b<Nel; ++b)
//     {
//       osh[1] = b;
//       xi[0] = static_cast<ind_t>(i[a][b]);
//       for (auto sub: SubIt2(_shape, xi))
//       {
//         for (ind_t j=0; j<_shape.size(); ++j) osh[j+1]=sub[j];
//         out[sub] = _data[this->s2l_d(xi)];
//       }
//     }
//   }
//   return out;
// }

template<class T>
Array2<T>
Array2<T>::extract(const Array2<bool>& i) const {
  shape_t isize = i.shape();
  if (isize[0] > _shape[0])
    throw std::runtime_error("Boolean Array2 extraction requires no more bools than the first Array2 dimension");
  shape_t osize{_shape};
  osize[0] = i.count();
  Array2<T> out(osize);
  ind_t n = isize[0] < _shape[0] ? isize[0] : _shape[0];
  ind_t idx{0};
  for (ind_t j=0; j<n; ++j) if (i[j]) out.set(idx++, this->view(j));
  return out;
}

template<class T>
Array2<T>
Array2<T>::extract(const std::vector<bool>& i) const {
  if (i.size() > _shape[0])
    throw std::runtime_error("Boolean Array2 extraction requires no more bools than the first Array2 dimension");
  shape_t osize{_shape};
  auto count = std::count(i.begin(), i.end(), true);
  osize[0] = static_cast<ind_t>(count);
  Array2<T> out(osize);
  ind_t isize{static_cast<ind_t>(i.size())};
  ind_t n = isize < _shape[0] ? isize : _shape[0];
  ind_t idx{0};
  for (ind_t j=0; j<n; ++j) if (i[j]) out.set(idx++, this->view(j));
  return out;
}

template<class T>
bool Array2<T>::set(const ind_t i, const Array2<T>& in){
  // we might be able to do this better/faster if we both *this and in
  // have the same strides_ and we account for any offset. For now calculate
  // the 'hard' way no matter what:
  shape_t inshape = in.shape();
  if (_shape[1] != inshape[1])
    throw std::runtime_error("Set requires equal dimensions beyond the first dimension");
  inshape[0] = 1u; // this *should* be the case anyway
  for(auto sub: SubIt2<ind_t>(inshape)){
    const T& tmp = in[sub];
    sub[0] = i; // [0,0,…,0],[0,0,…,1],…,[0,1,…,0],… to [i,0,…,0],[i,0,…,1],…,[i,1,…,0],…
    _data[this->s2l_d(sub)] = tmp;
  }
  return true;
}

template<class T>
template<class R>
bool Array2<T>::set(const ind_t i, const Array2<R>& in){
  // we might be able to do this better/faster if we both *this and in
  // have the same strides_ and we account for any offset. For now calculate
  // the 'hard' way no matter what:
  shape_t inshape = in.shape();
  if (_shape[1] != inshape[1])
    throw std::runtime_error("Set requires equal dimensions beyond the first dimension");
  inshape[0] = 1u; // this *should* be the case anyway
  for(auto sub: SubIt2<ind_t>(inshape)){
    const R& tmp = in[sub];
    sub[0] = i; // [0,0,…,0],[0,0,…,1],…,[0,1,…,0],… to [i,0,…,0],[i,0,…,1],…,[i,1,…,0],…
    _data[this->s2l_d(sub)] = static_cast<T>(tmp);
  }
  return true;
}

template<class T>
bool Array2<T>::set(const ind_t i, const std::vector<T>& in){
  if (this->numel() != _shape[0]*in.size())
    throw std::runtime_error("Set requires the correct number of elements");
  shape_t tsize = this->shape();
  tsize[0] = 1u;
  // in is (hopefully) a row-ordered linear indexing
  size_t idx{0};
  for (auto sub: SubIt2<ind_t>(tsize)){
    sub[0] = i;
    _data[this->s2l_d(sub)] = in[idx++];
  }
  return true;
}

template<class T>
template<size_t Nel>
bool Array2<T>::set(const ind_t i, const std::array<T, Nel>& in){
  if (this->numel() != _shape[0]*Nel)
    throw std::runtime_error("Set requires the correct number of elements");
  shape_t tsize = this->shape();
  tsize[0] = 1u;
  // in is (hopefully) a row-ordered linear indexing
  size_t idx{0};
  for (auto sub: SubIt2<ind_t>(tsize)){
    sub[0] = i;
    _data[this->s2l_d(sub)] = in[idx++];
  }
  return true;
}

template<class T>
T Array2<T>::set(const shape_t& sub, const T in){
  auto ind = this->s2l_d(sub);
  _data[ind] = in;
  return _data[ind];
}

template<class T>
std::string Array2<T>::to_string() const{
  if (this->_num == 0) return std::string("Unallocated Array2\n");
  size_t width{0};
  // If we loop from i=0 to i<_num we cover the *whole* array, not just the part we are viewing!
  for (auto sub: this->subItr()){
    auto ms = my_to_string(_data[this->s2l_d(sub)]);
    size_t l = ms.size();
    if (l > width) width = l;
  }
  std::string out;
  std::vector<bool> isin(this->ndim(), false);
  bool preamble{false};
  size_t ndim = _shape.size();
  for (auto sub: this->subItr()){
    if (!preamble){
      for (ind_t i=0; i<ndim-1; ++i)
      if (sub[i] == 0 && !isin[i]){
        out += "[";
        isin[i] = true;
      }
      else
        out += " ";
      out += "["; // for i=ndim-1;
      preamble=true;
    }
    out += my_to_string(_data[this->s2l_d(sub)], width);
    // out += std::to_string(_data[this->s2l_d(sub)]);
    if (sub[ndim-1]+1 < _shape[ndim-1]){
      out += ", ";
    } else {
      for (ind_t i=0; i<ndim-1; ++i){
        bool isend{true};
        for (ind_t k=i; k<ndim; k++) isend &= sub[k]+1 == _shape[k];
        if (isend){
          out += "]";
          isin[i] = false;
        }
      }
      out += "],"; // ']' for i=ndim-1;
      size_t nret = std::count(isin.begin(), isin.end(), false);
      for (size_t n=0; n<nret; ++n) out += "\n";
      preamble=false;
    }
  }
  for (size_t i=0; i<ndim; ++i) out.pop_back(); // remove the trailing '\n's
  out.pop_back(); // and the trailing ,
  out += "\n";
  return out;
}

template<class T>
std::string Array2<T>::to_string(const ind_t i) const {
  auto out = this->view(i).to_string();
  out.pop_back(); // remove the trailing \n
  return out;
}

template<class T>
Array2<T>&
Array2<T>::reshape(const shape_t& ns){
  ind_t num = this->size_from_shape(ns);
  info_update_if(num > _num, "Array2::reshape only intended for equal-element number changes.");
  // assert( num <= _num );
  if (!this->is_contiguous())
    throw std::runtime_error("Array2::reshape does not work for strided arrays");
  _shape = ns;
  _stride = this->calculate_stride(ns);
  return *this;
}

/*! \brief Modify the number of arrays

Allocate a new T* and copy the overlapping region. The new shape
*must* have the same number of dimensions as the old shape.

Resizing an array will decouple it from any other arrays sharing their
underlying data. Such an operation should not be taken lightly.

@param ns the new shape
@param init an optional initialization value for any non-overlapping region
*/
template<class T>
Array2<T>&
Array2<T>::resize(const shape_t& ns, const T init) {
  ind_t nnum = this->size_from_shape(ns);
  // determine which axes are changing and how
  std::vector<bool> shrinking, growing;
  for (size_t i=0; i<ns.size(); ++i){
    shrinking.push_back(ns[i] <= _shape[i]);
    growing.push_back(ns[i] >= _shape[i]);
  }
  bool shrink = std::all_of(shrinking.begin(), shrinking.end(), [](bool a){return a;});
  bool grow = !shrink && std::all_of(growing.begin(), growing.end(), [](bool a){return a;});
  if (!shrink && !grow){
    std::string msg = "This method is not able to resize from { ";
    for (auto x : _shape) msg += std::to_string(x) + " ";
    msg += "} to { ";
    for (auto x : ns) msg += std::to_string(x) + " ";
    msg += "}";
    throw std::runtime_error(msg);
  }
  // make the new array
  T* nd = new T[nnum]();
  // fill the whole thing instead of trying to figure out difference indices
  if (grow) std::fill(nd, nd+nnum, init);
  // loop over the subscripted incides which fit inside both arrays
  shape_t inside = shrink ? ns : _shape;
  // find the new stride (maintaining row or column order)
  shape_t nt = calculate_stride(ns);
  // ensure our simple indexing will work
  if (_shift > 0)
    throw std::runtime_error("Resizing only works for zero-offset Array2s at present. Please extend.");
  // copy into the new array if the old array isn't the null pointer
  if (_data != nullptr && nd != nullptr)
    for (auto x : SubIt2(inside)) nd[sub2lin(x, nt)] = _data[sub2lin(x, _stride)];
  // delete the old _data if we hold the only reference
  if (_own && _ref.use_count()==1 && _data != nullptr) delete[] _data;
  // update data, number, ownership, reference, size, stride, and mutability
  _data = nd;
  _num = nnum;
  _own = true;
  _ref = std::make_shared<char>(); // resizing in place can not change the template parameter
  _shape = ns;
  _stride = nt;
  _mutable = true;
  // pass back a reference to this object
  return *this;
}
template<class T> template<class I>
Array2<T>&
Array2<T>::resize(const I ns, const T init) {
  shape_t nshape{_shape};
  if (nshape.size()>0)
    nshape[0] = static_cast<ind_t>(ns);
  else{
    throw std::runtime_error("Resizing a null-shaped Array2 not supported (yet)");
    //nshape.push_back(static_cast<ind_t>(ns));
  }
  return this->resize(nshape, init);
}

template<class T>
template<class R>
std::enable_if_t<std::is_convertible_v<R,T>, Array2<T>&>
Array2<T>::append(const ind_t dim, const Array2<R>& extra) {
  if constexpr (std::is_same_v<T, R>) {
      assert(this != &extra);
  }
  ind_t ndim = this->ndim();
  assert(dim<ndim);
  auto eshape = extra.shape();
  for (size_t i=0; i<ndim; ++i) if (i!=dim && eshape[i]!=_shape[i])
    throw std::runtime_error("Incompatible Array2 to append");
  // the new expanded size only changes along dim
  eshape[dim] += _shape[dim];
  // stash this original shape along dim for use in the loop:
  ind_t tshapedim = _shape[dim];
  // and resizing the Array2 to the new size copies existing entries
  this->resize(eshape);
  // so we need only iterate over the extra Array2
  for (auto x : SubIt2(extra.shape())){
    // offsetting its subscript to the new position
    auto y = x;
    y[dim] += tshapedim; // offset by the original shape along the dimension
    // and copying the contents
    (*this)[y] = static_cast<T>(extra[x]);
  }
  return *this;
}

template<class T>
bool Array2<T>::all(const ind_t n) const{
  ind_t nml = this->numel();
  ind_t count = (n > 0 && n < nml) ? n : nml;
  for (ind_t i=0; i<count; ++i) if (!_data[this->l2l_d(i)]) return false;
  return true;
}
template<class T>
bool Array2<T>::any(const ind_t n) const{
  ind_t nml = this->numel();
  ind_t count = (n > 0 && n < nml) ? n : nml;
  for (ind_t i=0; i<count; ++i) if (_data[this->l2l_d(i)]) return true;
  return false;
}
template<class T>
bool Array2<T>::all(const T val, const ind_t n) const{
  ind_t nml = this->numel();
  ind_t count = (n > 0 && n < nml) ? n : nml;
  for (ind_t i=0; i<count; ++i) if (val != _data[this->l2l_d(i)]) return false;
  return true;
}
template<class T>
bool Array2<T>::any(const T val, const ind_t n) const{
  ind_t nml = this->numel();
  ind_t count = (n > 0 && n < nml) ? n : nml;
  for (ind_t i=0; i<count; ++i) if (val == _data[this->l2l_d(i)]) return true;
  return false;
}

template<class T>
ind_t
Array2<T>::count(const ind_t n) const{
  ind_t el = this->numel();
  ind_t no = (n > 0 && n < el) ? n : el;
  ind_t count{0};
  for (ind_t i=0; i<no; ++i) if (_data[this->l2l_d(i)]) ++count;
  return count;
}
template<class T>
ind_t
Array2<T>::count(const T val, const ind_t n) const{
  ind_t el = this->numel();
  ind_t no = (n > 0 && n < el) ? n : el;
  ind_t count{0};
  for (ind_t i=0; i<no; ++i) if (val == _data[this->l2l_d(i)]) ++count;
  return count;
}

template<class T>
ind_t
Array2<T>::first(const ind_t n) const{
  ind_t el = this->numel();
  ind_t no = (n > 0 && n < el) ? n : el;
  for (ind_t i=0; i<no; ++i) if (_data[this->l2l_d(i)]) return i;
  return no;
}
template<class T>
ind_t
Array2<T>::first(const T val, const ind_t n) const{
  ind_t el = this->numel();
  ind_t no = (n > 0 && n < el) ? n : el;
  for (ind_t i=0; i<no; ++i) if (val == _data[this->l2l_d(i)]) return i;
  return no;
}
template<class T>
ind_t
Array2<T>::last(const ind_t n) const{
  ind_t el = this->numel();
  ind_t no = (n > 0 && n < el) ? n : el;
  for (ind_t i=no; i--;) if (_data[this->l2l_d(i)]) return i;
  return no;
}
template<class T>
ind_t
Array2<T>::last(const T val, const ind_t n) const{
  ind_t el = this->numel();
  ind_t no = (n > 0 && n < el) ? n : el;
  for (ind_t i=no; i--;) if (val == _data[this->l2l_d(i)]) return i;
  return no;
}

#define ARRAY_ELEMENTWISE_INT_TRANSFORM(X) template<class T>\
Array2<int>\
Array2<T>:: X () const {\
  Array2<int> out(_shape, _stride);\
  for (auto x: out.subItr())\
    out[x] = static_cast<int>(std:: X (_data[this->s2l_d(x)]));\
  return out;\
}
ARRAY_ELEMENTWISE_INT_TRANSFORM(round)
ARRAY_ELEMENTWISE_INT_TRANSFORM(floor)
ARRAY_ELEMENTWISE_INT_TRANSFORM(ceil)
#undef ARRAY_ELEMENTWISE_INT_TRANSFORM

template<class T>
Array2<T>
Array2<T>::sum(const ind_t dim) const{
  assert(dim < _shape.size());
  shape_t osize = this->shape();
  osize[dim] = 1u;
  shape_t ostride = this->calculate_stride(osize); // preserve orderness
  Array2<T> out(osize, ostride);
  for (auto oidx: out.subItr()) {
    T tmp{0};
    auto idx = oidx;
    for (ind_t i=0; i<_shape[dim]; ++i) {
      idx[dim] = i;
      tmp += _data[this->s2l_d(idx)];
    }
    out[oidx] = tmp;
  }
  return out;
}

template<class T>
Array2<T>
Array2<T>::prod(const ind_t dim) const{
  assert(dim < _shape.size());
  shape_t osize = this->shape();
  osize[dim] = 1u;
  shape_t ostride = this->calculate_stride(osize); // preserve orderness
  Array2<T> out(osize, ostride);
  for (auto oidx: out.subItr()) {
    T tmp{1};
    auto idx = oidx;
    for (ind_t i=0; i<_shape[dim]; ++i) {
      idx[dim] = i;
      tmp *= _data[this->s2l_d(idx)];
    }
    out[oidx] = tmp;
  }
  return out;
}

template<class T>
Array2<T>
Array2<T>::min(const ind_t dim) const{
  assert(dim < _shape.size());
  shape_t osize = this->shape();
  osize[dim] = 1u;
  shape_t ostride = this->calculate_stride(osize); // preserve orderness
  Array2<T> out(osize, ostride);
  for (auto oidx: out.subItr()) {
    T tmp{(std::numeric_limits<T>::max)()};
    auto idx = oidx;
    for (ind_t i=0; i<_shape[dim]; ++i) {
      idx[dim] = i;
      T tst = _data[this->s2l_d(idx)];
      if (tst < tmp) tmp = tst;
    }
    out[oidx] = tmp;
  }
  return out;
}

template<class T>
Array2<T>
Array2<T>::max(const ind_t dim) const{
  size_t ndim = _shape.size();
  assert(dim < ndim);
  shape_t osize = this->shape();
  osize[dim] = 1u;
  shape_t ostride = this->calculate_stride(osize); // preserve orderness
  // the stride along any dimension can not be zero. protect against it:
  for (ind_t i=0; i<ndim; ++i) if (ostride[i] < 1u) ostride[i] = 1u;

  Array2<T> out(osize, ostride);
  for (auto oidx: out.subItr()) {
    T tmp{std::numeric_limits<T>::lowest()};
    auto idx = oidx;
    for (ind_t i=0; i<_shape[dim]; ++i) {
      idx[dim] = i;
      T tst = _data[this->s2l_d(idx)];
      if (tst > tmp) tmp = tst;
    }
    out[oidx] = tmp;
  }
  return out;
}

template<class T>
T Array2<T>::sum() const{
  T out{0};
  for (auto x : this->subItr()) out += _data[this->s2l_d(x)];
  return out;
}

template<class T>
T Array2<T>::prod() const{
  T out{1};
  for (auto x : this->subItr()) out *= _data[this->s2l_d(x)];
  return out;
}

template<class T>
template<class R, size_t Nel>
bool Array2<T>::match(const ind_t i, const ind_t j, const std::array<R,Nel>& rot, const int order, const T Ttol, const int tol) const {
  assert(this->ndim() == 2u); // only defined for 2-D Array2s
  assert(Nel == _shape[1]*_shape[1]);
  ind_t n = _shape[1];
  std::vector<T> tmp(n,T(0));
  for (ind_t k=0; k<n; ++k) tmp[k] = this->val(j,k);
  brille::Comparer<T,T> eq(brille::cmp::eq, Ttol, Ttol, tol);
  if (order < 0){
    int o{0};
    do{
      //check against the current tmp vector whether this order rotation has moved j to i
      if (eq(n, this->ptr(i), _stride[1], tmp.data(), 1u)) return true;
      // otherwise apply the rotation (again)
      brille::utils::mul_mat_vec_inplace(n, rot.data(), tmp.data());
    } while (o++ < std::abs(order));
    return false;
  } else {
    // rotate exactly order times
    for (int o=0; o<order; ++o)
      brille::utils::mul_mat_vec_inplace(n, rot.data(), tmp.data());
    return eq(n, this->ptr(i), _stride[1], tmp.data(), 1u);
  }
}

template<class T>
bool Array2<T>::match(const ind_t i, const ind_t j, brille::ops op, T val, const T Ttol, const int tol) const {
  auto ai= this->view(i);
  auto aj= this->view(j);
  brille::Comparer<T,T> neq(brille::cmp::neq, Ttol, Ttol, tol);
  ind_t no = this->numel()/_shape[0];
  switch (op){
    case brille::ops::plus:
    for (ind_t k=0; k<no; ++k) if (neq(ai[k], aj[k]+val)) return false;
    break;
    case brille::ops::minus:
    for (ind_t k=0; k<no; ++k) if (neq(ai[k], aj[k]-val)) return false;
    break;
    case brille::ops::times:
    for (ind_t k=0; k<no; ++k) if (neq(ai[k], aj[k]*val)) return false;
    break;
    case brille::ops::rdiv:
    for (ind_t k=0; k<no; ++k) if (neq(ai[k], aj[k]/val)) return false;
    break;
    case brille::ops::ldiv:
    for (ind_t k=0; k<no; ++k) if (neq(ai[k], val/aj[k])) return false;
    break;
    default:
    throw std::runtime_error(std::string("Unhandled operator ")+brille::to_string(op));
  }
  return true;
}

template<class T>
bool Array2<T>::all(const brille::cmp expr, const T val, const T Ttol, const int tol) const {
  if (brille::cmp::le_ge == expr)
    return this->all(brille::cmp::le, val) || this->all(brille::cmp::ge, val);
  ind_t no = this->numel();
  brille::Comparer<T,T> op(expr, Ttol, Ttol, tol);
  for (ind_t k=0; k<no; ++k) if(!op(_data[this->l2l_d(k)], val)) return false;
  return true;
}
template<class T>
bool Array2<T>::any(const brille::cmp expr, const T val, const T Ttol, int tol) const {
  return this->first(expr, val, Ttol, tol) < this->numel();
}
template<class T>
ind_t
Array2<T>::first(const brille::cmp expr, const T val, const T Ttol, const int tol) const {
  ind_t no = this->numel();
  brille::Comparer<T,T> op(expr, Ttol, Ttol, tol);
  for (ind_t k=0; k<no; ++k) if(op(_data[this->l2l_d(k)], val)) return k;
  return no;
}
template<class T>
ind_t
Array2<T>::last(const brille::cmp expr, const T val, const T Ttol, const int tol) const {
  ind_t no = this->numel();
  brille::Comparer<T,T> op(expr, Ttol, Ttol, tol);
  for (ind_t k=no; k--;) if(op(_data[this->l2l_d(k)], val)) return k;
  return no;
}
template<class T>
ind_t
Array2<T>::count(const brille::cmp expr, const T val, const T Ttol, const int tol) const {
  ind_t no = this->numel();
  brille::Comparer<T,T> op(expr, Ttol, Ttol, tol);
  ind_t cnt{0};
  for (ind_t k=0; k<no; ++k) if(op(_data[this->l2l_d(k)], val)) ++cnt;
  return cnt;
}

template<class T>
Array2<bool>
Array2<T>::is(const brille::cmp expr, const T val, const T Ttol, const int tol) const {
  Array2<bool> out(_shape, _stride, true);
  ind_t no = this->numel();
  brille::Comparer<T,T> op(expr, Ttol, Ttol, tol);
  for (ind_t k=0; k<no; ++k) out[k] = op(_data[this->l2l_d(k)], val);
  return out;
}

template<class T>
std::vector<ind_t>
Array2<T>::find(const brille::cmp expr, const T val, const T Ttol, const int tol) const {
  Array2<bool> this_is = this->is(expr, val, Ttol, tol);
  ind_t no = this->numel();
  std::vector<ind_t> out;
  for (ind_t k=0; k<no; ++k) if (this_is[k]) out.push_back(k);
  return out;
}

template<class T>
template<class R>
Array2<bool>
Array2<T>::is(const brille::cmp expr, const Array2<R>& that, const T Ttol, const R Rtol, const int tol) const {
  // To handle singleton-dimension broadcasting, this function needs to be split
  auto tsize = that.shape();
  if (!std::equal(_shape.begin(), _shape.end(), tsize.begin(),
      [](ind_t a, ind_t b){return 1==b || a==b;})){
    std::string msg = "An Array2 with size ( ";
    for (auto x: tsize) msg += std::to_string(x) + " ";
    msg += ") can not be broadcast to match one with size ( ";
    for (auto x: _shape) msg += std::to_string(x) + " ";
    msg += ").";
    throw std::runtime_error(msg);
  }
  Array2<bool> out(_shape, _stride, true);
  if (std::equal(_shape.begin(), _shape.end(), tsize.begin())){
    // No broadcast
    brille::Comparer<T,R> op(expr, Ttol, Rtol, tol);
    // no guarantees about same stride, so use subscript iterator:
    for (auto sub: this->subItr()) out[sub] = op(_data[this->s2l_d(sub)], that[sub]);
  } else {
    // Broadcast
    size_t ndim = _shape.size();
    for (ind_t i=1; i<ndim; ++i) if (_shape[i]!=tsize[i])
      throw std::runtime_error("Broadcasting beyond the first dimension requires viewing beyond the first dimension!");
    for (ind_t i=0; i<_shape[0]; ++i)
      out.set(i, this->view(i).is(expr, that, Ttol, Rtol, tol));
  }
  return out;
}

template<class T>
template<class R>
std::vector<bool>
Array2<T>::row_is(const brille::cmp expr, const std::vector<R>& row, const T Ttol, const R Rtol, const int tol) const{
  assert(row.size() == _shape[1]);
  std::vector<bool> out;
  brille::Comparer<T,R> op(expr, Ttol, Rtol, tol);
  out.reserve(_shape[0]);
  // loop over rows, comparing each row to the input values (a std::vector has stride of 1)
  for (ind_t i=0; i<_shape[0]; ++i)
    out.push_back(op(_shape[1], this->ptr(i), _stride[1], row.data(), 1u));
  return out;
}

template<class T>
template<class R>
Array2<bool> Array2<T>::row_is(const brille::cmp expr, const Array2<R>& rows, const T Ttol, const R Rtol, const int tol) const {
  // To handle singleton-dimension broadcasting, this function needs to be split
  auto r_shape = rows.shape();
  if (!std::equal(_shape.begin(), _shape.end(), r_shape.begin(), [](ind_t a, ind_t b){return 1 == b || a == b;})){
    std::string msg = "An Array2 with size ( ";
    for (auto x: r_shape) msg += std::to_string(x) + " ";
    msg += ") can not be broadcast to match one with size ( ";
    for (auto x: _shape) msg += std::to_string(x) + " ";
    msg += ").";
    throw std::runtime_error(msg);
  }
  Array2<bool> out(_shape[0], true); // we only support singleton 'rows' not 'this'
  brille::Comparer<T,R> op(expr, Ttol, Rtol, tol);
  auto r_stride = rows.stride();

  if (std::equal(_shape.begin(), _shape.end(), r_shape.begin())){
    // No broadcast
    for (ind_t i=0; i<_shape[0]; ++i)
      out[i] = op(_shape[1], this->ptr(i), _stride[1], rows.ptr(i), r_stride[1]);
  } else {
    // Broadcast
    for (ind_t i=1; i<_shape.size(); ++i) if (_shape[i] != r_shape[i])
        throw std::runtime_error("Broadcasting beyond the first dimension requires viewing beyond the first dimension!");
    auto r_ptr = rows.ptr(0);
    for (ind_t i=0; i<_shape[0]; ++i)
      out[i] = op(_shape[1], this->ptr(i), _stride[1], r_ptr, r_stride[1]);
  }

//  if (std::equal(_shape.begin(), _shape.end(), r_shape.begin())){
//    // No broadcast
//    brille::Comparer<T,R> op(expr, Ttol, Rtol, tol);
//    auto r_stride = rows.stride();
//    for (ind_t i=0; i<_shape[0]; ++i)
//      out[i] = op(_shape[1], this->ptr(i), _stride[1], rows.ptr(i), r_stride[1]);
//  } else {
//    // Broadcast
//    for (ind_t i=1; i<_shape.size(); ++i) if (_shape[i] != r_shape[i])
//        throw std::runtime_error("Broadcasting beyond the first dimension requires viewing beyond the first dimension!");
//    for (ind_t i=0; i<_shape[0]; ++i)
//      out.set(i, this->view(i).row_is(expr, rows, Ttol, Rtol, tol));
//  }
  return out;
}

template<class T>
template<class R>
ind_t Array2<T>::first(const brille::cmp expr, const Array2<R>& rows, const T Ttol, const R Rtol, const int tol) const {
  // To handle singleton-dimension broadcasting, this function needs to be split
  auto r_shape = rows.shape();
  if (!std::equal(_shape.begin(), _shape.end(), r_shape.begin(), [](ind_t a, ind_t b){return 1 == b || a == b;})){
    std::string msg = "An Array2 with size ( ";
    for (auto x: r_shape) msg += std::to_string(x) + " ";
    msg += ") can not be broadcast to match one with size ( ";
    for (auto x: _shape) msg += std::to_string(x) + " ";
    msg += ").";
    throw std::runtime_error(msg);
  }
  if (std::equal(_shape.begin(), _shape.end(), r_shape.begin())){
    // No broadcast
    brille::Comparer<T,R> op(expr, Ttol, Rtol, tol);
    auto r_stride = rows.stride();
    for (ind_t i=0; i<_shape[0]; ++i) if (op(_shape[1], this->ptr(i), _stride[1], rows.ptr(i), r_stride[1])) return i;
  } else {
    // Broadcast
    for (ind_t i=1; i<_shape.size(); ++i) if (_shape[i] != r_shape[i])
        throw std::runtime_error("Broadcasting beyond the first dimension requires viewing beyond the first dimension!");
    for (ind_t i=0; i<_shape[0]; ++i) if (this->view(i).row_is(expr, rows, Ttol, Rtol, tol).all()) return i;
  }
  return _shape[0];
}
template<class T>
template<class R>
ind_t Array2<T>::last(const brille::cmp expr, const Array2<R>& rows, const T Ttol, const R Rtol, const int tol) const {
  // To handle singleton-dimension broadcasting, this function needs to be split
  auto r_shape = rows.shape();
  if (!std::equal(_shape.begin(), _shape.end(), r_shape.begin(), [](ind_t a, ind_t b){return 1 == b || a == b;})){
    std::string msg = "An Array2 with size ( ";
    for (auto x: r_shape) msg += std::to_string(x) + " ";
    msg += ") can not be broadcast to match one with size ( ";
    for (auto x: _shape) msg += std::to_string(x) + " ";
    msg += ").";
    throw std::runtime_error(msg);
  }
  if (std::equal(_shape.begin(), _shape.end(), r_shape.begin())){
    // No broadcast
    brille::Comparer<T,R> op(expr, Ttol, Rtol, tol);
    auto r_stride = rows.stride();
    for (ind_t i=_shape[0]; i--;) if (op(_shape[1], this->ptr(i), _stride[1], rows.ptr(i), r_stride[1])) return i;
  } else {
    // Broadcast
    for (ind_t i=1; i<_shape.size(); ++i) if (_shape[i] != r_shape[i])
        throw std::runtime_error("Broadcasting beyond the first dimension requires viewing beyond the first dimension!");
    for (ind_t i=_shape[0]; i--;) if (this->view(i).row_is(expr, rows, Ttol, Rtol, tol).all()) return i;
  }
  return _shape[0];
}
template<class T>
template<class R>
ind_t Array2<T>::count(const brille::cmp expr, const Array2<R>& rows, const T Ttol, const R Rtol, const int tol) const {
  // To handle singleton-dimension broadcasting, this function needs to be split
  auto r_shape = rows.shape();
  if (!std::equal(_shape.begin(), _shape.end(), r_shape.begin(), [](ind_t a, ind_t b){return 1 == b || a == b;})){
    std::string msg = "An Array2 with size ( ";
    for (auto x: r_shape) msg += std::to_string(x) + " ";
    msg += ") can not be broadcast to match one with size ( ";
    for (auto x: _shape) msg += std::to_string(x) + " ";
    msg += ").";
    throw std::runtime_error(msg);
  }
  ind_t number{0};
  if (std::equal(_shape.begin(), _shape.end(), r_shape.begin())){
    // No broadcast
    brille::Comparer<T,R> op(expr, Ttol, Rtol, tol);
    auto r_stride = rows.stride();
    for (ind_t i=0; i<_shape[0]; ++i) if (op(_shape[1], this->ptr(i), _stride[1], rows.ptr(i), r_stride[1])) ++number;
  } else {
    // Broadcast
    for (ind_t i=1; i<_shape.size(); ++i) if (_shape[i] != r_shape[i])
        throw std::runtime_error("Broadcasting beyond the first dimension requires viewing beyond the first dimension!");
    for (ind_t i=0; i<_shape[0]; ++i) if (this->view(i).row_is(expr, rows, Ttol, Rtol, tol).all()) ++number;
  }
  return number;
}
template<class T>
template<class R>
std::vector<ind_t> Array2<T>::find(const brille::cmp expr, const Array2<R>& rows, const T Ttol, const R Rtol, const int tol) const {
  // To handle singleton-dimension broadcasting, this function needs to be split
  auto r_shape = rows.shape();
  if (!std::equal(_shape.begin(), _shape.end(), r_shape.begin(), [](ind_t a, ind_t b){return 1 == b || a == b;})){
    std::string msg = "An Array2 with size ( ";
    for (auto x: r_shape) msg += std::to_string(x) + " ";
    msg += ") can not be broadcast to match one with size ( ";
    for (auto x: _shape) msg += std::to_string(x) + " ";
    msg += ").";
    throw std::runtime_error(msg);
  }
  std::vector<ind_t> out;
  if (std::equal(_shape.begin(), _shape.end(), r_shape.begin())){
    // No broadcast
    brille::Comparer<T,R> op(expr, Ttol, Rtol, tol);
    auto r_stride = rows.stride();
    for (ind_t i=0; i<_shape[0]; ++i) if (op(_shape[1], this->ptr(i), _stride[1], rows.ptr(i), r_stride[1])) out.push_back(i);
  } else {
    // Broadcast
    for (ind_t i=1; i<_shape.size(); ++i) if (_shape[i] != r_shape[i])
        throw std::runtime_error("Broadcasting beyond the first dimension requires viewing beyond the first dimension!");
    for (ind_t i=0; i<_shape[0]; ++i) if (this->view(i).row_is(expr, rows, Ttol, Rtol, tol).all()) out.push_back(i);
  }
  return out;
}

template<class T>
template<class R>
std::vector<bool>
Array2<T>::each_is(const brille::cmp expr, const std::vector<R>& vals, const T Ttol, const R Rtol, const int tol) const{
  auto no = this->numel();
  assert(vals.size() == no);
  std::vector<bool> out;
  brille::Comparer<T,R> op(expr, Ttol, Rtol, tol);
  out.reserve(no);
  // loop over whole array in linear-index order
  for (ind_t i=0; i<no; ++i)
    out.push_back(op(this->val(i), vals[i]));
  return out;
}

template<class T>
template<class R>
bool Array2<T>::is(const Array2<R>& that, const T Ttol, const R Rtol, const int tol) const {
  return this->is(brille::cmp::eq, that, Ttol, Rtol, tol).all(true);
}

template<class T>
std::vector<bool>
Array2<T>::is_unique(const T Ttol, const int tol) const{
  if (_shape[0] < 1u) return std::vector<bool>();
  std::vector<bool> out(1u, true);
  out.reserve(_shape[0]);
  brille::Comparer<T,T> op(brille::cmp::neq, Ttol, Ttol, tol);
  ind_t sz{_shape[1]}, st{_stride[1]};
  for (ind_t i=1; i<_shape[0]; ++i){
    bool isu{true};
    for (ind_t j=0; j<i; ++j){
      isu &= op(sz, this->ptr(i), st, this->ptr(j), st);
      if (!isu) break;
    }
    out.push_back(isu);
  }
  return out;
}
template<class T>
std::vector<ind_t>
Array2<T>::unique_idx(const T Ttol, const int tol) const{
  if (_shape[0] < 1u) return std::vector<ind_t>();
  std::vector<ind_t> out(1u, 0u);
  out.reserve(_shape[0]);
  brille::Comparer<T,T> op(brille::cmp::eq, Ttol, Ttol, tol);
  ind_t sz{_shape[1]}, st{_stride[1]};
  for (ind_t i=1; i<_shape[0]; ++i){
    ind_t idx{i};
    for (ind_t j=0; j<i; ++j)
    if(j==out[j] && op(sz, this->ptr(i), st, this->ptr(j), st)){
      idx=j;
      break;
    }
    out.push_back(idx);
  }
  return out;
}
template<class T>
Array2<T>
Array2<T>::unique(const T Ttol, const int tol) const {
  std::vector<bool> isu = this->is_unique(Ttol, tol);
  size_t u_count = std::count(isu.begin(), isu.end(), true);
  shape_t osize{_shape};
  osize[0] = static_cast<ind_t>(u_count);
  Array2<T> out(osize, _stride);
  for (ind_t i=0,u=0; i<_shape[0]; ++i) if (isu[i]) out.set(u++, this->view(i));
  return out;
}
template<class T>
T Array2<T>::dot(ind_t i, ind_t j) const {
  assert(i<_shape[0]);
  assert(j<_shape[0]);
  ind_t sz{_shape[1]}, st{_stride[1]};
  std::vector<T> prods(sz,0);
  brille::RawBinaryOperator<T> prod(brille::ops::times);
  // perform elementwise multiplication on the views of i and j
  prod(sz, prods.data(), 1u, this->ptr(i), st, this->ptr(j), st);
  // find the sum of the products
  // std::reduce can do this in parallel but gcc<v9.3 does not implement all of C++17
  return std::accumulate(prods.begin(), prods.end(), T(0));
}
template<class T>
T Array2<T>::norm(ind_t i) const {
  return std::sqrt(this->dot(i,i));
}

template<class T>
Array2<T>
Array2<T>::operator-() const{
  Array2<T> neg(_shape, _stride);
  for (auto s: this->subItr()) neg[s] = -_data[this->s2l_d(s)];
  return neg;
}


template<class T>
template<class I, typename>
void Array2<T>::permute(std::vector<I>& p){
  std::vector<I> s=p, o(_shape[0]);
  std::iota(o.begin(), o.end(), 0u);
  std::sort(s.begin(), s.end());
  debug_update_if(!std::includes(o.begin(),o.end(),s.begin(),s.end()),"The permutation vector ",p," is invalid. Expected permutation of ",o);
  // get the inverse permutation to enable element swapping:
  for (size_t i=0; i<p.size(); ++i) s[p[i]] = static_cast<I>(i);
  // perform the swaps until we have no more to do:
  for (ind_t i=0; i<_shape[0];){
    if (s[i]!=i){
      this->swap(i,s[i]);
      std::swap(s[i], s[s[i]]);
    } else{
      ++i;
    }
  }
  debug_update_if(!std::is_sorted(s.begin(),s.end()), "Undoing the permutation ",p," failed. End result is ",s);
}

template<class T>
template<class R>
bool Array2<T>::is_permutation(const Array2<R>& other, const T Ttol, const R Rtol, int tol) const{
  auto osh = other.shape();
  for (ind_t i=0; i < ndim(); ++i) if (osh[i] != _shape[i]) return false;
  for (ind_t i=0; i < _shape[0]; ++i){
    const auto x{this->view(i)};
    // if *this contains the same thing more than once, other must as well:
    // NaN values would break this functionality since NaN == NaN is never true
    if (this->count(cmp::eq, x) != other.count(cmp::eq, x, Ttol, Rtol, tol)) {
      info_update(x.to_string(0), " is in \n", this->to_string(), this->count(cmp::eq, x), " times but in\n",
                  other.to_string(), other.count(cmp::eq, x, tol), " times!");
      return false;
    }
//    // the reverse should be true too, but is extraneous
//    const auto y{other.view(i)};
//    if (this->count(cmp::eq, y) != other.count(cmp::eq, y)) return false;
  }
  // *this and other share the same shape and all entries in both share the same multiplicity, so they're permutations
  return true;
}

template<class T>
template<class R>
std::vector<ind_t> Array2<T>::permutation_vector(const Array2<R>& other, const T Ttol, const R Rtol, int tol) const {
  auto not_present = [](const auto & a, const auto & b){
    if (a.size() == 0) throw std::runtime_error("Can not find a not in b since a is empty");
    if (b.size() == 0) return a[0];
    const auto ptr = std::find_first_of(a.begin(), a.end(), b.begin(), b.end(),
                                        [](const auto & x, const auto & y){return x != y;});
    if (ptr == a.end())
      throw std::runtime_error("b contains all of a!");
    return *ptr;
  };
  std::vector<ind_t> perm;
  for (ind_t i=0; i < _shape[0]; ++i){
    auto others = other.find(cmp::eq, this->view(i), Ttol, Rtol, tol);
    perm.push_back(not_present(others, perm));
  }
  return perm;
}

template<class T>
bool Array2<T>::swap(const ind_t a, const ind_t b){
  assert(a<_shape[0] && b<_shape[0]);
  shape_t sub{_shape};
  sub[0] = a; // fix the 0th index to 'a' in the iterator
  for (auto aidx: this->subItr(sub)){
    // for each of the [a,...] subscripted indices, construct [b,...]
    shape_t bidx{aidx};
    bidx[0] = b;
    // precalculate the offset/stride/shaped subscripts:
    auto sa = this->s2l_d(aidx);
    auto sb = this->s2l_d(bidx);
    // perform the actual swap:
    T _store_ = _data[sa];
    _data[sa] = _data[sb];
    _data[sb] = _store_;
  }
  return true;
}
template<class T>
bool Array2<T>::swap(ind_t i, ind_t a, ind_t b){
  assert(i < _shape[0] && a < _shape[1] && b < _shape[1]);
  auto la{this->ij2l_d(i,a)}, lb{this->ij2l_d(i,b)};
  T _store_ = _data[la];
  _data[la] = _data[lb];
  _data[lb] = _store_;
  return true;
}

template<class T>
std::vector<T> Array2<T>::to_std() const {
  std::vector<T> out;
  for (auto x: this->valItr()) out.push_back(x);
  return out;
}

template<class T> template<class R>
std::enable_if_t<std::is_base_of_v<HighFive::Object, R>, bool>
Array2<T>::add_hdf_attributes(R& obj) const {
  obj.createAttribute("size", numel());
  obj.createAttribute("shape", shape());
  return true;
}

template<class T> template<class R>
std::enable_if_t<std::is_base_of_v<HighFive::Object, R>, bool>
// bool
Array2<T>::to_hdf(R& obj, const std::string& node) const {
  if (numel()) {
    // Copy the data into a structure that HighFive can handle :/
    std::vector<std::vector<T>> hf;
    for (ind_t i = 0; i < _shape[0]; ++i) {
      std::vector<T> inner;
      for (ind_t j = 0; j < _shape[1]; ++j) {
        inner.push_back(this->val(i, j));
      }
      hf.push_back(inner);
    }
    auto ds = overwrite_data(obj, node, hf);
    add_hdf_attributes(ds);
  } else {
    auto group = overwrite_group(obj, node);
    add_hdf_attributes(group);
  }
  return true;
}

template<class T>
bool Array2<T>::to_hdf(const std::string& filename, const std::string& node, const unsigned permissions) const {
    using namespace HighFive;
    File file(filename, permissions);
    return this->to_hdf(file, node);
}

template<class T>
T* Array2<T>::ptr(const ind_t i0){
  assert(i0 < _shape[0]);
  return _data + ij2l_d(i0, 0u);
}
template<class T>
T* Array2<T>::ptr(const ind_t i0, const ind_t j0){
  assert(i0 < _shape[0] && j0 < _shape[1]);
  return _data + ij2l_d(i0, j0);
}
template<class T>
T* Array2<T>::ptr(const shape_t& p){
  assert(p[0]<_shape[0] && p[1]<_shape[1]);
  return _data + s2l_d(p);
}

template<class T>
const T* Array2<T>::ptr(const ind_t i0) const {
  assert(i0 < _shape[0]);
  return _data + ij2l_d(i0, 0u);
}
template<class T>
const T* Array2<T>::ptr(const ind_t i0, const ind_t j0) const {
  assert(i0 < _shape[0] && j0 < _shape[1]);
  return _data + ij2l_d(i0, j0);
}
template<class T>
const T* Array2<T>::ptr(const shape_t& p) const {
  assert(p[0]<_shape[0] && p[1]<_shape[1]);
  return _data + s2l_d(p);
}


template<class T>
T& Array2<T>::val(const ind_t i0){
  assert(i0 < _shape[0]);
  return _data[ij2l_d(i0, 0u)];
}
template<class T>
T& Array2<T>::val(const ind_t i0, const ind_t j0){
  assert(i0 < _shape[0] && j0 < _shape[1]);
  return _data[ij2l_d(i0, j0)];
}
template<class T>
T& Array2<T>::val(const shape_t& p){
  assert(p[0]<_shape[0] && p[1]<_shape[1]);
  return _data[s2l_d(p)];
}
template<class T>
template<class I>
T& Array2<T>::val(std::initializer_list<I> l){
  shape_t idx{l};
  return this->val(idx);
}

template<class T>
const T& Array2<T>::val(const ind_t i0) const {
  assert(i0 < _shape[0]);
  return _data[ij2l_d(i0, 0u)];
}
template<class T>
const T& Array2<T>::val(const ind_t i0, const ind_t j0) const {
  assert(i0 < _shape[0] && j0 < _shape[1]);
  return _data[ij2l_d(i0, j0)];
}
template<class T>
const T& Array2<T>::val(const shape_t& p) const {
  assert(p[0]<_shape[0] && p[1]<_shape[1]);
  return _data[s2l_d(p)];
}
template<class T>
template<class I>
const T& Array2<T>::val(std::initializer_list<I> l) const {
  shape_t idx{l};
  return this->val(idx);
}

template<class T>
Array2<T> Array2<T>::contiguous_copy() const {
  if (this->is_contiguous()) return Array2<T>(*this);
  Array2<T> out(_shape, this->calculate_stride(_shape));
  for (auto x : this->subItr()) out[x] = (*this)[x];
  return out;
}

template<class T>
Array2<T> Array2<T>::contiguous_row_ordered_copy() const {
  if (this->is_row_ordered() && this->is_contiguous()) return Array2<T>(*this);
  Array2<T> out(_shape, this->calculate_stride(_shape));
  for (auto x : this->subItr()) out[x] = (*this)[x];
  return out;
}

template<class T>
Array2<T> Array2<T>::abs() const {
  Array2<T> out(_shape, this->calculate_stride(_shape));
  for (auto x: this->subItr()) out[x] = std::abs((*this)[x]);
  return out;
}

template<class T>
static void mutable_check(const Array2<T>& a){
  if (!a.ismutable())
  throw std::runtime_error("Immutable Array2 objects can not be modified!");
}

#define SCALAR_INPLACE_OP(X) template<class T>\
Array2<T>& Array2<T>::operator X (const T& val){\
  mutable_check(*this);\
  for (auto& v: this->valItr()) v X val;\
  return *this;\
}
SCALAR_INPLACE_OP(+=)
SCALAR_INPLACE_OP(-=)
SCALAR_INPLACE_OP(*=)
SCALAR_INPLACE_OP(/=)
#undef SCALAR_INPLACE_OP
// template<class T>
// Array2<T>&
// Array2<T>::operator+=(const T& val){
//   mutable_check(*this);
//   for (auto& v: this->valItr()) v += val;
//   return *this;
// }
// template<class T>
// Array2<T>&
// Array2<T>::operator-=(const T& val){
//   mutable_check(*this);
//   for (auto& v: this->valItr()) v -= val;
//   return *this;
// }
// template<class T>
// Array2<T>&
// Array2<T>::operator*=(const T& val){
//   mutable_check(*this);
//   auto itr = this->valItr();
//   for (auto & v: itr){
//     v *= val;
//   }
//   return *this;
// }
// template<class T>
// Array2<T>&
// Array2<T>::operator/=(const T& val){
//   mutable_check(*this);
//   for (auto& v: this->valItr()) v /= val;
//   return *this;
// }

template<class T>
Array2<T> Array2<T>::operator^(const T& val){
  Array2<T> out(this->shape());
  for (auto & v: this->subItr()) out[v] = std::pow(this->operator[](v), val);
  return out;
}

/*! \brief Verify that two Array2 shapes can be broadcast together

\param a The shape of the first Array2
\param b The shape of the second Array2
\return true if both dimensions match or either `a` or `b` is singleton
*/
template<class T>
bool broadcast_shape_check(const std::array<T,2>& a, const std::array<T,2>&b){
  bool ok = (a[0]==b[0] && a[1]==b[1]);
  if (!ok){
    std::string msg = "In place broadcasting is not possible for { ";
    for (auto x: a) msg += std::to_string(x) + " ";
    msg += "} shaped Array2 and { ";
    for (auto x: b) msg += std::to_string(x) + " ";
    msg += "} shaped operand";
    throw std::runtime_error(msg);
  }
  return ok;
}

#define ARRAY_INPLACE_OP(X) template<class T> template<class R>\
Array2<T>& Array2<T>::operator X (const Array2<R>& val){\
  mutable_check(*this);\
  auto itr = this->broadcastItr(val);\
  broadcast_shape_check(_shape, itr.shape());\
  for (auto [ox, tx, vx]: itr) _data[this->s2l_d(tx)] X val[vx];\
  return *this;\
}
ARRAY_INPLACE_OP(+=)
ARRAY_INPLACE_OP(-=)
ARRAY_INPLACE_OP(*=)
ARRAY_INPLACE_OP(/=)
#undef ARRAY_INPLACE_OP
