#include <algorithm>
#include <vector>
#include "caffe2/core/tensor.h"
#include "caffe2/utils/eigen_utils.h"
#include "caffe2/utils/math.h"

namespace caffe2 {
namespace {

using t_tuple = std::tuple<Tensor, Tensor>;

template <typename T>
T copy_ctor(const T& x) {
  return x;
}

template <>
Tensor copy_ctor(const Tensor& X) {
  return X.UnsafeSharedInstance();
}

template <>
t_tuple copy_ctor(const t_tuple& X) {
  return std::make_tuple(copy_ctor(std::get<0>(X)), copy_ctor(std::get<1>(X)));
}

template <>
std::pair<t_tuple, t_tuple> copy_ctor(const std::pair<t_tuple, t_tuple>& X) {
  return std::make_pair(copy_ctor(X.first), copy_ctor(X.second));
}

template <>
std::vector<Tensor> copy_ctor(const std::vector<Tensor>& X) {
  std::vector<Tensor> Y(X.size());
  std::transform(X.begin(), X.end(), Y.begin(), [](const Tensor& x) {
    return copy_ctor(x);
  });
  return Y;
}

template <>
std::vector<t_tuple> copy_ctor(const std::vector<t_tuple>& X) {
  std::vector<t_tuple> Y(X.size());
  std::transform(X.begin(), X.end(), Y.begin(), [](const t_tuple& x) {
    return copy_ctor(x);
  });
  return Y;
}

template <>
std::vector<std::pair<t_tuple, t_tuple>> copy_ctor(
    const std::vector<std::pair<t_tuple, t_tuple>>& X) {
  std::vector<std::pair<t_tuple, t_tuple>> Y(X.size());
  std::transform(
      X.begin(), X.end(), Y.begin(), [](const std::pair<t_tuple, t_tuple>& x) {
        return copy_ctor(x);
      });
  return Y;
}

// Gathers every two elements of a vector in a vector of pairs
template <typename T>
static std::vector<std::pair<T, T>> pair_vec(const std::vector<T>& vals) {
  CAFFE_ENFORCE_EQ(
      vals.size() % 2,
      0,
      "Odd number of params or hiddens given to a bidirectional RNN");
  std::vector<std::pair<T, T>> result;
  result.reserve(vals.size() / 2);
  for (int64_t i = 0; i < vals.size(); i += 2) {
    result.emplace_back(copy_ctor(vals[i]), copy_ctor(vals[i + 1]));
  }
  return result;
}

// Flattens a vector of pairs
template <typename T>
static std::vector<T> unpair_vec(std::vector<std::pair<T, T>>&& vals) {
  std::vector<T> result;
  result.reserve(vals.size() * 2);
  for (int64_t i = 0; i < vals.size(); i++) {
    result.push_back(std::move(vals[i].first));
    result.push_back(std::move(vals[i].second));
  }
  return result;
}

Tensor matmul(const Tensor& X, const Tensor& W, CPUContext* context) {
  const auto canonical_axis = X.canonical_axis_index(1);
  const auto M = X.size_to_dim(canonical_axis);
  const auto K = X.size_from_dim(canonical_axis);
  const auto canonical_axis_w = W.canonical_axis_index(1);
  const int N = W.size_to_dim(canonical_axis_w);
  auto output_size = X.sizes().vec();
  output_size.resize(canonical_axis + 1);
  output_size[canonical_axis] = N;
  Tensor C(output_size, CPU);
  math::Gemm<float, CPUContext>(
      CblasNoTrans,
      CblasTrans,
      M,
      N,
      K,
      1,
      X.template data<float>(),
      W.template data<float>(),
      0,
      C.template mutable_data<float>(),
      context);
  return C;
}

Tensor
linear(const Tensor& X, const Tensor& W, const Tensor& B, CPUContext* context) {
  auto output = matmul(X, W, context);
  if (B) {
    const auto canonical_axis = X.canonical_axis_index(1);
    const auto M = X.size_to_dim(canonical_axis);
    const auto canonical_axis_w = W.canonical_axis_index(1);
    const int N = W.size_to_dim(canonical_axis_w);
    auto bias_multiplier_ = caffe2::empty({M}, CPU);
    math::Set<float, CPUContext>(
        M, 1, bias_multiplier_.template mutable_data<float>(), context);
    math::Gemm<float, CPUContext>(
        CblasNoTrans,
        CblasNoTrans,
        M,
        N,
        1,
        1,
        bias_multiplier_.template data<float>(),
        B.template data<float>(),
        1,
        output.template mutable_data<float>(),
        context);
  }
  return output;
}

std::vector<Tensor>
chunk(const Tensor& input, int chunks, int axis, CPUContext* context) {
  int canonical_axis = input.canonical_axis_index(axis);
  CAFFE_ENFORCE_LT(
      canonical_axis, input.dim(), "Axis not in input ndim range.");
  const int input_channels = input.dim32(canonical_axis);
  CAFFE_ENFORCE_EQ(
      input_channels % chunks,
      0,
      "input channels should be divisible by the number of chunks.");
  auto split_size = input_channels / chunks;
  vector<int64_t> output_dims(input.sizes().vec());
  int before = 1, after = 1;
  for (int i = 0; i < canonical_axis; ++i) {
    before *= input.dim32(i);
  }
  for (int i = canonical_axis + 1; i < input.dim(); ++i) {
    after *= input.dim32(i);
  }
  size_t input_offset = 0;
  std::vector<Tensor> outputs;
  for (int i = 0; i < chunks; ++i) {
    auto axis_dim = split_size;
    output_dims[canonical_axis] = split_size;
    Tensor output(output_dims, CPU);
    math::CopyMatrix<CPUContext>(
        input.itemsize(),
        before,
        axis_dim * after,
        static_cast<const char*>(input.raw_data()) + input_offset,
        input.dim32(canonical_axis) * after,
        output.raw_mutable_data(input.dtype()),
        axis_dim * after,
        context,
        input.dtype().copy());
    input_offset += axis_dim * after * input.itemsize();
    outputs.push_back(std::move(output));
  }
  return outputs;
}

std::vector<Tensor> unbind(const Tensor& input, int axis, CPUContext* context) {
  // 1 - Chunk the input tensor along the given axis into N chunks where
  // N is the dim(axis)
  auto chunks = chunk(input, input.sizes()[axis], axis, context);
  // 2 - Compute new dimensions
  std::vector<int64_t> newDims = input.sizes().vec();
  newDims.erase(newDims.begin() + axis);

  // 3 - Reshape chunks to drop the extra dimension
  for (int i = 0; i < chunks.size(); i++) {
    CAFFE_ENFORCE_EQ(
        chunks[i].sizes()[axis], 1, "Got an unexpected chunk size");
    chunks[i].Reshape(newDims);
  }
  return chunks;
}

Tensor
cat(const std::vector<Tensor>& tensorList, int axis, CPUContext* context) {
  // Adopted from C2's concat operator
  auto input_zero = copy_ctor(tensorList.at(0));
  vector<int64_t> outputDims(input_zero.sizes().vec());
  CAFFE_ENFORCE(outputDims.size() > 0);
  for (int i = 1; i < tensorList.size(); i++) {
    CAFFE_ENFORCE(input_zero.dtype() == tensorList.at(i).dtype());
    outputDims[axis] += tensorList.at(i).sizes()[axis];
  }
  auto output_channels = outputDims[axis];
  Tensor output(outputDims, CPU);
  int before = 1, after = 1;
  for (int i = 0; i < tensorList.at(0).dim(); ++i) {
    if (i == axis) {
      continue;
    }
    int dim = input_zero.dim32(i);
    if (i < axis) {
      before *= dim;
    } else {
      after *= dim;
    }
  }
  size_t output_offset = 0;
  for (const auto& input : tensorList) {
    auto axis_dim = input.dim32(axis);
    math::CopyMatrix<CPUContext>(
        input.itemsize(),
        before,
        axis_dim * after,
        input.raw_data(),
        axis_dim * after,
        static_cast<char*>(output.raw_mutable_data(input_zero.dtype())) +
            output_offset,
        output_channels * after,
        context,
        input_zero.dtype().copy());
    output_offset += axis_dim * after * input.itemsize();
  }

  return output;
}

Tensor
stack(const std::vector<Tensor>& tensorList, int axis, CPUContext* context) {
  // 1 - Compute new dimensions
  std::vector<int64_t> newDims(tensorList[0].sizes().vec());
  std::vector<Tensor> expandedTensorList;
  newDims.insert(newDims.begin() + axis, 1);
  for (int i = 0; i < tensorList.size(); i++) {
    expandedTensorList.emplace_back(tensorList[i].Clone());
    expandedTensorList.at(i).Reshape(newDims);
  }
  return cat(expandedTensorList, axis, context);
}

Tensor sigmoid(const Tensor& X) {
  Tensor Y(X.sizes(), CPU);
  auto N = X.numel();
  EigenVectorArrayMap<float>(Y.template mutable_data<float>(), N) = 1.0 /
      (1.0 +
       (-ConstEigenVectorArrayMap<float>(X.template data<float>(), N)).exp());
  return Y;
}

Tensor tanh(const Tensor& X, CPUContext* context) {
  Tensor Y(X.sizes(), CPU);
  math::Tanh<float, CPUContext>(
      X.numel(),
      X.template data<float>(),
      Y.template mutable_data<float>(),
      context);
  return Y;
}

Tensor add(const Tensor& X, const Tensor& Y, CPUContext* context) {
  Tensor Z(X.sizes().vec(), CPU);
  math::Add<float, CPUContext>(
      X.numel(),
      X.template data<float>(),
      Y.template data<float>(),
      Z.template mutable_data<float>(),
      context);
  return Z;
}

Tensor mul(const Tensor& X, const Tensor& Y, CPUContext* context) {
  Tensor Z(X.sizes().vec(), CPU);
  math::Mul<float, CPUContext>(
      X.numel(),
      X.template data<float>(),
      Y.template data<float>(),
      Z.template mutable_data<float>(),
      context);
  return Z;
}

Tensor transpose(const Tensor& X, int dim0, int dim1, CPUContext* context) {
  int ndim = X.dim();
  CAFFE_ENFORCE(ndim > dim0 && ndim > dim1, "Invalid transpose dimensions");
  std::vector<int> axes(ndim);
  std::iota(axes.begin(), axes.end(), 0);
  std::swap(axes[dim0], axes[dim1]);
  std::vector<int> Y_dims(ndim);
  std::vector<int> X_dims(X.sizes().cbegin(), X.sizes().cend());
  for (int i = 0; i < ndim; ++i) {
    Y_dims[i] = X_dims[axes[i]];
  }
  Tensor Y(Y_dims, CPU);
  math::Transpose<float, CPUContext>(
      ndim,
      X_dims.data(),
      axes.data(),
      X.template data<float>(),
      Y.template mutable_data<float>(),
      context);
  return Y;
}
} // namespace
} // namespace caffe2
