#include <vtkm/worklet/WorkletMapField.h>

#include <vtkm/cont/ArrayHandle.h>
#include <vtkm/cont/ArrayHandleCounting.h>
#include <vtkm/cont/ArrayHandleVirtual.h>

#include <vtkm/VirtualObjectBase.h>
#include <vtkm/cont/DeviceAdapterListTag.h>
#include <vtkm/cont/DeviceAdapterTag.h>
#include <vtkm/cont/VirtualObjectHandle.h>

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


namespace
{

static constexpr vtkm::Id ARRAY_SIZE = 10;

////
//// BEGIN-EXAMPLE UsingVirtualObjectBase.cxx
////
class VTKM_ALWAYS_EXPORT ShapeType : public vtkm::VirtualObjectBase
{
public:
  using Point = vtkm::Vec<vtkm::FloatDefault, 2>;

  VTKM_EXEC_CONT virtual bool Inside(const Point& point) const = 0;
  VTKM_EXEC_CONT bool Inside(vtkm::FloatDefault x, vtkm::FloatDefault y) const
  {
    return this->Inside(vtkm::Vec<vtkm::FloatDefault, 2>(x, y));
  }
};

class VTKM_ALWAYS_EXPORT Square : public ShapeType
{
public:
  VTKM_EXEC_CONT Square()
    : LowerLeft(Point(vtkm::FloatDefault(0)))
    , UpperRight(Point(vtkm::FloatDefault(1)))
  {
  }

  VTKM_EXEC_CONT Square(const Point& lowerLeft, const Point& upperRight)
    : LowerLeft(lowerLeft)
    , UpperRight(upperRight)
  {
  }

  VTKM_CONT void SetLowerLeft(const Point& point)
  {
    this->LowerLeft = point;
    this->Modified();
  }

  VTKM_CONT void SetUpperRight(const Point& point)
  {
    this->UpperRight = point;
    this->Modified();
  }

  VTKM_EXEC_CONT const Point& GetLowerLeft() const { return this->LowerLeft; }
  VTKM_EXEC_CONT const Point& GetUpperRight() const { return this->UpperRight; }

  VTKM_EXEC_CONT bool Inside(const Point& point) const final
  {
    if (point[0] > LowerLeft[0] && point[0] < UpperRight[0] &&
        point[1] > LowerLeft[1] && point[1] < UpperRight[1])
    {
      return true;
    }
    return false;
  }

private:
  Point LowerLeft;
  Point UpperRight;
};
////
//// END-EXAMPLE UsingVirtualObjectBase.cxx
////


////
//// BEGIN-EXAMPLE UsingVirtualObjectHandle.cxx
////
class VTKM_ALWAYS_EXPORT ShapeHandle
  : public vtkm::cont::VirtualObjectHandle<ShapeType>
{
private:
  using Superclass = vtkm::cont::VirtualObjectHandle<ShapeType>;

public:
  ShapeHandle() = default;

  template<typename Shape,
           typename DeviceAdapterList = VTKM_DEFAULT_DEVICE_ADAPTER_LIST_TAG>
  explicit ShapeHandle(Shape* function,
                       bool acquireOwnership = true,
                       DeviceAdapterList devices = DeviceAdapterList())
    : Superclass(function, acquireOwnership, devices)
  {
  }
};
////
//// END-EXAMPLE UsingVirtualObjectHandle.cxx
////

////
//// BEGIN-EXAMPLE UsingVirtualObject.cxx
////
class EvaluateShape : public vtkm::worklet::WorkletMapField
{
public:
  using ControlSignature = void(FieldIn, FieldOut);
  using ExecutionSignature = void(_1, _2);

  EvaluateShape(const ShapeType* shape)
    : Shape(shape)
  {
  }

  VTKM_EXEC void operator()(vtkm::Vec<vtkm::FloatDefault, 2>& point,
                            bool& inside) const
  {
    inside = this->Shape->Inside(point);
  }

private:
  const ShapeType* Shape;
};


VTKM_CONT void GetPointsInSquare(
  vtkm::cont::ArrayHandle<bool>& inside,
  vtkm::cont::ArrayHandleVirtual<vtkm::Vec<vtkm::FloatDefault, 2>> points)
{
  Square square;
  ShapeHandle squareHandle(&square, false);

  vtkm::cont::Invoker invoker;
  EvaluateShape eval(
    squareHandle.PrepareForExecution(vtkm::cont::DeviceAdapterTagSerial()));
  invoker(eval, points, inside);
  //// PAUSE-EXAMPLE
  std::array<bool, ARRAY_SIZE> expected = {
    { false, true, true, true, true, false, false, false, false, false }
  };
  VTKM_TEST_ASSERT(
    vtkm::cont::testing::implicit_function_detail::TestArrayEqual(inside, expected),
    "Result does not match expected values");
  //// RESUME-EXAMPLE
}
////
//// END-EXAMPLE UsingVirtualObject.cxx
////

void TestVirtualObjectSquare()
{
  using Point = vtkm::Vec<vtkm::FloatDefault, 2>;

  VTKM_LOG_S(vtkm::cont::LogLevel::Info,
             "Testing Virtual Object Square Implementation");
  vtkm::cont::ArrayHandleCounting<Point> points(
    Point{ 0, 0 },
    Point{ vtkm::FloatDefault(0.2), vtkm::FloatDefault(0.2) },
    ARRAY_SIZE);
  vtkm::cont::ArrayHandle<bool> inside;
  GetPointsInSquare(inside, points);
}

static void Test()
{
  TestVirtualObjectSquare();
}

} // anonymous namespace

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