// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Copyright (C) 2014 Benoit Steiner <benoit.steiner.goog@gmail.com>
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

#ifndef EIGEN_CXX11_TENSOR_TENSOR_BROADCASTING_H
#define EIGEN_CXX11_TENSOR_TENSOR_BROADCASTING_H

namespace Eigen {

/** \class TensorBroadcasting
  * \ingroup CXX11_Tensor_Module
  *
  * \brief Tensor broadcasting class.
  *
  *
  */
namespace internal {
template<typename Broadcast, typename XprType>
struct traits<TensorBroadcastingOp<Broadcast, XprType> > : public traits<XprType>
{
  typedef typename XprType::Scalar Scalar;
  typedef traits<XprType> XprTraits;
  typedef typename XprTraits::StorageKind StorageKind;
  typedef typename XprTraits::Index Index;
  typedef typename XprType::Nested Nested;
  typedef typename remove_reference<Nested>::type _Nested;
  static const int NumDimensions = XprTraits::NumDimensions;
  static const int Layout = XprTraits::Layout;
  typedef typename XprTraits::PointerType PointerType;
};

template<typename Broadcast, typename XprType>
struct eval<TensorBroadcastingOp<Broadcast, XprType>, Eigen::Dense>
{
  typedef const TensorBroadcastingOp<Broadcast, XprType> EIGEN_DEVICE_REF type;
};

template<typename Broadcast, typename XprType>
struct nested<TensorBroadcastingOp<Broadcast, XprType>, 1, typename eval<TensorBroadcastingOp<Broadcast, XprType> >::type>
{
  typedef TensorBroadcastingOp<Broadcast, XprType> type;
};

template <typename Dims>
struct is_input_scalar {
  static const bool value = false;
};
template <>
struct is_input_scalar<Sizes<> > {
  static const bool value = true;
};
#ifndef EIGEN_EMULATE_CXX11_META_H
template <typename std::ptrdiff_t... Indices>
struct is_input_scalar<Sizes<Indices...> > {
  static const bool value = (Sizes<Indices...>::total_size == 1);
};
#endif

}  // end namespace internal



template<typename Broadcast, typename XprType>
class TensorBroadcastingOp : public TensorBase<TensorBroadcastingOp<Broadcast, XprType>, ReadOnlyAccessors>
{
  public:
  typedef typename Eigen::internal::traits<TensorBroadcastingOp>::Scalar Scalar;
  typedef typename Eigen::NumTraits<Scalar>::Real RealScalar;
  typedef typename XprType::CoeffReturnType CoeffReturnType;
  typedef typename Eigen::internal::nested<TensorBroadcastingOp>::type Nested;
  typedef typename Eigen::internal::traits<TensorBroadcastingOp>::StorageKind StorageKind;
  typedef typename Eigen::internal::traits<TensorBroadcastingOp>::Index Index;

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE TensorBroadcastingOp(const XprType& expr, const Broadcast& broadcast)
      : m_xpr(expr), m_broadcast(broadcast) {}

    EIGEN_DEVICE_FUNC
    const Broadcast& broadcast() const { return m_broadcast; }

    EIGEN_DEVICE_FUNC
    const typename internal::remove_all<typename XprType::Nested>::type&
    expression() const { return m_xpr; }

  protected:
    typename XprType::Nested m_xpr;
    const Broadcast m_broadcast;
};


// Eval as rvalue
template<typename Broadcast, typename ArgType, typename Device>
struct TensorEvaluator<const TensorBroadcastingOp<Broadcast, ArgType>, Device>
{
  typedef TensorBroadcastingOp<Broadcast, ArgType> XprType;
  typedef typename XprType::Index Index;
  static const int NumDims = internal::array_size<typename TensorEvaluator<ArgType, Device>::Dimensions>::value;
  typedef DSizes<Index, NumDims> Dimensions;
  typedef typename XprType::Scalar Scalar;
  typedef typename TensorEvaluator<ArgType, Device>::Dimensions InputDimensions;
  typedef typename XprType::CoeffReturnType CoeffReturnType;
  typedef typename PacketType<CoeffReturnType, Device>::type PacketReturnType;
  static const int PacketSize = PacketType<CoeffReturnType, Device>::size;
  protected: //  all the non-static fields must have the same access control, otherwise the TensorEvaluator wont be standard layout;
  bool isCopy, nByOne, oneByN;
  public:
  typedef StorageMemory<CoeffReturnType, Device> Storage;
  typedef typename Storage::Type EvaluatorPointerType;

  enum {
    IsAligned         = true,
    PacketAccess      = TensorEvaluator<ArgType, Device>::PacketAccess,
    BlockAccess       = TensorEvaluator<ArgType, Device>::BlockAccess,
    PreferBlockAccess = true,
    Layout            = TensorEvaluator<ArgType, Device>::Layout,
    RawAccess         = false
  };

