//============================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt for details.
//  This software is distributed WITHOUT ANY WARRANTY; without even
//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
//  PURPOSE.  See the above copyright notice for more information.
//
//  Copyright 2014 National Technology & Engineering Solutions of Sandia, LLC (NTESS).
//  Copyright 2014 UT-Battelle, LLC.
//  Copyright 2014 Los Alamos National Security.
//
//  Under the terms of Contract DE-NA0003525 with NTESS,
//  the U.S. Government retains certain rights in this software.
//
//  Under the terms of Contract DE-AC52-06NA25396 with Los Alamos National
//  Laboratory (LANL), the U.S. Government retains certain rights in
//  this software.
//============================================================================
#ifndef vtk_m_interop_testing_TestingOpenGLInterop_h
#define vtk_m_interop_testing_TestingOpenGLInterop_h

#include <vtkm/cont/ArrayHandle.h>
#include <vtkm/cont/ArrayHandleConstant.h>
#include <vtkm/worklet/DispatcherMapField.h>
#include <vtkm/worklet/Magnitude.h>

#include <vtkm/interop/TransferToOpenGL.h>

#include <vtkm/cont/testing/Testing.h>
// #include <vtkm/cont/testing/TestingGridGenerator.h>

#include <algorithm>
#include <iterator>
#include <random>
#include <vector>

namespace vtkm
{
namespace interop
{
namespace testing
{

/// This class has a single static member, Run, that tests the templated
/// DeviceAdapter for support for opengl interop.
///
struct TestingOpenGLInterop
{
private:
  struct DoPushToDevice
  {
    template <typename Device, typename ArrayType>
    VTKM_CONT bool operator()(Device, const ArrayType& array) const
    {
      VTKM_IS_ARRAY_HANDLE(ArrayType);
      array.PrepareForDevice(Device());
      return true;
    }
  };

  // Push a given array to a device (to ensure that it works from a device). The testing
  // infrastructure should be controling which device it goes to.
  template <typename ArrayType>
  static void PushToDevice(const ArrayType& array)
  {
    VTKM_IS_ARRAY_HANDLE(ArrayType);
    vtkm::cont::TryExecute(DoPushToDevice{}, array);
  }

  //fill the array with a collection of values and return it wrapped in
  //an vtkm array handle
  template <typename T>
  static vtkm::cont::ArrayHandle<T> FillArray(std::vector<T>& data, std::size_t length)
  {
    using iterator = typename std::vector<T>::iterator;
    //make sure the data array is exactly the right length
    data.clear();
    data.resize(length);
    vtkm::Id pos = 0;
    for (iterator i = data.begin(); i != data.end(); ++i, ++pos)
    {
      *i = TestValue(pos, T());
    }

    std::random_device rng;
    std::mt19937 urng(rng());
    std::shuffle(data.begin(), data.end(), urng);
    return vtkm::cont::make_ArrayHandle(data);
  }

  //Transfer the data in a vtkm ArrayHandle to open gl while making sure
  //we don't throw any errors
  template <typename ArrayHandleType>
  static void SafelyTransferArray(ArrayHandleType array, GLuint& handle)
  {
    VTKM_IS_ARRAY_HANDLE(ArrayHandleType);
    PushToDevice(array);
    try
    {
      vtkm::interop::BufferState state(handle);
      vtkm::interop::TransferToOpenGL(array, state);
    }
    catch (vtkm::cont::ErrorBadAllocation& error)
    {
      std::cout << error.GetMessage() << std::endl;
      VTKM_TEST_ASSERT(true == false,
                       "Got an unexpected Out Of Memory error transferring to openGL");
    }
    catch (vtkm::cont::ErrorBadValue& bvError)
    {
      std::cout << bvError.GetMessage() << std::endl;
      VTKM_TEST_ASSERT(true == false, "Got an unexpected Bad Value error transferring to openGL");
    }
  }

  template <typename ArrayHandleType>
  static void SafelyTransferArray(ArrayHandleType array, GLuint& handle, GLenum type)
  {
    VTKM_IS_ARRAY_HANDLE(ArrayHandleType);
    PushToDevice(array);
    try
    {
      vtkm::interop::BufferState state(handle, type);
      vtkm::interop::TransferToOpenGL(array, state);
    }
    catch (vtkm::cont::ErrorBadAllocation& error)
    {
      std::cout << error.GetMessage() << std::endl;
      VTKM_TEST_ASSERT(true == false,
                       "Got an unexpected Out Of Memory error transferring to openGL");
    }
    catch (vtkm::cont::ErrorBadValue& bvError)
    {
      std::cout << bvError.GetMessage() << std::endl;
      VTKM_TEST_ASSERT(true == false, "Got an unexpected Bad Value error transferring to openGL");
    }
  }

