/**************************************************************************
 *
 * Copyright 2011 Jose Fonseca
 * All Rights Reserved.
 *
 * 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.
 *
 **************************************************************************/


#include <string.h>

#include "glproc.hpp"
#include "retrace.hpp"
#include "retrace_swizzle.hpp"
#include "glretrace.hpp"


#define CGL_PBUFFER_HEIGHT 0x8040
#define CGL_PBUFFER_WIDTH 0x8041

#define kCGLNoError 0

#define kCGLPFAAllRenderers            1
#define kCGLPFATripleBuffer            3
#define kCGLPFADoubleBuffer            5
#define kCGLPFAStereo                  6
#define kCGLPFAAuxBuffers              7
#define kCGLPFAColorSize               8
#define kCGLPFAAlphaSize              11
#define kCGLPFADepthSize              12
#define kCGLPFAStencilSize            13
#define kCGLPFAAccumSize              14
#define kCGLPFAMinimumPolicy          51
#define kCGLPFAMaximumPolicy          52
#define kCGLPFAOffScreen              53
#define kCGLPFAFullScreen             54
#define kCGLPFASampleBuffers          55
#define kCGLPFASamples                56
#define kCGLPFAAuxDepthStencil        57
#define kCGLPFAColorFloat             58
#define kCGLPFAMultisample            59
#define kCGLPFASupersample            60
#define kCGLPFASampleAlpha            61
#define kCGLPFARendererID             70
#define kCGLPFASingleRenderer         71
#define kCGLPFANoRecovery             72
#define kCGLPFAAccelerated            73
#define kCGLPFAClosestPolicy          74
#define kCGLPFARobust                 75
#define kCGLPFABackingStore           76
#define kCGLPFABackingVolatile        77
#define kCGLPFAMPSafe                 78
#define kCGLPFAWindow                 80
#define kCGLPFAMultiScreen            81
#define kCGLPFACompliant              83
#define kCGLPFADisplayMask            84
#define kCGLPFAPBuffer                90
#define kCGLPFARemotePBuffer          91
#define kCGLPFAAllowOfflineRenderers  96
#define kCGLPFAAcceleratedCompute     97
#define kCGLPFAOpenGLProfile          99
#define kCGLPFASupportsAutomaticGraphicsSwitching  101
#define kCGLPFAVirtualScreenCount    128

#define kCGLOGLPVersion_Legacy   0x1000
#define kCGLOGLPVersion_3_2_Core 0x3200
#define kCGLOGLPVersion_GL3_Core 0x3200
#define kCGLOGLPVersion_GL4_Core 0x4100


using namespace glretrace;


typedef std::map<unsigned long long, glws::Drawable *> DrawableMap;
typedef std::map<unsigned long long, Context *> ContextMap;
typedef std::map<unsigned long long, int> FBConfigMap;

// sid -> Drawable* map
static DrawableMap drawable_map;

static DrawableMap pbuffer_map;

static FBConfigMap fbconfig_map;

// ctx -> Context* map
static ContextMap context_map;

static Context *sharedContext = NULL;


struct PixelFormat
{
    glfeatures::Profile profile;

    PixelFormat() :
        profile(glfeatures::API_GL, 1, 0)
    {}
};


static glws::Drawable *
getDrawable(unsigned long drawable_id, glfeatures::Profile profile) {
    if (drawable_id == 0) {
        return NULL;
    }

    DrawableMap::const_iterator it;
    it = drawable_map.find(drawable_id);
    if (it == drawable_map.end()) {
        return (drawable_map[drawable_id] = glretrace::createDrawable(profile));
    }

    return it->second;
}

static glws::Drawable *
getDrawable(unsigned long long hdc) {
    if (hdc == 0) {
        return NULL;
    }

    DrawableMap::const_iterator it;
    it = drawable_map.find(hdc);
    if (it == drawable_map.end()) {
        return (drawable_map[hdc] = glretrace::createDrawable());
    }

    return it->second;
}

static Context *
getContext(unsigned long long ctx) {
    if (ctx == 0) {
        return NULL;
    }

    ContextMap::const_iterator it;
    it = context_map.find(ctx);
    if (it == context_map.end()) {
        Context *context;
        context_map[ctx] = context = glretrace::createContext(sharedContext);
        if (!sharedContext) {
            sharedContext = context;
        }
        return context;
    }

    return it->second;
}


