// Copyright 2015 Shoestring Research, LLC.  All rights reserved.
package com.projecttango.experiments.javapointcloud;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.net.Uri;
import android.opengl.GLES11Ext;
import android.opengl.GLSurfaceView;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;

import com.google.atap.tangoservice.TangoCameraIntrinsics;
import com.projecttango.experiments.javapointcloud.PointCloudActivity;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

// Need OpenGL ES 3.0 for RGBA8 renderbuffer.
import static android.opengl.GLES30.*;

class FrameRenderer implements GLSurfaceView.Renderer {
    // Vertex program flips Y when drawing on screen, doesn't flip
    // when drawing offscreen for saving.
    static final String videoVertexSource =
            "uniform mediump int cap;\n" +
                    "attribute vec4 a_v;\n" +
                    "varying vec2 t;\n" +
                    "void main() {\n" +
                    "	gl_Position = a_v;\n" +
                    "	t = 0.5*vec2(a_v.x, cap != 0 ? a_v.y : -a_v.y) + vec2(0.5,0.5);\n" +
                    "}\n";

    // Fragment buffer reorders color components when drawing offscreen
    // for saving.
    static final String videoFragmentSource =
            "#extension GL_OES_EGL_image_external : require\n" +
                    "precision mediump float;\n" +
                    "uniform mediump int cap;\n" +
                    "varying vec2 t;\n" +
                    "uniform samplerExternalOES colorTex;\n" +
                    "void main() {\n" +
                    "  vec4 c = texture2D(colorTex, t);\n" +
                    "	gl_FragColor = cap != 0 ? c.bgra : c;\n" +
                    "}\n";

    PointCloudActivity activity_;

    int videoProgram_;
    int videoVertexAttribute_;
    int videoVertexBuffer_;
    int videoTextureName_;

    int offscreenBuffer_;
    Point offscreenSize_;

    volatile boolean saveNextFrame_ = false;

    FrameRenderer(PointCloudActivity activity) {
        activity_ = activity;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        glClearColor(0.3f, 0.3f, 0.3f, 1.0f);

        IntBuffer bufferNames = IntBuffer.allocate(1);
        glGenBuffers(1, bufferNames);
        videoVertexBuffer_ = bufferNames.get(0);

        // Create a bi-unit square geometry.
        glBindBuffer(GL_ARRAY_BUFFER, videoVertexBuffer_);
        glBufferData(GL_ARRAY_BUFFER, 8, null, GL_STATIC_DRAW);
        ((ByteBuffer)glMapBufferRange(
                GL_ARRAY_BUFFER,
                0, 8,
                GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT))
                .order(ByteOrder.nativeOrder())
                .put(new byte[] { -1, 1,  -1, -1,  1, 1,  1, -1 });
        glUnmapBuffer(GL_ARRAY_BUFFER);

        // Create the video texture.
        IntBuffer textureNames = IntBuffer.allocate(1);
        glGenTextures(1, textureNames);
        videoTextureName_ = textureNames.get(0);

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTextureName_);

        // Connect the texture to Tango.
        activity_.attachTexture(TangoCameraIntrinsics.TANGO_CAMERA_COLOR, videoTextureName_);

        // Prepare the shader program.
        videoProgram_ = createShaderProgram(videoVertexSource, videoFragmentSource);
        glUseProgram(videoProgram_);
        videoVertexAttribute_ = glGetAttribLocation(videoProgram_, "a_v");
        glUniform1i(
                glGetUniformLocation(videoProgram_, "colorTex"),
                0);  // GL_TEXTURE0
        glUniform1i(
                glGetUniformLocation(videoProgram_, "cap"),
                0);

        // Get the camera frame dimensions.
        offscreenSize_ = activity_.getCameraFrameSize(TangoCameraIntrinsics.TANGO_CAMERA_COLOR);

