package org.opencv.android;

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

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

import org.opencv.android.CameraGLSurfaceView.CameraTextureListener;

import android.annotation.TargetApi;
import android.graphics.SurfaceTexture;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.util.Log;
import android.view.View;

@TargetApi(15)
public abstract class CameraGLRendererBase implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {

    protected final String LOGTAG = "CameraGLRendererBase";

    // shaders
    private final String vss = ""
            + "attribute vec2 vPosition;\n"
            + "attribute vec2 vTexCoord;\n" + "varying vec2 texCoord;\n"
            + "void main() {\n" + "  texCoord = vTexCoord;\n"
            + "  gl_Position = vec4 ( vPosition.x, vPosition.y, 0.0, 1.0 );\n"
            + "}";

    private final String fssOES = ""
            + "#extension GL_OES_EGL_image_external : require\n"
            + "precision mediump float;\n"
            + "uniform samplerExternalOES sTexture;\n"
            + "varying vec2 texCoord;\n"
            + "void main() {\n"
            + "  gl_FragColor = texture2D(sTexture,texCoord);\n" + "}";

    private final String fss2D = ""
            + "precision mediump float;\n"
            + "uniform sampler2D sTexture;\n"
            + "varying vec2 texCoord;\n"
            + "void main() {\n"
            + "  gl_FragColor = texture2D(sTexture,texCoord);\n" + "}";

    // coord-s
    private final float[] vertices = {
           -1, -1,
           -1,  1,
            1, -1,
            1,  1 };
    private final float[] texCoordOES = {
            0,  1,
            0,  0,
            1,  1,
            1,  0 };
    private final float[] texCoord2D = {
            0,  0,
            0,  1,
            1,  0,
            1,  1 };

    private final int[] texCamera = {0};
    private final int[] texFBO = {0};
    private final int[] texDraw = {0};
    private final int[] FBO = {0};
    private int progOES = -1, prog2D = -1;
    private int vPosOES, vTCOES, vPos2D, vTC2D;

    private final FloatBuffer vert;
    private final FloatBuffer texOES;
    private final FloatBuffer tex2D;

    protected int mCameraWidth = -1, mCameraHeight = -1;
    protected int mFBOWidth = -1, mFBOHeight = -1;
    protected int mMaxCameraWidth = -1, mMaxCameraHeight = -1;
    protected int mCameraIndex = CameraBridgeViewBase.CAMERA_ID_ANY;

    protected SurfaceTexture mSTexture;

    protected boolean mHaveSurface = false;
    protected boolean mHaveFBO = false;
    protected boolean mUpdateST = false;
    protected boolean mEnabled = true;
    protected boolean mIsStarted = false;

    protected CameraGLSurfaceView mView;

    protected abstract void openCamera(int id);
    protected abstract void closeCamera();
    protected abstract void setCameraPreviewSize(int width, int height); // updates mCameraWidth & mCameraHeight

