#define EGL_EGLEXT_PROTOTYPES
#include <EGL/egl.h>
#include <EGL/eglext.h>

#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glext.h>

#include <string>
#include <vector>
#include <cstdint>

#include <assimp/cimport.h>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"


FILE* LOG = stdout;


const char* usage = "gnome-model-thumnailer -s 128 -i INPUT -o OUTPUT";

void close_log() {
    fprintf(LOG, "Exiting.\n");
    fflush(LOG);
    fclose(LOG);
}

void quit(int status=1) {
    close_log();
    quit(status);
}

static void calc_node_bounds(
    const C_STRUCT aiScene* scene,
    C_STRUCT aiNode* nd,
    C_STRUCT aiVector3D* min, C_STRUCT aiVector3D* max, C_STRUCT aiMatrix4x4* trafo) {

    C_STRUCT aiMatrix4x4 prev;
    unsigned int n = 0, t;

    prev = *trafo;
    aiMultiplyMatrix4(trafo,&nd->mTransformation);

    for (; n < nd->mNumMeshes; ++n) {
        const C_STRUCT aiMesh* mesh = scene->mMeshes[nd->mMeshes[n]];
        for (t = 0; t < mesh->mNumVertices; ++t) {

            C_STRUCT aiVector3D tmp = mesh->mVertices[t];
            aiTransformVecByMatrix4(&tmp,trafo);

            min->x = std::min(min->x, tmp.x);
            min->y = std::min(min->y, tmp.y);
            min->z = std::min(min->z, tmp.z);

            max->x = std::max(max->x, tmp.x);
            max->y = std::max(max->y, tmp.y);
            max->z = std::max(max->z, tmp.z);
        }
    }

    for (n = 0; n < nd->mNumChildren; ++n) {
        calc_node_bounds(scene, nd->mChildren[n], min, max, trafo);
    }

    *trafo = prev;
}

void calc_scene_bounds(const C_STRUCT aiScene* scene, C_STRUCT aiVector3D* min, C_STRUCT aiVector3D* max) {
	C_STRUCT aiMatrix4x4 trafo;
	aiIdentityMatrix4(&trafo);

	min->x = min->y = min->z = std::numeric_limits<float>::max();
	max->x = max->y = max->z = std::numeric_limits<float>::lowest();

	calc_node_bounds(scene, scene->mRootNode, min, max, &trafo);
}


void init_GL(int32_t size) {
    constexpr EGLint context_attribs [] = {
        EGL_CONTEXT_MAJOR_VERSION, 2,
        EGL_CONTEXT_MINOR_VERSION, 1,
        EGL_NONE
    };

    constexpr EGLint surface_attribs [] = {
        EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
        EGL_RED_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE, 8,
        EGL_ALPHA_SIZE, 8,
        EGL_NONE
    };

    const EGLint pbuffer_attribs [] = {
        EGL_WIDTH, size,
        EGL_HEIGHT, size,
        EGL_NONE,
    };

    static const int MAX_DEVICES = 4;
    EGLDeviceEXT eglDevs[MAX_DEVICES];
    EGLint numDevices;

    PFNEGLQUERYDEVICESEXTPROC eglQueryDevicesEXT =
    (PFNEGLQUERYDEVICESEXTPROC)
    eglGetProcAddress("eglQueryDevicesEXT");

    eglQueryDevicesEXT(MAX_DEVICES, eglDevs, &numDevices);

    PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT =
    (PFNEGLGETPLATFORMDISPLAYEXTPROC)
    eglGetProcAddress("eglGetPlatformDisplayEXT");

    fprintf(LOG, "Detected %d devices\n", numDevices);

    EGLDisplay display = eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, eglDevs[0], 0);

    if(display == EGL_NO_DISPLAY) {
        fprintf(LOG, "eglGetDisplay: Unable to get display");
        quit(1);
    }

    EGLBoolean result = eglInitialize(display, 0, 0);
    if(result != EGL_TRUE) {
        fprintf(LOG, "eglInitialize: 0x%x\n", eglGetError());
        quit(1);
    }

    int config_count;
    EGLConfig config;
    result = eglChooseConfig(display, surface_attribs, &config, 1, &config_count);
    if(result != EGL_TRUE) {
        fprintf(LOG, "eglChooseConfig: 0x%x\n", eglGetError());
        quit(1);
    }

    if(config_count != 1) {
        fprintf(LOG, "Error: eglChooseConfig(): config not found.\n");
        quit(-1);
    }

    fprintf(LOG, "Creating surface...\n");

    EGLSurface surface;
    surface = eglCreatePbufferSurface(display, config, pbuffer_attribs);

    eglBindAPI(EGL_OPENGL_API);

    fprintf(LOG, "Creating context...\n");

    EGLContext context;
    context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attribs);

    eglMakeCurrent(display, surface, surface, context);
}

