/*
 * Apple // emulator for *ix
 *
 * This software package is subject to the GNU General Public License
 * version 3 or later (your choice) as published by the Free Software
 * Foundation.
 *
 * Copyright 2013-2015 Aaron Culliney
 *
 */

// Modified sample code from https://developer.apple.com/library/mac/samplecode/GLEssentials/Introduction/Intro.html

#include "modelUtil.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if 0
#error untested codepaths after changing/iterating GLModel structure ...
typedef struct modelHeader {
    char fileIdentifier[30];
    unsigned int majorVersion;
    unsigned int minorVersion;
} modelHeader;

typedef struct modelTOC {
    unsigned int attribHeaderSize;
    unsigned int byteElementOffset;
    unsigned int bytePositionOffset;
    unsigned int byteTexcoordOffset;
    unsigned int byteNormalOffset;
} modelTOC;

typedef struct modelAttrib {
    unsigned int byteSize;
    GLenum datatype;
    GLenum primType; //If index data
    unsigned int sizePerElement;
    unsigned int numElements;
} modelAttrib;

GLModel *mdlLoadModel(const char *filepathname) {
    if (!filepathname) {
        return NULL;
    }
    GLModel *model = (GLModel *)CALLOC(sizeof(GLModel), 1);
    if (!model) {
        return NULL;
    }

    size_t sizeRead = 0;
    int error = 0;
    FILE *curFile = fopen(filepathname, "r");

    if (!curFile) {
        mdlDestroyModel(&model);
        return NULL;
    }

    modelHeader header = { { 0 } };
    sizeRead = fread(&header, 1, sizeof(modelHeader), curFile);
    if (sizeRead != sizeof(modelHeader)) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    if (strncmp(header.fileIdentifier, "AppleOpenGLDemoModelWWDC2010", sizeof(header.fileIdentifier))) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    if (header.majorVersion != 0 && header.minorVersion != 1) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    modelTOC toc = { 0 };
    sizeRead = fread(&toc, 1, sizeof(modelTOC), curFile);
    if (sizeRead != sizeof(modelTOC)) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    if (toc.attribHeaderSize > sizeof(modelAttrib)) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    modelAttrib attrib = { 0 };
    error = fseek(curFile, toc.byteElementOffset, SEEK_SET);
    if (error < 0) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    sizeRead = fread(&attrib, 1, toc.attribHeaderSize, curFile);
    if (sizeRead != toc.attribHeaderSize) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    model->elementArraySize = attrib.byteSize;
    model->elementType = attrib.datatype;
    model->numElements = attrib.numElements;

    // OpenGL ES cannot use UNSIGNED_INT elements
    // so if the model has UI element...
    if (GL_UNSIGNED_INT == model->elementType) {
        // ...load the UI elements and convert to UNSIGNED_SHORT

        GLubyte *uiElements = (GLubyte *)MALLOC(model->elementArraySize);
        size_t ushortElementArraySize = model->numElements * sizeof(GLushort);
        model->elements = (GLubyte *)MALLOC(ushortElementArraySize);

        sizeRead = fread(uiElements, 1, model->elementArraySize, curFile);
        if (sizeRead != model->elementArraySize) {
            fclose(curFile);
            mdlDestroyModel(&model);
            return NULL;
        }

        GLuint elemNum = 0;
        for (elemNum = 0; elemNum < model->numElements; elemNum++) {
            // can't handle this model if an element is out of the UNSIGNED_INT range
            if (((GLuint *)uiElements)[elemNum] >= 0xFFFF) {
                fclose(curFile);
                mdlDestroyModel(&model);
                return NULL;
            }

            ((GLushort *)model->elements)[elemNum] = ((GLuint *)uiElements)[elemNum];
        }
        FREE(uiElements);

        model->elementType = GL_UNSIGNED_SHORT;
        model->elementArraySize = model->numElements * sizeof(GLushort);
    } else {
        model->elements = (GLubyte*)MALLOC(model->elementArraySize);

        sizeRead = fread(model->elements, 1, model->elementArraySize, curFile);

        if (sizeRead != model->elementArraySize) {
            fclose(curFile);
            mdlDestroyModel(&model);
            return NULL;
        }
    }

    fseek(curFile, toc.bytePositionOffset, SEEK_SET);
    sizeRead = fread(&attrib, 1, toc.attribHeaderSize, curFile);
    if (sizeRead != toc.attribHeaderSize) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    model->positionArraySize = attrib.byteSize;
    model->positionType = attrib.datatype;
    model->positionSize = attrib.sizePerElement;
    model->numVertices = attrib.numElements;
    model->positions = (GLubyte*)MALLOC(model->positionArraySize);

    sizeRead = fread(model->positions, 1, model->positionArraySize, curFile);
    if (sizeRead != model->positionArraySize) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    error = fseek(curFile, toc.byteTexcoordOffset, SEEK_SET);
    if (error < 0) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    sizeRead = fread(&attrib, 1, toc.attribHeaderSize, curFile);
    if (sizeRead != toc.attribHeaderSize) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    model->texcoordArraySize = attrib.byteSize;
    model->texcoordType = attrib.datatype;
    model->texcoordSize = attrib.sizePerElement;

    // must have the same number of texcoords as positions
    if (model->numVertices != attrib.numElements) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    model->texCoords = (GLubyte*)MALLOC(model->texcoordArraySize);

    sizeRead = fread(model->texCoords, 1, model->texcoordArraySize, curFile);
    if (sizeRead != model->texcoordArraySize) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    error = fseek(curFile, toc.byteNormalOffset, SEEK_SET);
    if (error < 0) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    sizeRead = fread(&attrib, 1, toc.attribHeaderSize, curFile);

    if (sizeRead !=  toc.attribHeaderSize) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    model->normalArraySize = attrib.byteSize;
    model->normalType = attrib.datatype;
    model->normalSize = attrib.sizePerElement;

    // must have the same number of normals as positions
    if (model->numVertices != attrib.numElements) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    model->normals = (GLubyte*)MALLOC(model->normalArraySize );

    sizeRead =  fread(model->normals, 1, model->normalArraySize , curFile);
    if (sizeRead != model->normalArraySize) {
        fclose(curFile);
        mdlDestroyModel(&model);
        return NULL;
    }

    fclose(curFile);

    return model;
}

