/**TODO:  Add copyright*/

#define BOOST_TEST_MODULE ActivationFunctionTensor test suite
#include <boost/test/included/unit_test.hpp>
#include <EvoNet/ml/ActivationFunctionTensor.h>

#include <iostream>

using namespace EvoNet;
using namespace std;

BOOST_AUTO_TEST_SUITE(activationFunction)

/**
  ReLUTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorReluTensorOp)
{
  ReLUTensorOp<double, Eigen::DefaultDevice>* ptrReLU = nullptr;
  ReLUTensorOp<double, Eigen::DefaultDevice>* nullPointerReLU = nullptr;
  BOOST_CHECK_EQUAL(ptrReLU, nullPointerReLU);
}

BOOST_AUTO_TEST_CASE(destructorReluTensorOp)
{
  ReLUTensorOp<double, Eigen::DefaultDevice>* ptrReLU = nullptr;
	ptrReLU = new ReLUTensorOp<double, Eigen::DefaultDevice>();
  delete ptrReLU;
}

BOOST_AUTO_TEST_CASE(gettersAndSettersReluTensorOp)
{
  // Test defaults
  ReLUTensorOp<double, Eigen::DefaultDevice> operation_defaults;
  BOOST_CHECK_CLOSE(operation_defaults.getEps(), 1e-24, 1e-6);
  BOOST_CHECK_CLOSE(operation_defaults.getMin(), -1e9, 1e-6);
  BOOST_CHECK_CLOSE(operation_defaults.getMax(), 1e9, 1e-6);

  // Test setters
  operation_defaults.setEps(10);
  operation_defaults.setMin(20);
  operation_defaults.setMax(30);
  BOOST_CHECK_CLOSE(operation_defaults.getEps(), 10, 1e-6);
  BOOST_CHECK_CLOSE(operation_defaults.getMin(), 20, 1e-6);
  BOOST_CHECK_CLOSE(operation_defaults.getMax(), 30, 1e-6);

  // Test constructor
  ReLUTensorOp<double, Eigen::DefaultDevice> operation(10, 20, 30);
  BOOST_CHECK_CLOSE(operation.getEps(), 10, 1e-6);
  BOOST_CHECK_CLOSE(operation.getMin(), 20, 1e-6);
  BOOST_CHECK_CLOSE(operation.getMax(), 30, 1e-6);
}

BOOST_AUTO_TEST_CASE(operationfunctionReluTensorOp)
{
  ReLUTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{0,0}, {0,0}},
		{{0,0}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameReLUTensorOp)
{
	ReLUTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "ReLUTensorOp");
}

/**
  ReLUGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorReluGradTensorOp)
{
  ReLUGradTensorOp<double, Eigen::DefaultDevice>* ptrReLUGrad = nullptr;
  ReLUGradTensorOp<double, Eigen::DefaultDevice>* nullPointerReLUGrad = nullptr;
  BOOST_CHECK_EQUAL(ptrReLUGrad, nullPointerReLUGrad);
}

BOOST_AUTO_TEST_CASE(destructorReluGradTensorOp)
{
  ReLUGradTensorOp<double, Eigen::DefaultDevice>* ptrReLUGrad = nullptr;
	ptrReLUGrad = new ReLUGradTensorOp<double, Eigen::DefaultDevice>();
  delete ptrReLUGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionReluGradTensorOp)
{
  ReLUGradTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{1,1}, {0,0}},
		{{1,1}, {0,0}},
		{{0,0}, {0,0}},
		{{0,0}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameReLUGradTensorOp)
{
	ReLUGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "ReLUGradTensorOp");
}

/**
  ELUTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorEluTensorOp)
{
  ELUTensorOp<double, Eigen::DefaultDevice>* ptrELU = nullptr;
  ELUTensorOp<double, Eigen::DefaultDevice>* nullPointerELU = nullptr;
  BOOST_CHECK_EQUAL(ptrELU, nullPointerELU);
}

BOOST_AUTO_TEST_CASE(destructorEluTensorOp)
{
  ELUTensorOp<double, Eigen::DefaultDevice>* ptrELU = nullptr;
	ptrELU = new ELUTensorOp<double, Eigen::DefaultDevice>();
  delete ptrELU;
}

BOOST_AUTO_TEST_CASE(gettersAndSettersEluTensorOp)
{
  ELUTensorOp<double, Eigen::DefaultDevice> operation;
  operation.setAlpha(1.0);

  BOOST_CHECK_EQUAL(operation.getAlpha(), 1.0);
}

BOOST_AUTO_TEST_CASE(operationfunctionEluTensorOp)
{
  ELUTensorOp<double, Eigen::DefaultDevice> operation(1.0);
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-0.63212055882855767,-0.63212055882855767}, {0,0}},
		{{-0.99995460007023751,-0.99995460007023751}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameELUTensorOp)
{
	ELUTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "ELUTensorOp");
}

/**
  ELUGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorEluGradTensorOp)
{
  ELUGradTensorOp<double, Eigen::DefaultDevice>* ptrELU = nullptr;
  ELUGradTensorOp<double, Eigen::DefaultDevice>* nullPointerELU = nullptr;
  BOOST_CHECK_EQUAL(ptrELU, nullPointerELU);
}

BOOST_AUTO_TEST_CASE(destructorEluGradTensorOp)
{
  ELUGradTensorOp<double, Eigen::DefaultDevice>* ptrELU = nullptr;
	ptrELU = new ELUGradTensorOp<double, Eigen::DefaultDevice>();
  delete ptrELU;
}

BOOST_AUTO_TEST_CASE(gettersAndSettersEluGradTensorOp)
{
  ELUGradTensorOp<double, Eigen::DefaultDevice> operation;
  operation.setAlpha(1.0);

  BOOST_CHECK_EQUAL(operation.getAlpha(), 1.0);
}

BOOST_AUTO_TEST_CASE(operationfunctionEluGradTensorOp)
{
  ELUGradTensorOp<double, Eigen::DefaultDevice> operation(1.0);
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{1,1}, {0,0}},
		{{1,1}, {0,0}},
		{{0.36787944117144233,0.36787944117144233}, {0,0}},
		{{4.5399929762490743e-05,4.5399929762490743e-05}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameELUGradTensorOp)
{
	ELUGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "ELUGradTensorOp");
}

/**
  SigmoidTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorSigmoidTensorOp)
{
  SigmoidTensorOp<double, Eigen::DefaultDevice>* ptrSigmoid = nullptr;
  SigmoidTensorOp<double, Eigen::DefaultDevice>* nullPointerSigmoid = nullptr;
  BOOST_CHECK_EQUAL(ptrSigmoid, nullPointerSigmoid);
}

BOOST_AUTO_TEST_CASE(destructorSigmoidTensorOp)
{
  SigmoidTensorOp<double, Eigen::DefaultDevice>* ptrSigmoid = nullptr;
	ptrSigmoid = new SigmoidTensorOp<double, Eigen::DefaultDevice>();
  delete ptrSigmoid;
}

BOOST_AUTO_TEST_CASE(operationfunctionSigmoidTensorOp)
{
  SigmoidTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0.5,0.5}, {0,0}},
		{{0.7310585786300049,0.7310585786300049}, {0,0}},
		{{0.99995460213129761,0.99995460213129761}, {0,0}},
		{{0.2689414213699951,0.2689414213699951}, {0,0}},
		{{4.5397868702434395e-05,4.5397868702434395e-05}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameSigmoidTensorOp)
{
	SigmoidTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "SigmoidTensorOp");
}

/**
  SigmoidGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorSigmoidGradTensorOp)
{
  SigmoidGradTensorOp<double, Eigen::DefaultDevice>* ptrSigmoidGrad = nullptr;
  SigmoidGradTensorOp<double, Eigen::DefaultDevice>* nullPointerSigmoidGrad = nullptr;
  BOOST_CHECK_EQUAL(ptrSigmoidGrad, nullPointerSigmoidGrad);
}

BOOST_AUTO_TEST_CASE(destructorSigmoidGradTensorOp)
{
  SigmoidGradTensorOp<double, Eigen::DefaultDevice>* ptrSigmoidGrad = nullptr;
	ptrSigmoidGrad = new SigmoidGradTensorOp<double, Eigen::DefaultDevice>();
  delete ptrSigmoidGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionSigmoidGradTensorOp)
{
  SigmoidGradTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0.25,0.25}, {0,0}},
		{{0.19661193324148185,0.19661193324148185}, {0,0}},
		{{4.5395807735907655e-05,4.5395807735907655e-05}, {0,0}},
		{{0.19661193324148185,0.19661193324148185}, {0,0}},
		{{4.53958091e-05,4.53958091e-05}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameSigmoidGradTensorOp)
{
	SigmoidGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "SigmoidGradTensorOp");
}

/**
  TanHTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorTanHTensorOp)
{
  TanHTensorOp<double, Eigen::DefaultDevice>* ptrTanH = nullptr;
  TanHTensorOp<double, Eigen::DefaultDevice>* nullPointerTanH = nullptr;
  BOOST_CHECK_EQUAL(ptrTanH, nullPointerTanH);
}

BOOST_AUTO_TEST_CASE(destructorTanHTensorOp)
{
  TanHTensorOp<double, Eigen::DefaultDevice>* ptrTanH = nullptr;
	ptrTanH = new TanHTensorOp<double, Eigen::DefaultDevice>();
  delete ptrTanH;
}

BOOST_AUTO_TEST_CASE(operationfunctionTanHTensorOp)
{
  TanHTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0.0,0.0}, {0,0}},
		{{0.76159415595576485,0.76159415595576485}, {0,0}},
		{{0.99999999587769262,0.99999999587769262}, {0,0}},
		{{-0.76159415595576485,-0.76159415595576485}, {0,0}},
		{{-0.99999999587769262,-0.99999999587769262}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameTanHTensorOp)
{
	TanHTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "TanHTensorOp");
}

/**
  TanHGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorTanHGradTensorOp)
{
  TanHGradTensorOp<double, Eigen::DefaultDevice>* ptrTanHGrad = nullptr;
  TanHGradTensorOp<double, Eigen::DefaultDevice>* nullPointerTanHGrad = nullptr;
  BOOST_CHECK_EQUAL(ptrTanHGrad, nullPointerTanHGrad);
}

BOOST_AUTO_TEST_CASE(destructorTanHGradTensorOp)
{
  TanHGradTensorOp<double, Eigen::DefaultDevice>* ptrTanHGrad = nullptr;
	ptrTanHGrad = new TanHGradTensorOp<double, Eigen::DefaultDevice>();
  delete ptrTanHGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionTanHGradTensorOp)
{
  TanHGradTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{0.41997434161402614,0.41997434161402614}, {0,0}},
		{{8.2446145466263943e-09,8.2446145466263943e-09}, {0,0}},
		{{0.41997434161402614,0.41997434161402614}, {0,0}},
		{{8.2446145466263943e-09,8.2446145466263943e-09}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameTanHGradTensorOp)
{
	TanHGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "TanHGradTensorOp");
}

/**
  ReTanHTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorReTanHTensorOp)
{
  ReTanHTensorOp<double, Eigen::DefaultDevice>* ptrReTanH = nullptr;
  ReTanHTensorOp<double, Eigen::DefaultDevice>* nullPointerReTanH = nullptr;
  BOOST_CHECK_EQUAL(ptrReTanH, nullPointerReTanH);
}

BOOST_AUTO_TEST_CASE(destructorReTanHTensorOp)
{
  ReTanHTensorOp<double, Eigen::DefaultDevice>* ptrReTanH = nullptr;
	ptrReTanH = new ReTanHTensorOp<double, Eigen::DefaultDevice>();
  delete ptrReTanH;
}

// [TODO: need to re-implement]
//BOOST_AUTO_TEST_CASE(operationfunctionReTanHTensorOp)
//{
//  ReTanHTensorOp<double, Eigen::DefaultDevice> operation;
//	const int batch_size = 5;
//	const int memory_size = 2;
//	const int layer_size = 2;
//	Eigen::DefaultDevice device;
//	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
//	input.setValues({
//		{{0,0}, {0,0}},
//		{{1,1}, {0,0}},
//		{{10,10}, {0,0}},
//		{{-1,-1}, {0,0}},
//		{{-10,-10}, {0,0}} });
//	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
//	output.setZero();
//	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
//	test.setValues({
//		{{0,0}, {0,0}},
//		{{0.76159415595576485,0.76159415595576485}, {0,0}},
//		{{0.99999999587769262,0.99999999587769262}, {0,0}},
//		{{0,0}, {0,0}},
//		{{0,0}, {0,0}} });
//
//	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
//
//	// Test
//	for (int i = 0; i < batch_size; ++i) {
//		for (int j = 0; j < memory_size; ++j) {
//			for (int k = 0; k < layer_size; ++k) {
//				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
//			}
//		}
//	}
//}

BOOST_AUTO_TEST_CASE(getNameReTanHTensorOp)
{
	ReTanHTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "ReTanHTensorOp");
}

/**
  ReTanHGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorReTanHGradTensorOp)
{
  ReTanHGradTensorOp<double, Eigen::DefaultDevice>* ptrReTanHGrad = nullptr;
  ReTanHGradTensorOp<double, Eigen::DefaultDevice>* nullPointerReTanHGrad = nullptr;
  BOOST_CHECK_EQUAL(ptrReTanHGrad, nullPointerReTanHGrad);
}

BOOST_AUTO_TEST_CASE(destructorReTanHGradTensorOp)
{
  ReTanHGradTensorOp<double, Eigen::DefaultDevice>* ptrReTanHGrad = nullptr;
	ptrReTanHGrad = new ReTanHGradTensorOp<double, Eigen::DefaultDevice>();
  delete ptrReTanHGrad;
}

// TODO: need to re-implement
//BOOST_AUTO_TEST_CASE(operationfunctionReTanHGradTensorOp)
//{
//  ReTanHGradTensorOp<double, Eigen::DefaultDevice> operation;
//	const int batch_size = 5;
//	const int memory_size = 2;
//	const int layer_size = 2;
//	Eigen::DefaultDevice device;
//	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
//	input.setValues({
//		{{0,0}, {0,0}},
//		{{1,1}, {0,0}},
//		{{10,10}, {0,0}},
//		{{-1,-1}, {0,0}},
//		{{-10,-10}, {0,0}} });
//	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
//	output.setZero();
//	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
//	test.setValues({
//		{{0,0}, {0,0}},
//		{{0.41997434161402614,0.41997434161402614}, {0,0}},
//		{{8.2446147686709992e-09,8.2446147686709992e-09}, {0,0}},
//		{{0,0}, {0,0}},
//		{{0,0}, {0,0}} });
//
//	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
//
//	// Test
//	for (int i = 0; i < batch_size; ++i) {
//		for (int j = 0; j < memory_size; ++j) {
//			for (int k = 0; k < layer_size; ++k) {
//				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
//			}
//		}
//	}
//}

BOOST_AUTO_TEST_CASE(getNameReTanHGradTensorOp)
{
	ReTanHGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "ReTanHGradTensorOp");
}

/**
LinearTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorLinearTensorOp)
{
	LinearTensorOp<double, Eigen::DefaultDevice>* ptrLinear = nullptr;
	LinearTensorOp<double, Eigen::DefaultDevice>* nullPointerLinear = nullptr;
	BOOST_CHECK_EQUAL(ptrLinear, nullPointerLinear);
}

BOOST_AUTO_TEST_CASE(destructorLinearTensorOp)
{
	LinearTensorOp<double, Eigen::DefaultDevice>* ptrLinear = nullptr;
	ptrLinear = new LinearTensorOp<double, Eigen::DefaultDevice>();
	delete ptrLinear;
}

BOOST_AUTO_TEST_CASE(operationfunctionLinearTensorOp)
{
	LinearTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameLinearTensorOp)
{
	LinearTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "LinearTensorOp");
}

/**
LinearGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorLinearGradTensorOp)
{
	LinearGradTensorOp<double, Eigen::DefaultDevice>* ptrLinearGrad = nullptr;
	LinearGradTensorOp<double, Eigen::DefaultDevice>* nullPointerLinearGrad = nullptr;
	BOOST_CHECK_EQUAL(ptrLinearGrad, nullPointerLinearGrad);
}

BOOST_AUTO_TEST_CASE(destructorLinearGradTensorOp)
{
	LinearGradTensorOp<double, Eigen::DefaultDevice>* ptrLinearGrad = nullptr;
	ptrLinearGrad = new LinearGradTensorOp<double, Eigen::DefaultDevice>();
	delete ptrLinearGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionLinearGradTensorOp)
{
	LinearGradTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{1,1}, {0,0}},
		{{1,1}, {0,0}},
		{{1,1}, {0,0}},
		{{1,1}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameLinearGradTensorOp)
{
	LinearGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "LinearGradTensorOp");
}

/**
InverseTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorInverseTensorOp)
{
	InverseTensorOp<double, Eigen::DefaultDevice>* ptrInverse = nullptr;
	InverseTensorOp<double, Eigen::DefaultDevice>* nullPointerInverse = nullptr;
	BOOST_CHECK_EQUAL(ptrInverse, nullPointerInverse);
}

BOOST_AUTO_TEST_CASE(destructorInverseTensorOp)
{
	InverseTensorOp<double, Eigen::DefaultDevice>* ptrInverse = nullptr;
	ptrInverse = new InverseTensorOp<double, Eigen::DefaultDevice>();
	delete ptrInverse;
}

BOOST_AUTO_TEST_CASE(operationfunctionInverseTensorOp)
{
	InverseTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{0.1,0.1}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-0.1,-0.1}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i,j,k), test(i,j,k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameInverseTensorOp)
{
	InverseTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "InverseTensorOp");
}

/**
InverseGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorInverseGradTensorOp)
{
	InverseGradTensorOp<double, Eigen::DefaultDevice>* ptrInverseGrad = nullptr;
	InverseGradTensorOp<double, Eigen::DefaultDevice>* nullPointerInverseGrad = nullptr;
	BOOST_CHECK_EQUAL(ptrInverseGrad, nullPointerInverseGrad);
}

BOOST_AUTO_TEST_CASE(destructorInverseGradTensorOp)
{
	InverseGradTensorOp<double, Eigen::DefaultDevice>* ptrInverseGrad = nullptr;
	ptrInverseGrad = new InverseGradTensorOp<double, Eigen::DefaultDevice>();
	delete ptrInverseGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionInverseGradTensorOp)
{
	InverseGradTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0,0}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-0.01,-0.01}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-0.01,-0.01}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameInverseGradTensorOp)
{
	InverseGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "InverseGradTensorOp");
}

/**
ExponentialTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorExponentialTensorOp)
{
	ExponentialTensorOp<double, Eigen::DefaultDevice>* ptrExponential = nullptr;
	ExponentialTensorOp<double, Eigen::DefaultDevice>* nullPointerExponential = nullptr;
	BOOST_CHECK_EQUAL(ptrExponential, nullPointerExponential);
}

BOOST_AUTO_TEST_CASE(destructorExponentialTensorOp)
{
	ExponentialTensorOp<double, Eigen::DefaultDevice>* ptrExponential = nullptr;
	ptrExponential = new ExponentialTensorOp<double, Eigen::DefaultDevice>();
	delete ptrExponential;
}

BOOST_AUTO_TEST_CASE(operationfunctionExponentialTensorOp)
{
	ExponentialTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{2.718281828,2.718281828}, {0,0}},
		{{22026.46579,22026.46579}, {0,0}},
		{{0.367879441,0.367879441}, {0,0}},
		{{4.53999E-05,4.53999E-05}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameExponentialTensorOp)
{
	ExponentialTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "ExponentialTensorOp");
}

/**
ExponentialGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorExponentialGradTensorOp)
{
	ExponentialGradTensorOp<double, Eigen::DefaultDevice>* ptrExponentialGrad = nullptr;
	ExponentialGradTensorOp<double, Eigen::DefaultDevice>* nullPointerExponentialGrad = nullptr;
	BOOST_CHECK_EQUAL(ptrExponentialGrad, nullPointerExponentialGrad);
}

BOOST_AUTO_TEST_CASE(destructorExponentialGradTensorOp)
{
	ExponentialGradTensorOp<double, Eigen::DefaultDevice>* ptrExponentialGrad = nullptr;
	ptrExponentialGrad = new ExponentialGradTensorOp<double, Eigen::DefaultDevice>();
	delete ptrExponentialGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionExponentialGradTensorOp)
{
	ExponentialGradTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{2.718281828,2.718281828}, {0,0}},
		{{22026.46579,22026.46579}, {0,0}},
		{{0.367879441,0.367879441}, {0,0}},
		{{4.53999E-05,4.53999E-05}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameExponentialGradTensorOp)
{
	ExponentialGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "ExponentialGradTensorOp");
}

/**
LogTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorLogTensorOp)
{
	LogTensorOp<double, Eigen::DefaultDevice>* ptrLog = nullptr;
	LogTensorOp<double, Eigen::DefaultDevice>* nullPointerLog = nullptr;
	BOOST_CHECK_EQUAL(ptrLog, nullPointerLog);
}

BOOST_AUTO_TEST_CASE(destructorLogTensorOp)
{
	LogTensorOp<double, Eigen::DefaultDevice>* ptrLog = nullptr;
	ptrLog = new LogTensorOp<double, Eigen::DefaultDevice>();
	delete ptrLog;
}

BOOST_AUTO_TEST_CASE(operationfunctionLogTensorOp)
{
	LogTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}}
		});
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{-55.262042231857095,-55.262042231857095}, {0,0}},
		{{0,0}, {0,0}},
		{{2.3025850929940459,2.3025850929940459}, {0,0}},
		{{-55.262042231857095,-55.262042231857095}, {0,0}},
		{{-55.262042231857095,-55.262042231857095}, {0,0}}
		});

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameLogTensorOp)
{
	LogTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "LogTensorOp");
}

/**
LogGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorLogGradTensorOp)
{
	LogGradTensorOp<double, Eigen::DefaultDevice>* ptrLogGrad = nullptr;
	LogGradTensorOp<double, Eigen::DefaultDevice>* nullPointerLogGrad = nullptr;
	BOOST_CHECK_EQUAL(ptrLogGrad, nullPointerLogGrad);
}

BOOST_AUTO_TEST_CASE(destructorLogGradTensorOp)
{
	LogGradTensorOp<double, Eigen::DefaultDevice>* ptrLogGrad = nullptr;
	ptrLogGrad = new LogGradTensorOp<double, Eigen::DefaultDevice>();
	delete ptrLogGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionLogGradTensorOp)
{
	LogGradTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0,0}, {0,0}}, // was 1e9 prior to selection
		{{1,1}, {0,0}},
		{{0.1,0.1}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-0.1,-0.1}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameLogGradTensorOp)
{
	LogGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "LogGradTensorOp");
}

/**
	PowTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorPowTensorOp)
{
	PowTensorOp<double, Eigen::DefaultDevice>* ptrPow = nullptr;
	PowTensorOp<double, Eigen::DefaultDevice>* nullPointerPow = nullptr;
	BOOST_CHECK_EQUAL(ptrPow, nullPointerPow);
}

BOOST_AUTO_TEST_CASE(destructorPowTensorOp)
{
	PowTensorOp<double, Eigen::DefaultDevice>* ptrPow = nullptr;
	ptrPow = new PowTensorOp<double, Eigen::DefaultDevice>(2);
	delete ptrPow;
}

BOOST_AUTO_TEST_CASE(operationfunctionPowTensorOp)
{
	PowTensorOp<double, Eigen::DefaultDevice> operation(0.5);
	const int batch_size = 7;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
    {{1e12,1e12}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}},
    {{-1e12,-1e12}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{3.1622776601683795,3.1622776601683795}, {0,0}},
    {{31622.776601683792,31622.776601683792}, {0,0}},
		{{0,0}, {0,0}},
		{{0,0}, {0,0}},
    {{0,0}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
			}
		}
	}

  PowTensorOp<double, Eigen::DefaultDevice> operation2(2.0);

  output.setZero();
  test.setValues({
    {{0,0}, {0,0}},
    {{1,1}, {0,0}},
    {{100,100}, {0,0}},
    {{1e9,1e9}, {0,0}},
    {{1,1}, {0,0}},  // TODO: Clip does not fix -nan(ind)
    {{100,100}, {0,0}},  // TODO: Clip does not fix -nan(ind)
    {{1e9,1e9}, {0,0}} });

  operation2(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

  // Test
  for (int i = 0; i < batch_size; ++i) {
    for (int j = 0; j < memory_size; ++j) {
      for (int k = 0; k < layer_size; ++k) {
        BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
      }
    }
  }
}

BOOST_AUTO_TEST_CASE(getNamePowTensorOp)
{
	PowTensorOp<double, Eigen::DefaultDevice> operation(0.5);

	BOOST_CHECK_EQUAL(operation.getName(), "PowTensorOp");
}

/**
	PowGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorPowGradTensorOp)
{
	PowGradTensorOp<double, Eigen::DefaultDevice>* ptrPowGrad = nullptr;
	PowGradTensorOp<double, Eigen::DefaultDevice>* nullPointerPowGrad = nullptr;
	BOOST_CHECK_EQUAL(ptrPowGrad, nullPointerPowGrad);
}

BOOST_AUTO_TEST_CASE(destructorPowGradTensorOp)
{
	PowGradTensorOp<double, Eigen::DefaultDevice>* ptrPowGrad = nullptr;
	ptrPowGrad = new PowGradTensorOp<double, Eigen::DefaultDevice>(0.5);
	delete ptrPowGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionPowGradTensorOp)
{
	PowGradTensorOp<double, Eigen::DefaultDevice> operation(0.5);
	const int batch_size = 7;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
    {{0,0}, {0,0}},
    {{1,1}, {0,0}},
    {{10,10}, {0,0}},
    {{1e9,1e9}, {0,0}},
    {{-1,-1}, {0,0}},
    {{-10,-10}, {0,0}},
    {{-1e9,-1e9}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1.0e9,1.0e9}, {0,0}},
		{{0.5,0.5}, {0,0}},
		{{0.15811388300841897,0.15811388300841897}, {0,0}},
    {{1.5811388300841898e-05,1.5811388300841898e-05}, {0,0}},
		{{1.0e9,1.0e9}, {0,0}},  // TODO: why is this not zero?
		{{1.0e9,1.0e9}, {0,0}},
    {{1.0e9,1.0e9}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
			}
		}
	}

  PowGradTensorOp<double, Eigen::DefaultDevice> operation2(2);
  input.setValues({
    {{0,0}, {0,0}},
    {{1,1}, {0,0}},
    {{10,10}, {0,0}},
    {{1e9,1e9}, {0,0}},
    {{-1,-1}, {0,0}},
    {{-10,-10}, {0,0}},
    {{-1e9,-1e9}, {0,0}} });
  output.setZero();
  test.setValues({
    {{0,0}, {0,0}},
    {{2.0,2.0}, {0,0}},
    {{20,20}, {0,0}},
    {{1e9,1e9}, {0,0}},
    {{-2,-2}, {0,0}},
    {{-20,-20}, {0,0}},
    {{-1e9,-1e9}, {0,0}} });

  operation2(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

  // Test
  for (int i = 0; i < batch_size; ++i) {
    for (int j = 0; j < memory_size; ++j) {
      for (int k = 0; k < layer_size; ++k) {
        BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
      }
    }
  }
}

BOOST_AUTO_TEST_CASE(getNamePowGradTensorOp)
{
	PowGradTensorOp<double, Eigen::DefaultDevice> operation(0.5);

	BOOST_CHECK_EQUAL(operation.getName(), "PowGradTensorOp");
}

/**
	LeakyReLUTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorLeakyReLUTensorOp)
{
	LeakyReLUTensorOp<double, Eigen::DefaultDevice>* ptrLeakyReLU = nullptr;
	LeakyReLUTensorOp<double, Eigen::DefaultDevice>* nullPointerLeakyReLU = nullptr;
	BOOST_CHECK_EQUAL(ptrLeakyReLU, nullPointerLeakyReLU);
}

BOOST_AUTO_TEST_CASE(destructorLeakyReLUTensorOp)
{
	LeakyReLUTensorOp<double, Eigen::DefaultDevice>* ptrLeakyReLU = nullptr;
	ptrLeakyReLU = new LeakyReLUTensorOp<double, Eigen::DefaultDevice>();
	delete ptrLeakyReLU;
}

BOOST_AUTO_TEST_CASE(gettersAndSettersLeakyReLUTensorOp)
{
	LeakyReLUTensorOp<double, Eigen::DefaultDevice> operation;
	operation.setAlpha(1.0);

	BOOST_CHECK_EQUAL(operation.getAlpha(), 1.0);
}

BOOST_AUTO_TEST_CASE(operationfunctionLeakyReLUTensorOp)
{
	LeakyReLUTensorOp<double, Eigen::DefaultDevice> operation(0.1);
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-0.1,-0.1}, {0,0}},
		{{-1,-1}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameLeakyReLUTensorOp)
{
	LeakyReLUTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "LeakyReLUTensorOp");
}

/**
	LeakyReLUGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorLeakyReLUGradTensorOp)
{
	LeakyReLUGradTensorOp<double, Eigen::DefaultDevice>* ptrLeakyReLU = nullptr;
	LeakyReLUGradTensorOp<double, Eigen::DefaultDevice>* nullPointerLeakyReLU = nullptr;
	BOOST_CHECK_EQUAL(ptrLeakyReLU, nullPointerLeakyReLU);
}

BOOST_AUTO_TEST_CASE(destructorLeakyReLUGradTensorOp)
{
	LeakyReLUGradTensorOp<double, Eigen::DefaultDevice>* ptrLeakyReLU = nullptr;
	ptrLeakyReLU = new LeakyReLUGradTensorOp<double, Eigen::DefaultDevice>();
	delete ptrLeakyReLU;
}

BOOST_AUTO_TEST_CASE(gettersAndSettersLeakyReLUGradTensorOp)
{
	LeakyReLUGradTensorOp<double, Eigen::DefaultDevice> operation;
	operation.setAlpha(1.0);

	BOOST_CHECK_EQUAL(operation.getAlpha(), 1.0);
}

BOOST_AUTO_TEST_CASE(operationfunctionLeakyReLUGradTensorOp)
{
	LeakyReLUGradTensorOp<double, Eigen::DefaultDevice> operation(0.1);
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{1,1}, {0,0}},
		{{1,1}, {0,0}},
		{{0.1,0.1}, {0,0}},
		{{0.1,0.1}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4);
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameLeakyReLUGradTensorOp)
{
	LeakyReLUGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "LeakyReLUGradTensorOp");
}

/**
SinTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorSinTensorOp)
{
	SinTensorOp<double, Eigen::DefaultDevice>* ptrSin = nullptr;
	SinTensorOp<double, Eigen::DefaultDevice>* nullPointerSin = nullptr;
	BOOST_CHECK_EQUAL(ptrSin, nullPointerSin);
}

BOOST_AUTO_TEST_CASE(destructorSinTensorOp)
{
	SinTensorOp<double, Eigen::DefaultDevice>* ptrSin = nullptr;
	ptrSin = new SinTensorOp<double, Eigen::DefaultDevice>();
	delete ptrSin;
}

BOOST_AUTO_TEST_CASE(operationfunctionSinTensorOp)
{
	SinTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{2.718281828,2.718281828}, {0,0}},
		{{22026.46579,22026.46579}, {0,0}},
		{{0.367879441,0.367879441}, {0,0}},
		{{4.53999E-05,4.53999E-05}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				//BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4); //TODO: fixme
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameSinTensorOp)
{
	SinTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "SinTensorOp");
}

/**
SinGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorSinGradTensorOp)
{
	SinGradTensorOp<double, Eigen::DefaultDevice>* ptrSinGrad = nullptr;
	SinGradTensorOp<double, Eigen::DefaultDevice>* nullPointerSinGrad = nullptr;
	BOOST_CHECK_EQUAL(ptrSinGrad, nullPointerSinGrad);
}

BOOST_AUTO_TEST_CASE(destructorSinGradTensorOp)
{
	SinGradTensorOp<double, Eigen::DefaultDevice>* ptrSinGrad = nullptr;
	ptrSinGrad = new SinGradTensorOp<double, Eigen::DefaultDevice>();
	delete ptrSinGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionSinGradTensorOp)
{
	SinGradTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{2.718281828,2.718281828}, {0,0}},
		{{22026.46579,22026.46579}, {0,0}},
		{{0.367879441,0.367879441}, {0,0}},
		{{4.53999E-05,4.53999E-05}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				//BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4); //TODO: fixme
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameSinGradTensorOp)
{
	SinGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "SinGradTensorOp");
}

/**
CosTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorCosTensorOp)
{
	CosTensorOp<double, Eigen::DefaultDevice>* ptrCos = nullptr;
	CosTensorOp<double, Eigen::DefaultDevice>* nullPointerCos = nullptr;
	BOOST_CHECK_EQUAL(ptrCos, nullPointerCos);
}

BOOST_AUTO_TEST_CASE(destructorCosTensorOp)
{
	CosTensorOp<double, Eigen::DefaultDevice>* ptrCos = nullptr;
	ptrCos = new CosTensorOp<double, Eigen::DefaultDevice>();
	delete ptrCos;
}

BOOST_AUTO_TEST_CASE(operationfunctionCosTensorOp)
{
	CosTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{2.718281828,2.718281828}, {0,0}},
		{{22026.46579,22026.46579}, {0,0}},
		{{0.367879441,0.367879441}, {0,0}},
		{{4.53999E-05,4.53999E-05}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				//BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4); //TODO: fixme
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameCosTensorOp)
{
	CosTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "CosTensorOp");
}

/**
CosGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorCosGradTensorOp)
{
	CosGradTensorOp<double, Eigen::DefaultDevice>* ptrCosGrad = nullptr;
	CosGradTensorOp<double, Eigen::DefaultDevice>* nullPointerCosGrad = nullptr;
	BOOST_CHECK_EQUAL(ptrCosGrad, nullPointerCosGrad);
}

BOOST_AUTO_TEST_CASE(destructorCosGradTensorOp)
{
	CosGradTensorOp<double, Eigen::DefaultDevice>* ptrCosGrad = nullptr;
	ptrCosGrad = new CosGradTensorOp<double, Eigen::DefaultDevice>();
	delete ptrCosGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionCosGradTensorOp)
{
	CosGradTensorOp<double, Eigen::DefaultDevice> operation;
	const int batch_size = 5;
	const int memory_size = 2;
	const int layer_size = 2;
	Eigen::DefaultDevice device;
	Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
	input.setValues({
		{{0,0}, {0,0}},
		{{1,1}, {0,0}},
		{{10,10}, {0,0}},
		{{-1,-1}, {0,0}},
		{{-10,-10}, {0,0}} });
	Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
	output.setZero();
	Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
	test.setValues({
		{{1,1}, {0,0}},
		{{2.718281828,2.718281828}, {0,0}},
		{{22026.46579,22026.46579}, {0,0}},
		{{0.367879441,0.367879441}, {0,0}},
		{{4.53999E-05,4.53999E-05}, {0,0}} });

	operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

	// Test
	for (int i = 0; i < batch_size; ++i) {
		for (int j = 0; j < memory_size; ++j) {
			for (int k = 0; k < layer_size; ++k) {
				//BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4); //TODO: fixme
			}
		}
	}
}

BOOST_AUTO_TEST_CASE(getNameCosGradTensorOp)
{
	CosGradTensorOp<double, Eigen::DefaultDevice> operation;

	BOOST_CHECK_EQUAL(operation.getName(), "CosGradTensorOp");
}

BOOST_AUTO_TEST_CASE(operationfunctionBatchNormTensorOp)
{
  BatchNormTensorOp<double, Eigen::DefaultDevice> operation;
  const int batch_size = 5;
  const int memory_size = 2;
  const int layer_size = 2;
  Eigen::DefaultDevice device;
  Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
  input.setValues({
    {{0,0.1}, {0,0}},
    {{1,1}, {0,0}},
    {{10,10}, {0,0}},
    {{-1,-1}, {0,0}},
    {{-10,-10}, {0,0}} });
  Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
  output.setZero();
  Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
  test.setValues({
    {{0,2.2360679774997898}, {0,0}},
    {{2.2360679774997898,2.2360679774997898}, {0,0}},
    {{2.2360679774997898,2.2360679774997898}, {0,0}},
    {{-2.2360679774997898,-2.2360679774997898}, {0,0}},
    {{-2.2360679774997898,-2.2360679774997898}, {0,0}} });

  operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

  // Test
  for (int i = 0; i < batch_size; ++i) {
    for (int j = 0; j < memory_size; ++j) {
      for (int k = 0; k < layer_size; ++k) {
        BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4); //TODO: fixme
      }
    }
  }
}

BOOST_AUTO_TEST_CASE(getNameBatchNormTensorOp)
{
  BatchNormTensorOp<double, Eigen::DefaultDevice> operation;

  BOOST_CHECK_EQUAL(operation.getName(), "BatchNormTensorOp");
}

/**
BatchNormGradTensorOp Tests
*/
BOOST_AUTO_TEST_CASE(constructorBatchNormGradTensorOp)
{
  BatchNormGradTensorOp<double, Eigen::DefaultDevice>* ptrBatchNormGrad = nullptr;
  BatchNormGradTensorOp<double, Eigen::DefaultDevice>* nullPointerBatchNormGrad = nullptr;
  BOOST_CHECK_EQUAL(ptrBatchNormGrad, nullPointerBatchNormGrad);
}

