/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2019-2020 Baldur Karlsson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

#if defined(WIN32)

#include "../d3d11/d3d11_test.h"
#include "gl_test.h"

#include "3rdparty/glad/glad_wgl.h"

RD_TEST(GL_DX_Interop, OpenGLGraphicsTest)
{
  static constexpr const char *Description =
      "Test interop between GL and DX (Create and render to a DX surface and include into "
      "GL rendering)";

  std::string dxcommon = R"EOSHADER(

struct v2f
{
  float4 pos : SV_Position;
  float2 uv : UV;
};

)EOSHADER";

  std::string dxvertex = R"EOSHADER(

v2f main(uint vid : SV_VertexID)
{
	float2 positions[] = {
		float2(-1.0f, -1.0f),
		float2(-1.0f,  1.0f),
		float2( 1.0f, -1.0f),
		float2( 1.0f,  1.0f),
	};

  v2f OUT = (v2f)0;

	OUT.pos = float4(positions[vid]*0.8f, 0, 1);
  OUT.uv = positions[vid]*0.5f + 0.5f;

  return OUT;
}

)EOSHADER";

  std::string dxpixel = R"EOSHADER(

Texture2D<float4> tex : register(t0);

float4 main(v2f IN) : SV_Target0
{
	return tex.Load(int3(IN.uv.xy*1024.0f, 0));
}

)EOSHADER";

  std::string common = R"EOSHADER(

#version 420 core

#define v2f v2f_block \
{                     \
	vec4 pos;           \
	vec4 col;           \
	vec4 uv;            \
}

)EOSHADER";

  std::string vertex = R"EOSHADER(

layout(location = 0) in vec3 Position;
layout(location = 1) in vec4 Color;
layout(location = 2) in vec2 UV;

out v2f vertOut;

uniform vec2 wave;

void main()
{
	vertOut.pos = vec4(Position.xyz, 1);
  vertOut.pos.xy += wave*0.2f;
	gl_Position = vertOut.pos;
	vertOut.col = Color;
	vertOut.uv = vec4(UV.xy, 0, 1);
}

)EOSHADER";

  std::string pixel = R"EOSHADER(

in v2f vertIn;

layout(binding = 0) uniform sampler2D tex2D;

layout(location = 0, index = 0) out vec4 Color;

