package com.regar007.shapesinopengles20.Shapes;

/**
 * Created by regar007.
 * This implementation make use of VBO's(vertex buffer objects) to draw cubes.
 * i.e., Instantiate once and draw always using just render() function.
 *
 * This class takes "Activity", "Points in {x1, x2 y1, y2, z1, z2} order" and "Colors in {r, g, b, a} order".
 * Use(Once): aHeightMap = new HeightMap(activity, new float{0, 0, 0, 1, 1, 1}, new float{1, 0, 0, 1, 0, 1, 0, 1});
 * Note: Use(OnDrawFrame) call createBuffer() function with changed values.
 * render function takes "MVP Matrix to draw point/points".
 * Use(OnDrawFrame): aCubes.render(mvpMatrix);
 */

import android.content.Context;
import android.opengl.GLES20;
import android.opengl.Matrix;
import android.util.Log;

import com.regar007.shapesinopengles20.R;
import com.regar007.shapesinopengles20.Utils.GlUtil;
import com.regar007.shapesinopengles20.Utils.MathUtils;
import com.regar007.shapesinopengles20.Utils.RawResourceReader;
import com.regar007.shapesinopengles20.Utils.ShaderHelper;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

public class HeightMap {
    private final static String TAG = "HeightMap";
    public static boolean isActive = true;

    static float MIN_POSITION = -1.1f;
    static float POSITION_RANGE = 2.2f;
    static float AMPLITUDE_FACTOR = 5.0f;

    static final int[] vbo = new int[1];
    static final int[] ibo = new int[1];
    private final static int NORMAL_DATA_SIZE = 3;
    private final static int COLOR_DATA_SIZE = 4;
    private final static int BYTES_PER_FLOAT = 4;
    private final static int BYTES_PER_SHORT = 2;
    private final static int POSITION_DATA_SIZE = 3;
    private final int STRIDE = (POSITION_DATA_SIZE + NORMAL_DATA_SIZE + COLOR_DATA_SIZE)
            * BYTES_PER_FLOAT;
    private static float[] xzRangeValues;
    private static float[] aHeightMapVertexData;
    private static short[] aHeightMapIndexData;

    int indexCount;

    private int aPositionHandle;
    private int aNormalHandle;
    private int aColorHandle;
    private int aProgramHandle;
    private int aLightPosUniform;
    private int aMVPMatrixHandle;
    private int aMVMatrixHandle;