    public CameraGLRendererBase(CameraGLSurfaceView view) {
        mView = view;
        int bytes = vertices.length * Float.SIZE / Byte.SIZE;
        vert   = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()).asFloatBuffer();
        texOES = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()).asFloatBuffer();
        tex2D  = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()).asFloatBuffer();
        vert.put(vertices).position(0);
        texOES.put(texCoordOES).position(0);
        tex2D.put(texCoord2D).position(0);
    }

    @Override
    public synchronized void onFrameAvailable(SurfaceTexture surfaceTexture) {
        //Log.i(LOGTAG, "onFrameAvailable");
        mUpdateST = true;
        mView.requestRender();
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        //Log.i(LOGTAG, "onDrawFrame start");

        if (!mHaveFBO)
            return;

        synchronized(this) {
            if (mUpdateST) {
                mSTexture.updateTexImage();
                mUpdateST = false;
            }

            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

            CameraTextureListener texListener = mView.getCameraTextureListener();
            if(texListener != null) {
                //Log.d(LOGTAG, "haveUserCallback");
                // texCamera(OES) -> texFBO
                drawTex(texCamera[0], true, FBO[0]);

                // call user code (texFBO -> texDraw)
                boolean modified = texListener.onCameraTexture(texFBO[0], texDraw[0], mCameraWidth, mCameraHeight);

                if(modified) {
                    // texDraw -> screen
                    drawTex(texDraw[0], false, 0);
                } else {
                    // texFBO -> screen
                    drawTex(texFBO[0], false, 0);
                }
            } else {
                Log.d(LOGTAG, "texCamera(OES) -> screen");
                // texCamera(OES) -> screen
                drawTex(texCamera[0], true, 0);
            }
            //Log.i(LOGTAG, "onDrawFrame end");
        }
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int surfaceWidth, int surfaceHeight) {
        Log.i(LOGTAG, "onSurfaceChanged("+surfaceWidth+"x"+surfaceHeight+")");
        mHaveSurface = true;
        updateState();
        setPreviewSize(surfaceWidth, surfaceHeight);
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.i(LOGTAG, "onSurfaceCreated");
        initShaders();
    }

    private void initShaders() {
        String strGLVersion = GLES20.glGetString(GLES20.GL_VERSION);
        if (strGLVersion != null)
            Log.i(LOGTAG, "OpenGL ES version: " + strGLVersion);

        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);

        progOES = loadShader(vss, fssOES);
        vPosOES = GLES20.glGetAttribLocation(progOES, "vPosition");
        vTCOES  = GLES20.glGetAttribLocation(progOES, "vTexCoord");
        GLES20.glEnableVertexAttribArray(vPosOES);
        GLES20.glEnableVertexAttribArray(vTCOES);

        prog2D  = loadShader(vss, fss2D);
        vPos2D = GLES20.glGetAttribLocation(prog2D, "vPosition");
        vTC2D  = GLES20.glGetAttribLocation(prog2D, "vTexCoord");
        GLES20.glEnableVertexAttribArray(vPos2D);
        GLES20.glEnableVertexAttribArray(vTC2D);
    }

    private void initSurfaceTexture() {
        Log.d(LOGTAG, "initSurfaceTexture");
        deleteSurfaceTexture();
        initTexOES(texCamera);
        mSTexture = new SurfaceTexture(texCamera[0]);
        mSTexture.setOnFrameAvailableListener(this);
    }

    private void deleteSurfaceTexture() {
        Log.d(LOGTAG, "deleteSurfaceTexture");
        if(mSTexture != null) {
            mSTexture.release();
            mSTexture = null;
            deleteTex(texCamera);
        }
    }

    private void initTexOES(int[] tex) {
        if(tex.length == 1) {
            GLES20.glGenTextures(1, tex, 0);
            GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex[0]);
            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
        }
    }

    private static void deleteTex(int[] tex) {
        if(tex.length == 1) {
            GLES20.glDeleteTextures(1, tex, 0);
        }
    }

    private static int loadShader(String vss, String fss) {
        Log.d("CameraGLRendererBase", "loadShader");
        int vshader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
        GLES20.glShaderSource(vshader, vss);
        GLES20.glCompileShader(vshader);
        int[] status = new int[1];
        GLES20.glGetShaderiv(vshader, GLES20.GL_COMPILE_STATUS, status, 0);
        if (status[0] == 0) {
            Log.e("CameraGLRendererBase", "Could not compile vertex shader: "+GLES20.glGetShaderInfoLog(vshader));
            GLES20.glDeleteShader(vshader);
            vshader = 0;
            return 0;
        }

        int fshader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
        GLES20.glShaderSource(fshader, fss);
        GLES20.glCompileShader(fshader);
        GLES20.glGetShaderiv(fshader, GLES20.GL_COMPILE_STATUS, status, 0);
        if (status[0] == 0) {
            Log.e("CameraGLRendererBase", "Could not compile fragment shader:"+GLES20.glGetShaderInfoLog(fshader));
            GLES20.glDeleteShader(vshader);
            GLES20.glDeleteShader(fshader);
            fshader = 0;
            return 0;
        }

        int program = GLES20.glCreateProgram();
        GLES20.glAttachShader(program, vshader);
        GLES20.glAttachShader(program, fshader);
        GLES20.glLinkProgram(program);
        GLES20.glDeleteShader(vshader);
        GLES20.glDeleteShader(fshader);
        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, status, 0);
        if (status[0] == 0) {
            Log.e("CameraGLRendererBase", "Could not link shader program: "+GLES20.glGetProgramInfoLog(program));
            program = 0;
            return 0;
        }
        GLES20.glValidateProgram(program);
        GLES20.glGetProgramiv(program, GLES20.GL_VALIDATE_STATUS, status, 0);
        if (status[0] == 0)
        {
            Log.e("CameraGLRendererBase", "Shader program validation error: "+GLES20.glGetProgramInfoLog(program));
            GLES20.glDeleteProgram(program);
            program = 0;
            return 0;
        }

        Log.d("CameraGLRendererBase", "Shader program is built OK");

        return program;
    }

    private void deleteFBO()
    {
        Log.d(LOGTAG, "deleteFBO("+mFBOWidth+"x"+mFBOHeight+")");
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
        GLES20.glDeleteFramebuffers(1, FBO, 0);

        deleteTex(texFBO);
        deleteTex(texDraw);
        mFBOWidth = mFBOHeight = 0;
    }

    private void initFBO(int width, int height)
    {
        Log.d(LOGTAG, "initFBO("+width+"x"+height+")");

        deleteFBO();

        GLES20.glGenTextures(1, texDraw, 0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texDraw[0]);
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);

        GLES20.glGenTextures(1, texFBO, 0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texFBO[0]);
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);

        //int hFBO;
        GLES20.glGenFramebuffers(1, FBO, 0);
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, FBO[0]);
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, texFBO[0], 0);
        Log.d(LOGTAG, "initFBO error status: " + GLES20.glGetError());

        int FBOstatus = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
        if (FBOstatus != GLES20.GL_FRAMEBUFFER_COMPLETE)
            Log.e(LOGTAG, "initFBO failed, status: " + FBOstatus);

        mFBOWidth  = width;
        mFBOHeight = height;
    }

    // draw texture to FBO or to screen if fbo == 0
    private void drawTex(int tex, boolean isOES, int fbo)
    {
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fbo);

        if(fbo == 0)
            GLES20.glViewport(0, 0, mView.getWidth(), mView.getHeight());
        else
            GLES20.glViewport(0, 0, mFBOWidth, mFBOHeight);

        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        if(isOES) {
            GLES20.glUseProgram(progOES);
            GLES20.glVertexAttribPointer(vPosOES, 2, GLES20.GL_FLOAT, false, 4*2, vert);
            GLES20.glVertexAttribPointer(vTCOES,  2, GLES20.GL_FLOAT, false, 4*2, texOES);
        } else {
            GLES20.glUseProgram(prog2D);
            GLES20.glVertexAttribPointer(vPos2D, 2, GLES20.GL_FLOAT, false, 4*2, vert);
            GLES20.glVertexAttribPointer(vTC2D,  2, GLES20.GL_FLOAT, false, 4*2, tex2D);
        }

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

        if(isOES) {
            GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex);
            GLES20.glUniform1i(GLES20.glGetUniformLocation(progOES, "sTexture"), 0);
        } else {
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, tex);
            GLES20.glUniform1i(GLES20.glGetUniformLocation(prog2D, "sTexture"), 0);
        }

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
        GLES20.glFlush();
    }

    public synchronized void enableView() {
        Log.d(LOGTAG, "enableView");
        mEnabled = true;
        updateState();
    }

    public synchronized void disableView() {
        Log.d(LOGTAG, "disableView");
        mEnabled = false;
        updateState();
    }

    protected void updateState() {
        Log.d(LOGTAG, "updateState");
        Log.d(LOGTAG, "mEnabled="+mEnabled+", mHaveSurface="+mHaveSurface);
        boolean willStart = mEnabled && mHaveSurface && mView.getVisibility() == View.VISIBLE;
        if (willStart != mIsStarted) {
            if(willStart) doStart();
            else doStop();
        } else {
            Log.d(LOGTAG, "keeping State unchanged");
        }
        Log.d(LOGTAG, "updateState end");
    }

    protected synchronized void doStart() {
        Log.d(LOGTAG, "doStart");
        initSurfaceTexture();
        openCamera(mCameraIndex);
        mIsStarted = true;
        if(mCameraWidth>0 && mCameraHeight>0)
            setPreviewSize(mCameraWidth, mCameraHeight); // start preview and call listener.onCameraViewStarted()
    }


    protected void doStop() {
        Log.d(LOGTAG, "doStop");
        synchronized(this) {
            mUpdateST = false;
            mIsStarted = false;
            mHaveFBO = false;
            closeCamera();
            deleteSurfaceTexture();
        }
        CameraTextureListener listener = mView.getCameraTextureListener();
        if(listener != null) listener.onCameraViewStopped();

    }

    protected void setPreviewSize(int width, int height) {
        synchronized(this) {
            mHaveFBO = false;
            mCameraWidth  = width;
            mCameraHeight = height;
            setCameraPreviewSize(width, height); // can change mCameraWidth & mCameraHeight
            initFBO(mCameraWidth, mCameraHeight);
            mHaveFBO = true;
        }

        CameraTextureListener listener = mView.getCameraTextureListener();
        if(listener != null) listener.onCameraViewStarted(mCameraWidth, mCameraHeight);
    }

    public void setCameraIndex(int cameraIndex) {
        disableView();
        mCameraIndex = cameraIndex;
        enableView();
    }

    public void setMaxCameraPreviewSize(int maxWidth, int maxHeight) {
        disableView();
        mMaxCameraWidth  = maxWidth;
        mMaxCameraHeight = maxHeight;
        enableView();
    }

    public void onResume() {
        Log.i(LOGTAG, "onResume");
    }

    public void onPause() {
        Log.i(LOGTAG, "onPause");
        mHaveSurface = false;
        updateState();
        mCameraWidth = mCameraHeight = -1;
    }

}