  typedef typename internal::remove_const<Scalar>::type ScalarNoConst;

  // Block based access to the XprType (input) tensor.
  typedef internal::TensorBlock<ScalarNoConst, Index, NumDims, Layout>
      TensorBlock;
  typedef internal::TensorBlockReader<ScalarNoConst, Index, NumDims, Layout>
      TensorBlockReader;

  // We do block based broadcasting using a trick with 2x tensor rank and 0
  // strides. See block method implementation for details.
  typedef DSizes<Index, 2 * NumDims> BroadcastDimensions;
  typedef internal::TensorBlock<ScalarNoConst, Index, 2 * NumDims, Layout>
      BroadcastTensorBlock;
  typedef internal::TensorBlockReader<ScalarNoConst, Index, 2 * NumDims, Layout>
      BroadcastTensorBlockReader;

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE TensorEvaluator(const XprType& op,
                                                        const Device& device)
      : isCopy(false), nByOne(false), oneByN(false),
        m_device(device), m_broadcast(op.broadcast()), m_impl(op.expression(), device)
  {

    // The broadcasting op doesn't change the rank of the tensor. One can't broadcast a scalar
    // and store the result in a scalar. Instead one should reshape the scalar into a a N-D
    // tensor with N >= 1 of 1 element first and then broadcast.
    EIGEN_STATIC_ASSERT((NumDims > 0), YOU_MADE_A_PROGRAMMING_MISTAKE);
    const InputDimensions& input_dims = m_impl.dimensions();
    isCopy = true;
    for (int i = 0; i < NumDims; ++i) {
      eigen_assert(input_dims[i] > 0);
      m_dimensions[i] = input_dims[i] * m_broadcast[i];
      if (m_broadcast[i] != 1) {
        isCopy = false;
      }
    }

    if (static_cast<int>(Layout) == static_cast<int>(ColMajor)) {
      m_inputStrides[0] = 1;
      m_outputStrides[0] = 1;
      for (int i = 1; i < NumDims; ++i) {
        m_inputStrides[i] = m_inputStrides[i-1] * input_dims[i-1];
        m_outputStrides[i] = m_outputStrides[i-1] * m_dimensions[i-1];
      }
    } else {
      m_inputStrides[NumDims-1] = 1;
      m_outputStrides[NumDims-1] = 1;
      for (int i = NumDims-2; i >= 0; --i) {
        m_inputStrides[i] = m_inputStrides[i+1] * input_dims[i+1];
        m_outputStrides[i] = m_outputStrides[i+1] * m_dimensions[i+1];
      }
    }

    if (input_dims[0] == 1) {
      oneByN = true;
      for (int i = 1; i < NumDims; ++i) {
        if (m_broadcast[i] != 1) {
          oneByN = false;
          break;
        }
      }
    } else if (input_dims[NumDims-1] == 1) {
      nByOne = true;
      for (int i = 0; i < NumDims-1; ++i) {
        if (m_broadcast[i] != 1) {
          nByOne = false;
          break;
        }
      }
    }

    // Handle special format like NCHW, its input shape is '[1, N..., 1]' and
    // broadcast shape is '[N, 1..., N]'
    if (!oneByN && !nByOne) {
      if (input_dims[0] == 1 && input_dims[NumDims-1] == 1 && NumDims > 2) {
        nByOne = true;
        oneByN = true;
        for (int i = 1; i < NumDims-1; ++i) {
          if (m_broadcast[i] != 1) {
            nByOne = false;
            oneByN = false;
            break;
          }
        }
      }
    }
  }

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE const Dimensions& dimensions() const { return m_dimensions; }

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE bool evalSubExprsIfNeeded(EvaluatorPointerType) {
    m_impl.evalSubExprsIfNeeded(NULL);
    return true;
  }

#ifdef EIGEN_USE_THREADS
  template <typename EvalSubExprsCallback>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void evalSubExprsIfNeededAsync(
      EvaluatorPointerType, EvalSubExprsCallback done) {
    m_impl.evalSubExprsIfNeededAsync(nullptr, [done](bool) { done(true); });
  }
#endif  // EIGEN_USE_THREADS

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void cleanup() {
    m_impl.cleanup();
  }

  EIGEN_DEVICE_FUNC EIGEN_ALWAYS_INLINE CoeffReturnType coeff(Index index) const
  {
    if (internal::is_input_scalar<typename internal::remove_all<InputDimensions>::type>::value) {
      return m_impl.coeff(0);
    }

    if (static_cast<int>(Layout) == static_cast<int>(ColMajor)) {
      if (isCopy) {
        return m_impl.coeff(index);
      } else {
        return coeffColMajor(index);
      }
    } else {
      if (isCopy) {
        return m_impl.coeff(index);
      } else {
        return coeffRowMajor(index);
      }
    }
  }

