/**************************************************************************
 *
 * 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 <algorithm>
#include <iostream>

#include "image.hpp"
#include "state_writer.hpp"
#include "glproc.hpp"
#include "glws.hpp"
#include "glsize.hpp"
#include "glstate.hpp"
#include "glstate_internal.hpp"


namespace glstate {


PixelPackState::PixelPackState(const Context &context)
{
    desktop = !context.ES;
    texture_3d = context.texture_3d;
    pixel_buffer_object = context.pixel_buffer_object;
    color_buffer_float = context.ARB_color_buffer_float;

    // Start with default state
    pack_alignment = 4;
    pack_image_height = 0;
    pack_lsb_first = GL_FALSE;
    pack_row_length = 0;
    pack_skip_images = 0;
    pack_skip_pixels = 0;
    pack_skip_rows = 0;
    pack_swap_bytes = GL_FALSE;
    pixel_pack_buffer_binding = 0;
    clamp_read_color = GL_FIXED_ONLY;

    // Get current state
    glGetIntegerv(GL_PACK_ALIGNMENT, &pack_alignment);
    if (desktop) {
        glGetIntegerv(GL_PACK_LSB_FIRST, &pack_lsb_first);
        glGetIntegerv(GL_PACK_ROW_LENGTH, &pack_row_length);
        glGetIntegerv(GL_PACK_SKIP_PIXELS, &pack_skip_pixels);
        glGetIntegerv(GL_PACK_SKIP_ROWS, &pack_skip_rows);
        glGetIntegerv(GL_PACK_SWAP_BYTES, &pack_swap_bytes);
        if (texture_3d) {
            glGetIntegerv(GL_PACK_IMAGE_HEIGHT, &pack_image_height);
            glGetIntegerv(GL_PACK_SKIP_IMAGES, &pack_skip_images);
        }
    }
    if (pixel_buffer_object) {
        glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &pixel_pack_buffer_binding);
    }
    if (color_buffer_float) {
        glGetIntegerv(GL_CLAMP_READ_COLOR, &clamp_read_color);
    }

    // Reset state for compact images
    glPixelStorei(GL_PACK_ALIGNMENT, 1);
    if (desktop) {
        glPixelStorei(GL_PACK_LSB_FIRST, GL_FALSE);
        glPixelStorei(GL_PACK_ROW_LENGTH, 0);
        glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
        glPixelStorei(GL_PACK_SKIP_ROWS, 0);
        glPixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE);
        if (texture_3d) {
            glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0);
            glPixelStorei(GL_PACK_SKIP_IMAGES, 0);
        }
    }
    if (pixel_buffer_object) {
        glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    }
    if (color_buffer_float) {
        glClampColor(GL_CLAMP_READ_COLOR, GL_FALSE);
    }
}

PixelPackState::~PixelPackState() {
    glPixelStorei(GL_PACK_ALIGNMENT, pack_alignment);
    if (desktop) {
        glPixelStorei(GL_PACK_LSB_FIRST, pack_lsb_first);
        glPixelStorei(GL_PACK_ROW_LENGTH, pack_row_length);
        glPixelStorei(GL_PACK_SKIP_PIXELS, pack_skip_pixels);
        glPixelStorei(GL_PACK_SKIP_ROWS, pack_skip_rows);
        glPixelStorei(GL_PACK_SWAP_BYTES, pack_swap_bytes);
        if (texture_3d) {
            glPixelStorei(GL_PACK_IMAGE_HEIGHT, pack_image_height);
            glPixelStorei(GL_PACK_SKIP_IMAGES, pack_skip_images);
        }
    }
    if (pixel_buffer_object) {
        glBindBuffer(GL_PIXEL_PACK_BUFFER, pixel_pack_buffer_binding);
    }
    if (color_buffer_float) {
        glClampColor(GL_CLAMP_READ_COLOR, clamp_read_color);
    }
}

TempId::TempId(GLenum _target) :
    target(_target),
    id(0)
{
    switch (target){
    case GL_ARRAY_BUFFER:
        glGenBuffers(1, &id);
        break;
    case GL_FRAMEBUFFER:
        glGenFramebuffers(1, &id);
        break;
    case GL_PROGRAM:
        id = glCreateProgram();
        break;
    case GL_RENDERBUFFER:
        glGenRenderbuffers(1, &id);
        break;
    case GL_TEXTURE:
        glGenTextures(1, &id);
        break;
    case GL_VERTEX_ARRAY:
        glGenVertexArrays(1, &id);
        break;
    default:
        assert(false);
        id = 0;
        return;
    }
}

TempId::~TempId()
{
    switch (target){
    case GL_ARRAY_BUFFER:
        glDeleteBuffers(1, &id);
        break;
    case GL_FRAMEBUFFER:
        glDeleteFramebuffers(1, &id);
        break;
    case GL_PROGRAM:
        glDeleteProgram(id);
        break;
    case GL_RENDERBUFFER:
        glGenRenderbuffers(1, &id);
        break;
    case GL_TEXTURE:
        glDeleteTextures(1, &id);
        break;
    case GL_VERTEX_ARRAY:
        glGenVertexArrays(1, &id);
        break;
    default:
        assert(false);
        id = 0;
        return;
    }
}

TextureBinding::TextureBinding(GLenum _target, GLuint _id) :
    target(_target),
    id(_id),
    prev_id(0)
{
    GLenum binding = getTextureBinding(target);
    glGetIntegerv(binding, (GLint *) &prev_id);
    if (prev_id != id) {
        glBindTexture(target, id);
    }
}

TextureBinding::~TextureBinding() {
    if (prev_id != id) {
        glBindTexture(target, prev_id);
    }
}

static GLenum
getBufferBinding(GLenum target) {
    switch (target) {
    case GL_ARRAY_BUFFER:
        return GL_ARRAY_BUFFER_BINDING;
    case GL_ATOMIC_COUNTER_BUFFER:
        return GL_ATOMIC_COUNTER_BUFFER_BINDING;
    case GL_COPY_READ_BUFFER:
        return GL_COPY_READ_BUFFER_BINDING;
    case GL_COPY_WRITE_BUFFER:
        return GL_COPY_WRITE_BUFFER_BINDING;
    case GL_DRAW_BUFFER:
        return GL_DRAW_BUFFER;
    case GL_DRAW_INDIRECT_BUFFER:
        return GL_DRAW_INDIRECT_BUFFER_BINDING;
    case GL_FRAMEBUFFER:
        return GL_FRAMEBUFFER_BINDING;
    case GL_DISPATCH_INDIRECT_BUFFER:
        return GL_DISPATCH_INDIRECT_BUFFER_BINDING;
    case GL_ELEMENT_ARRAY_BUFFER:
        return GL_ELEMENT_ARRAY_BUFFER_BINDING;
    case GL_PIXEL_PACK_BUFFER:
        return GL_PIXEL_PACK_BUFFER_BINDING;
    case GL_PIXEL_UNPACK_BUFFER:
        return GL_PIXEL_UNPACK_BUFFER_BINDING;
    case GL_QUERY_BUFFER:
        return GL_QUERY_BUFFER_BINDING;
    case GL_READ_BUFFER:
        return GL_READ_BUFFER;
    case GL_RENDERBUFFER:
        return GL_RENDERBUFFER_BINDING;
    case GL_SHADER_STORAGE_BUFFER:
        return GL_SHADER_STORAGE_BUFFER_BINDING;
    case GL_TEXTURE_BUFFER:
        return GL_TEXTURE_BUFFER;
    case GL_TRANSFORM_FEEDBACK_BUFFER:
        return GL_TRANSFORM_FEEDBACK_BUFFER_BINDING;
    case GL_UNIFORM_BUFFER:
        return GL_UNIFORM_BUFFER_BINDING;
    default:
        assert(false);
        return GL_NONE;
    }
}


BufferBinding::BufferBinding(GLenum _target, GLuint _buffer) :
    target(_target),
    buffer(_buffer),
    prevBuffer(0)
{
    GLenum binding = getBufferBinding(target);
    glGetIntegerv(binding, (GLint *) &prevBuffer);

    if (prevBuffer != buffer) {
        switch(target)
        {
        case GL_DRAW_BUFFER:
            glDrawBuffer(buffer);
            break;
        case GL_READ_BUFFER:
            glReadBuffer(buffer);
            break;
        case GL_FRAMEBUFFER:
            glBindFramebuffer(target, buffer);
            break;
        case GL_RENDERBUFFER:
            glBindRenderbuffer(target, buffer);
            break;
        default:
            glBindBuffer(target, buffer);
            break;
        }
    }
}

BufferBinding::~BufferBinding() {
    if (prevBuffer != buffer) {
        switch(target)
        {
        case GL_DRAW_BUFFER:
            glDrawBuffer(prevBuffer);
            break;
        case GL_READ_BUFFER:
            glReadBuffer(prevBuffer);
            break;
        case GL_FRAMEBUFFER:
            glBindFramebuffer(target, prevBuffer);
            break;
        case GL_RENDERBUFFER:
            glBindRenderbuffer(target, prevBuffer);
            break;
        default:
            glBindBuffer(target, prevBuffer);
            break;
        }
    }
}


BufferMapping::BufferMapping() :
    target(GL_NONE),
    buffer(0),
    map_pointer(NULL),
    unmap(false)
{
}

GLvoid *
BufferMapping::map(GLenum _target, GLuint _buffer)
{
    if (target == _target && buffer == _buffer) {
        return map_pointer;
    }

    target = _target;
    buffer = _buffer;
    map_pointer = NULL;
    unmap = false;

    BufferBinding bb(target, buffer);

    // Recursive mappings of the same buffer are not allowed.  And with the
    // pursuit of persistent mappings for performance this will become more
    // and more common.
    GLint mapped = GL_FALSE;
    glGetBufferParameteriv(target, GL_BUFFER_MAPPED, &mapped);
    if (mapped) {
        glGetBufferPointerv(target, GL_BUFFER_MAP_POINTER, &map_pointer);
        assert(map_pointer != NULL);

        GLint map_offset = 0;
        glGetBufferParameteriv(target, GL_BUFFER_MAP_OFFSET, &map_offset);
        if (map_offset != 0) {
            std::cerr << "warning: " << enumToString(target) << " buffer " << buffer << " is already mapped with offset " << map_offset << "\n";
            // FIXME: This most likely won't work.  We should remap the
            // buffer with the full range, then re-map when done.  This
            // should never happen in practice with persistent mappings
            // though.
            map_pointer = (GLubyte *)map_pointer - map_offset;
        }
    } else {
        map_pointer = glMapBuffer(target, GL_READ_ONLY);
        if (map_pointer) {
            unmap = true;
        }
    }

    return map_pointer;
}

BufferMapping::~BufferMapping() {
    if (unmap) {
        BufferBinding bb(target, buffer);

        GLenum ret = glUnmapBuffer(target);
        assert(ret == GL_TRUE);
        (void)ret;
    }
}


void
dumpBoolean(StateWriter &writer, GLboolean value)
{
    switch (value) {
    case GL_FALSE:
        writer.writeString("GL_FALSE");
        break;
    case GL_TRUE:
        writer.writeString("GL_TRUE");
        break;
    default:
        writer.writeInt(static_cast<GLint>(value));
        break;
    }
}


void
dumpEnum(StateWriter &writer, GLenum pname)
{
    const char *s = enumToString(pname);
    if (s) {
        writer.writeString(s);
    } else {
        writer.writeInt(pname);
    }
}


/**
 * Get a GL_KHR_debug/GL_EXT_debug_label object label.
 *
 * The returned string should be destroyed with free() when no longer
 * necessary.
 */
