package com.foxx.androidcast.receiver.av;

import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Surface;

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

/**
 * GLES2 bridge: decoder renders to an external-OES {@link SurfaceTexture}, then a preset shader
 * draws into the {@link android.view.TextureView} display surface.
 */
final class ReceiverVideoGlRenderer {
    private static final String TAG = "ReceiverVideoGl";
    private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;

    private static final String VERT_OES =
            "#extension GL_OES_EGL_image_external : require\n"
                    + "attribute vec4 aPosition;\n"
                    + "attribute vec2 aTexCoord;\n"
                    + "varying vec2 vTexCoord;\n"
                    + "void main() {\n"
                    + "  gl_Position = aPosition;\n"
                    + "  vTexCoord = aTexCoord;\n"
                    + "}\n";

    private static final String FRAG_OES =
            "#extension GL_OES_EGL_image_external : require\n"
                    + "precision mediump float;\n"
                    + "varying vec2 vTexCoord;\n"
                    + "uniform samplerExternalOES uTexture;\n"
                    + "uniform float uIntensity;\n"
                    + "uniform int uPreset;\n"
                    + "void main() {\n"
                    + "  vec2 uv = vTexCoord;\n"
                    + "  vec4 src = texture2D(uTexture, uv);\n"
                    + "  vec3 c = src.rgb;\n"
                    + "  float t = clamp(uIntensity, 0.0, 1.0);\n"
                    + "  if (t <= 0.001) {\n"
                    + "    gl_FragColor = src;\n"
                    + "    return;\n"
                    + "  }\n"
                    + "  vec2 px = vec2(1.0 / 720.0, 1.0 / 1280.0);\n"
                    + "  vec3 n = texture2D(uTexture, uv + vec2(px.x, 0.0)).rgb;\n"
                    + "  vec3 s = texture2D(uTexture, uv - vec2(px.x, 0.0)).rgb;\n"
                    + "  vec3 e = texture2D(uTexture, uv + vec2(0.0, px.y)).rgb;\n"
                    + "  vec3 w = texture2D(uTexture, uv - vec2(0.0, px.y)).rgb;\n"
                    + "  vec3 blur = (n + s + e + w + c) * 0.2;\n"
                    + "  vec3 outCol;\n"
                    + "  if (uPreset == 1) {\n"
                    + "    vec3 hdr = c * (1.0 + 0.45 * t);\n"
                    + "    hdr = hdr / (hdr + vec3(0.55));\n"
                    + "    hdr = pow(hdr, vec3(0.9));\n"
                    + "    float luma = dot(hdr, vec3(0.299, 0.587, 0.114));\n"
                    + "    hdr = mix(vec3(luma), hdr, 1.0 + 0.3 * t);\n"
                    + "    outCol = mix(c, hdr, t);\n"
                    + "  } else {\n"
                    + "    vec3 denoise = mix(c, blur, 0.4 * t);\n"
                    + "    vec3 sharp = denoise + (denoise - blur) * (0.35 * t);\n"
                    + "    float luma = dot(sharp, vec3(0.299, 0.587, 0.114));\n"
                    + "    outCol = mix(vec3(luma), sharp, 1.0 + 0.08 * t);\n"
                    + "    outCol = mix(c, outCol, t);\n"
                    + "  }\n"
                    + "  gl_FragColor = vec4(clamp(outCol, 0.0, 1.0), src.a);\n"
                    + "}\n";

    private static final float[] QUAD = {
            -1f, -1f, 0f, 1f,
            1f, -1f, 1f, 1f,
            -1f, 1f, 0f, 0f,
            1f, 1f, 1f, 0f,
    };

    private final Handler mainHandler = new Handler(Looper.getMainLooper());

    private EGLDisplay eglDisplay = EGL14.EGL_NO_DISPLAY;
    private EGLContext eglContext = EGL14.EGL_NO_CONTEXT;
    private EGLSurface outputEglSurface = EGL14.EGL_NO_SURFACE;

