/* ===============================================================================
 * Copyright (c) 2016 Pikkart S.r.l. All Rights Reserved.
 * Pikkart is a trademark of Pikkart S.r.l., registered in Europe,
 * the United States and other countries.
 *
 * This file is part of Pikkart AR SDK Tutorial series, a series of tutorials
 * explaining how to use and fully exploits Pikkart's AR SDK.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ===============================================================================*/
package com.pikkart.tutorial.rendering;

import android.content.res.AssetManager;
import android.opengl.GLES20;
import android.support.annotation.NonNull;
import android.util.JsonReader;
import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.nio.ShortBuffer;

/**
 * \class Mesh
 * \brief A Mesh: a collection of vertices, normals, texture coordinates and related opengl rendering functions
 */
public class Mesh
{
    private String mName; /**< mesh name */
    /**
     * \brief Get this mesh name.
     * @return the mesh name as a String.
     */
    public String Name() { return mName; }


    private ByteBuffer mVertices_Buffer; /**< vertices data */
    private ByteBuffer mTexCoords_Buffer; /**< texture coordinates data */
    private ByteBuffer mNormals_Buffer; /**< normals data */
    private ByteBuffer mIndex_Buffer; /**< triangle indices data */

    private int mIndices_Number = 0; /**< number of indices */
    private int mVertices_Number = 0; /**< number of vertices */

    private int mTexture_GL_ID = 0; /**< mesh texture opengl id */

    private int mProgram_GL_ID = 0; /**< mesh shader program opengl id */

    /**
     * This mesh vertex shader code. A very basic vetex shader
     */
    public static final String MESH_VERTEX_SHADER = " \n" + "\n"
            + "attribute vec4 vertexPosition; \n"
            + "attribute vec2 vertexTexCoord; \n" + "\n"
            + "varying vec2 texCoord; \n" + "\n"
            + "uniform mat4 modelViewProjectionMatrix; \n" + "\n"
            + "void main() \n" + "{ \n"
            + "   gl_Position = modelViewProjectionMatrix * vertexPosition; \n"
            + "   texCoord = vertexTexCoord; \n"
            + "} \n";
    /**
     * This mesh fragment shader code. A very basic fragment shader
     */
    public static final String MESH_FRAGMENT_SHADER = " \n" + "\n"
            + "precision mediump float; \n" + " \n"
            + "varying vec2 texCoord; \n" + " \n"
            + "uniform sampler2D texSampler2D; \n" + " \n"
            + "void main() \n" + "{ \n"
            + "   gl_FragColor = texture2D(texSampler2D, texCoord); \n"
            + "} \n";

    /**
     * \brief Generate a ByteBuffer from a float array.
     * @param array the float array.
     * @return the generated ByteBuffer.
     */
    protected ByteBuffer fillBuffer(float[] array)
    {
        ByteBuffer bb = ByteBuffer.allocateDirect(4 * array.length);
        bb.order(ByteOrder.LITTLE_ENDIAN);
        for (float d : array) {
            bb.putFloat(d);
        }
        bb.rewind();
        return bb;
    }

    /**
     * \brief Generate a ByteBuffer from a short array.
     * @param array the short array.
     * @return the generated ByteBuffer.
     */
    protected ByteBuffer fillBuffer(short[] array)
    {
        ByteBuffer bb = ByteBuffer.allocateDirect(2 * array.length);
        bb.order(ByteOrder.LITTLE_ENDIAN);
        for (short s : array) {
            bb.putShort(s);
        }
        bb.rewind();
        return bb;
    }