void render_mesh_node(const C_STRUCT aiScene* scene, const C_STRUCT aiNode* node) {
	C_STRUCT aiMatrix4x4 m = node->mTransformation;

	/* update transform */
	aiTransposeMatrix4(&m);
	glPushMatrix();
	glMultMatrixf((float*)&m);

	/* draw all meshes assigned to this node */
	for (uint32_t n = 0; n < node->mNumMeshes; ++n) {
		const C_STRUCT aiMesh* mesh = scene->mMeshes[node->mMeshes[n]];

		//apply_material(sc->mMaterials[mesh->mMaterialIndex]);

		if(mesh->mNormals == NULL) {
			glDisable(GL_LIGHTING);
		} else {
			glEnable(GL_LIGHTING);
		}

		for (uint32_t t = 0; t < mesh->mNumFaces; ++t) {
			const C_STRUCT aiFace* face = &mesh->mFaces[t];
			GLenum face_mode;

			switch(face->mNumIndices) {
				case 1: face_mode = GL_POINTS; break;
				case 2: face_mode = GL_LINES; break;
				case 3: face_mode = GL_TRIANGLES; break;
				default: face_mode = GL_POLYGON; break;
			}

			glBegin(face_mode);

			for(uint32_t i = 0; i < face->mNumIndices; i++) {
				int index = face->mIndices[i];
				if(mesh->mColors[0] != NULL)
					glColor4fv((GLfloat*)&mesh->mColors[0][index]);
				if(mesh->mNormals != NULL)
					glNormal3fv(&mesh->mNormals[index].x);
				glVertex3fv(&mesh->mVertices[index].x);
			}

			glEnd();
		}

	}

	for (uint32_t n = 0; n < node->mNumChildren; ++n) {
		render_mesh_node(scene, node->mChildren[n]);
	}

	glPopMatrix();
}

void load_and_render(const std::string& model, uint32_t size, std::vector<uint8_t>& data) {
    fprintf(LOG, "Rendering\n");

    assert(glGetError() == GL_NO_ERROR);

    const float ratio = float(size) / float(size);
    const float fov = 45.0f;

    glEnable(GL_LIGHT0);
    glDisable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);

    /* Set up projection matrix */
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(fov, ratio, 1.0, 1000.0);
	glViewport(0, 0, size, size);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    const C_STRUCT aiScene* scene = aiImportFile(
        model.c_str(), aiProcessPreset_TargetRealtime_MaxQuality
    );

    if(!scene) {
        fprintf(LOG, "%s\n", aiGetErrorString());
        quit(1);
    }

    assert(scene);

    GLuint scene_list = 0;
    C_STRUCT aiVector3D scene_min, scene_max, scene_center;

    calc_scene_bounds(scene, &scene_min, &scene_max);
    scene_center.x = scene_min.x + ((scene_max.x - scene_min.x) / 2.0f);
    scene_center.y = scene_min.y + ((scene_max.y - scene_min.y) / 2.0f);
    scene_center.z = scene_min.z + ((scene_max.z - scene_min.z) / 2.0f);

    float scale = scene_max.x - scene_min.x;
	scale = std::max(scene_max.y - scene_min.y, scale);
    scale = std::max(scene_max.z - scene_min.z, scale);

    printf("%f\n", scale);
    printf("%f %f %f\n", scene_min.x, scene_min.y, scene_min.z);
    printf("%f %f %f\n", scene_max.x, scene_max.y, scene_max.z);
    printf("%f %f %f\n", scene_center.x, scene_center.y, scene_center.z);

    gluLookAt(
        scene_center.x, scene_center.y, scene_center.z + 2.5f,
        scene_center.x, scene_center.y, scene_center.z,
        0.0f, 1.0f, 0.0f
    );

    /* Clear the background */
    glClearColor(0.75f, 0.75f, 0.75f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glDisable(GL_CULL_FACE);

    glScalef(1.0f / scale, 1.0f / scale, 1.0f / scale);

    /* Rendering time \o/ */
    render_mesh_node(scene, scene->mRootNode);

    GLint format = 0, type = 0;
    glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &format);
    glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &type);

    // FIXME: Better checking

    printf("Format: 0x%x Type: 0x%x\n", format, type);

    assert(format == GL_RGBA);
    assert(type == GL_UNSIGNED_BYTE);

    data.resize(size * size * 4);

    glReadPixels(0, 0, size, size, format, type, &data[0]);
}

int main(int argc, char* argv[]) {
    LOG = fopen("/tmp/log.txt", "wt");
    atexit(close_log);

    fprintf(LOG, "Initializing\n");
    fflush(LOG);

    std::string size, input, output;

    for(int i = 1; i < argc - 1; i += 2) {
        std::string arg0 = argv[i];
        std::string arg1 = argv[i + 1];

        if(arg0 == "-s") {
            size = arg1;
        } else if(arg0 == "-i") {
            input = arg1;
        } else if(arg0 == "-o") {
            output = arg1;
        } else {
            printf("%s\n", usage);
            return -1;
        }
    }

    if(size.empty() || input.empty() || output.empty()) {
        printf("%s\n", usage);
        return -2;
    }

    fprintf(LOG, "Loading: %s\n", input.c_str());
    fflush(LOG);

    if(input.find("file://") == 0) {
        auto l = std::string("file://").length();
        input = input.substr(l, input.size() - l);
    }

    fprintf(LOG, "Loading: %s\n", input.c_str());
    fflush(LOG);

    int px = std::stoi(size);

    init_GL(px);

    std::vector<uint8_t> image_data;

    load_and_render(input, px, image_data);

    stbi_write_png(output.c_str(), px, px, 4, &image_data[0], px * 4);

    return 0;
}