    private int program;
    private int oesTextureId;
    private SurfaceTexture decoderTexture;
    private Surface decoderSurface;
    private SurfaceTexture displayTexture;
    private int viewportW = 1;
    private int viewportH = 1;
    private volatile ReceiverVideoPreset preset = ReceiverVideoPreset.IMMERSIVE;
    private volatile int intensity = 5;
    private volatile boolean frameAvailable;
    private FloatBuffer quadBuffer;

    Surface bindDisplay(SurfaceTexture display, int width, int height) {
        release();
        displayTexture = display;
        viewportW = Math.max(1, width);
        viewportH = Math.max(1, height);
        display.setDefaultBufferSize(viewportW, viewportH);
        initEgl(new Surface(display));
        createDecoderInput();
        return decoderSurface;
    }

    void setPreset(ReceiverVideoPreset preset, int intensity) {
        this.preset = preset != null ? preset : ReceiverVideoPreset.IMMERSIVE;
        this.intensity = Math.max(0, Math.min(10, intensity));
    }

    void onDecoderFrameAvailable() {
        frameAvailable = true;
        drawFrameInternal();
    }

    void release() {
        if (Looper.myLooper() == mainHandler.getLooper()) {
            releaseOnGlThread();
        } else {
            mainHandler.post(this::releaseOnGlThread);
        }
    }

    private void createDecoderInput() {
        int[] textures = new int[1];
        GLES20.glGenTextures(1, textures, 0);
        oesTextureId = textures[0];
        GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, oesTextureId);
        GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