  // TODO: attempt to speed this up. The integer divisions and modulo are slow
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Index indexColMajor(Index index) const {
    Index inputIndex = 0;
    EIGEN_UNROLL_LOOP
    for (int i = NumDims - 1; i > 0; --i) {
      const Index idx = index / m_outputStrides[i];
      if (internal::index_statically_eq<Broadcast>(i, 1)) {
        eigen_assert(idx < m_impl.dimensions()[i]);
        inputIndex += idx * m_inputStrides[i];
      } else {
        if (internal::index_statically_eq<InputDimensions>(i, 1)) {
          eigen_assert(idx % m_impl.dimensions()[i] == 0);
        } else {
          inputIndex += (idx % m_impl.dimensions()[i]) * m_inputStrides[i];
        }
      }
      index -= idx * m_outputStrides[i];
    }
    if (internal::index_statically_eq<Broadcast>(0, 1)) {
      eigen_assert(index < m_impl.dimensions()[0]);
      inputIndex += index;
    } else {
      if (internal::index_statically_eq<InputDimensions>(0, 1)) {
        eigen_assert(index % m_impl.dimensions()[0] == 0);
      } else {
        inputIndex += (index % m_impl.dimensions()[0]);
      }
    }
    return inputIndex;
  }

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE CoeffReturnType coeffColMajor(Index index) const
  {
    return m_impl.coeff(indexColMajor(index));
  }

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Index indexRowMajor(Index index) const {
    Index inputIndex = 0;
    EIGEN_UNROLL_LOOP
    for (int i = 0; i < NumDims - 1; ++i) {
      const Index idx = index / m_outputStrides[i];
      if (internal::index_statically_eq<Broadcast>(i, 1)) {
        eigen_assert(idx < m_impl.dimensions()[i]);
        inputIndex += idx * m_inputStrides[i];
      } else {
        if (internal::index_statically_eq<InputDimensions>(i, 1)) {
          eigen_assert(idx % m_impl.dimensions()[i] == 0);
        } else {
          inputIndex += (idx % m_impl.dimensions()[i]) * m_inputStrides[i];
        }
      }
      index -= idx * m_outputStrides[i];
    }
    if (internal::index_statically_eq<Broadcast>(NumDims - 1, 1)) {
      eigen_assert(index < m_impl.dimensions()[NumDims - 1]);
      inputIndex += index;
    } else {
      if (internal::index_statically_eq<InputDimensions>(NumDims - 1, 1)) {
        eigen_assert(index % m_impl.dimensions()[NumDims - 1] == 0);
      } else {
        inputIndex += (index % m_impl.dimensions()[NumDims - 1]);
      }
    }
    return inputIndex;
  }

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE CoeffReturnType coeffRowMajor(Index index) const
  {
    return m_impl.coeff(indexRowMajor(index));
  }

  template<int LoadMode>
  EIGEN_DEVICE_FUNC EIGEN_ALWAYS_INLINE PacketReturnType packet(Index index) const
  {
    if (internal::is_input_scalar<typename internal::remove_all<InputDimensions>::type>::value) {
      return internal::pset1<PacketReturnType>(m_impl.coeff(0));
    }

    if (static_cast<int>(Layout) == static_cast<int>(ColMajor)) {
      if (isCopy) {
        #ifdef EIGEN_GPU_COMPILE_PHASE
        // See PR 437: on NVIDIA P100 and K20m we observed a x3-4 speed up by enforcing
        // unaligned loads here. The reason is unclear though.
        return m_impl.template packet<Unaligned>(index);
        #else
        return m_impl.template packet<LoadMode>(index);
        #endif
      } else if (oneByN && !nByOne) {
        return packetNByOne<LoadMode>(index);
      } else if (!oneByN && nByOne) {
        return packetOneByN<LoadMode>(index);
      } else if (oneByN && nByOne) {
        return packetOneByNByOne<LoadMode>(index);
      } else {
        return packetColMajor<LoadMode>(index);
      }
    } else {
      if (isCopy) {
        #ifdef EIGEN_GPU_COMPILE_PHASE
        // See above.
        return m_impl.template packet<Unaligned>(index);
        #else
        return m_impl.template packet<LoadMode>(index);
        #endif
      } else if (oneByN && !nByOne) {
        return packetOneByN<LoadMode>(index);
      } else if (!oneByN && nByOne) {
        return packetNByOne<LoadMode>(index);
      } else if (oneByN && nByOne) {
        return packetOneByNByOne<LoadMode>(index);
      } else {
        return packetRowMajor<LoadMode>(index);
      }
    }
  }