GLModel *mdlLoadQuadModel(void) {
    GLfloat posArray[] = {
        -200.0f, 0.0f, -200.0f,
         200.0f, 0.0f, -200.0f,
         200.0f, 0.0f,  200.0f,
        -200.0f, 0.0f,  200.0f
    };

    GLfloat texcoordArray[] = {
        0.0f,  1.0f,
        1.0f,  1.0f,
        1.0f,  0.0f,
        0.0f,  0.0f
    };

    GLfloat normalArray[] = {
        0.0f, 0.0f, 1.0,
        0.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f,
    };

    GLushort elementArray[] = {
        0, 2, 1,
        0, 3, 2
    };

    GLModel *model = (GLModel *)CALLOC(sizeof(GLModel), 1);

    if (!model) {
        return NULL;
    }

    model->positionType = GL_FLOAT;
    model->positionSize = 3;
    model->positionArraySize = sizeof(posArray);
    model->positions = (GLubyte*)MALLOC(model->positionArraySize);
    memcpy(model->positions, posArray, model->positionArraySize);

    model->texcoordType = GL_FLOAT;
    model->texcoordSize = 2;
    model->texcoordArraySize = sizeof(texcoordArray);
    model->texCoords = (GLubyte*)MALLOC(model->texcoordArraySize);
    memcpy(model->texCoords, texcoordArray, model->texcoordArraySize );

    model->normalType = GL_FLOAT;
    model->normalSize = 3;
    model->normalArraySize = sizeof(normalArray);
    model->normals = (GLubyte*)MALLOC(model->normalArraySize);
    memcpy(model->normals, normalArray, model->normalArraySize);

    model->elementArraySize = sizeof(elementArray);
    model->elements    = (GLubyte*)MALLOC(model->elementArraySize);
    memcpy(model->elements, elementArray, model->elementArraySize);

    model->primType = GL_TRIANGLES;

    model->numElements = sizeof(elementArray) / sizeof(GLushort);
    model->elementType = GL_UNSIGNED_SHORT;
    model->numVertices = model->positionArraySize / (model->positionSize * sizeof(GLfloat));

    return model;
}
#endif // 0