        decoderTexture = new SurfaceTexture(oesTextureId);
        decoderTexture.setDefaultBufferSize(viewportW, viewportH);
        decoderTexture.setOnFrameAvailableListener(st -> onDecoderFrameAvailable(), mainHandler);
        decoderSurface = new Surface(decoderTexture);
    }

    private void initEgl(Surface outputSurface) {
        eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (eglDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new IllegalStateException("eglGetDisplay failed");
        }
        int[] version = new int[2];
        if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {
            throw new IllegalStateException("eglInitialize failed");
        }
        EGLConfig config = chooseConfig(eglDisplay);
        int[] ctxAttr = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
        eglContext = EGL14.eglCreateContext(eglDisplay, config, EGL14.EGL_NO_CONTEXT, ctxAttr, 0);
        outputEglSurface = EGL14.eglCreateWindowSurface(eglDisplay, config, outputSurface,
                new int[] {EGL14.EGL_NONE}, 0);
        if (!EGL14.eglMakeCurrent(eglDisplay, outputEglSurface, outputEglSurface, eglContext)) {
            throw new IllegalStateException("eglMakeCurrent failed");
        }
        program = buildProgram(VERT_OES, FRAG_OES);
        quadBuffer = ByteBuffer.allocateDirect(QUAD.length * 4).order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        quadBuffer.put(QUAD).position(0);
        GLES20.glViewport(0, 0, viewportW, viewportH);
        Log.i(TAG, "GLES video bridge " + viewportW + "x" + viewportH);
    }

    private void drawFrameInternal() {
        if (decoderTexture == null || outputEglSurface == EGL14.EGL_NO_SURFACE || !frameAvailable) {
            return;
        }
        frameAvailable = false;
        try {
            if (!EGL14.eglMakeCurrent(eglDisplay, outputEglSurface, outputEglSurface, eglContext)) {
                return;
            }
            decoderTexture.updateTexImage();
            GLES20.glViewport(0, 0, viewportW, viewportH);
            GLES20.glClearColor(0f, 0f, 0f, 1f);
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            GLES20.glUseProgram(program);
            int aPos = GLES20.glGetAttribLocation(program, "aPosition");
            int aTex = GLES20.glGetAttribLocation(program, "aTexCoord");
            int uTex = GLES20.glGetUniformLocation(program, "uTexture");
            int uInt = GLES20.glGetUniformLocation(program, "uIntensity");
            int uPreset = GLES20.glGetUniformLocation(program, "uPreset");
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, oesTextureId);
            GLES20.glUniform1i(uTex, 0);
            GLES20.glUniform1f(uInt, intensity / 10f);
            GLES20.glUniform1i(uPreset, preset == ReceiverVideoPreset.HDR ? 1 : 0);
            quadBuffer.position(0);
            GLES20.glEnableVertexAttribArray(aPos);
            GLES20.glVertexAttribPointer(aPos, 2, GLES20.GL_FLOAT, false, 16, quadBuffer);
            quadBuffer.position(2);
            GLES20.glEnableVertexAttribArray(aTex);
            GLES20.glVertexAttribPointer(aTex, 2, GLES20.GL_FLOAT, false, 16, quadBuffer);
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
            GLES20.glDisableVertexAttribArray(aPos);
            GLES20.glDisableVertexAttribArray(aTex);
            EGL14.eglSwapBuffers(eglDisplay, outputEglSurface);
        } catch (Exception e) {
            Log.w(TAG, "drawFrame: " + e.getMessage());
        }
    }

    private void releaseOnGlThread() {
        if (decoderSurface != null) {
            decoderSurface.release();
            decoderSurface = null;
        }
        if (decoderTexture != null) {
            decoderTexture.release();
            decoderTexture = null;
        }
        if (oesTextureId != 0) {
            int[] t = new int[] {oesTextureId};
            GLES20.glDeleteTextures(1, t, 0);
            oesTextureId = 0;
        }
        if (eglDisplay != EGL14.EGL_NO_DISPLAY) {
            EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                    EGL14.EGL_NO_CONTEXT);
            if (outputEglSurface != EGL14.EGL_NO_SURFACE) {
                EGL14.eglDestroySurface(eglDisplay, outputEglSurface);
                outputEglSurface = EGL14.EGL_NO_SURFACE;
            }
            if (eglContext != EGL14.EGL_NO_CONTEXT) {
                EGL14.eglDestroyContext(eglDisplay, eglContext);
                eglContext = EGL14.EGL_NO_CONTEXT;
            }
            EGL14.eglTerminate(eglDisplay);
            eglDisplay = EGL14.EGL_NO_DISPLAY;
        }
        displayTexture = null;
    }

    private static EGLConfig chooseConfig(EGLDisplay display) {
        int[] attribs = {
                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
                EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_ALPHA_SIZE, 8,
                EGL14.EGL_NONE
        };
        EGLConfig[] configs = new EGLConfig[1];
        int[] num = new int[1];
        if (!EGL14.eglChooseConfig(display, attribs, 0, configs, 0, 1, num, 0)) {
            throw new IllegalStateException("eglChooseConfig failed");
        }
        return configs[0];
    }

    private static int buildProgram(String vert, String frag) {
        int vs = compile(GLES20.GL_VERTEX_SHADER, vert);
        int fs = compile(GLES20.GL_FRAGMENT_SHADER, frag);
        int prog = GLES20.glCreateProgram();
        GLES20.glAttachShader(prog, vs);
        GLES20.glAttachShader(prog, fs);
        GLES20.glLinkProgram(prog);
        int[] link = new int[1];
        GLES20.glGetProgramiv(prog, GLES20.GL_LINK_STATUS, link, 0);
        if (link[0] == 0) {
            String log = GLES20.glGetProgramInfoLog(prog);
            GLES20.glDeleteProgram(prog);
            throw new IllegalStateException("Program link failed: " + log);
        }
        GLES20.glDeleteShader(vs);
        GLES20.glDeleteShader(fs);
        return prog;
    }

    private static int compile(int type, String src) {
        int shader = GLES20.glCreateShader(type);
        GLES20.glShaderSource(shader, src);
        GLES20.glCompileShader(shader);
        int[] ok = new int[1];
        GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, ok, 0);
        if (ok[0] == 0) {
            String log = GLES20.glGetShaderInfoLog(shader);
            GLES20.glDeleteShader(shader);
            throw new IllegalStateException("Shader compile failed: " + log);
        }
        return shader;
    }
}