char *
getObjectLabel(Context &context, GLenum identifier, GLuint name)
{
    if (!name) {
        return NULL;
    }

    GLsizei length = 0;
    if (context.KHR_debug) {
        /*
         * XXX: According to
         * http://www.khronos.org/registry/gles/extensions/KHR/debug.txt
         * description of glGetObjectLabel:
         *
         *   "If <label> is NULL and <length> is non-NULL then no string will
         *   be returned and the length of the label will be returned in
         *   <length>."
         *
         * However NVIDIA 319.60 drivers return a zero length in such
         * circumstances.  310.14 drivers worked fine though.  So, just rely on
         * GL_MAX_LABEL_LENGTH instead, which might waste a bit of memory, but
         * should work reliably everywhere.
         */
        if (0) {
            glGetObjectLabel(identifier, name, 0, &length, NULL);
        } else {
            glGetIntegerv(GL_MAX_LABEL_LENGTH, &length);
        }
    }
    if (context.EXT_debug_label) {
        glGetObjectLabelEXT(identifier, name, 0, &length, NULL);
    }
    if (!length) {
        return NULL;
    }

    char *label = (char *)calloc(length + 1, 1);
    if (!label) {
        return NULL;
    }

    if (context.KHR_debug) {
        glGetObjectLabel(identifier, name, length + 1, NULL, label);
    }
    if (context.EXT_debug_label) {
        glGetObjectLabelEXT(identifier, name, length + 1, NULL, label);
    }

    if (label[0] == '\0') {
        free(label);
        return NULL;
    }

    return label;
}


