/*
 * Copyright 2013 serso aka se.solovyev
 *
 * 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.
 *
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * Contact details
 *
 * Email: se.solovyev@gmail.com
 * Site:  http://se.solovyev.org
 */

package org.solovyev.android.calculator.plot;

import android.graphics.Color;

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

import javax.annotation.Nonnull;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;

class Graph3d {

    // vertices count per polygon (triangle = 3)
    public static final int VERTICES_COUNT = 3;

    // color components count per color
    public static final int COLOR_COMPONENTS_COUNT = 4;

    // linear polygons count
    private final int n;

    private final boolean useHighQuality3d;
    private ShortBuffer verticeIdx;
    private FloatBuffer vertexBuf;
    private ByteBuffer colorBuf;
    private int vertexVbo, colorVbo, vertexElementVbo;
    private boolean useVBO;
    private int polygonsⁿ;

    Graph3d(GL11 gl, boolean useHighQuality3d) {
        this.useHighQuality3d = useHighQuality3d;
        this.n = useHighQuality3d ? 36 : 24;

        short[] b = new short[n * n];
        int p = 0;
        for (int i = 0; i < n; i++) {
            short v = 0;
            for (int j = 0; j < n; v += n + n, j += 2) {
                b[p++] = (short) (v + i);
                b[p++] = (short) (v + n + n - 1 - i);
            }
            v = (short) (n * (n - 2));
            i++;
            for (int j = n - 1; j >= 0; v -= n + n, j -= 2) {
                b[p++] = (short) (v + n + n - 1 - i);
                b[p++] = (short) (v + i);
            }
        }
        verticeIdx = buildBuffer(b);

        String extensions = gl.glGetString(GL10.GL_EXTENSIONS);
        useVBO = extensions.indexOf("vertex_buffer_object") != -1;
        //Calculator.log("VBOs support: " + useVBO + " version " + gl.glGetString(GL10.GL_VERSION));

        if (useVBO) {
            int[] out = new int[3];
            gl.glGenBuffers(3, out, 0);
            vertexVbo = out[0];
            colorVbo = out[1];
            vertexElementVbo = out[2];
        }
    }

    private static FloatBuffer buildBuffer(float[] b) {
        ByteBuffer bb = ByteBuffer.allocateDirect(b.length << 2);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer sb = bb.asFloatBuffer();
        sb.put(b);
        sb.position(0);
        return sb;
    }

    private static ShortBuffer buildBuffer(short[] b) {
        ByteBuffer bb = ByteBuffer.allocateDirect(b.length << 1);
        bb.order(ByteOrder.nativeOrder());
        ShortBuffer sb = bb.asShortBuffer();
        sb.put(b);
        sb.position(0);
        return sb;
    }

    private static ByteBuffer buildBuffer(byte[] b) {
        ByteBuffer bb = ByteBuffer.allocateDirect(b.length << 1);
        bb.order(ByteOrder.nativeOrder());
        bb.put(b);
        bb.position(0);
        return bb;
    }

    public void update(@Nonnull GL11 gl, @Nonnull PlotFunction fpd, @Nonnull Graph2dDimensions dimensions) {
        final XyFunction function = fpd.getXyFunction();
        final PlotLineDef lineDef = fpd.getPlotLineDef();
        final int NTICK = useHighQuality3d ? 5 : 0;

        //Calculator.log("update VBOs " + vertexVbo + ' ' + colorVbo + ' ' + vertexElementVbo);
        polygonsⁿ = n * n + 6 + 8 + NTICK * 6;

        // triangle polygon => 3 vertices per polygon
        final float vertices[] = new float[polygonsⁿ * VERTICES_COUNT];

        float maxAbsZ = fillFunctionPolygonVertices(function, dimensions, vertices);
        final byte[] colors = prepareFunctionPolygonColors(lineDef, vertices, maxAbsZ);


        int base = n * n * 3;
        int colorBase = n * n * 4;
        final int baseSize = 2;

        fillBasePolygonVectors(vertices, colors, base, colorBase, baseSize);

        base += 8 * 3;
        colorBase += 8 * 4;

        fillAxisPolygonVectors(vertices, colors, base, colorBase);

        base += 6 * 3;
        colorBase += 6 * 4;

        fillAxisGridPolygonVectors(NTICK, vertices, colors, base, colorBase);

        vertexBuf = buildBuffer(vertices);
        colorBuf = buildBuffer(colors);

        if (useVBO) {
            gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, vertexVbo);
            gl.glBufferData(GL11.GL_ARRAY_BUFFER, vertexBuf.capacity() * 4, vertexBuf, GL11.GL_STATIC_DRAW);
            vertexBuf = null;

            gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, colorVbo);
            gl.glBufferData(GL11.GL_ARRAY_BUFFER, colorBuf.capacity(), colorBuf, GL11.GL_STATIC_DRAW);
            gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
            colorBuf = null;

            gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, vertexElementVbo);
            gl.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, verticeIdx.capacity() * 2, verticeIdx, GL11.GL_STATIC_DRAW);
            gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);
        }
    }

    private void fillAxisGridPolygonVectors(int NTICK, float[] vertices, byte[] colors, int base, int colorBase) {
        int p = base;
        final float tick = .03f;
        final float offset = .01f;
        for (int i = 1; i <= NTICK; ++i) {
            vertices[p] = i - tick;
            vertices[p + 1] = -offset;
            vertices[p + 2] = -offset;

            vertices[p + 3] = i + tick;
            vertices[p + 4] = offset;
            vertices[p + 5] = offset;
            p += 6;

            vertices[p] = -offset;
            vertices[p + 1] = i - tick;
            vertices[p + 2] = -offset;

            vertices[p + 3] = offset;
            vertices[p + 4] = i + tick;
            vertices[p + 5] = offset;
            p += 6;

            vertices[p] = -offset;
            vertices[p + 1] = -offset;
            vertices[p + 2] = i - tick;

            vertices[p + 3] = offset;
            vertices[p + 4] = offset;
            vertices[p + 5] = i + tick;
            p += 6;

        }
        for (int i = colorBase + NTICK * 6 * 4 - 1; i >= colorBase; --i) {
            colors[i] = (byte) 255;
        }
    }

    private void fillAxisPolygonVectors(float[] vertices, byte[] colors, int base, int colorBase) {
        final float unit = 2;
        final float axis[] = {
                0, 0, 0,
                unit, 0, 0,
                0, 0, 0,
                0, unit, 0,
                0, 0, 0,
                0, 0, unit,
        };
        System.arraycopy(axis, 0, vertices, base, 6 * 3);
        for (int i = colorBase; i < colorBase + 6 * 4; i += 4) {
            colors[i] = (byte) 255;
            colors[i + 1] = (byte) 255;
            colors[i + 2] = (byte) 255;
            colors[i + 3] = (byte) 255;
        }
    }

    private void fillBasePolygonVectors(float[] vertices, byte[] colors, int base, int colorBase, int baseSize) {
        int p = base;
        for (int i = -baseSize; i <= baseSize; i += 2 * baseSize) {
            vertices[p] = i;
            vertices[p + 1] = -baseSize;
            vertices[p + 2] = 0;
            p += 3;
            vertices[p] = i;
            vertices[p + 1] = baseSize;
            vertices[p + 2] = 0;
            p += 3;
            vertices[p] = -baseSize;
            vertices[p + 1] = i;
            vertices[p + 2] = 0;
            p += 3;
            vertices[p] = baseSize;
            vertices[p + 1] = i;
            vertices[p + 2] = 0;
            p += 3;
        }

        for (int i = colorBase; i < colorBase + 8 * 4; i += 4) {
            colors[i] = (byte) 255;
            colors[i + 1] = (byte) 255;
            colors[i + 2] = (byte) 255;
            colors[i + 3] = (byte) 255;
        }
    }

    private float fillFunctionPolygonVertices(XyFunction function, @Nonnull Graph2dDimensions dimensions, float[] vertices) {
        final int arity = function.getArity();

        final float xMin = dimensions.getXMin();
        final float xMax = dimensions.getXMax();

        final float yMin = dimensions.getXMin();
        final float yMax = dimensions.getXMax();

        float Δx = (xMax - xMin) / (n - 1);
        float Δy = (yMax - yMin) / (n - 1);

        float y = yMin;
        float x = xMin - Δx;

        float maxAbsZ = 0;

        float z = 0;
        if (arity == 0) {
            z = (float) function.eval();
        }

        int k = 0;
        for (int i = 0; i < n; i++, y += Δy) {
            float xinc = (i & 1) == 0 ? Δx : -Δx;

            x += xinc;

            if (arity == 1) {
                z = (float) function.eval(y);
            }

            for (int j = 0; j < n; j++, x += xinc, k += VERTICES_COUNT) {

                if (arity == 2) {
                    z = (float) function.eval(y, x);
                }

                vertices[k] = x;
                vertices[k + 1] = y;
                vertices[k + 2] = z;

                if (!Float.isNaN(z)) {
                    final float absZ = Math.abs(z);
                    if (absZ > maxAbsZ) {
                        maxAbsZ = absZ;
                    }
                } else {
                    vertices[k + 2] = 0;
                }
            }
        }

        return maxAbsZ;
    }

    private byte[] prepareFunctionPolygonColors(PlotLineDef lineDef, float[] vertices, float maxAbsZ) {
        // 4 color components per polygon (color[i] = red, color[i+1] = green, color[i+2] = blue, color[i+3] = alpha )
        final byte colors[] = new byte[polygonsⁿ * COLOR_COMPONENTS_COUNT];

        final int lineColor = lineDef.getLineColor();
        final int colorComponentsCount = n * n * COLOR_COMPONENTS_COUNT;
        for (int i = 0, j = VERTICES_COUNT - 1; i < colorComponentsCount; i += COLOR_COMPONENTS_COUNT, j += VERTICES_COUNT) {
            final float z = vertices[j];

            if (!Float.isNaN(z)) {
                if (lineDef.getLineColorType() == PlotLineColorType.color_map) {
                    final float color = z / maxAbsZ;
                    final float abs = Math.abs(color);
                    colors[i] = floatToByte(color);
                    colors[i + 1] = floatToByte(1 - abs * .3f);
                    colors[i + 2] = floatToByte(-color);
                } else {
                    colors[i] = (byte) Color.red(lineColor);
                    colors[i + 1] = (byte) Color.green(lineColor);
                    colors[i + 2] = (byte) Color.blue(lineColor);
                }
                colors[i + 3] = (byte) 255;
            } else {
                colors[i] = 0;
                colors[i + 1] = 0;
                colors[i + 2] = 0;
                colors[i + 3] = 0;
            }
        }
        return colors;
    }

    private byte floatToByte(float v) {
        if (v <= 0) {
            return (byte) 0;
        } else {
            if (v >= 1) {
                return (byte) 255;
            } else {
                return (byte) (v * 255);
            }
        }
    }

    public void draw(GL11 gl) {
        if (useVBO) {
            gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, vertexVbo);
            gl.glVertexPointer(3, GL10.GL_FLOAT, 0, 0);

            gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, colorVbo);
            gl.glColorPointer(4, GL10.GL_UNSIGNED_BYTE, 0, 0);

            gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
            // gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, N*N);

            gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, vertexElementVbo);
            gl.glDrawElements(GL10.GL_LINE_STRIP, n * n, GL10.GL_UNSIGNED_SHORT, 0);
            gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);
        } else {
            gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuf);
            gl.glColorPointer(4, GL10.GL_UNSIGNED_BYTE, 0, colorBuf);
            gl.glDrawElements(GL10.GL_LINE_STRIP, n * n, GL10.GL_UNSIGNED_SHORT, verticeIdx);
        }
        final int N2 = n * n;
        gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, N2);
        gl.glDrawArrays(GL10.GL_LINES, N2, polygonsⁿ - N2);
    }

    public boolean isUseHighQuality3d() {
        return useHighQuality3d;
    }
}