  template<int LoadMode>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE PacketReturnType packetOneByNByOne
  (Index index) const
  {
    EIGEN_STATIC_ASSERT((PacketSize > 1), YOU_MADE_A_PROGRAMMING_MISTAKE)
    eigen_assert(index+PacketSize-1 < dimensions().TotalSize());

    EIGEN_ALIGN_MAX typename internal::remove_const<CoeffReturnType>::type values[PacketSize];
    Index startDim, endDim;
    Index inputIndex, outputOffset, batchedIndex;

    if (static_cast<int>(Layout) == static_cast<int>(ColMajor)) {
      startDim = NumDims - 1;
      endDim = 1;
    } else {
      startDim = 0;
      endDim = NumDims - 2;
    }

    batchedIndex = index % m_outputStrides[startDim];
    inputIndex   = batchedIndex / m_outputStrides[endDim];
    outputOffset = batchedIndex % m_outputStrides[endDim];

    if (outputOffset + PacketSize <= m_outputStrides[endDim]) {
      values[0] = m_impl.coeff(inputIndex);
      return internal::pload1<PacketReturnType>(values);
    } else {
      EIGEN_UNROLL_LOOP
      for (int i = 0, cur = 0; i < PacketSize; ++i, ++cur) {
        if (outputOffset + cur < m_outputStrides[endDim]) {
          values[i] = m_impl.coeff(inputIndex);
        } else {
          ++inputIndex;
          inputIndex = (inputIndex == m_inputStrides[startDim] ? 0 : inputIndex);
          values[i] = m_impl.coeff(inputIndex);
          outputOffset = 0;
          cur = 0;
        }
      }
      return internal::pload<PacketReturnType>(values);
    }
  }

  template<int LoadMode>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE PacketReturnType packetOneByN(Index index) const
  {
    EIGEN_STATIC_ASSERT((PacketSize > 1), YOU_MADE_A_PROGRAMMING_MISTAKE)
    eigen_assert(index+PacketSize-1 < dimensions().TotalSize());

    Index dim, inputIndex;

    if (static_cast<int>(Layout) == static_cast<int>(ColMajor)) {
      dim = NumDims - 1;
    } else {
      dim = 0;
    }

    inputIndex = index % m_inputStrides[dim];
    if (inputIndex + PacketSize <= m_inputStrides[dim]) {
      return m_impl.template packet<Unaligned>(inputIndex);
    } else {
      EIGEN_ALIGN_MAX typename internal::remove_const<CoeffReturnType>::type values[PacketSize];
      EIGEN_UNROLL_LOOP
      for (int i = 0; i < PacketSize; ++i) {
        if (inputIndex > m_inputStrides[dim]-1) {
          inputIndex = 0;
        }
        values[i] = m_impl.coeff(inputIndex++);
      }
      return internal::pload<PacketReturnType>(values);
    }
  }

  template<int LoadMode>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE PacketReturnType packetNByOne(Index index) const
  {
    EIGEN_STATIC_ASSERT((PacketSize > 1), YOU_MADE_A_PROGRAMMING_MISTAKE)
    eigen_assert(index+PacketSize-1 < dimensions().TotalSize());

    EIGEN_ALIGN_MAX typename internal::remove_const<CoeffReturnType>::type values[PacketSize];
    Index dim, inputIndex, outputOffset;

    if (static_cast<int>(Layout) == static_cast<int>(ColMajor)) {
      dim = 1;
    } else {
      dim = NumDims - 2;
    }

    inputIndex   = index / m_outputStrides[dim];
    outputOffset = index % m_outputStrides[dim];
    if (outputOffset + PacketSize <= m_outputStrides[dim]) {
      values[0] = m_impl.coeff(inputIndex);
      return internal::pload1<PacketReturnType>(values);
    } else {
      EIGEN_UNROLL_LOOP
      for (int i = 0, cur = 0; i < PacketSize; ++i, ++cur) {
        if (outputOffset + cur < m_outputStrides[dim]) {
          values[i] = m_impl.coeff(inputIndex);
        } else {
          values[i] = m_impl.coeff(++inputIndex);
          outputOffset = 0;
          cur = 0;
        }
      }
      return internal::pload<PacketReturnType>(values);
    }
  }