static void retrace_CGLChoosePixelFormat(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    bool singleBuffer = true;
    int profile = 0;
    int sample_buffers = 0;
    int samples = 0;

    const trace::Array * attribs = call.arg(0).toArray();
    if (attribs) {
        size_t i = 0;
        while (i < attribs->values.size()) {
            int param = attribs->values[i++]->toSInt();
            if (param == 0) {
                break;
            }

            switch (param) {

            // Boolean attributes

            case kCGLPFAAllRenderers:
            case kCGLPFAStereo:
            case kCGLPFAMinimumPolicy:
            case kCGLPFAMaximumPolicy:
            case kCGLPFAOffScreen:
            case kCGLPFAFullScreen:
            case kCGLPFAAuxDepthStencil:
            case kCGLPFAColorFloat:
            case kCGLPFAMultisample:
            case kCGLPFASupersample:
            case kCGLPFASampleAlpha:
            case kCGLPFASingleRenderer:
            case kCGLPFANoRecovery:
            case kCGLPFAAccelerated:
            case kCGLPFAClosestPolicy:
            case kCGLPFARobust:
            case kCGLPFABackingStore:
            case kCGLPFABackingVolatile:
            case kCGLPFAMPSafe:
            case kCGLPFAWindow:
            case kCGLPFAMultiScreen:
            case kCGLPFACompliant:
            case kCGLPFAPBuffer:
            case kCGLPFARemotePBuffer:
            case kCGLPFAAllowOfflineRenderers:
            case kCGLPFAAcceleratedCompute:
            case kCGLPFASupportsAutomaticGraphicsSwitching:
                break;

            case kCGLPFADoubleBuffer:
            case kCGLPFATripleBuffer:
                singleBuffer = false;
                break;

            // Valued attributes

            case kCGLPFAColorSize:
            case kCGLPFAAlphaSize:
            case kCGLPFADepthSize:
            case kCGLPFAStencilSize:
            case kCGLPFAAuxBuffers:
            case kCGLPFAAccumSize:
            case kCGLPFARendererID:
            case kCGLPFADisplayMask:
            case kCGLPFAVirtualScreenCount:
                ++i;
                break;

            case kCGLPFASampleBuffers:
                sample_buffers = attribs->values[i++]->toSInt();
                break;
            case kCGLPFASamples:
                samples = attribs->values[i++]->toSInt();
                break;
            case kCGLPFAOpenGLProfile:
                profile = attribs->values[i++]->toSInt();
                break;

            default:
                retrace::warning(call) << "unexpected attribute " << param << "\n";
                break;
            }
        }
    }

    trace::Value & pix = call.argByName("pix")[0];

    PixelFormat *pixelFormat = new PixelFormat;

    // TODO: Do this on a per visual basis
    switch (profile) {
    case 0:
        break;
    case kCGLOGLPVersion_Legacy:
        pixelFormat->profile = glfeatures::Profile(glfeatures::API_GL, 1, 0);
        break;
    case kCGLOGLPVersion_GL3_Core:
        pixelFormat->profile = glfeatures::Profile(glfeatures::API_GL, 3, 2, true, true);
        break;
    case kCGLOGLPVersion_GL4_Core:
        pixelFormat->profile = glfeatures::Profile(glfeatures::API_GL, 4, 1, true, true);
        break;
    default:
        retrace::warning(call) << "unexpected opengl profile " << std::hex << profile << std::dec << "\n";
        break;
    }

    // XXX: Generalize this, don't override command line options.
    retrace::doubleBuffer = !singleBuffer;

    if (sample_buffers && samples > 0)
        pixelFormat->profile.samples = samples;

    retrace::addObj(call, pix, pixelFormat);
}


static void retrace_CGLDestroyPixelFormat(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    trace::Value & pix = call.argByName("pix");

    PixelFormat *pixelFormat = retrace::asObjPointer<PixelFormat>(call, pix);
    delete pixelFormat;

    retrace::delObj(pix);
}


static void retrace_CGLCreateContext(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    trace::Value & pix = call.argByName("pix");
    const PixelFormat *pixelFormat = retrace::asObjPointer<PixelFormat>(call, pix);
    glfeatures::Profile profile = pixelFormat ? pixelFormat->profile : glretrace::defaultProfile;

    unsigned long long share = call.arg(1).toUIntPtr();
    Context *sharedContext = getContext(share);

    const trace::Array *ctx_ptr = call.arg(2).toArray();
    assert(ctx_ptr);
    unsigned long long ctx = ctx_ptr->values[0]->toUIntPtr();

    Context *context = glretrace::createContext(sharedContext, profile);
    context_map[ctx] = context;
}


static void retrace_CGLDestroyContext(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    unsigned long long ctx = call.arg(0).toUIntPtr();

    ContextMap::iterator it;
    it = context_map.find(ctx);
    if (it == context_map.end()) {
        return;
    }

    it->second->release();

    context_map.erase(it);
}

