/*
 * Copyright 2019 Gustl22
 * Copyright 2019 schedul-xor
 *
 * 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.light;

import org.oscim.backend.GL;
import org.oscim.renderer.ExtrusionRenderer;
import org.oscim.renderer.GLMatrix;
import org.oscim.renderer.GLShader;
import org.oscim.renderer.GLState;
import org.oscim.renderer.GLUtils;
import org.oscim.renderer.GLViewport;
import org.oscim.renderer.LayerRenderer;
import org.oscim.renderer.MapRenderer;
import org.oscim.utils.math.MathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.FloatBuffer;

import static org.oscim.backend.GLAdapter.gl;

public class ShadowRenderer extends LayerRenderer {
    private static final Logger log = LoggerFactory.getLogger(ShadowRenderer.class);

    public static boolean DEBUG = false;

    private ExtrusionRenderer mRenderer;

    private float SHADOWMAP_RESOLUTION = 2048f;
    private int mGroundQuad;
    //private int mGroundShadowQuad;
    private ShadowFrameBuffer mFrameBuffer;

    private float[] mOrthoMat = new float[16];
    private float[] mViewProjTmp = new float[16];
    private GLMatrix mLightMat = new GLMatrix();
    private GLMatrix mRotTmp = new GLMatrix();

    static float[] texUnitConverterF = new float[]{
            0.5f, 0.0f, 0.0f, 0.0f,
            0.0f, 0.5f, 0.0f, 0.0f,
            0.0f, 0.0f, 0.5f, 0.0f,
            0.5f, 0.5f, 0.5f, 1.0f};
    static GLMatrix texUnitConverter = new GLMatrix();

    static {
        texUnitConverter.set(texUnitConverterF);
    }

    /**
     * Shader to draw the extrusions.
     */
    private Shader mExtrusionShader;

    /**
     * Shader to draw the ground.
     */
    private GroundShader mGroundShader;

    /**
     * Shader to create shadow map (of ground and extrusions) from lights view.
     */
    private ExtrusionRenderer.Shader mLightShader;

    public static class GroundShader extends GLShader {
        int aPos, uLightColor, uLightMvp, uMVP, uShadowMap, uShadowRes;

        public GroundShader(String shader) {
            if (!createDirective(shader, "#define SHADOW 1\n"))
                return;

            aPos = getAttrib("a_pos");
            uLightColor = getUniform("u_lightColor");
            uLightMvp = getUniform("u_light_mvp");
            uMVP = getUniform("u_mvp");
            uShadowMap = getUniform("u_shadowMap");
            uShadowRes = getUniform("u_shadowRes");
        }
    }

    public static class Shader extends ExtrusionRenderer.Shader {
        /**
         * For temporary use.
         */
        static final GLMatrix lightMvp = new GLMatrix();

        /**
         * The light view projection matrix.
         */
        GLMatrix lightMat = null;

        /**
         * The light color and shadow transparency as uniform.
         */
        int uLightColor;

        /**
         * The light mvp for shadow as uniform.
         */
        int uLightMvp;

        /**
         * The shadow map texture as uniform.
         */
        int uShadowMap;

        /**
         * The shadow map resolution as uniform.
         */
        int uShadowRes;

        public Shader(String shader) {
            super(shader, "#define SHADOW 1\n");
            uLightColor = getUniform("u_lightColor");
            uLightMvp = getUniform("u_light_mvp");
            uShadowMap = getUniform("u_shadowMap");
            uShadowRes = getUniform("u_shadowRes");
        }

        public void setLightMVP(GLMatrix model) {
            if (lightMat == null) return;
            synchronized (lightMvp) {
                lightMvp.copy(model);
                lightMvp.multiplyLhs(lightMat);
                //lightMvp.addDepthOffset(delta);
                lightMvp.setAsUniform(uLightMvp);
            }
        }
    }

    public ShadowRenderer(ExtrusionRenderer renderer) {
        setRenderer(renderer);
    }

    public void setRenderer(ExtrusionRenderer renderer) {
        mRenderer = renderer;
    }

    /**
     * Bind a plane to easily draw all over the ground.
     */
    private static int bindPlane(float width, float height) {
        int vertexBuffer;
        int[] vboIds = GLUtils.glGenBuffers(1);
        FloatBuffer floatBuffer = MapRenderer.getFloatBuffer(8);
        // indices:  0 1 2 - 2 1 3
        float[] quad = new float[]{
                -width, height,
                width, height,
                -width, -height,
                width, -height
        };
        floatBuffer.put(quad);
        floatBuffer.flip();
        vertexBuffer = vboIds[0];
        GLState.bindVertexBuffer(vertexBuffer);
        gl.bufferData(GL.ARRAY_BUFFER,
                quad.length * 4, floatBuffer,
                GL.STATIC_DRAW);
        GLState.bindVertexBuffer(GLState.UNBIND);
        return vertexBuffer;
    }

    @Override
    public boolean setup() {
        // Ground plane for shadow map
        //mGroundShadowQuad = bindPlane(SHADOWMAP_RESOLUTION * 1.1f, SHADOWMAP_RESOLUTION * 1.1f);
        if (!DEBUG) {
            // Ground plane to draw shadows an
            mGroundQuad = bindPlane(Short.MAX_VALUE, Short.MAX_VALUE);
        } else {
            mGroundQuad = bindPlane(SHADOWMAP_RESOLUTION * 1.1f, SHADOWMAP_RESOLUTION * 1.1f);
        }

        // Shader
        mGroundShader = new GroundShader("extrusion_shadow_ground");
        mLightShader = new ExtrusionRenderer.Shader("extrusion_shadow_light");
        if (mRenderer.isMesh())
            mExtrusionShader = new Shader("extrusion_layer_mesh");
        else
            mExtrusionShader = new Shader("extrusion_layer_ext");

        mFrameBuffer = new ShadowFrameBuffer((int) SHADOWMAP_RESOLUTION, (int) SHADOWMAP_RESOLUTION);

        //mRenderer.setup(); // No need to setup, as shaders are taken from here

        return super.setup();
    }

    @Override
    public void update(GLViewport viewport) {
        mRenderer.update(viewport);
        setReady(mRenderer.isReady());
    }

    @Override
    public void render(GLViewport viewport) {
        /* Prepare rendering from lights view: */
        // Store vp-matrix temporarily and use vp-matrix as lightMat
        viewport.viewproj.get(mViewProjTmp);

        float projWidth = SHADOWMAP_RESOLUTION;
        float projHeight = SHADOWMAP_RESOLUTION;
        if (DEBUG) {
            projWidth *= (3. / 4);
            projHeight *= (3. / 4);
        }
        GLMatrix.orthoM(mOrthoMat, 0, -projWidth, projWidth, projHeight, -projHeight, -SHADOWMAP_RESOLUTION, SHADOWMAP_RESOLUTION);
        viewport.viewproj.set(mOrthoMat);

        // Rotate from light direction
        float[] lightPos = mRenderer.getSun().getPosition();
        float rot = (float) Math.acos(lightPos[2] / 1.) * MathUtils.radiansToDegrees;
        mRotTmp.setRotation(rot, 1f, 0, 0); // tilt
        viewport.viewproj.multiplyRhs(mRotTmp);

        rot = MathUtils.atan2(lightPos[0], lightPos[1]) * MathUtils.radiansToDegrees;
        mRotTmp.setRotation(rot, 0, 0, 1f); // bearing
        viewport.viewproj.multiplyRhs(mRotTmp);

        // DRAW SHADOW MAP
        {
            // START DEPTH FRAMEBUFFER
            mFrameBuffer.bindFrameBuffer();

            GLState.blend(false);  // depth cannot be transparent
            gl.depthMask(true);
            GLState.test(true, false);

            // Clear color (for gl20) and depth (for gl30)
            gl.clear(gl.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT);

            // Draw GROUND shadow map (usually the ground does not cast any shadow)
            /*{
                mLightShader.useProgram();

                viewport.viewproj.setAsUniform(mLightShader.uMVP);
                gl.uniform1f(mLightShader.uAlpha, 1f);
                GLState.bindVertexBuffer(mGroundShadowQuad);
                GLState.enableVertexArrays(mLightShader.aPos, GLState.DISABLED);
                gl.vertexAttribPointer(mLightShader.aPos, 2, GL.FLOAT, false, 0, 0);
                MapRenderer.bindQuadIndicesVBO();

                gl.drawElements(GL.TRIANGLES, 6, GL.UNSIGNED_SHORT, 0);
            }*/

            // Draw EXTRUSION shadow map (in ExtrusionRenderer)
            {
                //ExtrusionRenderer.Shader tmpShader = mRenderer.getShader();
                mRenderer.setShader(mLightShader);
                mRenderer.useLight(false);
                mRenderer.render(viewport);
                //mRenderer.setShader(tmpShader);
            }

            // END DEPTH FRAMEBUFFER
            mFrameBuffer.unbindFrameBuffer();
        }
        mLightMat.copy(viewport.viewproj); // save lightMat
        mLightMat.multiplyLhs(texUnitConverter); // apply shadow map converter to mLightMat

        viewport.viewproj.set(mViewProjTmp); // write back stored vp-matrix

        // DRAW SCENE
        {
            int lightColor = mRenderer.getSun().getColor();
            GLState.test(false, false);
            gl.clear(GL.DEPTH_BUFFER_BIT);

            // Bind shadow map texture
            gl.activeTexture(gl.TEXTURE2);
            GLState.bindTex2D(mFrameBuffer.getShadowMap());

            // Draw GROUND
            {
                mGroundShader.useProgram();
                viewport.viewproj.setAsUniform(mGroundShader.uMVP);

                gl.uniform1i(mGroundShader.uShadowMap, 2); // TEXTURE2 for shadows
                GLUtils.setColor(mGroundShader.uLightColor, lightColor);
                gl.uniform1f(mGroundShader.uShadowRes, SHADOWMAP_RESOLUTION);
                mLightMat.setAsUniform(mGroundShader.uLightMvp);

                // Bind VBO
                GLState.bindVertexBuffer(mGroundQuad);
                GLState.enableVertexArrays(mGroundShader.aPos, GLState.DISABLED);
                gl.vertexAttribPointer(mGroundShader.aPos, 2, GL.FLOAT, false, 0, 0);
                MapRenderer.bindQuadIndicesVBO();
                GLState.blend(true);  // allow transparency
                gl.blendFunc(GL.ZERO, GL.SRC_COLOR); // multiply frame colors
                gl.drawElements(GL.TRIANGLES, 6, GL.UNSIGNED_SHORT, 0);
                GLState.blend(false);
                gl.blendFunc(GL.ONE, GL.ONE_MINUS_SRC_ALPHA); // Reset to default func
            }

            // Draw EXTRUSIONS (in ExtrusionRenderer)
            {
                //ExtrusionRenderer.Shader tmpShader = mRenderer.getShader();
                mExtrusionShader.useProgram();
                gl.uniform1i(mExtrusionShader.uShadowMap, 2); // TEXTURE2 for shadows
                GLUtils.setColor(mExtrusionShader.uLightColor, lightColor);
                gl.uniform1f(mExtrusionShader.uShadowRes, SHADOWMAP_RESOLUTION);

                mExtrusionShader.lightMat = mLightMat;
                mRenderer.setShader(mExtrusionShader);
                mRenderer.useLight(true);
                mRenderer.render(viewport);
                //mRenderer.setShader(tmpShader);
            }

            gl.activeTexture(GL.TEXTURE0); // reset active Texture

        }
    }
}