/**
 * Dump a GL_KHR_debug/GL_EXT_debug_label object label.
 */
void
dumpObjectLabel(StateWriter &writer, Context &context, GLenum identifier, GLuint name, const char *member) {
    char *label = getObjectLabel(context, identifier, name);
    if (!label) {
        return;
    }

    writer.writeStringMember(member, label);
    free(label);
}


static const GLenum bindings[] = {
    GL_DRAW_BUFFER,
    GL_READ_BUFFER,
    GL_PIXEL_PACK_BUFFER_BINDING,
    GL_PIXEL_UNPACK_BUFFER_BINDING,
    GL_TEXTURE_BINDING_1D,
    GL_TEXTURE_BINDING_1D_ARRAY,
    GL_TEXTURE_BINDING_2D,
    GL_TEXTURE_BINDING_2D_ARRAY,
    GL_TEXTURE_BINDING_2D_MULTISAMPLE,
    GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY,
    GL_TEXTURE_BINDING_3D,
    GL_TEXTURE_BINDING_RECTANGLE,
    GL_TEXTURE_BINDING_CUBE_MAP,
    GL_TEXTURE_BINDING_CUBE_MAP_ARRAY,
    GL_DRAW_FRAMEBUFFER_BINDING,
    GL_READ_FRAMEBUFFER_BINDING,
    GL_RENDERBUFFER_BINDING,
    GL_DRAW_BUFFER0,
    GL_DRAW_BUFFER1,
    GL_DRAW_BUFFER2,
    GL_DRAW_BUFFER3,
    GL_DRAW_BUFFER4,
    GL_DRAW_BUFFER5,
    GL_DRAW_BUFFER6,
    GL_DRAW_BUFFER7,
    GL_TRANSFORM_FEEDBACK_BUFFER_BINDING,
    GL_UNIFORM_BUFFER_BINDING,
};