/*
  CGLError CGLCreatePBuffer (long width,
    long height,
    unsigned long target,
    unsigned long internalFormat,
    long max_level,
    CGLPBufferObj *pbuffer);
*/
static void retrace_CGLCreatePBuffer(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    int width = (call.arg(0)).toSInt();
    int height = (call.arg(1)).toSInt();
    int target = (call.arg(2)).toUInt();
    int internalFormat = (call.arg(3)).toUInt();
    int max_level = (call.arg(4)).toSInt();
    unsigned long long pb = call.arg(5).toUIntPtr();

    glws::pbuffer_info pbInfo = {0, 0, false};
    pbInfo.texFormat = internalFormat;
    pbInfo.texTarget = target;
    
    glws::Drawable *drawable = glretrace::createPbuffer(width, height, &pbInfo);
    
    drawable->cubeFace   = 0;
    drawable->mipmapLevel = max_level;

    pbuffer_map[pb]  = drawable;
    drawable_map[pb] = drawable;
}

static void retrace_CGLDestroyPBuffer(trace::Call &call) {
    glws::Drawable *drawable = getDrawable(call.arg(0).toUInt());

    if (!drawable) {
        return;
    }

    delete drawable;
}

/*
  CGLError CGLGetPBuffer(CGLContextObj ctx,
    CGLPBufferObj *pbuffer,
    unsigned long *face,
    long *level,
    long *screen);
*/
static void retrace_CGLGetPBuffer(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    unsigned long long ctx = call.arg(0).toUIntPtr();
    unsigned long long pbuffer;
    trace::Array *array = call.arg(1).toArray();
    if (array) {
        pbuffer = array->values[0]->toUIntPtr();
    } else {
        return;
    }

    glws::Drawable *drawable = pbuffer_map[ctx];

    if (!drawable) {
        return;
    }

    drawable_map[pbuffer] = drawable;
}

/*
CGLSetPBuffer
Attaches a pixel buffer object to a rendering context.
CGLError CGLSetPBuffer(CGLContextObj ctx,
CGLPBufferObj pbuffer,
unsigned long face,
long level,
long screen);
*/
static void retrace_CGLSetPBuffer(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    unsigned long long ctx = call.arg(0).toUIntPtr();
    unsigned long long pbuffer = call.arg(1).toUIntPtr();

    glws::Drawable *drawable = pbuffer_map[ctx];

    if (!drawable) {
        glws::pbuffer_info pbInfo = {0, 0, false};

        drawable = glretrace::createPbuffer(CGL_PBUFFER_WIDTH, CGL_PBUFFER_HEIGHT, &pbInfo);
    }

    drawable->cubeFace = (call.arg(2)).toUInt();
    drawable->mipmapLevel = (call.arg(3)).toSInt();

    pbuffer_map[ctx] = drawable;

    drawable_map[pbuffer] = drawable;
}

static void retrace_CGLSetSurface(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    unsigned long long ctx = call.arg(0).toUIntPtr();
    unsigned long long cid = call.arg(1).toUInt();
    int wid = call.arg(2).toUInt();
    int sid = call.arg(3).toUInt();

    (void)cid;
    (void)wid;

    Context *context = getContext(ctx);
    if (context) {
        glws::Drawable *drawable = getDrawable(sid, context->profile());
        context->drawable = drawable;
    }
}


static void retrace_CGLClearDrawable(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    unsigned long long ctx = call.arg(0).toUIntPtr();
    Context *context = getContext(ctx);
    if (context) {
        context->drawable = NULL;
    }
}


static void retrace_CGLSetCurrentContext(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    unsigned long long ctx = call.arg(0).toUIntPtr();

    Context *new_context = getContext(ctx);
    glws::Drawable *new_drawable = NULL;
    if (new_context) {
        if (!new_context->drawable) {
            glfeatures::Profile profile = new_context->profile();
            new_context->drawable = glretrace::createDrawable(profile);
        }
        new_drawable = new_context->drawable;
    }

    glretrace::makeCurrent(call, new_drawable, new_context);
}


static void retrace_CGLFlushDrawable(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    unsigned long long ctx = call.arg(0).toUIntPtr();
    Context *context = getContext(ctx);

    if (context) {
        glws::Drawable *drawable = context->drawable;
        if (drawable) {
            if (retrace::doubleBuffer) {
                drawable->swapBuffers();
            } else {
                glFlush();
            }
            frame_complete(call);
        } else {
            if (retrace::debug > 0) {
                retrace::warning(call) << "context has no drawable\n";
            }
        }
    }
}


