<commit_before>////////////////////////////////////////////////////////////////////////////////
// FILE: operators.hpp
// DATE: 2014-07-30
// AUTH: <NAME> <<EMAIL>>
// DESC: Defines operators for an N-dimensional templated array class

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

#ifndef WILT_OPERATORS_HPP
#define WILT_OPERATORS_HPP

#include "util.h"
#include "narray.hpp"

namespace wilt
{
  // helper structures for applying move semantics to operators
  template <class Ret, class T, class U, std::size_t N>
  struct move_op
  {
    template <class Operator>
    static NArray<Ret, N> none(const NArray<T, N>& lhs, const NArray<U, N>& rhs, Operator op)
    {
      NArray<Ret, N> ret(lhs.sizes());
      binaryOp2_(
        ret.base(), lhs.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<Ret, N> left(NArray<T, N>&& lhs, const NArray<U, N>& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
    template <class Operator>
    static NArray<Ret, N> right(const NArray<T, N>& lhs, NArray<U, N>&& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
  
    template <class Operator>
    static NArray<Ret, N> none(const NArray<T, N>& lhs, Operator op)
    {
      NArray<Ret, N> ret(lhs.sizes());
      unaryOp2_(
        ret.base(), lhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<Ret, N> none(const NArray<U, N>& rhs, Operator op)
    {
      NArray<Ret, N> ret(rhs.sizes());
      unaryOp2_(
        ret.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<Ret, N> left(NArray<T, N>&& lhs, Operator op)
    {
      return none(lhs, op);
    }
    template <class Operator>
    static NArray<Ret, N> right(NArray<U, N>&& rhs, Operator op)
    {
      return none(rhs, op);
    }
  };

  template <class T, class U, std::size_t N>
  struct move_op<T, T, U, N>
  {
    template <class Operator>
    static NArray<T, N> none(const NArray<T, N>& lhs, const NArray<U, N>& rhs, Operator op)
    {
      NArray<T, N> ret(lhs.sizes());
      binaryOp2_(
        ret.base(), lhs.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> left(NArray<T, N>&& lhs, const NArray<U, N>& rhs, Operator op)
    {
      NArray<T, N> ret(std::forward<NArray<U, N>>(lhs));
      binaryOp2_(
        ret.base(), ret.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> right(const NArray<T, N>& lhs, NArray<U, N>&& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
  
    template <class Operator>
    static NArray<T, N> none(const NArray<T, N>& lhs, Operator op)
    {
      NArray<T, N> ret(lhs.sizes());
      unaryOp2_(
        ret.base(), lhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> none(const NArray<U, N>& rhs, Operator op)
    {
      NArray<T, N> ret(rhs.sizes());
      unaryOp2_(
        ret.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> left(NArray<T, N>&& lhs, Operator op)
    {
      NArray<T, N> ret(std::forward<wilt::NArray<T, N>>(lhs)); 
      unaryOp2_(
        ret.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> right(NArray<U, N>&& rhs, Operator op)
    {
      return none(rhs, op);
    }
  };

  template <class T, class U, std::size_t N>
  struct move_op<U, T, U, N>
  {
    template <class Operator>
    static NArray<U, N> none(const NArray<T, N>& lhs, const NArray<U, N>& rhs, Operator op)
    {
      NArray<U, N> ret(lhs.sizes());
      binaryOp2_(
        ret.base(), lhs.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<U, N> left(NArray<T, N>&& lhs, const NArray<U, N>& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
    template <class Operator>
    static NArray<U, N> right(const NArray<T, N>& lhs, NArray<U, N>&& rhs, Operator op)
    {
      NArray<U, N> ret(std::forward<NArray<U, N>>(rhs));
      binaryOp2_(
        ret.base(), lhs.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
  
    template <class Operator>
    static NArray<U, N> none(const NArray<T, N>& lhs, Operator op)
    {
      NArray<U, N> ret(lhs.sizes());
      unaryOp2_(
        ret.base(), lhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<U, N> none(const NArray<U, N>& rhs, Operator op)
    {
      NArray<U, N> ret(rhs.sizes());
      unaryOp2_(
        ret.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<U, N> left(NArray<T, N>&& lhs, Operator op)
    {
      return none(lhs, op);
    }
    template <class Operator>
    static NArray<U, N> right(NArray<U, N>&& rhs, Operator op)
    {
      NArray<U, N> ret(std::forward<wilt::NArray<U, N>>(rhs)); 
      unaryOp2_(
        ret.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
  };

  template <class T, std::size_t N>
  struct move_op<T, T, T, N>
  {
    template <class Operator>
    static NArray<T, N> none(const NArray<T, N>& lhs, const NArray<T, N>& rhs, Operator op)
    {
      NArray<T, N> ret(lhs.sizes());
      binaryOp2_(
        ret.base(), lhs.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> left(NArray<T, N>&& lhs, const NArray<T, N>& rhs, Operator op)
    {
      NArray<T, N> ret(std::forward<wilt::NArray<T, N>>(lhs));
      binaryOp2_(
        ret.base(), ret.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> right(const NArray<T, N>& lhs, NArray<T, N>&& rhs, Operator op)
    {
      NArray<T, N> ret(std::forward<wilt::NArray<T, N>>(rhs));
      binaryOp2_(
        ret.base(), lhs.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
  
    template <class Operator>
    static NArray<T, N> none(const NArray<T, N>& lhs, Operator op)
    {
      NArray<T, N> ret(lhs.sizes());
      unaryOp2_(
        ret.base(), lhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> left(NArray<T, N>&& lhs, Operator op)
    {
      NArray<T, N> ret(std::forward<wilt::NArray<T, N>>(lhs));
      unaryOp2_(
        ret.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> right(NArray<T, N>&& rhs, Operator op)
    {
      NArray<T, N> ret(std::forward<NArray<T, N>>(rhs)); 
      unaryOp2_(
        ret.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
  };

  template <class Ret, class T, std::size_t N>
  struct move_op<Ret, T, T, N>
  {
    template <class Operator>
    static NArray<Ret, N> none(const NArray<T, N>& lhs, const NArray<T, N>& rhs, Operator op)
    {
      NArray<Ret, N> ret(lhs.sizes());
      binaryOp2_(
        ret.base(), lhs.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<Ret, N> left(NArray<T, N>&& lhs, const NArray<T, N>& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
    template <class Operator>
    static NArray<Ret, N> right(const NArray<T, N>& lhs, NArray<T, N>&& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
  
    template <class Operator>
    static NArray<Ret, N> none(const NArray<T, N>& lhs, Operator op)
    {
      NArray<Ret, N> ret(lhs.sizes());
      unaryOp2_(
        ret.base(), lhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> left(NArray<T, N>&& lhs, Operator op)
    {
      return none(lhs, op);
    }
    template <class Operator>
    static NArray<T, N> right(NArray<T, N>&& rhs, Operator op)
    {
      return none(rhs, op);
    }
  };

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t + u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("add(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t + u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("add(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t + u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("add(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t + u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("add(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t + rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t + rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<wilt::NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs + u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs + u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t - u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("sub(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t - u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("sub(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t - u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("sub(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t - u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("sub(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t - rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t - rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs - u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs - u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<wilt::NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t * u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mul(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t * u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mul(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t * u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mul(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t * u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mul(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t * rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t * rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs * u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs * u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t / u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("div(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t / u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("div(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t / u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("div(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t / u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("div(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t / rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t / rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<wilt::NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs / u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs / u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t % u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mod(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t % u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mod(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t % u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mod(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t % u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mod(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t % rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t % rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs % u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs % u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t & u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_and(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t & u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_and(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t & u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_and(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t & u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_and(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t & rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t & rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs & u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs & u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t | u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_or(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t | u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_or(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t | u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_or(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t | u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_or(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t | rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t | rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs | u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs | u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t ^ u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_xor(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t ^ u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_xor(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t ^ u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_xor(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t ^ u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_xor(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t ^ rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t ^ rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContinuous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs ^ u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs ^ u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContinuous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  enum CMP
  {
    LT, GT, EQ,
    GE, LE, NE
  }; // enum CMP

  template <class T, class U, std::size_t N>
  NArray<uint8_t, N> compare(const NArray<T, N>& lhs, const NArray<U, N>& rhs, CMP mode)
  {
    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("compare(): dimensions must match");
    if (lhs.empty())
      return NArray<uint8_t, N>();
<commit_msg>rename isContinuous() to isContiguous() again<commit_after>////////////////////////////////////////////////////////////////////////////////
// FILE: operators.hpp
// DATE: 2014-07-30
// AUTH: <NAME> <<EMAIL>>
// DESC: Defines operators for an N-dimensional templated array class

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

#ifndef WILT_OPERATORS_HPP
#define WILT_OPERATORS_HPP

#include "util.h"
#include "narray.hpp"

namespace wilt
{
  // helper structures for applying move semantics to operators
  template <class Ret, class T, class U, std::size_t N>
  struct move_op
  {
    template <class Operator>
    static NArray<Ret, N> none(const NArray<T, N>& lhs, const NArray<U, N>& rhs, Operator op)
    {
      NArray<Ret, N> ret(lhs.sizes());
      binaryOp2_(
        ret.base(), lhs.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<Ret, N> left(NArray<T, N>&& lhs, const NArray<U, N>& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
    template <class Operator>
    static NArray<Ret, N> right(const NArray<T, N>& lhs, NArray<U, N>&& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
  
    template <class Operator>
    static NArray<Ret, N> none(const NArray<T, N>& lhs, Operator op)
    {
      NArray<Ret, N> ret(lhs.sizes());
      unaryOp2_(
        ret.base(), lhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<Ret, N> none(const NArray<U, N>& rhs, Operator op)
    {
      NArray<Ret, N> ret(rhs.sizes());
      unaryOp2_(
        ret.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<Ret, N> left(NArray<T, N>&& lhs, Operator op)
    {
      return none(lhs, op);
    }
    template <class Operator>
    static NArray<Ret, N> right(NArray<U, N>&& rhs, Operator op)
    {
      return none(rhs, op);
    }
  };

  template <class T, class U, std::size_t N>
  struct move_op<T, T, U, N>
  {
    template <class Operator>
    static NArray<T, N> none(const NArray<T, N>& lhs, const NArray<U, N>& rhs, Operator op)
    {
      NArray<T, N> ret(lhs.sizes());
      binaryOp2_(
        ret.base(), lhs.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> left(NArray<T, N>&& lhs, const NArray<U, N>& rhs, Operator op)
    {
      NArray<T, N> ret(std::forward<NArray<U, N>>(lhs));
      binaryOp2_(
        ret.base(), ret.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> right(const NArray<T, N>& lhs, NArray<U, N>&& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
  
    template <class Operator>
    static NArray<T, N> none(const NArray<T, N>& lhs, Operator op)
    {
      NArray<T, N> ret(lhs.sizes());
      unaryOp2_(
        ret.base(), lhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> none(const NArray<U, N>& rhs, Operator op)
    {
      NArray<T, N> ret(rhs.sizes());
      unaryOp2_(
        ret.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> left(NArray<T, N>&& lhs, Operator op)
    {
      NArray<T, N> ret(std::forward<wilt::NArray<T, N>>(lhs)); 
      unaryOp2_(
        ret.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> right(NArray<U, N>&& rhs, Operator op)
    {
      return none(rhs, op);
    }
  };

  template <class T, class U, std::size_t N>
  struct move_op<U, T, U, N>
  {
    template <class Operator>
    static NArray<U, N> none(const NArray<T, N>& lhs, const NArray<U, N>& rhs, Operator op)
    {
      NArray<U, N> ret(lhs.sizes());
      binaryOp2_(
        ret.base(), lhs.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<U, N> left(NArray<T, N>&& lhs, const NArray<U, N>& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
    template <class Operator>
    static NArray<U, N> right(const NArray<T, N>& lhs, NArray<U, N>&& rhs, Operator op)
    {
      NArray<U, N> ret(std::forward<NArray<U, N>>(rhs));
      binaryOp2_(
        ret.base(), lhs.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
  
    template <class Operator>
    static NArray<U, N> none(const NArray<T, N>& lhs, Operator op)
    {
      NArray<U, N> ret(lhs.sizes());
      unaryOp2_(
        ret.base(), lhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<U, N> none(const NArray<U, N>& rhs, Operator op)
    {
      NArray<U, N> ret(rhs.sizes());
      unaryOp2_(
        ret.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<U, N> left(NArray<T, N>&& lhs, Operator op)
    {
      return none(lhs, op);
    }
    template <class Operator>
    static NArray<U, N> right(NArray<U, N>&& rhs, Operator op)
    {
      NArray<U, N> ret(std::forward<wilt::NArray<U, N>>(rhs)); 
      unaryOp2_(
        ret.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
  };

  template <class T, std::size_t N>
  struct move_op<T, T, T, N>
  {
    template <class Operator>
    static NArray<T, N> none(const NArray<T, N>& lhs, const NArray<T, N>& rhs, Operator op)
    {
      NArray<T, N> ret(lhs.sizes());
      binaryOp2_(
        ret.base(), lhs.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> left(NArray<T, N>&& lhs, const NArray<T, N>& rhs, Operator op)
    {
      NArray<T, N> ret(std::forward<wilt::NArray<T, N>>(lhs));
      binaryOp2_(
        ret.base(), ret.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> right(const NArray<T, N>& lhs, NArray<T, N>&& rhs, Operator op)
    {
      NArray<T, N> ret(std::forward<wilt::NArray<T, N>>(rhs));
      binaryOp2_(
        ret.base(), lhs.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
  
    template <class Operator>
    static NArray<T, N> none(const NArray<T, N>& lhs, Operator op)
    {
      NArray<T, N> ret(lhs.sizes());
      unaryOp2_(
        ret.base(), lhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> left(NArray<T, N>&& lhs, Operator op)
    {
      NArray<T, N> ret(std::forward<wilt::NArray<T, N>>(lhs));
      unaryOp2_(
        ret.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> right(NArray<T, N>&& rhs, Operator op)
    {
      NArray<T, N> ret(std::forward<NArray<T, N>>(rhs)); 
      unaryOp2_(
        ret.base(), ret.base(), ret.sizes().data(), 
        ret.steps().data(), ret.steps().data(), op, N);
      return ret;
    }
  };

  template <class Ret, class T, std::size_t N>
  struct move_op<Ret, T, T, N>
  {
    template <class Operator>
    static NArray<Ret, N> none(const NArray<T, N>& lhs, const NArray<T, N>& rhs, Operator op)
    {
      NArray<Ret, N> ret(lhs.sizes());
      binaryOp2_(
        ret.base(), lhs.base(), rhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), rhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<Ret, N> left(NArray<T, N>&& lhs, const NArray<T, N>& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
    template <class Operator>
    static NArray<Ret, N> right(const NArray<T, N>& lhs, NArray<T, N>&& rhs, Operator op)
    {
      return none(lhs, rhs, op);
    }
  
    template <class Operator>
    static NArray<Ret, N> none(const NArray<T, N>& lhs, Operator op)
    {
      NArray<Ret, N> ret(lhs.sizes());
      unaryOp2_(
        ret.base(), lhs.base(), ret.sizes().data(), 
        ret.steps().data(), lhs.steps().data(), op, N);
      return ret;
    }
    template <class Operator>
    static NArray<T, N> left(NArray<T, N>&& lhs, Operator op)
    {
      return none(lhs, op);
    }
    template <class Operator>
    static NArray<T, N> right(NArray<T, N>&& rhs, Operator op)
    {
      return none(rhs, op);
    }
  };

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t + u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("add(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t + u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("add(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t + u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("add(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t + u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("add(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t + rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t + rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<wilt::NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs + u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> add(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs + u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t - u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("sub(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t - u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("sub(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t - u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("sub(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t - u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("sub(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t - rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t - rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs - u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> sub(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs - u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<wilt::NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t * u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mul(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t * u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mul(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t * u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mul(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t * u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mul(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t * rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t * rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs * u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mul(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs * u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t / u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("div(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t / u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("div(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t / u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("div(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t / u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("div(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t / rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t / rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<wilt::NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs / u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> div(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs / u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t % u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mod(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t % u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mod(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t % u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mod(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t % u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("mod(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t % rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t % rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs % u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> mod(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs % u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t & u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_and(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t & u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_and(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t & u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_and(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t & u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_and(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t & rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t & rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs & u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_and(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs & u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t | u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_or(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t | u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_or(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t | u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_or(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t | u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_or(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t | rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t | rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs | u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_or(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs | u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(const NArray<T, N>& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t ^ u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_xor(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(NArray<T, N>&& lhs, const NArray<U, N>& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t ^ u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_xor(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(const NArray<T, N>& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t ^ u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_xor(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(NArray<T, N>&& lhs, NArray<U, N>&& rhs)
  {
    auto op = [](Ret& r, const T& t, const U& u){ r = t ^ u; };

    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("bit_xor(): dimensions must match");
    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), rhs, op);
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(lhs, std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(lhs, rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(const NArray<T, N>& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t ^ rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(NArray<T, N>&& lhs, const U& rhs)
  {
    auto op = [&rhs](Ret& r, const T& t){ r = t ^ rhs; };

    if (lhs.empty())
      return NArray<Ret, N>();
    if (lhs.unique() && lhs.isContiguous() && lhs.isAligned())
      return move_op<Ret, T, U, N>::left(std::forward<NArray<T, N>>(lhs), op);
    
    return move_op<Ret, T, U, N>::none(lhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(const T& lhs, const NArray<U, N>& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs ^ u; };

    if (rhs.empty())
      return NArray<Ret, N>();

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  template <class Ret, class T, class U, std::size_t N>
  NArray<Ret, N> bit_xor(const T& lhs, NArray<U, N>&& rhs)
  {
    auto op = [&lhs](Ret& r, const U& u){ r = lhs ^ u; };

    if (rhs.empty())
      return NArray<Ret, N>();
    if (rhs.unique() && rhs.isContiguous() && rhs.isAligned())
      return move_op<Ret, T, U, N>::right(std::forward<NArray<U, N>>(rhs), op);

    return move_op<Ret, T, U, N>::none(rhs, op);
  }

  enum CMP
  {
    LT, GT, EQ,
    GE, LE, NE
  }; // enum CMP

  template <class T, class U, std::size_t N>
  NArray<uint8_t, N> compare(const NArray<T, N>& lhs, const NArray<U, N>& rhs, CMP mode)
  {
    if (lhs.sizes() != rhs.sizes())
      throw std::invalid_argument("compare(): dimensions must match");
    if (lhs.empty())
      return NArray<uint8_t, N>();

{'fraction_non_alphanumeric': 0.20589258210364186, 'fraction_numerical': 0.0008694788095592984, 'mean_word_length': 3.029277813923227, 'pattern_counts': {'":': 0, '<': 1333, '<?xml version=': 0, '>': 1333, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6242966', 'n_tokens_mistral': 31988, 'n_tokens_neox': 31912, 'n_words': 9004}