#define NUM_BINDINGS sizeof(bindings)/sizeof(bindings[0])


static void APIENTRY
debugMessageCallback(GLenum source, GLenum type, GLuint id, GLenum severity,
                     GLsizei length, const GLchar* message, const void *userParam)
{
    // Ignore NVIDIA buffer usage warnings: when dumping we inevitably use
    // buffers differently than declared
    if (source == GL_DEBUG_SOURCE_API &&
        type == GL_DEBUG_TYPE_OTHER &&
        id == 131188 &&
        severity == GL_DEBUG_SEVERITY_LOW) {
        return;
    };

    const char *severityStr = "";
    switch (severity) {
    case GL_DEBUG_SEVERITY_HIGH:
        severityStr = " high";
        break;
    case GL_DEBUG_SEVERITY_MEDIUM:
        break;
    case GL_DEBUG_SEVERITY_LOW:
        severityStr = " low";
        break;
    case GL_DEBUG_SEVERITY_NOTIFICATION:
        /* ignore */
        return;
    default:
        assert(0);
    }

    const char *sourceStr = "";
    switch (source) {
    case GL_DEBUG_SOURCE_API:
        break;
    case GL_DEBUG_SOURCE_WINDOW_SYSTEM:
        sourceStr = " window system";
        break;
    case GL_DEBUG_SOURCE_SHADER_COMPILER:
        sourceStr = " shader compiler";
        break;
    case GL_DEBUG_SOURCE_THIRD_PARTY:
        sourceStr = " third party";
        break;
    case GL_DEBUG_SOURCE_APPLICATION:
        sourceStr = " application";
        break;
    case GL_DEBUG_SOURCE_OTHER:
        break;
    default:
        assert(0);
    }

    const char *typeStr = "";
    switch (type) {
    case GL_DEBUG_TYPE_ERROR:
        typeStr = " error";
        break;
    case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
        typeStr = " deprecated behaviour";
        break;
    case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
        typeStr = " undefined behaviour";
        break;
    case GL_DEBUG_TYPE_PORTABILITY:
        typeStr = " portability issue";
        break;
    case GL_DEBUG_TYPE_PERFORMANCE:
        return;
    default:
        assert(0);
        /* fall-through */
    case GL_DEBUG_TYPE_OTHER:
        typeStr = " issue";
        break;
    case GL_DEBUG_TYPE_MARKER:
    case GL_DEBUG_TYPE_PUSH_GROUP:
    case GL_DEBUG_TYPE_POP_GROUP:
        return;
    }

    std::cerr << "warning: message:" << severityStr << sourceStr << typeStr;

    if (id) {
        std::cerr << " " << id;
    }

    std::cerr << ": ";

    std::cerr << message;

    // Write new line if not included in the message already.
    size_t messageLen = strlen(message);
    if (!messageLen ||
        (message[messageLen - 1] != '\n' &&
         message[messageLen - 1] != '\r')) {
       std::cerr << std::endl;
    }

    /* To help debug bugs in glstate. */
    if (0) {
        os::breakpoint();
    }
}