    public HeightMap(Context context, int xLen, int zLen, float plotRange, float plotMin) {
        int xLength = xLen;
        int zLength = zLen;
        MIN_POSITION = plotMin;
        POSITION_RANGE = plotRange;

        xzRangeValues = new float[xLength];

        initializeGLProgram(context);

        try {
            final int floatsPerVertex = POSITION_DATA_SIZE + NORMAL_DATA_SIZE
                    + COLOR_DATA_SIZE;

            aHeightMapVertexData = new float[xLength * zLength * floatsPerVertex];

            int offset = 0;
            int currZ = 0;

            // First, build the data for the vertex buffer
            for (int z = 0; z < zLength; z++) {
                for (int x = xLength; x > 0; x--) {
                    final float xRatio = x / (float) (xLength - 1);

                    // Build our heightmap from the top down, so that our triangles are counter-clockwise.
                    final float zRatio = 1f - (z / (float) (zLength - 1));

                    final float xPosition = MIN_POSITION + (xRatio * POSITION_RANGE);
                    final float zPosition = MIN_POSITION + (zRatio * POSITION_RANGE);

                    if(currZ == z) {
                        xzRangeValues[z] = zPosition;
                        currZ++;
                    }
                    // Position
                    aHeightMapVertexData[offset++] = xPosition;
                    aHeightMapVertexData[offset++] = 0.0f;//((xPosition * xPosition) + (zPosition * zPosition)) / 10f;
                    aHeightMapVertexData[offset++] = zPosition;
                    // Cheap normal using a derivative of the function.
                    // The slope for X will be 2X, for Z will be 2Z.
                    // Divide by 10 since the position's Y is also divided by 10.
                    final float xSlope = (2 * xPosition) / 1f;
                    final float zSlope = (2 * zPosition) / 1f;

                    // Calculate the normal using the cross product of the slopes.
                    final float[] planeVectorX = {1f, xSlope, 0f,};
                    final float[] planeVectorZ = {0f, zSlope, 1f};
                    final float[] normalVector = {
                            (planeVectorX[1] * planeVectorZ[2]) - (planeVectorX[2] * planeVectorZ[1]),
                            (planeVectorX[0] * planeVectorZ[1]) - (planeVectorX[1] * planeVectorZ[0]),
                            (planeVectorX[2] * planeVectorZ[0]) - (planeVectorX[0] * planeVectorZ[2]),
                    };

                    // Normalize the normal
                    final float length = Matrix.length(normalVector[0], normalVector[1], normalVector[2]);

                    aHeightMapVertexData[offset++] = normalVector[0] / length;
                    aHeightMapVertexData[offset++] = normalVector[1] / length;
                    aHeightMapVertexData[offset++] = normalVector[2] / length;

                    // Add some fancy colors.
                    float g = (float )(x)/(float)(xLength);
                    float b = (float )(z)/(float)(zLength);
                    aHeightMapVertexData[offset++] = 0.0f;
                    aHeightMapVertexData[offset++] = g;//1.0f;
                    aHeightMapVertexData[offset++] = b;//0.0f;
                    aHeightMapVertexData[offset++] = 1f;
                }
            }

            // Now build the index data
            final int numStripsRequired = zLength - 1;
            final int numDegensRequired = 2 * (numStripsRequired - 1);
            final int verticesPerStrip = 2 * xLength;

            aHeightMapIndexData = new short[(verticesPerStrip * numStripsRequired) + numDegensRequired];

            offset = 0;

            for (int z = 0; z < zLength - 1; z++) {
                if (z > 0) {
                    // Degenerate begin: repeat first vertex
                    aHeightMapIndexData[offset++] = (short) (z * zLength);
                }

                for (int x = 0; x < xLength; x++) {
                    // One part of the strip
                    aHeightMapIndexData[offset++] = (short) ((z * zLength) + x);
                    aHeightMapIndexData[offset++] = (short) (((z + 1) * zLength) + x);
                }

                if (z < zLength - 2) {
                    // Degenerate end: repeat last vertex
                    aHeightMapIndexData[offset++] = (short) (((z + 1) * zLength) + (xLength - 1));
                }
            }

            indexCount = aHeightMapIndexData.length;


        } catch (Throwable t) {
            Log.w(TAG, t);
        }
    }

    public static void pushDataPointsToHeightMap(float[] positions, float[] colors){
        final int floatsPerVertex = POSITION_DATA_SIZE + NORMAL_DATA_SIZE
                + COLOR_DATA_SIZE;
        try {
            for (int i = 0; i < positions.length; i = i + 3) {
                int posIdx = i;
                int xIdx = MathUtils.binarySearchNearest(xzRangeValues, (positions[posIdx]* POSITION_RANGE) + MIN_POSITION );
                int zIdx = MathUtils.binarySearchNearest(xzRangeValues, (positions[posIdx + 2] * POSITION_RANGE) + MIN_POSITION);

                int vertexIdx = floatsPerVertex * ((xIdx) + (zIdx * xzRangeValues.length));
                aHeightMapVertexData[vertexIdx + 1] =
                        (aHeightMapVertexData[vertexIdx + 1] + ((POSITION_RANGE/4) + ((positions[posIdx + 1] * POSITION_RANGE + MIN_POSITION ) / 2.0f))) * AMPLITUDE_FACTOR / 2.0f; // y axis
                aHeightMapVertexData[vertexIdx + POSITION_DATA_SIZE + NORMAL_DATA_SIZE] =
                        aHeightMapVertexData[vertexIdx + POSITION_DATA_SIZE + NORMAL_DATA_SIZE] + aHeightMapVertexData[vertexIdx + 1]; //red
                aHeightMapVertexData[vertexIdx + POSITION_DATA_SIZE + NORMAL_DATA_SIZE + 1] = 0.0f; //green
                //aHeightMapVertexData[vertexIdx + POSITION_DATA_SIZE + NORMAL_DATA_SIZE +2] = 0.0f; //blue
            }
            createBuffers();
        }catch (Exception e){
            Log.d(TAG,"data points integration failed!",e);
        }
    }