BOOST_AUTO_TEST_CASE(destructorBatchNormGradTensorOp)
{
  BatchNormGradTensorOp<double, Eigen::DefaultDevice>* ptrBatchNormGrad = nullptr;
  ptrBatchNormGrad = new BatchNormGradTensorOp<double, Eigen::DefaultDevice>();
  delete ptrBatchNormGrad;
}

BOOST_AUTO_TEST_CASE(operationfunctionBatchNormGradTensorOp)
{
  BatchNormGradTensorOp<double, Eigen::DefaultDevice> operation;
  const int batch_size = 5;
  const int memory_size = 2;
  const int layer_size = 2;
  Eigen::DefaultDevice device;
  Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
  input.setValues({
    {{0,0.1}, {0,0}},
    {{1,1}, {0,0}},
    {{10,10}, {0,0}},
    {{-1,-1}, {0,0}},
    {{-10,-10}, {0,0}} });
  Eigen::Tensor<double, 3> output(batch_size, memory_size, layer_size);
  output.setZero();
  Eigen::Tensor<double, 3> test(batch_size, memory_size, layer_size);
  test.setValues({
    {{0,4.5519144009631268e-15}, {0,0}},
    {{0,1.7948528498063337e-15}, {0,0}},
    {{0,1.776535187764253e-15}, {0,0}},
    {{0,1.7934306267839513e-15}, {0,0}},
    {{0,1.7765337666674127e-15}, {0,0}} });

  operation(input.data(), output.data(), batch_size, memory_size, layer_size, 0, device);

  // Test
  for (int i = 0; i < batch_size; ++i) {
    for (int j = 0; j < memory_size; ++j) {
      for (int k = 0; k < layer_size; ++k) {
        BOOST_CHECK_CLOSE(output(i, j, k), test(i, j, k), 1e-4); //TODO: fixme
      }
    }
  }
}