void main()
{
	Color = textureLod(tex2D, vertIn.uv.xy, 0.0f);
}

)EOSHADER";

  D3D11GraphicsTest d3d;

  void Prepare(int argc, char **argv)
  {
    d3d.headless = true;

    d3d.Prepare(argc, argv);

    OpenGLGraphicsTest::Prepare(argc, argv);
  }

  int main()
  {
    if(!d3d.Init())
      return 4;

    ID3DBlobPtr vsblob = d3d.Compile(dxcommon + dxvertex, "main", "vs_5_0");
    ID3DBlobPtr psblob = d3d.Compile(dxcommon + dxpixel, "main", "ps_5_0");

    ID3D11VertexShaderPtr vs = d3d.CreateVS(vsblob);
    ID3D11PixelShaderPtr ps = d3d.CreatePS(psblob);

    ID3D11Texture2DPtr d3d_fromd3d =
        d3d.MakeTexture(DXGI_FORMAT_R8G8B8A8_UNORM, 1024, 1024).RTV().Shared();
    ID3D11RenderTargetViewPtr rtv = d3d.MakeRTV(d3d_fromd3d);

    ID3D11Texture2DPtr d3d_tod3d =
        d3d.MakeTexture(DXGI_FORMAT_R8G8B8A8_UNORM, 1024, 1024).RTV().SRV().Shared();
    ID3D11ShaderResourceViewPtr srv = d3d.MakeSRV(d3d_tod3d);
    ID3D11RenderTargetViewPtr rtv2 = d3d.MakeRTV(d3d_tod3d);

    float base[4] = {0.2f, 0.2f, 0.2f, 1.0f};
    d3d.ctx->ClearRenderTargetView(rtv2, base);

    // initialise, create window, create context, etc
    if(!Init())
      return 3;

    DefaultA2V quad[] = {
        {Vec3f(-0.8f, -0.8f, 0.0f), Vec4f(1.0f, 0.0f, 0.0f, 1.0f), Vec2f(0.0f, 0.0f)},
        {Vec3f(-0.8f, 0.8f, 0.0f), Vec4f(0.0f, 1.0f, 0.0f, 1.0f), Vec2f(0.0f, 1.0f)},
        {Vec3f(0.8f, -0.8f, 0.0f), Vec4f(0.0f, 0.0f, 1.0f, 1.0f), Vec2f(1.0f, 0.0f)},
        {Vec3f(0.8f, 0.8f, 0.0f), Vec4f(0.0f, 0.0f, 1.0f, 1.0f), Vec2f(1.0f, 1.0f)},
    };

    ID3D11BufferPtr buf = d3d.MakeBuffer().Vertex().Data(quad).Shared();

    GLuint vao = MakeVAO();
    glBindVertexArray(vao);

    GLuint vb = MakeBuffer();
    glBindBuffer(GL_ARRAY_BUFFER, vb);

    HANDLE interop_dev = wglDXOpenDeviceNV(d3d.dev);

    HANDLE interop_d3dbuf = NULL;
    interop_d3dbuf = wglDXRegisterObjectNV(interop_dev, buf.GetInterfacePtr(), vb, GL_NONE,
                                           WGL_ACCESS_READ_ONLY_NV);

    TEST_ASSERT(interop_d3dbuf, "wglDXRegisterObjectNV buffer failed");

    if(!interop_d3dbuf)
      glBufferStorage(GL_ARRAY_BUFFER, sizeof(quad), quad, 0);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(DefaultA2V), (void *)(0));
    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(DefaultA2V), (void *)(sizeof(Vec3f)));
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(DefaultA2V),
                          (void *)(sizeof(Vec3f) + sizeof(Vec4f)));

    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glEnableVertexAttribArray(2);

    GLuint program = MakeProgram(common + vertex, common + pixel);

    GLuint gl_fromd3d = MakeTexture();
    HANDLE interop_fromd3d =
        wglDXRegisterObjectNV(interop_dev, d3d_fromd3d.GetInterfacePtr(), gl_fromd3d, GL_TEXTURE_2D,
                              WGL_ACCESS_READ_ONLY_NV);

    TEST_ASSERT(interop_fromd3d, "wglDXRegisterObjectNV texture fromd3d failed");

    GLuint gl_tod3d = MakeTexture();
    HANDLE interop_tod3d = wglDXRegisterObjectNV(interop_dev, d3d_tod3d.GetInterfacePtr(), gl_tod3d,
                                                 GL_TEXTURE_2D, WGL_ACCESS_READ_WRITE_NV);

    TEST_ASSERT(interop_tod3d, "wglDXRegisterObjectNV texture tod3d failed");

    GLuint fbo = MakeFBO();
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gl_tod3d, 0);

    GLenum bufs[] = {GL_COLOR_ATTACHMENT0};
    glDrawBuffers(1, bufs);

    glDepthFunc(GL_ALWAYS);
    glDisable(GL_DEPTH_TEST);

    ID3D11DeviceContextPtr ctx = d3d.ctx;

    HANDLE lockHandles[] = {interop_tod3d, interop_fromd3d};

    float delta = 0.0f;

    int frame = 0;

    while(Running())
    {
      frame++;

      BOOL res;
      if(interop_d3dbuf)
        res = wglDXLockObjectsNV(interop_dev, 1, &interop_d3dbuf);
      else
        res = TRUE;

      TEST_ASSERT(res, "wglDXLockObjectsNV buffer failed");

      float col2[] = {0.0f, 1.0f, 0.0f, 1.0f};
      ctx->ClearRenderTargetView(rtv, col2);

      ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);

      ctx->VSSetShader(vs, NULL, 0);
      ctx->PSSetShader(ps, NULL, 0);

      D3D11_VIEWPORT view = {0.0f, 0.0f, 1024.0f, 1024.0f, 0.0f, 1.0f};
      ctx->RSSetViewports(1, &view);

      ctx->OMSetRenderTargets(1, &rtv.GetInterfacePtr(), NULL);

      ctx->PSSetShaderResources(0, 1, &srv.GetInterfacePtr());

      ctx->Draw(4, 0);

      ctx->ClearState();

      ctx->Flush();

      res = wglDXLockObjectsNV(interop_dev, ARRAY_COUNT(lockHandles), lockHandles);

      TEST_ASSERT(res, "wglDXLockObjectsNV textures failed");

      glBindVertexArray(vao);

      glUseProgram(program);

      delta += 0.1f;

      glBindTexture(GL_TEXTURE_2D, gl_fromd3d);

      float col[] = {1.0f, 0.0f, 0.0f, 1.0f};

      // render back into d3d
      glViewport(0, 0, 1024, 1024);

      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
      glClearBufferfv(GL_COLOR, 0, col);
      glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

      // render to the backbuffer for visualisation
      glViewport(0, 0, GLsizei(screenWidth), GLsizei(screenHeight));

      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
      glClearBufferfv(GL_COLOR, 0, col);
      glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

      glBindTexture(GL_TEXTURE_2D, 0);

      res = wglDXUnlockObjectsNV(interop_dev, ARRAY_COUNT(lockHandles), lockHandles);

      TEST_ASSERT(res, "wglDXUnlockObjectsNV textures failed");

      if(interop_d3dbuf)
        res = wglDXUnlockObjectsNV(interop_dev, 1, &interop_d3dbuf);
      else
        res = TRUE;

      TEST_ASSERT(res, "wglDXUnlockObjectsNV buffer failed");

      Present();
    }

    wglDXUnregisterObjectNV(interop_dev, interop_d3dbuf);
    wglDXUnregisterObjectNV(interop_dev, interop_fromd3d);
    wglDXUnregisterObjectNV(interop_dev, interop_tod3d);
    wglDXCloseDeviceNV(interop_dev);

    return 0;
  }
};

REGISTER_TEST();

#endif
