/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2019-2022 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.
 ******************************************************************************/

#define GL_GLEXT_PROTOTYPES

#include "cgl_dispatch_table.h"
#include "gl_common.h"

#include "apple_gl_hook_defs.h"

// helpers defined in cgl_platform_helpers.mm
void Apple_getWindowSize(void *view, int &width, int &height);
void Apple_stopTrackingWindowSize(void *view);
void NSGL_init();
void *NSGL_createContext(void *view, void *shareContext);
void NSGL_makeCurrentContext(void *context);
void NSGL_update(void *context);
void NSGL_flushBuffer(void *context);
void NSGL_destroyContext(void *context);

// gl functions (used for quad rendering on legacy contexts)
extern "C" void glPushMatrix();
extern "C" void glLoadIdentity();
extern "C" void glMatrixMode(GLenum);
extern "C" void glOrtho(GLdouble, GLdouble, GLdouble, GLdouble, GLdouble, GLdouble);
extern "C" void glPopMatrix();
extern "C" void glBegin(GLenum);
extern "C" void glVertex2f(float, float);
extern "C" void glTexCoord2f(float, float);
extern "C" void glEnd();

#define QUAD_GL_FUNCS(FUNC) \
  FUNC(glGetIntegerv);      \
  FUNC(glPushMatrix);       \
  FUNC(glLoadIdentity);     \
  FUNC(glMatrixMode);       \
  FUNC(glOrtho);            \
  FUNC(glPopMatrix);        \
  FUNC(glBegin);            \
  FUNC(glVertex2f);         \
  FUNC(glTexCoord2f);       \
  FUNC(glEnd);

#define DECL_PTR(a) decltype(&a) a;
struct QuadGL
{
  QUAD_GL_FUNCS(DECL_PTR);
};

template <>
rdcstr DoStringise(const CGLError &el)
{
  BEGIN_ENUM_STRINGISE(CGLError);
  {
    STRINGISE_ENUM_NAMED(kCGLNoError, "no error");
    STRINGISE_ENUM_NAMED(kCGLBadAttribute, "invalid pixel format attribute");
    STRINGISE_ENUM_NAMED(kCGLBadProperty, "invalid renderer property");
    STRINGISE_ENUM_NAMED(kCGLBadPixelFormat, "invalid pixel format");
    STRINGISE_ENUM_NAMED(kCGLBadRendererInfo, "invalid renderer info");
    STRINGISE_ENUM_NAMED(kCGLBadContext, "invalid context");
    STRINGISE_ENUM_NAMED(kCGLBadDrawable, "invalid drawable");
    STRINGISE_ENUM_NAMED(kCGLBadDisplay, "invalid graphics device");
    STRINGISE_ENUM_NAMED(kCGLBadState, "invalid context state");
    STRINGISE_ENUM_NAMED(kCGLBadValue, "invalid numerical value");
    STRINGISE_ENUM_NAMED(kCGLBadMatch, "invalid share context");
    STRINGISE_ENUM_NAMED(kCGLBadEnumeration, "invalid enumerant");
    STRINGISE_ENUM_NAMED(kCGLBadOffScreen, "invalid offscreen drawable");
    STRINGISE_ENUM_NAMED(kCGLBadFullScreen, "invalid fullscreen drawable");
    STRINGISE_ENUM_NAMED(kCGLBadWindow, "invalid window");
    STRINGISE_ENUM_NAMED(kCGLBadAddress, "invalid pointer");
    STRINGISE_ENUM_NAMED(kCGLBadCodeModule, "invalid code module");
    STRINGISE_ENUM_NAMED(kCGLBadAlloc, "invalid memory allocation");
    STRINGISE_ENUM_NAMED(kCGLBadConnection, "invalid CoreGraphics connection");
  }
  END_ENUM_STRINGISE();
}

class CGLPlatform : public GLPlatform
{
  bool MakeContextCurrent(GLWindowingData data)
  {
    if(RenderDoc::Inst().IsReplayApp())
    {
      NSGL_makeCurrentContext(data.nsgl_ctx);
      return true;
    }
    else
    {
      if(CGL.CGLSetCurrentContext)
      {
        CGLError err = CGL.CGLSetCurrentContext(data.ctx);
        if(err == kCGLNoError)
          return true;
        RDCERR("MakeContextCurrent: %s", ToStr(err).c_str());
      }
    }
    return false;
  }
  GLWindowingData CloneTemporaryContext(GLWindowingData share)
  {
    GLWindowingData ret = share;

    ret.ctx = NULL;

    if(RenderDoc::Inst().IsReplayApp())
    {
      RDCASSERT(share.nsgl_ctx);
      ret.nsgl_ctx = NSGL_createContext(NULL, share.nsgl_ctx);
    }
    else
    {
      if(share.ctx && CGL.CGLCreateContext)
      {
        CGLError err = CGL.CGLCreateContext(share.pix, share.ctx, &ret.ctx);
        RDCASSERTMSG("Error creating temporary context", err == kCGLNoError, err);
      }
    }

    return ret;
  }