static void retrace_CGLSetVirtualScreen(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    GLint screen = call.arg(1).toSInt();
    if (screen != 0) {
        retrace::warning(call) << "multiple virtual screens unsupported\n";
    }
}


/**
 * We can't fully reimplement CGLTexImageIOSurface2D, as external IOSurface are
 * no longer present.  Simply emit a glTexImage2D to ensure the texture storage
 * is present.
 *
 * See also:
 * - /System/Library/Frameworks/OpenGL.framework/Headers/CGLIOSurface.h
 */
static void retrace_CGLTexImageIOSurface2D(trace::Call &call) {
    if (call.ret->toUInt() != kCGLNoError) {
        return;
    }

    if (retrace::debug > 0) {
        retrace::warning(call) << "external IOSurface not supported\n";
    }

    unsigned long long ctx = call.arg(0).toUIntPtr();
    Context *context = getContext(ctx);

    GLenum target;
    target = static_cast<GLenum>((call.arg(1)).toSInt());

    GLint level = 0;

    GLint internalformat;
    internalformat = static_cast<GLenum>((call.arg(2)).toSInt());

    GLsizei width;
    width = (call.arg(3)).toSInt();

    GLsizei height;
    height = (call.arg(4)).toSInt();

    GLint border = 0;

    GLenum format;
    format = static_cast<GLenum>((call.arg(5)).toSInt());

    GLenum type;
    type = static_cast<GLenum>((call.arg(6)).toSInt());

    GLvoid * pixels = NULL;

    glretrace::Context *currentContext = glretrace::getCurrentContext();
    if (retrace::debug > 0 && currentContext != context) {
        retrace::warning(call) << "current context mismatch\n";
    }

    glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);

    if (retrace::debug > 0 && currentContext && !currentContext->insideBeginEnd) {
        glretrace::checkGlError(call);
    }
}


const retrace::Entry glretrace::cgl_callbacks[] = {
    {"CGLChoosePixelFormat", &retrace_CGLChoosePixelFormat},
    {"CGLClearDrawable", &retrace_CGLClearDrawable},
    {"CGLCreateContext", &retrace_CGLCreateContext},
    {"CGLCreatePBuffer", &retrace_CGLCreatePBuffer},
    {"CGLDescribePixelFormat", &retrace::ignore},
    {"CGLDescribeRenderer", &retrace::ignore},
    {"CGLDestroyContext", &retrace_CGLDestroyContext},
    {"CGLDestroyPBuffer", &retrace_CGLDestroyPBuffer},
    {"CGLDestroyPixelFormat", &retrace_CGLDestroyPixelFormat},
    {"CGLDisable", &retrace::ignore},
    {"CGLEnable", &retrace::ignore},
    {"CGLErrorString", &retrace::ignore},
    {"CGLFlushDrawable", &retrace_CGLFlushDrawable},
    {"CGLGetCurrentContext", &retrace::ignore},
    {"CGLGetGlobalOption", &retrace::ignore},
    {"CGLGetOption", &retrace::ignore},
    {"CGLGetParameter", &retrace::ignore},
    {"CGLGetPBuffer", &retrace_CGLGetPBuffer},
    {"CGLGetPixelFormat", &retrace::ignore},
    {"CGLGetSurface", &retrace::ignore},
    {"CGLGetVersion", &retrace::ignore},
    {"CGLGetVirtualScreen", &retrace::ignore},
    {"CGLIsEnabled", &retrace::ignore},
    {"CGLLockContext", &retrace::ignore},
    {"CGLReleaseContext", &retrace::ignore},
    {"CGLReleasePixelFormat", &retrace::ignore},
    {"CGLRetainContext", &retrace::ignore},
    {"CGLRetainPixelFormat", &retrace::ignore},
    {"CGLSetCurrentContext", &retrace_CGLSetCurrentContext},
    {"CGLSetGlobalOption", &retrace::ignore},
    {"CGLSetOption", &retrace::ignore},
    {"CGLSetPBuffer", &retrace_CGLSetPBuffer},
    {"CGLSetParameter", &retrace::ignore},
    {"CGLSetSurface", &retrace_CGLSetSurface},
    {"CGLSetVirtualScreen", &retrace_CGLSetVirtualScreen},
    {"CGLTexImageIOSurface2D", &retrace_CGLTexImageIOSurface2D},
    {"CGLUnlockContext", &retrace::ignore},
    {"CGLUpdateContext", &retrace::ignore},
    {NULL, NULL},
};