static void _quadCreateVAOAndVBOs(GLModel *model) {

    // Create a vertex array object (VAO) to cache model parameters
#if USE_VAO
    glGenVertexArrays(1, &(model->vaoName));
    glBindVertexArray(model->vaoName);
#endif

    // Create a vertex buffer object (VBO) to store positions and load data
    glGenBuffers(1, &(model->posBufferName));
    glBindBuffer(GL_ARRAY_BUFFER, model->posBufferName);
    glBufferData(GL_ARRAY_BUFFER, model->positionArraySize, model->positions, model->positionUsageHint);

#if USE_VAO
    // Enable the position attribute for this VAO
    glEnableVertexAttribArray(POS_ATTRIB_IDX);

    // Get the size of the position type so we can set the stride properly
    GLsizei posTypeSize = getGLTypeSize(model->positionType);

    // Set up parmeters for position attribute in the VAO including,
    //  size, type, stride, and offset in the currenly bound VAO
    // This also attaches the position VBO to the VAO
    glVertexAttribPointer(POS_ATTRIB_IDX,        // What attibute index will this array feed in the vertex shader (see buildProgram)
                          model->positionSize,    // How many elements are there per position?
                          model->positionType,    // What is the type of this data?
                          GL_FALSE,                // Do we want to normalize this data (0-1 range for fixed-pont types)
                          model->positionSize*posTypeSize, // What is the stride (i.e. bytes between positions)?
                          0);    // What is the offset in the VBO to the position data?
#endif

    if (model->texCoords) {
        // Create a VBO to store texcoords
        glGenBuffers(1, &(model->texcoordBufferName));
        glBindBuffer(GL_ARRAY_BUFFER, model->texcoordBufferName);

        // Allocate and load texcoord data into the VBO
        glBufferData(GL_ARRAY_BUFFER, model->texcoordArraySize, model->texCoords, model->texcoordUsageHint);

#if USE_VAO
        // Enable the texcoord attribute for this VAO
        glEnableVertexAttribArray(TEXCOORD_ATTRIB_IDX);

        // Get the size of the texcoord type so we can set the stride properly
        GLsizei texcoordTypeSize = getGLTypeSize(model->texcoordType);

        // Set up parmeters for texcoord attribute in the VAO including,
        //   size, type, stride, and offset in the currenly bound VAO
        // This also attaches the texcoord VBO to VAO
        glVertexAttribPointer(TEXCOORD_ATTRIB_IDX,    // What attibute index will this array feed in the vertex shader (see buildProgram)
                              model->texcoordSize,    // How many elements are there per texture coord?
                              model->texcoordType,    // What is the type of this data in the array?
                              GL_TRUE,                // Do we want to normalize this data (0-1 range for fixed-point types)
                              model->texcoordSize*texcoordTypeSize,  // What is the stride (i.e. bytes between texcoords)?
                              0);    // What is the offset in the VBO to the texcoord data?
#endif
    }

    // Create a VBO to vertex array elements
    // This also attaches the element array buffer to the VAO
    glGenBuffers(1, &(model->elementBufferName));
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, model->elementBufferName);

    // Allocate and load vertex array element data into VBO
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, model->elementArraySize, model->elements, GL_STATIC_DRAW/* HACK TODO FIXME: investigate*/);