BOOST_AUTO_TEST_CASE(getNameBatchNormGradTensorOp)
{
  BatchNormGradTensorOp<double, Eigen::DefaultDevice> operation;

  BOOST_CHECK_EQUAL(operation.getName(), "BatchNormGradTensorOp");
}

BOOST_AUTO_TEST_CASE(operatorGradientCheckTensorOp)
{
  // Define the gradient checker
  GradientCheckTensorOp<double, Eigen::DefaultDevice> operation;
  operation.eps_ = 1e-7;

  // Setup the input
  const int batch_size = 5;
  const int memory_size = 2;
  const int layer_size = 2;
  Eigen::DefaultDevice device;
  Eigen::Tensor<double, 3> input(batch_size, memory_size, layer_size);
  input.setValues({
    {{0,0}, {0,0}},
    {{1,1}, {0,0}},
    {{10,10}, {0,0}},
    {{-1,-1}, {0,0}},
    {{-10,-10}, {0,0}} });
  Eigen::Tensor<double, 3> input_f_plus(batch_size, memory_size, layer_size);
  input_f_plus.setZero();
  Eigen::Tensor<double, 3> input_f_min(batch_size, memory_size, layer_size);
  input_f_min.setZero();
  Eigen::Tensor<double, 3> input_b(batch_size, memory_size, layer_size);
  input_b.setZero();
  Eigen::Tensor<double, 0> output;
  output.setZero();

  // Check Sigmoid
  operation.forward_ = std::make_shared<SigmoidTensorOp<double, Eigen::DefaultDevice>>(SigmoidTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<SigmoidGradTensorOp<double, Eigen::DefaultDevice>>(SigmoidGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);

  // Check ReLU
  input.setValues({ {{0.1,0.1}, {0,0}}, {{1,1}, {0,0}}, {{10,10}, {0,0}}, {{-1,-1}, {0,0}}, {{-10,-10}, {0,0}} });
  input_f_plus.setZero();
  input_f_min.setZero();
  input_b.setZero();
  output.setZero();
  operation.forward_ = std::make_shared<ReLUTensorOp<double, Eigen::DefaultDevice>>(ReLUTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<ReLUGradTensorOp<double, Eigen::DefaultDevice>>(ReLUGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);

  // Check ELU
  input.setValues({ {{0,0}, {0,0}}, {{1,1}, {0,0}}, {{10,10}, {0,0}}, {{-1,-1}, {0,0}}, {{-10,-10}, {0,0}} });
  input_f_plus.setZero();
  input_f_min.setZero();
  input_b.setZero();
  output.setZero();
  operation.forward_ = std::make_shared<ELUTensorOp<double, Eigen::DefaultDevice>>(ELUTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<ELUGradTensorOp<double, Eigen::DefaultDevice>>(ELUGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);

  // Check LeakyReLU
  input.setValues({ {{0.1,0.1}, {0,0}}, {{1,1}, {0,0}}, {{10,10}, {0,0}}, {{-1,-1}, {0,0}}, {{-10,-10}, {0,0}} });
  input_f_plus.setZero();
  input_f_min.setZero();
  input_b.setZero();
  output.setZero();
  operation.forward_ = std::make_shared<LeakyReLUTensorOp<double, Eigen::DefaultDevice>>(LeakyReLUTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<LeakyReLUGradTensorOp<double, Eigen::DefaultDevice>>(LeakyReLUGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);

  // Check TanH
  input.setValues({ {{0,0}, {0,0}}, {{1,1}, {0,0}}, {{10,10}, {0,0}}, {{-1,-1}, {0,0}}, {{-10,-10}, {0,0}} });
  input_f_plus.setZero();
  input_f_min.setZero();
  input_b.setZero();
  output.setZero();
  operation.forward_ = std::make_shared<TanHTensorOp<double, Eigen::DefaultDevice>>(TanHTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<TanHGradTensorOp<double, Eigen::DefaultDevice>>(TanHGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);

  // Check Linear
  input.setValues({ {{0,0}, {0,0}}, {{1,1}, {0,0}}, {{10,10}, {0,0}}, {{-1,-1}, {0,0}}, {{-10,-10}, {0,0}} });
  input_f_plus.setZero();
  input_f_min.setZero();
  input_b.setZero();
  output.setZero();
  operation.forward_ = std::make_shared<LinearTensorOp<double, Eigen::DefaultDevice>>(LinearTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<LinearGradTensorOp<double, Eigen::DefaultDevice>>(LinearGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);

  // Check Pow
  input.setValues({ {{0,0}, {0,0}}, {{1,1}, {0,0}}, {{10,10}, {0,0}}, {{-1,-1}, {0,0}}, {{-10,-10}, {0,0}} });
  input_f_plus.setZero();
  input_f_min.setZero();
  input_b.setZero();
  output.setZero();
  operation.forward_ = std::make_shared<PowTensorOp<double, Eigen::DefaultDevice>>(PowTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<PowGradTensorOp<double, Eigen::DefaultDevice>>(PowGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);

  // Check Inverse
  input.setValues({ {{0.1,0.1}, {0,0}}, {{1,1}, {0,0}}, {{10,10}, {0,0}}, {{-1,-1}, {0,0}}, {{-10,-10}, {0,0}} });
  input_f_plus.setZero();
  input_f_min.setZero();
  input_b.setZero();
  output.setZero();
  operation.forward_ = std::make_shared<InverseTensorOp<double, Eigen::DefaultDevice>>(InverseTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<InverseGradTensorOp<double, Eigen::DefaultDevice>>(InverseGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);

  // Check Log
  input.setValues({ {{0.1,0.1}, {0,0}}, {{1,1}, {0,0}}, {{10,10}, {0,0}}, {{100,100}, {0,0}}, {{1000,1000}, {0,0}} });
  input_f_plus.setZero();
  input_f_min.setZero();
  input_b.setZero();
  output.setZero();
  operation.forward_ = std::make_shared<LogTensorOp<double, Eigen::DefaultDevice>>(LogTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<LogGradTensorOp<double, Eigen::DefaultDevice>>(LogGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);

  // Check Exponential
  input.setValues({ {{0,0}, {0,0}}, {{1,1}, {0,0}}, {{10,10}, {0,0}}, {{-1,-1}, {0,0}}, {{-10,-10}, {0,0}} });
  input_f_plus.setZero();
  input_f_min.setZero();
  input_b.setZero();
  output.setZero();
  operation.forward_ = std::make_shared<ExponentialTensorOp<double, Eigen::DefaultDevice>>(ExponentialTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<ExponentialGradTensorOp<double, Eigen::DefaultDevice>>(ExponentialGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);

  // Check BatchNorm
  input.setValues({ {{10,0.1}, {0,0}}, {{1,1}, {0,0}}, {{10,10}, {0,0}}, {{-1,-1}, {0,0}}, {{-10,-10}, {0,0}} });
  input_f_plus.setZero();
  input_f_min.setZero();
  input_b.setZero();
  output.setZero();
  operation.forward_ = std::make_shared<BatchNormTensorOp<double, Eigen::DefaultDevice>>(BatchNormTensorOp<double, Eigen::DefaultDevice>());
  operation.reverse_ = std::make_shared<BatchNormGradTensorOp<double, Eigen::DefaultDevice>>(BatchNormGradTensorOp<double, Eigen::DefaultDevice>());
  operation(input.data(), input_f_plus.data(), input_f_min.data(), input_b.data(), output.data(), batch_size, memory_size, layer_size, 0, device);
  BOOST_CHECK(output(0) < operation.eps_);
  std::cout << "Gradient Check BatchNorm: " << output(0) << std::endl;
}

BOOST_AUTO_TEST_SUITE_END()


#include "util.hpp"

#include <set>
#include <unordered_set>

#include <boost/container/set.hpp>
#include <boost/unordered_set.hpp>

//////////////////////////// TASK 6 ////////////////////////////

using naive_assoc_container = std::unordered_set<int>;
using optim_assoc_container = std::set<int>;

//////////////////////////// DETAIL ////////////////////////////
BENCH(iteration, naive_assoc_container_iteration, naive_assoc_container{})->Range(8, 8<<10);
BENCH(iteration, optim_assoc_container_iteration, optim_assoc_container{})->Range(8, 8<<10);

BENCH(search_assoc, naive_assoc_container_search, naive_assoc_container{})->Range(8, 8<<10);
BENCH(search_assoc, optim_assoc_container_search, optim_assoc_container{})->Range(8, 8<<10);

BENCH(insertion_assoc, naive_assoc_container_insertion, naive_assoc_container{})->Range(8, 8<<10);
BENCH(insertion_assoc, optim_assoc_container_insertion, optim_assoc_container{})->Range(8, 8<<10);


/*
 * gpcxx/app/generate_random_test_data.hpp
 * Date: 2015-06-15
 * Author: Karsten Ahnert (karsten.ahnert@gmx.de)
 * Copyright: Karsten Ahnert
 *
 * Distributed under the Boost Software License, Version 1.0.
 * (See accompanying file LICENSE_1_0.txt or
 * copy at http://www.boost.org/LICENSE_1_0.txt)
 */

#ifndef GPCXX_APP_GENERATE_RANDOM_TEST_DATA_HPP_INCLUDED
#define GPCXX_APP_GENERATE_RANDOM_TEST_DATA_HPP_INCLUDED

#include <gpcxx/eval/regression_fitness.hpp>
#include <gpcxx/util/array_unpack.hpp>

#include <boost/fusion/functional/invocation/invoke.hpp>
#include <boost/fusion/adapted/std_pair.hpp>
#include <boost/fusion/adapted/std_tuple.hpp>

namespace gpcxx {

namespace detail {

template< typename T >
struct factory
{
    template< typename... Args >
    T operator()( Args&&... args ) const
    {
        return T( std::forward< Args >( args ) ... );
    }
};

}

template< size_t Dim , typename Dist , typename Rng , typename Params , typename Func , typename Value = double >
auto generate_random_test_data( Rng& rng , size_t num_of_points , Params const& params , Func f )
{
    std::array< Dist , Dim > dists;
    for( size_t i=0 ; i<Dim ; ++i )
        dists[i] = boost::fusion::invoke( detail::factory< Dist >() , params[i] );

    regression_training_data< Value , Dim > data;
    data.y.resize( num_of_points );
    for( size_t d=0 ; d<Dim ; ++d ) data.x[d].resize( num_of_points );

    for( size_t i=0 ; i<num_of_points ; ++i )
    {
        std::array< Value , Dim > arr;
        for( size_t d=0 ; d<Dim ; ++d )
        {
            arr[d] = dists[d]( rng );
            data.x[d][i] = arr[d];
        }
        data.y[i] = array_unpack( arr , f );
    }

    return data;
}

} // namespace gpcxx

#endif // GPCXX_APP_GENERATE_RANDOM_TEST_DATA_HPP_INCLUDED


// Distributed under the MIT License.
// See LICENSE.txt for details.

#pragma once

#include <boost/range/combine.hpp>
#include <boost/tuple/tuple.hpp>
#include <cstddef>

#include "DataStructures/Index.hpp"
#include "DataStructures/SliceIterator.hpp"
#include "DataStructures/Tensor/Tensor.hpp"
#include "DataStructures/Variables.hpp"
#include "Utilities/Gsl.hpp"
#include "Utilities/TMPL.hpp"

/// @{
/*!
 * \ingroup DataStructuresGroup
 * \brief Slices volume `Tensor`s into a `Variables`
 *
 * The slice has a constant logical coordinate in direction `sliced_dim`,
 * slicing the volume at `fixed_index` in that dimension.  For
 * example, to get the lower boundary of `sliced_dim`, pass `0` for
 * `fixed_index`; to get the upper boundary, pass
 * `extents[sliced_dim] - 1`.
 */
template <typename... TagsToSlice, size_t VolumeDim>
void data_on_slice(
    const gsl::not_null<Variables<tmpl::list<TagsToSlice...>>*> interface_vars,
    const Index<VolumeDim>& element_extents, const size_t sliced_dim,
    const size_t fixed_index, const typename TagsToSlice::type&... tensors) {
  const size_t interface_grid_points =
      element_extents.slice_away(sliced_dim).product();
  if (interface_vars->number_of_grid_points() != interface_grid_points) {
    *interface_vars =
        Variables<tmpl::list<TagsToSlice...>>(interface_grid_points);
  }
  for (SliceIterator si(element_extents, sliced_dim, fixed_index); si; ++si) {
    const auto lambda = [&si](auto& interface_tensor,
                              const auto& volume_tensor) {
      for (decltype(auto) interface_and_volume_tensor_components :
           boost::combine(interface_tensor, volume_tensor)) {
        boost::get<0>(
            interface_and_volume_tensor_components)[si.slice_offset()] =
            boost::get<1>(
                interface_and_volume_tensor_components)[si.volume_offset()];
      }
      return '0';
    };
    expand_pack(lambda(get<TagsToSlice>(*interface_vars), tensors)...);
  }
}

template <typename... TagsToSlice, size_t VolumeDim>
Variables<tmpl::list<TagsToSlice...>> data_on_slice(
    const Index<VolumeDim>& element_extents, const size_t sliced_dim,
    const size_t fixed_index, const typename TagsToSlice::type&... tensors) {
  Variables<tmpl::list<TagsToSlice...>> interface_vars(
      element_extents.slice_away(sliced_dim).product());
  data_on_slice<TagsToSlice...>(make_not_null(&interface_vars), element_extents,
                                sliced_dim, fixed_index, tensors...);
  return interface_vars;
}
/// @}


#include "InvKinInvDynRealTime.h"
#include "OpenSimData.h"
#include "XMLInterpreter.h"
#include "InvKinMarker.h"
#include "InvDyn.h"

#include <boost/timer/timer.hpp>

#include <OpenSim/OpenSim.h>

int main(int argc, char **argv) {
	try {
		string modelName(argv[1]);
		string xmlName(argv[2]);
		string trcName(argv[3]);
		OpenSimData openSimData(modelName, true);

		boost::shared_ptr<XMLInterpreter> xmlInterpreter(
				new XMLInterpreter(xmlName));
		xmlInterpreter->readXML();

		InvKinMarker ikMarker(openSimData.getState(), openSimData.getModel());
		ikMarker.init(xmlInterpreter);

		InvDyn invDyn(openSimData.getState(), openSimData.getModel());

		OpenSim::MarkersReference markerRef(trcName);
		const SimTK::Array_<std::string>& markerNames = markerRef.getNames();
		SimTK::Vec2 markersValidTimRange = markerRef.getValidTimeRange();
		double start_time = markersValidTimRange[0];
		double final_time = markersValidTimRange[1];
		double dt = 1.0 / markerRef.getSamplingFrequency();
		openSimData.getState()->updTime() = start_time;
		int Nframes = int((final_time - start_time) / dt) + 1;

		cout << "Numbers of DOF: "
				<< openSimData.getModel()->getCoordinateSet().getSize() << endl;

		int index;
		for (int i = 0;
				i < openSimData.getModel()->getCoordinateSet().getSize(); i++) {
			if (openSimData.getModel()->getCoordinateSet().get(i).getName()
					== "pelvis_tilt") {
				index = i;
				break;
			}
		}
		boost::timer::auto_cpu_timer t;
		for (int i = 0; i < Nframes; i++) {
			SimTK::Array_<SimTK::Vec3> values;
			markerRef.getValues(*(openSimData.getState()), values);
			std::map<string, std::vector<double> > markerToPosition;
			for (int j = 0; j < markerNames.size(); j++) {
				std::vector<double> markerPos;
				markerPos.push_back(values[j][0]);
				markerPos.push_back(values[j][1]);
				markerPos.push_back(values[j][2]);
				markerToPosition[markerNames[j]] = markerPos;
			}
			openSimData.getState()->updTime() = start_time + i * dt;
			ikMarker.computeKinematics(markerToPosition);
			SimTK::Vector vect = invDyn.computeTorque();
//			cout << "Angle: "
//					<< SimTK::convertRadiansToDegrees(
//							ikMarker.getAngle("pelvis_tilt"))
//					<< "  Torque: " << vect[index] << endl;
			openSimData.visualizerShow();
		}
		cout << "Numbers of frames: " << Nframes << endl;
	} catch (const std::exception &ex) {
		cout << "Error: " << ex.what() << endl;
		return 1;
	}

	cout << "OpenSim environment simulation completed successfully." << endl;
	return 0;
}


// Boost.Geometry (aka GGL, Generic Geometry Library)

// Copyright (c) 2007-2015 Barend Gehrels, Amsterdam, the Netherlands.
// Copyright (c) 2008-2015 Bruno Lalande, Paris, France.
// Copyright (c) 2009-2015 Mateusz Loskot, London, UK.
// Copyright (c) 2015 Adam Wulkiewicz, Lodz, Poland.

// This file was modified by Oracle on 2020-2021.
// Modifications copyright (c) 2020-2021, Oracle and/or its affiliates.
// Contributed and/or modified by Adam Wulkiewicz, on behalf of Oracle

// Parts of Boost.Geometry are redesigned from Geodan's Geographic Library
// (geolib/GGL), copyright (c) 1995-2010 Geodan, Amsterdam, the Netherlands.

// Use, modification and distribution is subject to the Boost Software License,
// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

#ifndef BOOST_GEOMETRY_EXTENSIONS_ITERATORS_SECTION_ITERATORS_HPP
#define BOOST_GEOMETRY_EXTENSIONS_ITERATORS_SECTION_ITERATORS_HPP

// 24-04-2010, Moved to extensions/iterators
//             because it was not yet used in the part.

// FILE WILL BE SPLITTED

#include <iterator>

#include <boost/range/begin.hpp>
#include <boost/range/end.hpp>

#include <boost/geometry/algorithms/overlaps.hpp>
#include <boost/geometry/core/access.hpp>
#include <boost/geometry/iterators/detail/iterator_base.hpp>
#include <boost/geometry/util/math.hpp>

namespace boost { namespace geometry
{

namespace detail
{

    template <size_t D, typename P, typename B>
    inline bool exceeding(short int dir, P const& point, B const& box)
    {
        return (dir == 1  && get<D>(point) > get<1, D>(box))
            || (dir == -1 && get<D>(point) < get<0, D>(box));
    }

    template <size_t D, typename P, typename B>
    inline bool preceding(short int dir, P const& point, B const& box)
    {
        return (dir == 1  && get<D>(point) < get<0, D>(box))
            || (dir == -1 && get<D>(point) > get<0, D>(box));
    }
}

// Iterator walking through ring/sections, delivering only those points of the ring
// which are inside the specified box (using the sections)
template<typename G, typename S, typename B, size_t D>
struct section_iterator : public detail::iterators::iterator_base<
                    section_iterator<G, S, B, D>,
                    typename boost::range_iterator<G const>::type
                >
{
    friend class boost::iterator_core_access;

    inline section_iterator(G const& ring, S const& sections, B const& box)
        : m_ring(ring)
        , m_sections(sections)
        , m_box(box)
        , m_section_iterator(boost::begin(m_sections))
    {
        stay_within_box();
    }

private :

    inline void increment()
    {
        (this->base_reference())++;

        // If end or exceeding specified box, go to next section
        if (this->base() == m_end
                || detail::exceeding<D>(m_section_iterator->directions[0], *this->base(), m_box))
        {
            m_section_iterator++;
            stay_within_box();
        }

    }

    // Check if iterator is still in box and if not, go to the next section and advance until it is in box
    void stay_within_box()
    {
        // Find section having overlap with specified box
        while (m_section_iterator != boost::end(m_sections)
                && ! overlaps(m_section_iterator->bounding_box, m_box))
        {
            m_section_iterator++;
        }
        if (m_section_iterator != boost::end(m_sections))
        {
            this->base_reference() = boost::begin(m_ring) + m_section_iterator->begin_index;
            m_end = boost::begin(m_ring) + m_section_iterator->end_index + 1;

            // While not yet at box, advance
            while(this->base() != m_end
                && detail::preceding<D>(m_section_iterator->directions[0], *this->base(), m_box))
            {
                ++(this->base_reference());
            }

            if (this->base() == m_end)
            {
                // This should actually not occur because of bbox check, but to be sure
                m_section_iterator++;
                stay_within_box();
            }
        }
        else
        {
            this->base_reference() = boost::end(m_ring);
            m_end = boost::end(m_ring);
        }
    }

    typedef typename boost::range_iterator<G const>::type IT;
    typedef typename boost::range_iterator<S const>::type SIT;

    G const& m_ring;
    S const& m_sections;
    B const& m_box;

    IT m_end;
    SIT m_section_iterator;
};

// Iterator walking through ring/sections, delivering only those points of the ring
// which are inside the specified box (using the sections)
template<typename G, typename SEC, typename B, size_t D>
struct one_section_segment_iterator : public detail::iterators::iterator_base<
                one_section_segment_iterator<G, SEC, B, D>
                , typename boost::range_iterator<G const>::type>
{
    friend class boost::iterator_core_access;
    typedef typename boost::range_iterator<G const>::type normal_iterator;

    inline one_section_segment_iterator(G const& ring, SEC const& section, B const& box)
        : m_box(&box)
        , m_dir(section.directions[0])
    {
        init(section, ring);
    }

    inline one_section_segment_iterator(normal_iterator end)
        : m_section_end(end)
        , m_ring_end(end)
        , m_box(NULL)
        , m_dir(0)
    {
        this->base_reference() = end;
    }

private :

    inline void increment()
    {
        m_previous = (this->base_reference())++;

        if (this->base() == m_section_end
            || detail::exceeding<D>(m_dir, *m_previous, *m_box))
        {
            this->base_reference() = m_ring_end;
        }
    }

    // Check if iterator is still in box and if not, go to the next section and advance until it is in box
    void init(SEC const& section, G const& ring)
    {
        //this->base_reference();
        m_section_end = boost::begin(ring) + section.end_index + 1;
        m_ring_end = boost::end(ring);

        this->base_reference() = boost::begin(ring) + section.begin_index;

        /* Performance, TO BE CHECKED!
        normal_iterator next = boost::begin(ring) + section.begin_index;
        if (next != m_section_end && next != m_ring_end)
        {

            // While (not end and) not yet at box, advance
            normal_iterator it = next++;
            while(next != m_section_end && next != m_ring_end
                    && detail::preceding<D>(m_dir, *next, *m_box))
            {
                it = next++;
            }

            if (it == m_section_end)
            {
                this->base_reference() = m_ring_end;
            }
            else
            {
                this->base_reference() = it;
            }
        }
        else
        {
            this->base_reference() = m_ring_end;
        }
        */

        m_previous = this->base();
    }

    const B* m_box;
    short int m_dir;

    normal_iterator m_previous;
    normal_iterator m_section_end;
    normal_iterator m_ring_end;
};

}} // namespace boost::geometry

#endif // BOOST_GEOMETRY_EXTENSIONS_ITERATORS_SECTION_ITERATORS_HPP


//---------------------------------------------------------------------------//
// Copyright (c) 2020 Nikita Kaskov <nbering@nil.foundation>
// Copyright (c) 2020 Alexander Sokolov <asokolov@nil.foundation>
//
// Distributed under the Boost Software License, Version 1.0
// See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt
//---------------------------------------------------------------------------//

#ifndef CRYPTO3_INJECT_HASH_HPP
#define CRYPTO3_INJECT_HASH_HPP

#include <boost/crypto3/detail/stream_endian.hpp>
#include <boost/crypto3/detail/basic_functions.hpp>
#include <boost/crypto3/detail/unbounded_shift.hpp>
#include <boost/crypto3/detail/endian_shift.hpp>

namespace boost {
    namespace crypto3 {
        namespace detail {

            template<typename Endianness, std::size_t WordBits, std::size_t BlockWords, std::size_t BlockBits>
            struct word_injector;

            template<int UnitBits, std::size_t WordBits, std::size_t BlockWords, std::size_t BlockBits>
            struct word_injector<stream_endian::big_unit_big_bit<UnitBits>, WordBits, BlockWords, BlockBits>
                : public basic_functions<WordBits> {

                constexpr static const std::size_t word_bits = basic_functions<WordBits>::word_bits;
                typedef typename basic_functions<WordBits>::word_type word_type;

                typedef std::array<word_type, BlockWords> block_type;

                static void inject(word_type w, std::size_t word_seen, block_type &b, std::size_t &block_seen) {
                    // Insert word_seen-bit part of word into the block b according to endianness

                    // Check whether we fall out of the block
                    if (block_seen + word_seen <= BlockBits) {
                        std::size_t last_word_ind = block_seen / word_bits;
                        std::size_t last_word_seen = block_seen % word_bits;

                        // Remove garbage
                        w &= high_bits<word_bits>(~word_type(), word_seen);
                        b[last_word_ind] &= high_bits<word_bits>(~word_type(), last_word_seen);

                        // Add significant word bits to block word
                        b[last_word_ind] |= unbounded_shr(w, last_word_seen);

                        // If we fall out of the block word, push the remainder of element to the next block word
                        if (last_word_seen + word_seen > word_bits)
                            b[last_word_ind + 1] = unbounded_shl(w, word_bits - last_word_seen);

                        block_seen += word_seen;
                    }
                }
            };

            template<int UnitBits, std::size_t WordBits, std::size_t BlockWords, std::size_t BlockBits>
            struct word_injector<stream_endian::little_unit_big_bit<UnitBits>, WordBits, BlockWords, BlockBits>
                : public basic_functions<WordBits> {

                constexpr static const std::size_t word_bits = basic_functions<WordBits>::word_bits;
                typedef typename basic_functions<WordBits>::word_type word_type;

                typedef std::array<word_type, BlockWords> block_type;

                static void inject(word_type w, std::size_t word_seen, block_type &b, std::size_t &block_seen) {
                    // Insert word_seen-bit part of word into the block b according to endianness

                    // Check whether we fall out of the block
                    if (block_seen + word_seen <= BlockBits) {
                        std::size_t last_word_ind = block_seen / word_bits;
                        std::size_t last_word_seen = block_seen % word_bits;

                        // Remove garbage
                        std::size_t w_rem = word_seen % UnitBits;
                        std::size_t w_unit_bits = word_seen - w_rem;
                        word_type mask =
                            low_bits<word_bits>(~word_type(), w_unit_bits) |
                            unbounded_shl(low_bits<word_bits>(~word_type(), w_rem), w_unit_bits + UnitBits - w_rem);
                        w &= mask;

                        std::size_t b_rem = last_word_seen % UnitBits;
                        std::size_t b_unit_bits = last_word_seen - b_rem;
                        mask = low_bits<word_bits>(~word_type(), b_unit_bits) |
                               unbounded_shl(low_bits<word_bits>(~word_type(), b_rem), b_unit_bits + UnitBits - b_rem);
                        b[last_word_ind] &= mask;

                        // Split and combine parts of unit values
                        std::size_t sz[2] = {UnitBits - b_rem, b_rem};
                        word_type masks[2];
                        masks[0] = unbounded_shl(low_bits<word_bits>(~word_type(), UnitBits - b_rem), b_rem);
                        masks[1] = low_bits<word_bits>(~word_type(), b_rem);
                        std::size_t bw_space = word_bits - last_word_seen;
                        std::size_t w_space = word_seen;
                        word_type w_split = 0;
                        std::size_t sz_ind = 0;

                        while (bw_space && w_space) {
                            w_split |= (!sz_ind ? unbounded_shr(w & masks[0], b_rem) :
                                                  unbounded_shl(w & masks[1], UnitBits + sz[0]));
                            bw_space -= sz[sz_ind];
                            w_space -= (w_space >= sz[sz_ind]) ? sz[sz_ind] : w_space;
                            masks[sz_ind] = unbounded_shl(masks[sz_ind], UnitBits);
                            sz_ind = 1 - sz_ind;
                        }

                        // Add significant word bits to block word
                        b[last_word_ind] |= unbounded_shl(w_split, b_unit_bits);

                        // If we fall out of the block word, push the remainder of element to the next block word
                        if (last_word_seen + word_seen > word_bits) {
                            w = unbounded_shr(w, word_bits - b_unit_bits - UnitBits);
                            w_split = 0;
                            masks[0] =
                                unbounded_shl(low_bits<word_bits>(~word_type(), UnitBits - b_rem), b_rem + UnitBits);
                            masks[1] = low_bits<word_bits>(~word_type(), b_rem);

                            while (w_space) {
                                w_split |= (!sz_ind ? unbounded_shr(w & masks[0], b_rem) :
                                                      unbounded_shl(w & masks[1], UnitBits + sz[0]));
                                w_space -= (w_space >= sz[sz_ind]) ? sz[sz_ind] : w_space;
                                masks[sz_ind] = unbounded_shl(masks[sz_ind], UnitBits);
                                sz_ind = 1 - sz_ind;
                            }

                            b[last_word_ind + 1] = unbounded_shr(w_split, UnitBits);
                        }

                        block_seen += word_seen;
                    }
                }
            };

            template<int UnitBits, std::size_t WordBits, std::size_t BlockWords, std::size_t BlockBits>
            struct word_injector<stream_endian::big_unit_little_bit<UnitBits>, WordBits, BlockWords, BlockBits>
                : public basic_functions<WordBits> {

                constexpr static const std::size_t word_bits = basic_functions<WordBits>::word_bits;
                typedef typename basic_functions<WordBits>::word_type word_type;

                typedef std::array<word_type, BlockWords> block_type;

                static void inject(word_type w, std::size_t word_seen, block_type &b, std::size_t &block_seen) {
                    // Insert word_seen-bit part of word into the block b according to endianness

                    // Check whether we fall out of the block
                    if (block_seen + word_seen <= BlockBits) {
                        std::size_t last_word_ind = block_seen / word_bits;
                        std::size_t last_word_seen = block_seen % word_bits;

                        // Remove garbage
                        std::size_t w_rem = word_seen % UnitBits;
                        std::size_t w_unit_bits = word_seen - w_rem;
                        word_type mask =
                            high_bits<word_bits>(~word_type(), w_unit_bits) |
                            unbounded_shr(high_bits<word_bits>(~word_type(), w_rem), w_unit_bits + UnitBits - w_rem);
                        w &= mask;
                        std::size_t b_rem = last_word_seen % UnitBits;
                        std::size_t b_unit_bits = last_word_seen - b_rem;
                        mask = high_bits<word_bits>(~word_type(), b_unit_bits) |
                               unbounded_shr(high_bits<word_bits>(~word_type(), b_rem), b_unit_bits + UnitBits - b_rem);
                        b[last_word_ind] &= mask;

                        // Split and combine parts of unit values
                        std::size_t sz[2] = {UnitBits - b_rem, b_rem};
                        word_type masks[2] = {
                            unbounded_shr(high_bits<word_bits>(~word_type(), UnitBits - b_rem), b_rem),
                            high_bits<word_bits>(~word_type(), b_rem)};
                        std::size_t bw_space = word_bits - last_word_seen;
                        std::size_t w_space = word_seen;
                        word_type w_split = 0;
                        std::size_t sz_ind = 0;

                        while (bw_space && w_space) {
                            w_split |= (!sz_ind ? unbounded_shl(w & masks[0], b_rem) :
                                                  unbounded_shr(w & masks[1], UnitBits + sz[0]));
                            bw_space -= sz[sz_ind];
                            w_space -= (w_space >= sz[sz_ind]) ? sz[sz_ind] : w_space;
                            masks[sz_ind] = unbounded_shr(masks[sz_ind], UnitBits);
                            sz_ind = 1 - sz_ind;
                        }

                        // Add significant word bits to block word
                        b[last_word_ind] |= unbounded_shr(w_split, b_unit_bits);

                        // If we fall out of the block word, push the remainder of element to the next block word
                        if (last_word_seen + word_seen > word_bits) {
                            w = unbounded_shl(w, word_bits - b_unit_bits - UnitBits);
                            w_split = 0;
                            masks[0] =
                                unbounded_shr(high_bits<word_bits>(~word_type(), UnitBits - b_rem), b_rem + UnitBits);
                            masks[1] = high_bits<word_bits>(~word_type(), b_rem);

                            while (w_space) {
                                w_split |= (!sz_ind ? unbounded_shl(w & masks[0], b_rem) :
                                                      unbounded_shr(w & masks[1], UnitBits + sz[0]));
                                w_space -= (w_space >= sz[sz_ind]) ? sz[sz_ind] : w_space;
                                masks[sz_ind] = unbounded_shr(masks[sz_ind], UnitBits);
                                sz_ind = 1 - sz_ind;
                            }

                            b[last_word_ind + 1] = unbounded_shl(w_split, UnitBits);
                        }
                        block_seen += word_seen;
                    }
                }
            };

            template<int UnitBits, std::size_t WordBits, std::size_t BlockWords, std::size_t BlockBits>
            struct word_injector<stream_endian::little_unit_little_bit<UnitBits>, WordBits, BlockWords, BlockBits>
                : public basic_functions<WordBits> {

                constexpr static const std::size_t word_bits = basic_functions<WordBits>::word_bits;
                typedef typename basic_functions<WordBits>::word_type word_type;

                typedef std::array<word_type, BlockWords> block_type;

                static void inject(word_type w, std::size_t word_seen, block_type &b, std::size_t &block_seen) {
                    // Insert word_seen-bit part of word into the block b according to endianness

                    // Check whether we fall out of the block
                    if (block_seen + word_seen <= BlockBits) {
                        std::size_t last_word_ind = block_seen / word_bits;
                        std::size_t last_word_seen = block_seen % word_bits;

                        // Remove garbage
                        w &= low_bits<word_bits>(~word_type(), word_seen);
                        b[last_word_ind] &= low_bits<word_bits>(~word_type(), last_word_seen);

                        // Add significant word bits to block word
                        b[last_word_ind] |= unbounded_shl(w, last_word_seen);

                        // If we fall out of the block word, push the remainder of element to the next block word
                        if (last_word_seen + word_seen > word_bits)
                            b[last_word_ind + 1] = unbounded_shr(w, word_bits - last_word_seen);

                        block_seen += word_seen;
                    }
                }
            };

            template<typename Endianness, std::size_t WordBits, std::size_t BlockWords, std::size_t BlockBits>
            struct injector : word_injector<Endianness, WordBits, BlockWords, BlockBits> {

                constexpr static const std::size_t word_bits = basic_functions<WordBits>::word_bits;
                typedef typename basic_functions<WordBits>::word_type word_type;

                typedef std::array<word_type, BlockWords> block_type;

                static void inject(const block_type &b_src, std::size_t b_src_seen, block_type &b_dst,
                                   std::size_t &b_dst_seen, std::size_t block_shift = 0) {
                    // Insert word_seen-bit part of word into the block b according to endianness

                    // Check whether we fall out of the block
                    if (b_src_seen + b_dst_seen <= BlockBits) {

                        std::size_t first_word_ind = block_shift / word_bits;
                        std::size_t word_shift = block_shift % word_bits;

                        std::size_t first_word_seen =
                            (word_bits - word_shift) > b_src_seen ? b_src_seen : (word_bits - word_shift);

                        inject(b_src[first_word_ind], first_word_seen, b_dst, b_dst_seen, word_shift);

                        b_src_seen -= first_word_seen;

                        for (std::size_t i = 0; i < (b_src_seen / word_bits); i++) {
                            inject(b_src[first_word_ind + 1 + i], word_bits, b_dst, b_dst_seen);
                        }

                        if (b_src_seen % word_bits) {
                            inject(b_src[first_word_ind + 1 + b_src_seen / word_bits], b_src_seen % word_bits, b_dst,
                                   b_dst_seen);
                        }
                    }
                }

                static void inject(word_type w, std::size_t word_seen, block_type &b, std::size_t &block_seen,
                                   std::size_t word_shift = 0) {

                    word_type word_shifted = w;

                    if (word_shift > 0) {
                        endian_shift<Endianness, word_bits>::to_msb(word_shifted, word_shift);
                    }

                    word_injector<Endianness, WordBits, BlockWords, BlockBits>::inject(word_shifted, word_seen, b,
                                                                                block_seen);
                }
            };
        }    // namespace detail
    }        // namespace crypto3
}    // namespace boost

#endif    // CRYPTO3_INJECT_HASH_HPP