  void DeleteClonedContext(GLWindowingData context)
  {
    if(RenderDoc::Inst().IsReplayApp())
    {
      NSGL_destroyContext(context.nsgl_ctx);
    }
    else
    {
      if(context.ctx && CGL.CGLDestroyContext)
        CGL.CGLDestroyContext(context.ctx);
    }
  }
  void DeleteReplayContext(GLWindowingData context)
  {
    RDCASSERT(context.nsgl_ctx);
    NSGL_destroyContext(context.nsgl_ctx);
    Apple_stopTrackingWindowSize(context.wnd);
  }
  void SwapBuffers(GLWindowingData context) { NSGL_flushBuffer(context.nsgl_ctx); }
  void WindowResized(GLWindowingData context) { NSGL_update(context.nsgl_ctx); }
  void GetOutputWindowDimensions(GLWindowingData context, int32_t &w, int32_t &h)
  {
    if(context.wnd)
    {
      Apple_getWindowSize(context.wnd, w, h);
    }
    else
    {
      w = h = 0;
    }
  }
  bool IsOutputWindowVisible(GLWindowingData context) { return true; }
  void *GetReplayFunction(const char *funcname)
  {
#undef APPLE_FUNC
#define APPLE_FUNC(function)                 \
  if(!strcmp(funcname, STRINGIZE(function))) \
    return (void *)&::function;

    ForEachAppleSupported();

    return NULL;
  }
  bool CanCreateGLContext() { return true; }
  bool CanCreateGLESContext() { return false; }
  bool PopulateForReplay() { return CGL.PopulateForReplay(); }
  GLWindowingData MakeOutputWindow(WindowingData window, bool depth, GLWindowingData share_context)
  {
    GLWindowingData ret = {};

    if(window.system == WindowingSystem::MacOS)
    {
      RDCASSERT(window.macOS.layer && window.macOS.view);

      ret.nsgl_ctx = NSGL_createContext(window.macOS.view, share_context.nsgl_ctx);
      ret.wnd = window.macOS.view;

      return ret;
    }
    else if(window.system == WindowingSystem::Unknown || window.system == WindowingSystem::Headless)
    {
      ret.nsgl_ctx = NSGL_createContext(NULL, share_context.nsgl_ctx);

      return ret;
    }
    else
    {
      RDCERR("Unexpected window system %u", system);
    }

    return ret;
  }

  void SetDriverType(RDCDriver api) {}
  RDResult InitialiseAPI(GLWindowingData &replayContext, RDCDriver api, bool debug)
  {
    RDCASSERT(api == RDCDriver::OpenGL);

    NSGL_init();
    replayContext.nsgl_ctx = NSGL_createContext(NULL, NULL);

    if(replayContext.nsgl_ctx != NULL)
      return ResultCode::Succeeded;

    RETURN_ERROR_RESULT(ResultCode::APIInitFailed, "Failed to create replay OpenGL context");
  }

  void DrawQuads(float width, float height, const rdcarray<Vec4f> &vertices)
  {
    static QuadGL quadGL = {};

    if(!quadGL.glBegin)
    {
#define FETCH_PTR(a) quadGL.a = &::a;
      QUAD_GL_FUNCS(FETCH_PTR)
    }
    ::DrawQuads(quadGL, width, height, vertices);
  }
} cglPlatform;

CGLDispatchTable CGL = {};

GLPlatform &GetGLPlatform()
{
  return cglPlatform;
}

bool CGLDispatchTable::PopulateForReplay()
{
  RDCASSERT(RenderDoc::Inst().IsReplayApp());

  RDCDEBUG("Initialising GL function pointers");

  bool symbols_ok = true;

#define LOAD_FUNC(func)                              \
  if(!this->func)                                    \
    this->func = &::func;                            \
                                                     \
  if(!this->func)                                    \
  {                                                  \
    symbols_ok = false;                              \
    RDCWARN("Unable to load '%s'", STRINGIZE(func)); \
  }

  CGL_HOOKED_SYMBOLS(LOAD_FUNC)
  CGL_NONHOOKED_SYMBOLS(LOAD_FUNC)

#undef LOAD_FUNC
  return symbols_ok;
}