        // Create an offscreen render target to capture a frame.
        IntBuffer renderbufferName = IntBuffer.allocate(1);
        glGenRenderbuffers(1, renderbufferName);
        glBindRenderbuffer(GL_RENDERBUFFER, renderbufferName.get(0));
        glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, offscreenSize_.x, offscreenSize_.y);

        IntBuffer framebufferName = IntBuffer.allocate(1);
        glGenFramebuffers(1, framebufferName);
        glBindFramebuffer(GL_FRAMEBUFFER, framebufferName.get(0));
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbufferName.get(0));

        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        offscreenBuffer_ = framebufferName.get(0);

        Log.i(activity_.TAG, "onSurfaceCreated");
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        glViewport(0, 0, width, height);
        Log.i(activity_.TAG, "onSurfaceChanged");
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        activity_.updateTexture(TangoCameraIntrinsics.TANGO_CAMERA_COLOR);

        if (!saveNextFrame_) {
            glBindBuffer(GL_ARRAY_BUFFER, videoVertexBuffer_);
            glVertexAttribPointer(videoVertexAttribute_, 2, GL_BYTE, false, 0, 0);
            glEnableVertexAttribArray(videoVertexAttribute_);
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTextureName_);
            glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        }
        else {
            Log.i(activity_.TAG, "Saving Image");
            // Switch to the offscreen buffer.
            glBindFramebuffer(GL_FRAMEBUFFER, offscreenBuffer_);

            // Save current viewport and change to offscreen size.
            IntBuffer viewport = IntBuffer.allocate(4);
            glGetIntegerv(GL_VIEWPORT, viewport);
            glViewport(0, 0, offscreenSize_.x, offscreenSize_.y);

            // Render in capture mode. Setting this flags tells the shader
            // program to draw the texture right-side up and change the color
            // order to ARGB for compatibility with Bitmap.
            glUniform1i(glGetUniformLocation(videoProgram_, "cap"), 1);

            // Render.
            glBindBuffer(GL_ARRAY_BUFFER, videoVertexBuffer_);
            glVertexAttribPointer(videoVertexAttribute_, 2, GL_BYTE, false, 0, 0);
            glEnableVertexAttribArray(videoVertexAttribute_);
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTextureName_);
            glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

            // Read offscreen buffer.
            IntBuffer intBuffer = ByteBuffer.allocateDirect(offscreenSize_.x * offscreenSize_.y * 4)
                    .order(ByteOrder.nativeOrder())
                    .asIntBuffer();
            glReadPixels(0, 0, offscreenSize_.x, offscreenSize_.y, GL_RGBA, GL_UNSIGNED_BYTE, intBuffer.rewind());

            // Restore onscreen state.
            glBindFramebuffer(GL_FRAMEBUFFER, 0);
            glViewport(viewport.get(0), viewport.get(1), viewport.get(2), viewport.get(3));
            glUniform1i(glGetUniformLocation(videoProgram_, "cap"), 0);

            // Convert to an array for Bitmap.createBitmap().
            int[] pixels = new int[intBuffer.capacity()];
            intBuffer.rewind();
            intBuffer.get(pixels);
            saveNextFrame_ = false;

            try {
                // Create/access a pictures subdirectory.
                File directory = new File(
                        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
                        "myScans");
                if (!directory.mkdirs() && !directory.isDirectory()) {
                    Toast.makeText(
                            activity_,
                            "Could not access save directory",
                            Toast.LENGTH_SHORT).show();
                    return;
                }

                // Get the current capture index to construct a unique filename.
                SharedPreferences prefs = activity_.getPreferences(Context.MODE_PRIVATE);

                // Create the capture file.
                String filename = String.format("tango%05d.png", activity_.scanNumber-1);
                Log.i(activity_.TAG, String.format("Saving to %s", filename));
                File file = new File(directory, filename);
                FileOutputStream fileOutputStream = new FileOutputStream(file);

                // Bitmap conveniently provides file output.
                Bitmap bitmap = Bitmap.createBitmap(pixels, offscreenSize_.x, offscreenSize_.y, Bitmap.Config.ARGB_8888);
                bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream);
                fileOutputStream.close();

                // Make the new file visible to other apps.
                activity_.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    void saveFrame() {
        saveNextFrame_ = true;
    }

    private int createShaderProgram(String vertexSource, String fragmentSource) {
        int vsName = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vsName, vertexSource);
        glCompileShader(vsName);
        System.out.println(glGetShaderInfoLog(vsName));

        int fsName = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fsName, fragmentSource);
        glCompileShader(fsName);
        System.out.println(glGetShaderInfoLog(fsName));

        int programName = glCreateProgram();
        glAttachShader(programName, vsName);
        glAttachShader(programName, fsName);
        glLinkProgram(programName);
        System.out.println(glGetProgramInfoLog(programName));

        return programName;
    }

}