/*
 * Copyright 2013 Hannes Janetzek
 * Copyright 2017 devemux86
 *
 * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.oscim.renderer.bucket;

import org.oscim.backend.GL;
import org.oscim.backend.canvas.Color;
import org.oscim.core.GeometryBuffer;
import org.oscim.core.MapPosition;
import org.oscim.core.MercatorProjection;
import org.oscim.renderer.GLShader;
import org.oscim.renderer.GLState;
import org.oscim.renderer.GLUtils;
import org.oscim.renderer.GLViewport;
import org.oscim.renderer.bucket.VertexData.Chunk;
import org.oscim.theme.styles.AreaStyle;
import org.oscim.utils.ColorUtil;
import org.oscim.utils.TessJNI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.oscim.backend.GLAdapter.gl;
import static org.oscim.renderer.MapRenderer.COORD_SCALE;

public class MeshBucket extends RenderBucket {
    static final Logger log = LoggerFactory.getLogger(MeshBucket.class);
    static final boolean dbgRender = false;

    public AreaStyle area;
    public float heightOffset;

    private TessJNI tess;

    private int numPoints;

    public MeshBucket(int level) {
        super(RenderBucket.MESH, true, false);
        this.level = level;
    }

    public void addMesh(GeometryBuffer geom) {
        numPoints += geom.pointNextPos;
        if (tess == null)
            tess = new TessJNI(8);

        tess.addContour2D(geom.index, geom.points);
    }

    public void addConvexMesh(GeometryBuffer geom) {
        short start = (short) numVertices;

        if (numVertices >= (1 << 16)) {
            return;
        }

        vertexItems.add(geom.points[0] * COORD_SCALE,
                geom.points[1] * COORD_SCALE);

        vertexItems.add(geom.points[2] * COORD_SCALE,
                geom.points[3] * COORD_SCALE);
        short prev = (short) (start + 1);

        numVertices += 2;

        for (int i = 4; i < geom.index[0]; i += 2) {

            vertexItems.add(geom.points[i + 0] * COORD_SCALE,
                    geom.points[i + 1] * COORD_SCALE);

            indiceItems.add(start, prev, ++prev);
            numVertices++;

            numIndices += 3;
        }

        //numPoints += geom.pointPos;
        //tess.addContour2D(geom.index, geom.points);
    }

    @Override
    protected void prepare() {
        if (tess == null)
            return;

        if (numPoints == 0) {
            tess.dispose();
            return;
        }
        if (!tess.tesselate()) {
            tess.dispose();
            log.debug("error in tessellation {}", numPoints);
            return;
        }

        int nelems = tess.getElementCount() * 3;

        //int startVertex = vertexItems.countSize();

        for (int offset = indiceItems.countSize(); offset < nelems; ) {
            int size = nelems - offset;
            if (size > VertexData.SIZE)
                size = VertexData.SIZE;

            Chunk chunk = indiceItems.obtainChunk();

            tess.getElements(chunk.vertices, offset, size);
            offset += size;

            //if (startVertex != 0)
            // FIXME

            indiceItems.releaseChunk(size);
        }

        int nverts = tess.getVertexCount() * 2;

        for (int offset = 0; offset < nverts; ) {
            int size = nverts - offset;
            if (size > VertexData.SIZE)
                size = VertexData.SIZE;

            Chunk chunk = vertexItems.obtainChunk();

            tess.getVertices(chunk.vertices, offset, size, COORD_SCALE);
            offset += size;

            vertexItems.releaseChunk(size);
        }

        this.numIndices += nelems;
        this.numVertices += nverts >> 1;

        tess.dispose();
    }

    public static class Renderer {
        static Shader shader;

        static boolean init() {
            shader = new Shader("mesh_layer_2D");
            return true;
        }

        static class Shader extends GLShader {
            int uMVP, uColor, uHeight, aPos;

            Shader(String shaderFile) {
                if (!create(shaderFile))
                    return;

                uMVP = getUniform("u_mvp");
                uColor = getUniform("u_color");
                uHeight = getUniform("u_height");
                aPos = getAttrib("a_pos");
            }
        }

        public static RenderBucket draw(RenderBucket l, GLViewport v) {
            GLState.blend(true);

            Shader s = shader;

            s.useProgram();
            GLState.enableVertexArrays(s.aPos, GLState.DISABLED);

            v.mvp.setAsUniform(s.uMVP);

            float heightOffset = 0;
            gl.uniform1f(s.uHeight, heightOffset);

            for (; l != null && l.type == MESH; l = l.next) {
                MeshBucket ml = (MeshBucket) l;
                AreaStyle area = ml.area.current();

                if (area.heightOffset != ml.heightOffset)
                    ml.heightOffset = area.heightOffset;
                if (ml.heightOffset != heightOffset) {
                    heightOffset = ml.heightOffset;

                    gl.uniform1f(s.uHeight, heightOffset /
                            MercatorProjection.groundResolution(v.pos));
                }

                if (ml.area == null)
                    GLUtils.setColor(s.uColor, Color.BLUE, 0.4f);
                else {
                    setColor(area, s, v.pos);
                }
                gl.vertexAttribPointer(s.aPos, 2, GL.SHORT,
                        false, 0, ml.vertexOffset);

                gl.drawElements(GL.TRIANGLES,
                        ml.numIndices,
                        GL.UNSIGNED_SHORT,
                        ml.indiceOffset);

                if (dbgRender) {
                    int c = (ml.area == null) ? Color.BLUE : ml.area.color;
                    gl.lineWidth(1);
                    //c = ColorUtil.shiftHue(c, 0.5);
                    c = ColorUtil.modHsv(c, 0.1, 1.0, 0.8, true);
                    GLUtils.setColor(s.uColor, c, 1);
                    gl.drawElements(GL.LINES,
                            ml.numIndices,
                            GL.UNSIGNED_SHORT,
                            ml.vertexOffset);
                }
            }
            return l;
        }

        private static final int OPAQUE = 0xff000000;

        static void setColor(AreaStyle a, Shader s, MapPosition pos) {
            float fade = a.getFade(pos.scale);
            float blend = a.getBlend(pos.scale);

            if (fade < 1.0f) {
                GLState.blend(true);
                GLUtils.setColor(s.uColor, a.color, fade);
            } else if (blend > 0.0f) {
                if (blend == 1.0f)
                    GLUtils.setColor(s.uColor, a.blendColor, 1);
                else
                    GLUtils.setColorBlend(s.uColor, a.color,
                            a.blendColor, blend);
            } else {
                /* test if color contains alpha */
                GLState.blend((a.color & OPAQUE) != OPAQUE);
                GLUtils.setColor(s.uColor, a.color, 1);
            }
        }
    }
}