  // Ignore the LoadMode and always use unaligned loads since we can't guarantee
  // the alignment at compile time.
  template<int LoadMode>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE PacketReturnType packetColMajor(Index index) const
  {
    EIGEN_STATIC_ASSERT((PacketSize > 1), YOU_MADE_A_PROGRAMMING_MISTAKE)
    eigen_assert(index+PacketSize-1 < dimensions().TotalSize());

    const Index originalIndex = index;

    Index inputIndex = 0;
    EIGEN_UNROLL_LOOP
    for (int i = NumDims - 1; i > 0; --i) {
      const Index idx = index / m_outputStrides[i];
      if (internal::index_statically_eq<Broadcast>(i, 1)) {
        eigen_assert(idx < m_impl.dimensions()[i]);
        inputIndex += idx * m_inputStrides[i];
      } else {
        if (internal::index_statically_eq<InputDimensions>(i, 1)) {
          eigen_assert(idx % m_impl.dimensions()[i] == 0);
        } else {
          inputIndex += (idx % m_impl.dimensions()[i]) * m_inputStrides[i];
        }
      }
      index -= idx * m_outputStrides[i];
    }
    Index innermostLoc;
    if (internal::index_statically_eq<Broadcast>(0, 1)) {
      eigen_assert(index < m_impl.dimensions()[0]);
      innermostLoc = index;
    } else {
      if (internal::index_statically_eq<InputDimensions>(0, 1)) {
        eigen_assert(index % m_impl.dimensions()[0] == 0);
        innermostLoc = 0;
      } else {
        innermostLoc = index % m_impl.dimensions()[0];
      }
    }
    inputIndex += innermostLoc;

    // Todo: this could be extended to the second dimension if we're not
    // broadcasting alongside the first dimension, and so on.
    if (innermostLoc + PacketSize <= m_impl.dimensions()[0]) {
      return m_impl.template packet<Unaligned>(inputIndex);
    } else {
      EIGEN_ALIGN_MAX typename internal::remove_const<CoeffReturnType>::type values[PacketSize];
      values[0] = m_impl.coeff(inputIndex);
      EIGEN_UNROLL_LOOP
      for (int i = 1; i < PacketSize; ++i) {
        if (innermostLoc + i < m_impl.dimensions()[0]) {
          values[i] = m_impl.coeff(inputIndex+i);
        } else {
          values[i] = coeffColMajor(originalIndex+i);
        }
      }
      PacketReturnType rslt = internal::pload<PacketReturnType>(values);
      return rslt;
    }
  }