#if USE_VAO
    // We're using VAOs we can destroy certain buffers since they are already
    // loaded into GL and we've saved anything else we need
    FREE(model->elements);
    //FREE(model->normals);
    FREE(model->texCoords);
#endif

    GL_ERRLOG("quad creation of VAO/VBOs");
}

static GLuint _quadCreateTexture(GLModel *model) {

    GLuint texName = UNINITIALIZED_GL;

    // Create a texture object to apply to model
    glGenTextures(1, &texName);
    glBindTexture(GL_TEXTURE_2D, texName);

    // Set up filter and wrap modes for this texture object
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    // Indicate that pixel rows are tightly packed (defaults to a stride of sizeof(PIXEL_TYPE) which is good for RGBA or
    // FLOAT data types)
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    // register texture with OpenGL
    glTexImage2D(GL_TEXTURE_2D, /*level*/0, /*internal format*/TEX_FORMAT_INTERNAL, model->texWidth, model->texHeight, /*border*/0, TEX_FORMAT, TEX_TYPE, NULL);

    GL_ERRLOG("quad texture creation");

    return texName;
}

GLModel *mdlCreateQuad(GLModelParams_s parms, GLCustom clazz) {

    const GLfloat skew_x = parms.skew_x;
    const GLfloat skew_y = parms.skew_y;
    const GLfloat obj_w = parms.obj_w;
    const GLfloat obj_h = parms.obj_h;
    const GLfloat z = parms.z;
    const GLsizei tex_w = parms.tex_w;
    const GLsizei tex_h = parms.tex_h;

    /* 2...3
     *  .
     *   .
     *    .
     * 0...1
     */

    const GLfloat obj_positions[] = {
        skew_x,        skew_y,       z, 1.0,
        skew_x+obj_w,  skew_y,       z, 1.0,
        skew_x,        skew_y+obj_h, z, 1.0,
        skew_x+obj_w,  skew_y+obj_h, z, 1.0,
    };
    const GLfloat obj_texcoords[] = {
        0.f, 1.f,
        1.f, 1.f,
        0.f, 0.f,
        1.f, 0.f,
    };
    const GLushort indices[] = {
        0, 1, 2, 2, 1, 3
    };

    GLModel *model = NULL;

    do {
        model = CALLOC(1, sizeof(GLModel));
        if (!model) {
            break;
        }
        model->numVertices = 4;
        model->numElements = 6;
        model->primType = GL_TRIANGLES;
        model->positionUsageHint = parms.positionUsageHint;
        model->texcoordUsageHint = parms.texcoordUsageHint;

        model->positions = MALLOC(sizeof(obj_positions));
        if (!(model->positions)) {
            break;
        }
        memcpy(model->positions, &obj_positions[0], sizeof(obj_positions));
        model->positionType = GL_FLOAT;
        model->positionSize = 4; // x,y,z coordinates
        model->positionArraySize = sizeof(obj_positions);

        model->texcoordType = UNINITIALIZED_GL;
        model->texcoordSize = 0;
        model->texcoordArraySize = 0;
        const bool useTexture = (tex_w > 0 && tex_h > 0);
        if (useTexture) {
            model->texCoords = MALLOC(sizeof(obj_texcoords));
            if (!(model->texCoords)) {
                break;
            }
            memcpy(model->texCoords, &obj_texcoords[0], sizeof(obj_texcoords));
            model->texcoordType = GL_FLOAT;
            model->texcoordSize = 2; // s,t coordinates
            model->texcoordArraySize = sizeof(obj_texcoords);
        }

#if 0
        {
            // NO NORMALS for now
            model->normals = NULL;
            model->normalType = GL_NONE;
            model->normalSize = GL_NONE;
            model->normalArraySize = 0;
        }
#endif

        model->elements = MALLOC(sizeof(indices));
        if (!(model->elements)) {
            break;
        }
        memcpy(model->elements, &indices[0], sizeof(indices));
        model->elementType = GL_UNSIGNED_SHORT;
        model->elementArraySize = sizeof(indices);

#if USE_VAO
        model->vaoName = UNINITIALIZED_GL;
#endif
        model->posBufferName = UNINITIALIZED_GL;
        model->texcoordBufferName = UNINITIALIZED_GL;
        model->elementBufferName = UNINITIALIZED_GL;
        model->texFormat = UNINITIALIZED_GL;

        _quadCreateVAOAndVBOs(model);
        if (model->posBufferName == UNINITIALIZED_GL || (useTexture && model->texcoordBufferName == UNINITIALIZED_GL) || model->elementBufferName == UNINITIALIZED_GL) {
            LOG("Error creating model buffers!");
            break;
        }

        model->texDirty = true;
        model->texWidth = tex_w;
        model->texHeight = tex_h;
        if (useTexture) {
            model->texFormat = TEX_FORMAT;
            model->texPixels = (GLvoid *)MALLOC(tex_w * tex_h * sizeof(PIXEL_TYPE));
            if (model->texPixels == NULL) {
                break;
            }
            model->textureName = _quadCreateTexture(model);
            if (model->textureName == UNINITIALIZED_GL) {
                LOG("Error creating model texture!");
                break;
            }
        }

        model->custom = NULL;
        if (clazz.create) {
            model->custom = clazz.create(model);
            if (!model->custom) {
                LOG("Error creating custom model!");
                break;
            }
            model->custom->create = NULL;
            model->custom->destroy = clazz.destroy;
        }

        GL_ERRLOG("quad creation");

        return model;
    } while (0);

    ERRLOG("error in quad creation");
    if (model) {
        mdlDestroyModel(&model);
    }

    return NULL;
}