  //bring the data back from openGL and into a std vector. Will bind the
  //passed in handle to the default buffer type for the type T
  template <typename T>
  static std::vector<T> CopyGLBuffer(GLuint& handle, T t)
  {
    //get the type we used for this buffer.
    GLenum type = vtkm::interop::internal::BufferTypePicker(t);

    //bind the buffer to the guessed buffer type, this way
    //we can call CopyGLBuffer no matter what it the active buffer
    glBindBuffer(type, handle);

    //get the size of the buffer
    int bytesInBuffer = 0;
    glGetBufferParameteriv(type, GL_BUFFER_SIZE, &bytesInBuffer);
    const std::size_t size = (static_cast<std::size_t>(bytesInBuffer) / sizeof(T));

    //get the buffer contents and place it into a vector
    std::vector<T> data;
    data.resize(size);
    glGetBufferSubData(type, 0, bytesInBuffer, &data[0]);

    return data;
  }

  struct TransferFunctor
  {
    template <typename T>
    void operator()(const T t) const
    {
      const std::size_t Size = 10;
      GLuint GLHandle;
      //verify that T is able to be transfer to openGL.
      //than pull down the results from the array buffer and verify
      //that they match the handles contents
      std::vector<T> tempData;
      vtkm::cont::ArrayHandle<T> temp = FillArray(tempData, Size);

      //verify that the signature that doesn't have type works
      SafelyTransferArray(temp, GLHandle);

      GLboolean is_buffer;
      is_buffer = glIsBuffer(GLHandle);
      VTKM_TEST_ASSERT(is_buffer == GL_TRUE, "OpenGL buffer not filled");

      std::vector<T> returnedValues = CopyGLBuffer(GLHandle, t);

      //verify the results match what is in the array handle
      temp.SyncControlArray();
      T* expectedValues = temp.GetStorage().GetArray();

      for (std::size_t i = 0; i < Size; ++i)
      {
        VTKM_TEST_ASSERT(test_equal(*(expectedValues + i), returnedValues[i]),
                         "Array Handle failed to transfer properly");
      }

      temp.ReleaseResources();
      temp = FillArray(tempData, Size * 2);
      GLenum type = vtkm::interop::internal::BufferTypePicker(t);
      SafelyTransferArray(temp, GLHandle, type);
      is_buffer = glIsBuffer(GLHandle);
      VTKM_TEST_ASSERT(is_buffer == GL_TRUE, "OpenGL buffer not filled");
      returnedValues = CopyGLBuffer(GLHandle, t);
      //verify the results match what is in the array handle
      temp.SyncControlArray();
      expectedValues = temp.GetStorage().GetArray();

      for (std::size_t i = 0; i < Size * 2; ++i)
      {
        VTKM_TEST_ASSERT(test_equal(*(expectedValues + i), returnedValues[i]),
                         "Array Handle failed to transfer properly");
      }

      //verify this work for a constant value array handle
      T constantValue = TestValue(2, T()); //verified by die roll
      vtkm::cont::ArrayHandleConstant<T> constant(constantValue, static_cast<vtkm::Id>(Size));
      SafelyTransferArray(constant, GLHandle);
      is_buffer = glIsBuffer(GLHandle);
      VTKM_TEST_ASSERT(is_buffer == GL_TRUE, "OpenGL buffer not filled");
      returnedValues = CopyGLBuffer(GLHandle, constantValue);
      for (std::size_t i = 0; i < Size; ++i)
      {
        VTKM_TEST_ASSERT(test_equal(returnedValues[i], constantValue),
                         "Constant value array failed to transfer properly");
      }
    }
  };

public:
  struct TestAll
  {
    VTKM_CONT void operator()() const
    {
      std::cout << "TestingOpenGLInterop Run() " << std::endl;

      //verify that we can transfer basic arrays and constant value arrays to opengl
      vtkm::testing::Testing::TryTypes(TransferFunctor());
    }
  };

  static int Run(int argc, char* argv[])
  {
    return vtkm::cont::testing::Testing::Run(TestAll(), argc, argv);
  }
};
}
}
}

#endif //vtk_m_interop_testing_TestingOpenGLInterop_h