  template<int LoadMode>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE PacketReturnType packetRowMajor(Index index) const
  {
    EIGEN_STATIC_ASSERT((PacketSize > 1), YOU_MADE_A_PROGRAMMING_MISTAKE)
    eigen_assert(index+PacketSize-1 < dimensions().TotalSize());

    const Index originalIndex = index;

    Index inputIndex = 0;
    EIGEN_UNROLL_LOOP
    for (int i = 0; i < NumDims - 1; ++i) {
      const Index idx = index / m_outputStrides[i];
      if (internal::index_statically_eq<Broadcast>(i, 1)) {
        eigen_assert(idx < m_impl.dimensions()[i]);
        inputIndex += idx * m_inputStrides[i];
      } else {
        if (internal::index_statically_eq<InputDimensions>(i, 1)) {
          eigen_assert(idx % m_impl.dimensions()[i] == 0);
        } else {
          inputIndex += (idx % m_impl.dimensions()[i]) * m_inputStrides[i];
        }
      }
      index -= idx * m_outputStrides[i];
    }
    Index innermostLoc;
    if (internal::index_statically_eq<Broadcast>(NumDims-1, 1)) {
      eigen_assert(index < m_impl.dimensions()[NumDims-1]);
      innermostLoc = index;
    } else {
      if (internal::index_statically_eq<InputDimensions>(NumDims-1, 1)) {
        eigen_assert(index % m_impl.dimensions()[NumDims-1] == 0);
        innermostLoc = 0;
      } else {
        innermostLoc = index % m_impl.dimensions()[NumDims-1];
      }
    }
    inputIndex += innermostLoc;

    // Todo: this could be extended to the second dimension if we're not
    // broadcasting alongside the first dimension, and so on.
    if (innermostLoc + PacketSize <= m_impl.dimensions()[NumDims-1]) {
      return m_impl.template packet<Unaligned>(inputIndex);
    } else {
      EIGEN_ALIGN_MAX typename internal::remove_const<CoeffReturnType>::type values[PacketSize];
      values[0] = m_impl.coeff(inputIndex);
      EIGEN_UNROLL_LOOP
      for (int i = 1; i < PacketSize; ++i) {
        if (innermostLoc + i < m_impl.dimensions()[NumDims-1]) {
          values[i] = m_impl.coeff(inputIndex+i);
        } else {
          values[i] = coeffRowMajor(originalIndex+i);
        }
      }
      PacketReturnType rslt = internal::pload<PacketReturnType>(values);
      return rslt;
    }
  }

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE TensorOpCost
  costPerCoeff(bool vectorized) const {
    double compute_cost = TensorOpCost::AddCost<Index>();
    if (!isCopy && NumDims > 0) {
      EIGEN_UNROLL_LOOP
      for (int i = NumDims - 1; i > 0; --i) {
        compute_cost += TensorOpCost::DivCost<Index>();
        if (internal::index_statically_eq<Broadcast>(i, 1)) {
          compute_cost +=
              TensorOpCost::MulCost<Index>() + TensorOpCost::AddCost<Index>();
        } else {
          if (!internal::index_statically_eq<InputDimensions>(i, 1)) {
            compute_cost += TensorOpCost::MulCost<Index>() +
                            TensorOpCost::ModCost<Index>() +
                            TensorOpCost::AddCost<Index>();
          }
        }
        compute_cost +=
            TensorOpCost::MulCost<Index>() + TensorOpCost::AddCost<Index>();
      }
    }
    return m_impl.costPerCoeff(vectorized) +
           TensorOpCost(0, 0, compute_cost, vectorized, PacketSize);
  }

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void getResourceRequirements(
      std::vector<internal::TensorOpResourceRequirements>* resources) const {
    // TODO(wuke): Targeting L1 size is 30% faster than targeting L{-1} on large
    // tensors. But this might need further tuning.
    Eigen::Index block_total_size_max = numext::maxi<Eigen::Index>(
        1, m_device.firstLevelCacheSize() / sizeof(Scalar));

    resources->push_back(internal::TensorOpResourceRequirements(
        internal::kSkewedInnerDims, block_total_size_max));

    m_impl.getResourceRequirements(resources);
  }

  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void block(
      TensorBlock* output_block) const {
    if (NumDims <= 0) {
      output_block->data()[0] = m_impl.coeff(0);
      return;
    }

    // Because we only support kSkewedInnerDims blocking, block size should be
    // equal to m_dimensions for inner dims, a smaller than m_dimensions[i] size
    // for the first outer dim, and 1 for other outer dims. This is guaranteed
    // by MergeResourceRequirements() in TensorBlock.h.
    const Dimensions& output_block_sizes = output_block->block_sizes();
    const Dimensions& output_block_strides = output_block->block_strides();

    // Find where outer dims start.
    int outer_dim_start = 0;
    Index outer_dim_size = 1, inner_dim_size = 1;
    for (int i = 0; i < NumDims; ++i) {
      const int dim = static_cast<int>(Layout) == static_cast<int>(ColMajor)
                          ? i
                          : NumDims - i - 1;
      if (i > outer_dim_start) {
        eigen_assert(output_block_sizes[dim] == 1);
      } else if (output_block_sizes[dim] != m_dimensions[dim]) {
        eigen_assert(output_block_sizes[dim] < m_dimensions[dim]);
        outer_dim_size = output_block_sizes[dim];
      } else {
        inner_dim_size *= output_block_sizes[dim];
        ++outer_dim_start;
      }
    }

    if (inner_dim_size == 0 || outer_dim_size == 0) {
      return;
    }

    const Dimensions& input_dims = Dimensions(m_impl.dimensions());

    // Pre-fill input_block_sizes, broadcast_block_sizes,
    // broadcast_block_strides, and broadcast_tensor_strides. Later on we will
    // only modify the outer_dim_start-th dimension on these arrays.

    // Calculate the input block size for looking into the input.
    Dimensions input_block_sizes;
    if (static_cast<int>(Layout) == static_cast<int>(ColMajor)) {
      for (int i = 0; i < outer_dim_start; ++i) {
        input_block_sizes[i] = input_dims[i];
      }
      for (int i = outer_dim_start; i < NumDims; ++i) {
        input_block_sizes[i] = 1;
      }
    } else {
      for (int i = 0; i < outer_dim_start; ++i) {
        input_block_sizes[NumDims - i - 1] = input_dims[NumDims - i - 1];
      }
      for (int i = outer_dim_start; i < NumDims; ++i) {
        input_block_sizes[NumDims - i - 1] = 1;
      }
    }

    // Broadcast with the 0-stride trick: Create 1 extra dim for each
    // broadcast, set the input stride to 0.
    //
    // When ColMajor:
    // - broadcast_block_sizes is [d_0, b_0, d_1, b_1, ...].
    //
    // - broadcast_block_strides is [output_block_strides[0],
    //                               output_block_strides[0] * d_0,
    //                               output_block_strides[1],
    //                               output_block_strides[1] * d_1,
    //                               ...].
    //
    // - broadcast_tensor_strides is [output_block_strides[0],
    //                                0,
    //                                output_block_strides[1],
    //                                0,
    //                                ...].
    BroadcastDimensions broadcast_block_sizes, broadcast_block_strides,
        broadcast_tensor_strides;

    for (int i = 0; i < outer_dim_start; ++i) {
      const int dim = static_cast<int>(Layout) == static_cast<int>(ColMajor)
                          ? i
                          : NumDims - i - 1;
      const int copy_dim =
          static_cast<int>(Layout) == static_cast<int>(ColMajor)
              ? 2 * i
              : 2 * NumDims - 2 * i - 1;
      const int broadcast_dim =
          static_cast<int>(Layout) == static_cast<int>(ColMajor) ? copy_dim + 1
                                                                 : copy_dim - 1;
      broadcast_block_sizes[copy_dim] = input_dims[dim];
      broadcast_block_sizes[broadcast_dim] = m_broadcast[dim];
      broadcast_block_strides[copy_dim] = output_block_strides[dim];
      broadcast_block_strides[broadcast_dim] =
          output_block_strides[dim] * input_dims[dim];
      broadcast_tensor_strides[copy_dim] = m_inputStrides[dim];
      broadcast_tensor_strides[broadcast_dim] = 0;
    }
    for (int i = 2 * outer_dim_start; i < 2 * NumDims; ++i) {
      const int dim = static_cast<int>(Layout) == static_cast<int>(ColMajor)
                          ? i
                          : 2 * NumDims - i - 1;
      broadcast_block_sizes[dim] = 1;
      broadcast_block_strides[dim] = 0;
      broadcast_tensor_strides[dim] = 0;
    }

    const int outer_dim = static_cast<int>(Layout) == static_cast<int>(ColMajor)
                              ? outer_dim_start
                              : NumDims - outer_dim_start - 1;

    if (outer_dim_size == 1) {
      // We just need one block read using the ready-set values above.
      BroadcastBlock(input_block_sizes, broadcast_block_sizes,
                     broadcast_block_strides, broadcast_tensor_strides, 0,
                     output_block);
    } else if (input_dims[outer_dim] == 1) {
      // Broadcast outer_dim_start-th dimension (< NumDims) by outer_dim_size.
      const int broadcast_outer_dim =
          static_cast<int>(Layout) == static_cast<int>(ColMajor)
              ? 2 * outer_dim_start + 1
              : 2 * NumDims - 2 * outer_dim_start - 2;
      broadcast_block_sizes[broadcast_outer_dim] = outer_dim_size;
      broadcast_tensor_strides[broadcast_outer_dim] = 0;
      broadcast_block_strides[broadcast_outer_dim] =
          output_block_strides[outer_dim];
      BroadcastBlock(input_block_sizes, broadcast_block_sizes,
                     broadcast_block_strides, broadcast_tensor_strides, 0,
                     output_block);
    } else {
      // The general case. Let's denote the output block as x[...,
      // a:a+outer_dim_size, :, ..., :], where a:a+outer_dim_size is a slice on
      // the outer_dim_start-th dimension (< NumDims). We need to split the
      // a:a+outer_dim_size into possibly 3 sub-blocks:
      //
      // (1) a:b, where b is the smallest multiple of
      // input_dims[outer_dim_start] in [a, a+outer_dim_size].
      //
      // (2) b:c, where c is the largest multiple of input_dims[outer_dim_start]
      // in [a, a+outer_dim_size].
      //
      // (3) c:a+outer_dim_size .
      //
      // Or, when b and c do not exist, we just need to process the whole block
      // together.

      // Find a.
      const Index outer_dim_left_index =
          output_block->first_coeff_index() / m_outputStrides[outer_dim];

      // Find b and c.
      const Index input_outer_dim_size = input_dims[outer_dim];

      // First multiple after a. This is b when <= outer_dim_left_index +
      // outer_dim_size.
      const Index first_multiple =
          divup<Index>(outer_dim_left_index, input_outer_dim_size) *
          input_outer_dim_size;

      if (first_multiple <= outer_dim_left_index + outer_dim_size) {
        // b exists, so does c. Find it.
        const Index last_multiple = (outer_dim_left_index + outer_dim_size) /
                                    input_outer_dim_size * input_outer_dim_size;
        const int copy_outer_dim =
            static_cast<int>(Layout) == static_cast<int>(ColMajor)
                ? 2 * outer_dim_start
                : 2 * NumDims - 2 * outer_dim_start - 1;
        const int broadcast_outer_dim =
            static_cast<int>(Layout) == static_cast<int>(ColMajor)
                ? 2 * outer_dim_start + 1
                : 2 * NumDims - 2 * outer_dim_start - 2;
        if (first_multiple > outer_dim_left_index) {
          const Index head_size = first_multiple - outer_dim_left_index;
          input_block_sizes[outer_dim] = head_size;
          broadcast_block_sizes[copy_outer_dim] = head_size;
          broadcast_tensor_strides[copy_outer_dim] = m_inputStrides[outer_dim];
          broadcast_block_strides[copy_outer_dim] =
              output_block_strides[outer_dim];
          broadcast_block_sizes[broadcast_outer_dim] = 1;
          broadcast_tensor_strides[broadcast_outer_dim] = 0;
          broadcast_block_strides[broadcast_outer_dim] =
              output_block_strides[outer_dim] * input_dims[outer_dim];
          BroadcastBlock(input_block_sizes, broadcast_block_sizes,
                         broadcast_block_strides, broadcast_tensor_strides, 0,
                         output_block);
        }
        if (first_multiple < last_multiple) {
          input_block_sizes[outer_dim] = input_outer_dim_size;
          broadcast_block_sizes[copy_outer_dim] = input_outer_dim_size;
          broadcast_tensor_strides[copy_outer_dim] = m_inputStrides[outer_dim];
          broadcast_block_strides[copy_outer_dim] =
              output_block_strides[outer_dim];
          broadcast_block_sizes[broadcast_outer_dim] =
              (last_multiple - first_multiple) / input_outer_dim_size;
          broadcast_tensor_strides[broadcast_outer_dim] = 0;
          broadcast_block_strides[broadcast_outer_dim] =
              output_block_strides[outer_dim] * input_dims[outer_dim];
          const Index offset = (first_multiple - outer_dim_left_index) *
                               m_outputStrides[outer_dim];
          BroadcastBlock(input_block_sizes, broadcast_block_sizes,
                         broadcast_block_strides, broadcast_tensor_strides,
                         offset, output_block);
        }
        if (last_multiple < outer_dim_left_index + outer_dim_size) {
          const Index tail_size =
              outer_dim_left_index + outer_dim_size - last_multiple;
          input_block_sizes[outer_dim] = tail_size;
          broadcast_block_sizes[copy_outer_dim] = tail_size;
          broadcast_tensor_strides[copy_outer_dim] = m_inputStrides[outer_dim];
          broadcast_block_strides[copy_outer_dim] =
              output_block_strides[outer_dim];
          broadcast_block_sizes[broadcast_outer_dim] = 1;
          broadcast_tensor_strides[broadcast_outer_dim] = 0;
          broadcast_block_strides[broadcast_outer_dim] =
              output_block_strides[outer_dim] * input_dims[outer_dim];
          const Index offset = (last_multiple - outer_dim_left_index) *
                               m_outputStrides[outer_dim];
          BroadcastBlock(input_block_sizes, broadcast_block_sizes,
                         broadcast_block_strides, broadcast_tensor_strides,
                         offset, output_block);
        }
      } else {
        // b and c do not exist.
        const int copy_outer_dim =
            static_cast<int>(Layout) == static_cast<int>(ColMajor)
                ? 2 * outer_dim_start
                : 2 * NumDims - 2 * outer_dim_start - 1;
        input_block_sizes[outer_dim] = outer_dim_size;
        broadcast_block_sizes[copy_outer_dim] = outer_dim_size;
        broadcast_tensor_strides[copy_outer_dim] = m_inputStrides[outer_dim];
        broadcast_block_strides[copy_outer_dim] =
            output_block_strides[outer_dim];
        BroadcastBlock(input_block_sizes, broadcast_block_sizes,
                       broadcast_block_strides, broadcast_tensor_strides, 0,
                       output_block);
      }
    }
  }