    /**
     * \brief Load mesh data from a json file stored in the app asset dir.
     * @param am the app AssetManager.
     * @param file the file to load
     * @return true on success.
     */
    private boolean LoadMesh(AssetManager am, String file)
    {
        InputStream is;
        try {
            is = am.open(file);
        } catch(IOException e) {
            Log.e("appTest","Error loading mesh file from app asset, msg:" + e.getMessage());
            return false;
        }
        Reader isReader = new InputStreamReader(is);
        JsonReader reader = new JsonReader(isReader);

        mVertices_Buffer=ByteBuffer.allocateDirect(524288);
        mVertices_Buffer.order(ByteOrder.LITTLE_ENDIAN);

        mTexCoords_Buffer=ByteBuffer.allocateDirect(524288);
        mTexCoords_Buffer.order(ByteOrder.LITTLE_ENDIAN);

        mNormals_Buffer=ByteBuffer.allocateDirect(524288);
        mNormals_Buffer.order(ByteOrder.LITTLE_ENDIAN);

        mIndex_Buffer=ByteBuffer.allocateDirect(524288);
        mIndex_Buffer.order(ByteOrder.LITTLE_ENDIAN);

        mIndices_Number = 0;
        mVertices_Number = 0;

        try {
            reader.beginObject();
            do {
                String txt = reader.nextName();
                if(txt.compareTo("name")==0) {
                    String val = reader.nextString();
                    mName = val;
                }
                else if(txt.compareTo("vertices")==0) {
                    reader.beginArray();
                    do {
                        reader.beginObject();
                        String element_name="";
                        int element_size=0;
                        String element_type="float32";
                        Boolean element_normalized = false;
                        do {
                            String elmnt = reader.nextName();
                            if(elmnt.compareTo("name")==0) {
                                element_name = reader.nextString();
                            }
                            else if(elmnt.compareTo("size")==0) {
                                element_size = reader.nextInt();
                            }
                            else if(elmnt.compareTo("type")==0) {
                                element_type = reader.nextString();
                            }
                            else if(elmnt.compareTo("normalized")==0) {
                                element_normalized = reader.nextBoolean();
                            }
                            else if(elmnt.compareTo("values")==0) {
                                reader.beginArray();
                                if(element_type.compareTo("float32")==0 && element_name.compareTo("position_buffer")==0) {
                                    do {
                                        float x = (float) reader.nextDouble() * -0.2f + 0.5f;
                                        reader.hasNext();
                                        float y = (float) reader.nextDouble() * -0.2f + 0.5f;
                                        reader.hasNext();
                                        float z = (float) reader.nextDouble() * -0.2f;
                                        mVertices_Buffer.putFloat(x);
                                        mVertices_Buffer.putFloat(y);
                                        mVertices_Buffer.putFloat(z);
                                        mVertices_Number++;
                                    }
                                    while (reader.hasNext());
                                }
                                else if(element_type.compareTo("float32")==0 && element_name.compareTo("normal_buffer")==0) {
                                    do {
                                        float v = (float) reader.nextDouble();
                                        mNormals_Buffer.putFloat(v);
                                    }
                                    while (reader.hasNext());
                                }
                                else if(element_type.compareTo("float32")==0 && element_name.compareTo("texcoord_buffer")==0) {
                                    do {
                                        float v = (float) reader.nextDouble();
                                        mTexCoords_Buffer.putFloat(v);
                                    }
                                    while (reader.hasNext());
                                }
                                reader.endArray();
                            }
                            else {
                                reader.skipValue();
                            }
                            Log.d("appTest", reader.toString());
                        }
                        while(reader.hasNext());
                        reader.endObject();
                    }
                    while(reader.hasNext());
                    reader.endArray();
                }
                else if(txt.compareTo("connectivity")==0) {
                    reader.beginArray();
                    do {
                        reader.beginObject();
                        String element_name="";
                        String element_mode="";
                        Boolean element_indexed=true;
                        String element_indexType="";
                        do {
                            String elmnt = reader.nextName();
                            if(elmnt.compareTo("name")==0) {
                                element_name = reader.nextString();
                            }
                            else if(elmnt.compareTo("mode")==0) {
                                element_mode = reader.nextString();
                            }
                            else if(elmnt.compareTo("indexed")==0) {
                                element_indexed = reader.nextBoolean();
                            }
                            else if(elmnt.compareTo("indexType")==0) {
                                element_indexType = reader.nextString();
                            }
                            else if(elmnt.compareTo("indices")==0) {
                                reader.beginArray();
                                if(element_mode.compareTo("triangles_list")==0 && element_indexed && element_indexType.compareTo("uint32")==0) {
                                    do {
                                        int v = reader.nextInt();
                                        mIndex_Buffer.putShort((short)v);
                                        mIndices_Number++;
                                    }
                                    while(reader.hasNext());
                                }
                                reader.endArray();
                            }
                            else {
                                reader.skipValue();
                            }
                            Log.d("appTest", reader.toString());

                        }
                        while(reader.hasNext());
                        reader.endObject();
                    }
                    while(reader.hasNext());
                    reader.endArray();
                }
                else {
                    reader.skipValue();
                }
                Log.d("appTest", reader.toString());
            }
            while (reader.hasNext());
            reader.endObject();
        }
        catch (Exception e) {
            Log.e("appTest","Error parsing mesh json, msg:" + e.getMessage());
            return false;
        }

        mVertices_Number /= 3;

        mVertices_Buffer.rewind();
        mTexCoords_Buffer.rewind();
        mNormals_Buffer.rewind();
        mIndex_Buffer.rewind();

        return true;
    }