void mdlDestroyModel(INOUT GLModel **model) {
    if (!model || (!*model)) {
        return;
    }

    GLModel *m = *model;

    FREE(m->elements);
    FREE(m->positions);
    //FREE(m->normals);
    FREE(m->texCoords);
    FREE(m->texPixels);

    glBindTexture(GL_TEXTURE_2D, 0);
    if (m->textureName != UNINITIALIZED_GL) {
        glDeleteTextures(1, &(m->textureName));
        m->textureName = UNINITIALIZED_GL;
    }
#if USE_VAO
    if (m->vaoName != UNINITIALIZED_GL) {
        glBindVertexArray(m->vaoName);

        // For every possible attribute set in the VAO
        for (GLuint index = 0; index < 16; index++) {
#warning FIXME TODO ... why magick hardcoded 16? ...
            // Get the VBO set for that attibute
            GLuint bufName = 0;
            glGetVertexAttribiv(index , GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING, (GLint*)&bufName);

            // If there was a VBO set...
            if (bufName) {
                //...delete the VBO
                glDeleteBuffers(1, &bufName);
            }
        }

        // Get any element array VBO set in the VAO
        {
            GLuint bufName = 0;
            glGetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, (GLint*)&bufName);

            // If there was a element array VBO set in the VAO
            if (bufName) {
                //...delete the VBO
                glDeleteBuffers(1, &bufName);
            }
        }

        // Finally, delete the VAO
        glDeleteVertexArrays(1, &(m->vaoName));
    }
#else

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    if (m->posBufferName != UNINITIALIZED_GL) {
        glDeleteBuffers(1, &(m->posBufferName));
        m->posBufferName = UNINITIALIZED_GL;
    }
    if (m->texcoordBufferName != UNINITIALIZED_GL) {
        glDeleteBuffers(1, &(m->texcoordBufferName));
        m->texcoordBufferName = UNINITIALIZED_GL;
    }
    if (m->elementBufferName != UNINITIALIZED_GL) {
        glDeleteBuffers(1, &(m->elementBufferName));
        m->elementBufferName = UNINITIALIZED_GL;
    }
#endif

    if (m->custom && m->custom->destroy) {
        m->custom->destroy(m);
        m->custom = NULL;
    }

    FREE(*model);
}