    public static void createBuffers() {
        final FloatBuffer heightMapVertexDataBuffer = ByteBuffer
                .allocateDirect(aHeightMapVertexData.length * BYTES_PER_FLOAT).order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        heightMapVertexDataBuffer.put(aHeightMapVertexData).position(0);

        final ShortBuffer heightMapIndexDataBuffer = ByteBuffer
                .allocateDirect(aHeightMapIndexData.length * BYTES_PER_SHORT).order(ByteOrder.nativeOrder())
                .asShortBuffer();
        heightMapIndexDataBuffer.put(aHeightMapIndexData).position(0);

        if (vbo[0] > 0 && ibo[0] > 0) {
            GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[0]);
            GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, heightMapVertexDataBuffer.capacity() * BYTES_PER_FLOAT,
                    heightMapVertexDataBuffer, GLES20.GL_STATIC_DRAW);

            GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, ibo[0]);
            GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, heightMapIndexDataBuffer.capacity()
                    * BYTES_PER_SHORT, heightMapIndexDataBuffer, GLES20.GL_STATIC_DRAW);

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

        } else {
            GlUtil.checkGlError("glGenBuffers");
        }
    }

    private void initializeGLProgram(Context context) {
        final String vertexShader = RawResourceReader.readTextFileFromRawResource(context,
                R.raw.heightmap_vertex_shader);
        final String fragmentShader = RawResourceReader.readTextFileFromRawResource(context,
                R.raw.heightmap_fragment_shader);

        final int vertexShaderHandle = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertexShader);
        final int fragmentShaderHandle = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);

        aProgramHandle = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle, new String[] {
                "a_Position", "a_Normal", "a_Color" });

        GLES20.glGenBuffers(1, vbo, 0);
        GLES20.glGenBuffers(1, ibo, 0);

        isActive = true;

    }

    public void render(float[] aMVPMatrix) {
        // Use culling to remove back faces.
        GLES20.glDisable(GLES20.GL_CULL_FACE);

        // Set our per-vertex lighting program.
        GLES20.glUseProgram(aProgramHandle);

        // Set program handles for cube drawing.
        aMVPMatrixHandle = GLES20.glGetUniformLocation(aProgramHandle, "u_MVPMatrix");
        aMVMatrixHandle = GLES20.glGetUniformLocation(aProgramHandle, "u_MVMatrix");
        aLightPosUniform = GLES20.glGetUniformLocation(aProgramHandle, "u_LightPos");
        aPositionHandle = GLES20.glGetAttribLocation(aProgramHandle, "a_Position");
        aNormalHandle = GLES20.glGetAttribLocation(aProgramHandle, "a_Normal");
        aColorHandle = GLES20.glGetAttribLocation(aProgramHandle, "a_Color");

        // Pass in the combined matrix.
        GLES20.glUniformMatrix4fv(aMVPMatrixHandle, 1, false, aMVPMatrix, 0);

        if (vbo[0] > 0 && ibo[0] > 0) {
            GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[0]);

            // Bind Attributes
            GLES20.glVertexAttribPointer(aPositionHandle, POSITION_DATA_SIZE, GLES20.GL_FLOAT, false,
                    STRIDE, 0);
            GLES20.glEnableVertexAttribArray(aPositionHandle);

            GLES20.glVertexAttribPointer(aNormalHandle, NORMAL_DATA_SIZE, GLES20.GL_FLOAT, false,
                    STRIDE, POSITION_DATA_SIZE * BYTES_PER_FLOAT);
            GLES20.glEnableVertexAttribArray(aNormalHandle);

            GLES20.glVertexAttribPointer(aColorHandle, COLOR_DATA_SIZE, GLES20.GL_FLOAT, false,
                    STRIDE, (POSITION_DATA_SIZE + NORMAL_DATA_SIZE) * BYTES_PER_FLOAT);
            GLES20.glEnableVertexAttribArray(aColorHandle);


            // Draw
            GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, ibo[0]);
            GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, indexCount, GLES20.GL_UNSIGNED_SHORT, 0);

            GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
            GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
        }

        // Use culling to remove back faces.
        GLES20.glEnable(GLES20.GL_CULL_FACE);
    }

    void release() {
        if (vbo[0] > 0) {
            GLES20.glDeleteBuffers(vbo.length, vbo, 0);
            vbo[0] = 0;
        }

        if (ibo[0] > 0) {
            GLES20.glDeleteBuffers(ibo.length, ibo, 0);
            ibo[0] = 0;
        }
    }
}