    /**
     * \brief Create a Mesh from a json file (comntaining mesh data) and a texture file.
     * @param am the app AssetManager.
     * @param mesh_file the mesh file to load
     * @param texture_file the texture file to load
     * @return true on success.
     */
    public boolean InitMesh(AssetManager am, String mesh_file, String texture_file)
    {
        LoadMesh(am,mesh_file);
        mTexture_GL_ID = RenderUtils.loadTextureFromApk(am, texture_file);
        mProgram_GL_ID = RenderUtils.createProgramFromShaderSrc(MESH_VERTEX_SHADER, MESH_FRAGMENT_SHADER);
        return true;
    }

    /**
     * \brief Draw this mesh (in OpenGL).
     * @param modelViewProjection this mesh model-view-projection matrix.
     */
    public void DrawMesh(float[] modelViewProjection)
    {
        //set up gl state
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);
        GLES20.glDisable(GLES20.GL_CULL_FACE);
        //GLES20.glCullFace(GLES20.GL_BACK);
        //GLES20.glFrontFace(GLES20.GL_CCW);

        //set shader program to use
        GLES20.glUseProgram(mProgram_GL_ID);
        RenderUtils.checkGLError("DrawMesh:glUseProgram");

        //find attrib and unifroms in shader program
        int vertexHandle = GLES20.glGetAttribLocation(mProgram_GL_ID, "vertexPosition");
        //int normalHandle = GLES20.glGetAttribLocation(Program_GL_ID, "vertexNormal");
        int textureCoordHandle = GLES20.glGetAttribLocation(mProgram_GL_ID, "vertexTexCoord");
        int mvpMatrixHandle = GLES20.glGetUniformLocation(mProgram_GL_ID, "modelViewProjectionMatrix");
        int texSampler2DHandle = GLES20.glGetUniformLocation(mProgram_GL_ID, "texSampler2D");
        RenderUtils.checkGLError("DrawMesh:get attribs and uniforms");

        //upload mesh data to OpenGL attribs
        GLES20.glVertexAttribPointer(vertexHandle, 3, GLES20.GL_FLOAT, false, 0, mVertices_Buffer);
        //GLES20.glVertexAttribPointer(normalHandle, 3, GLES20.GL_FLOAT, false, 0, Normals_Buffer);
        GLES20.glVertexAttribPointer(textureCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTexCoords_Buffer);
        RenderUtils.checkGLError("DrawMesh:put attrib pointers");

        //enable gl attribs to use
        GLES20.glEnableVertexAttribArray(vertexHandle);
        //GLES20.glEnableVertexAttribArray(normalHandle);
        GLES20.glEnableVertexAttribArray(textureCoordHandle);
        RenderUtils.checkGLError("DrawMesh:enable attrib arrays");

        // activate texture 0, bind it, and pass to shader
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexture_GL_ID);
        GLES20.glUniform1i(texSampler2DHandle, 0);
        RenderUtils.checkGLError("DrawMesh:activate texturing");

        // pass the model view matrix to the shader
        GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, modelViewProjection, 0);
        RenderUtils.checkGLError("DrawMesh:upload matrix");

        // finally draw the teapot
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, mIndices_Number, GLES20.GL_UNSIGNED_SHORT, mIndex_Buffer);
        RenderUtils.checkGLError("DrawMesh:draw elements");

        // disable the enabled arrays
        GLES20.glDisableVertexAttribArray(vertexHandle);
        //GLES20.glDisableVertexAttribArray(normalHandle);
        GLES20.glDisableVertexAttribArray(textureCoordHandle);
        RenderUtils.checkGLError("DrawMesh:disable attrib arrays");
    }
}