  EIGEN_DEVICE_FUNC EvaluatorPointerType data() const { return NULL; }

  const TensorEvaluator<ArgType, Device>& impl() const { return m_impl; }

  Broadcast functor() const { return m_broadcast; }
  #ifdef EIGEN_USE_SYCL
  // binding placeholder accessors to a command group handler for SYCL
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void bind(cl::sycl::handler &cgh) const {
    m_impl.bind(cgh);
  }
  #endif
 private:
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void BroadcastBlock(
      const Dimensions& input_block_sizes,
      const BroadcastDimensions& broadcast_block_sizes,
      const BroadcastDimensions& broadcast_block_strides,
      const BroadcastDimensions& broadcast_tensor_strides, Index offset,
      TensorBlock* output_block) const {
    TensorBlock input_view_block(
        static_cast<int>(Layout) == static_cast<int>(ColMajor)
            ? indexColMajor(output_block->first_coeff_index() + offset)
            : indexRowMajor(output_block->first_coeff_index() + offset),
        input_block_sizes, Dimensions(m_inputStrides),
        Dimensions(m_inputStrides), NULL);

    internal::TensorBlockView<ArgType, Device> input_block(m_device, m_impl,
                                                           input_view_block);
    BroadcastTensorBlock broadcast_block(
        0, broadcast_block_sizes, broadcast_block_strides,
        broadcast_tensor_strides, output_block->data() + offset);

    BroadcastTensorBlockReader::Run(&broadcast_block, input_block.data());
  }

protected:
  const Device EIGEN_DEVICE_REF m_device;
  const typename internal::remove_reference<Broadcast>::type m_broadcast;
  Dimensions m_dimensions;
  array<Index, NumDims> m_outputStrides;
  array<Index, NumDims> m_inputStrides;
  TensorEvaluator<ArgType, Device> m_impl;
};


} // end namespace Eigen

#endif // EIGEN_CXX11_TENSOR_TENSOR_BROADCASTING_H
