/*
 * Copyright 2017 nekocode
 *
 * 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.
 */

package cn.nekocode.musicviz.render;

import android.content.Context;
import android.graphics.SurfaceTexture;
import android.opengl.GLES20;
import android.view.TextureView;

import java.nio.ByteBuffer;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
import javax.microedition.khronos.opengles.GL10;

import cn.nekocode.musicviz.FFTFrame;
import cn.nekocode.musicviz.WaveFormFrame;

/**
 * @author nekocode ([email protected])
 */
public class VisualizerRenderer implements Runnable, TextureView.SurfaceTextureListener {
    private static final int EGL_OPENGL_ES2_BIT = 4;
    private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    private static final int DRAW_INTERVAL = 1000 / 30;
    private static final float DBM_INTERP = 0.75f;

    private Thread mRenderThread;
    private Context mContext;
    private SurfaceTexture mSurfaceTexture;
    private int mSurfaceWidth, mSurfaceHeight;

    private EGLDisplay mEglDisplay;
    private EGLSurface mEglSurface;
    private EGLContext mEglContext;
    private EGL10 mEgl10;

    private SceneController mSceneController;
    private int mTextureWidth;
    private WaveFormFrame mWaveFormFrame;
    private FFTFrame mFFTFrame;
    private float[] mLastDbmArray;


    public VisualizerRenderer(Context context, int textureWidth) {
        this.mContext = context;
        this.mTextureWidth = textureWidth;
    }

    public void setSceneController(SceneController controller) {
        mSceneController = controller;
    }

    public void updateWaveFormFrame(WaveFormFrame frame) {
        this.mWaveFormFrame = frame;
    }

    public void updateFFTFrame(FFTFrame frame) {
        this.mFFTFrame = frame;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
        mSurfaceWidth = -width;
        mSurfaceHeight = -height;
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        if (mRenderThread != null && mRenderThread.isAlive()) {
            mRenderThread.interrupt();
        }

        return true;
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        if (mRenderThread != null && mRenderThread.isAlive()) {
            mRenderThread.interrupt();
        }
        mRenderThread = new Thread(this);

        mSurfaceTexture = surface;
        mSurfaceWidth = -width;
        mSurfaceHeight = -height;

        // Start rendering
        mRenderThread.start();
    }

    @Override
    public void run() {
        if (!initGL(mSurfaceTexture)) {
            throw new RuntimeException("Initializing OpenGL failed");
        }

        final byte[] buf = new byte[mTextureWidth * 2];
        final int audioTexId = genAudioTexture(buf, mTextureWidth);
        if (mSceneController != null) {
            mSceneController.onSetup(mContext, audioTexId, mTextureWidth);
        }


        // Render loop
        while (!Thread.currentThread().isInterrupted()) {
            try {
                if (mSurfaceWidth < 0 && mSurfaceHeight < 0)
                    GLES20.glViewport(0, 0, mSurfaceWidth = -mSurfaceWidth, mSurfaceHeight = -mSurfaceHeight);

                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
                GLES20.glClearColor(1f, 0f, 0f, 0f);


                // Prepare the audio texture
                fillFFT(buf, 0, mTextureWidth);
                if (mWaveFormFrame != null) {
                    System.arraycopy(mWaveFormFrame.getRawWaveForm(), 0, buf, mTextureWidth, mTextureWidth);
                }
                GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, audioTexId);
                GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, mTextureWidth, 2,
                        GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, ByteBuffer.wrap(buf));


                // Draw
                GLScene scene;
                if (mSceneController != null && (scene = mSceneController.getActivedScene()) != null) {
                    scene.draw(mSurfaceWidth, mSurfaceHeight);
                }

                // Flush
                GLES20.glFlush();
                mEgl10.eglSwapBuffers(mEglDisplay, mEglSurface);

                Thread.sleep(DRAW_INTERVAL);

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        // Release something
        GLES20.glDeleteTextures(1, new int[]{audioTexId}, 0);
    }

    private void fillFFT(byte[] buf, int offset, int len) {
        if (mFFTFrame == null) return;
        if (offset + len > buf.length) return;

        float[] dBmArray = new float[len];
        mFFTFrame.calculate(len, dBmArray);

        float maxDbm = Float.MIN_VALUE;
        float minDbm = Float.MAX_VALUE;
        for (int i = 0; i < len; i++) {
            float dbm = dBmArray[i];
            if (dbm < 0f) dbm = 0f;
            if (dbm > 1f) dbm = 1f;

            if (mLastDbmArray != null) {
                float oldDbm = mLastDbmArray[i];
                if (oldDbm - dbm > 0.025f) {
                    dbm = oldDbm - 0.025f;
                } else {
                    dbm = mLastDbmArray[i] * DBM_INTERP + dbm * (1f - DBM_INTERP);
                }
            }

            if (dbm > maxDbm) maxDbm = dbm;
            if (dbm < minDbm) minDbm = dbm;

            dBmArray[i] = dbm;
            buf[offset + i] = (byte) (dbm * 255);
        }

//        float midDbm = (maxDbm + minDbm) / 2f;
//
//        for (int i = 0; i < len; i++) {
//            float dbm = dBmArray[i];
//            dbm = midDbm * 0.2f + dbm * 0.8f;
//            buf[offset + i] = (byte) (dbm * 255);
//        }
        mLastDbmArray = dBmArray;
    }

    private int genAudioTexture(byte[] buf, int width) {
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

        final int[] genBuf = new int[1];
        GLES20.glGenTextures(1, genBuf, 0);
        final int texId = genBuf[0];
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);

        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
        GLES20.glTexImage2D(GL10.GL_TEXTURE_2D, 0, GL10.GL_LUMINANCE,
                width, 2, 0, GL10.GL_LUMINANCE, GL10.GL_UNSIGNED_BYTE,
                ByteBuffer.wrap(buf));

        return texId;
    }

    private boolean initGL(SurfaceTexture texture) {
        mEgl10 = (EGL10) EGLContext.getEGL();

        mEglDisplay = mEgl10.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
        if (mEglDisplay == EGL10.EGL_NO_DISPLAY) return false;

        int[] version = new int[2];
        if (!mEgl10.eglInitialize(mEglDisplay, version)) return false;

        int[] configsCount = new int[1];
        EGLConfig[] configs = new EGLConfig[1];
        int[] configSpec = {
                EGL10.EGL_RENDERABLE_TYPE,
                EGL_OPENGL_ES2_BIT,
                EGL10.EGL_RED_SIZE, 8,
                EGL10.EGL_GREEN_SIZE, 8,
                EGL10.EGL_BLUE_SIZE, 8,
                EGL10.EGL_ALPHA_SIZE, 8,
                EGL10.EGL_DEPTH_SIZE, 0,
                EGL10.EGL_STENCIL_SIZE, 0,
                EGL10.EGL_NONE
        };

        EGLConfig eglConfig = null;
        if (!mEgl10.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) {
            return false;
        } else if (configsCount[0] > 0) {
            eglConfig = configs[0];
        }
        if (eglConfig == null) return false;

        mEglContext = mEgl10.eglCreateContext(
                mEglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT,
                new int[]{EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE});
        mEglSurface = mEgl10.eglCreateWindowSurface(mEglDisplay, eglConfig, texture, null);

        if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) return false;
        if (!mEgl10.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) return false;

        return true;
    }
}