void dumpCurrentContext(StateWriter &writer)
{

#ifndef NDEBUG
    GLint old_bindings[NUM_BINDINGS];
    for (unsigned i = 0; i < NUM_BINDINGS; ++i) {
        old_bindings[i] = 0;
        glGetIntegerv(bindings[i], &old_bindings[i]);
    }
#endif

    Context context;

    /* Temporarily disable messages, as dumpParameters blindlessly tries to
     * get state, regardless the respective extension is supported or not.
     */
    GLDEBUGPROC prevDebugCallbackFunction = 0;
    void *prevDebugCallbackUserParam = 0;
    if (context.KHR_debug) {
        glGetPointerv(GL_DEBUG_CALLBACK_FUNCTION, (GLvoid **) &prevDebugCallbackFunction);
        glGetPointerv(GL_DEBUG_CALLBACK_USER_PARAM, &prevDebugCallbackUserParam);
        glDebugMessageCallback(NULL, NULL);
    }

    dumpParameters(writer, context);

    // Use our own debug-message callback.
    if (context.KHR_debug) {
        glDebugMessageCallback(debugMessageCallback, NULL);
    }

    dumpShadersUniforms(writer, context);
    dumpTextures(writer, context);
    dumpFramebuffer(writer, context);

#ifndef NDEBUG
    for (unsigned i = 0; i < NUM_BINDINGS; ++i) {
        GLint new_binding = 0;
        glGetIntegerv(bindings[i], &new_binding);
        if (new_binding != old_bindings[i]) {
            std::cerr << "warning: " << enumToString(bindings[i]) << " was clobbered\n";
        }
    }
#endif

    // Restore debug message callback
    if (context.KHR_debug) {
        glDebugMessageCallback(prevDebugCallbackFunction, prevDebugCallbackUserParam);
    }
}


} /* namespace glstate */
