/*
 * libjingle
 * Copyright 2013, Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.appspot.apprtc;

import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.util.Log;

import org.webrtc.VideoRenderer.I420Frame;

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

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

/**
 * A GLSurfaceView{,.Renderer} that efficiently renders YUV frames from local &
 * remote VideoTracks using the GPU for CSC.  Clients will want to call the
 * constructor, setSize() and updateFrame() as appropriate, but none of the
 * other public methods of this class are of interest to clients (only to system
 * classes).
 */
public class VideoStreamsView
    extends GLSurfaceView
    implements GLSurfaceView.Renderer {

  /** Identify which of the two video streams is being addressed. */
  public static enum Endpoint { LOCAL, REMOTE };

  private final static String TAG = "VideoStreamsView";
  private EnumMap<Endpoint, Rect> rects =
      new EnumMap<Endpoint, Rect>(Endpoint.class);
  private Point screenDimensions;
  // [0] are local Y,U,V, [1] are remote Y,U,V.
  private int[][] yuvTextures = { { -1, -1, -1}, {-1, -1, -1 }};
  private int posLocation = -1;
  private long lastFPSLogTime = System.nanoTime();
  private long numFramesSinceLastLog = 0;
  private FramePool framePool = new FramePool();
  // Accessed on multiple threads!  Must be synchronized.
  private EnumMap<Endpoint, I420Frame> framesToRender =
      new EnumMap<Endpoint, I420Frame>(Endpoint.class);

  public VideoStreamsView(Context c, Point screenDimensions) {
    super(c);
    this.screenDimensions = screenDimensions;
    setPreserveEGLContextOnPause(true);
    setEGLContextClientVersion(2);
    setRenderer(this);
    setRenderMode(RENDERMODE_WHEN_DIRTY);
  }

  /** Queue |frame| to be uploaded. */
  public void queueFrame(final Endpoint stream, I420Frame frame) {
    // Paying for the copy of the YUV data here allows CSC and painting time
    // to get spent on the render thread instead of the UI thread.
    abortUnless(framePool.validateDimensions(frame), "Frame too large!");
    final I420Frame frameCopy = framePool.takeFrame(frame).copyFrom(frame);
    boolean needToScheduleRender;
    synchronized (framesToRender) {
      // A new render needs to be scheduled (via updateFrames()) iff there isn't
      // already a render scheduled, which is true iff framesToRender is empty.
      needToScheduleRender = framesToRender.isEmpty();
      I420Frame frameToDrop = framesToRender.put(stream, frameCopy);
      if (frameToDrop != null) {
        framePool.returnFrame(frameToDrop);
      }
    }
    if (needToScheduleRender) {
      queueEvent(new Runnable() {
          public void run() {
            updateFrames();
          }
        });
    }
  }

  // Upload the planes from |framesToRender| to the textures owned by this View.
  private void updateFrames() {
    I420Frame localFrame = null;
    I420Frame remoteFrame = null;
    synchronized (framesToRender) {
      localFrame = framesToRender.remove(Endpoint.LOCAL);
      remoteFrame = framesToRender.remove(Endpoint.REMOTE);
    }
    if (localFrame != null) {
      texImage2D(localFrame, yuvTextures[0]);
      framePool.returnFrame(localFrame);
    }
    if (remoteFrame != null) {
      texImage2D(remoteFrame, yuvTextures[1]);
      framePool.returnFrame(remoteFrame);
    }
    abortUnless(localFrame != null || remoteFrame != null,
                "Nothing to render!");
    requestRender();
  }

  /** Inform this View of the dimensions of frames coming from |stream|. */
  public void setSize(Endpoint stream, int width, int height) {
    // Generate 3 texture ids for Y/U/V and place them into |textures|,
    // allocating enough storage for |width|x|height| pixels.
    int[] textures = yuvTextures[stream == Endpoint.LOCAL ? 0 : 1];
    GLES20.glGenTextures(3, textures, 0);
    for (int i = 0; i < 3; ++i) {
      int w = i == 0 ? width : width / 2;
      int h = i == 0 ? height : height / 2;
      GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[i]);
      GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, w, h, 0,
          GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, null);
      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
          GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
          GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
          GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
          GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
    }
    checkNoGLES2Error();
  }

  @Override
  protected void onMeasure(int unusedX, int unusedY) {
    // Go big or go home!
    setMeasuredDimension(screenDimensions.x, screenDimensions.y);
  }

  @Override
  public void onSurfaceChanged(GL10 unused, int width, int height) {
    GLES20.glViewport(0, 0, width, height);
    checkNoGLES2Error();
  }

  @Override
  public void onDrawFrame(GL10 unused) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    drawRectangle(yuvTextures[1], remoteVertices);
    drawRectangle(yuvTextures[0], localVertices);
    ++numFramesSinceLastLog;
    long now = System.nanoTime();
    if (lastFPSLogTime == -1 || now - lastFPSLogTime > 1e9) {
      double fps = numFramesSinceLastLog / ((now - lastFPSLogTime) / 1e9);
      Log.d(TAG, "Rendered FPS: " + fps);
      lastFPSLogTime = now;
      numFramesSinceLastLog = 1;
    }
    checkNoGLES2Error();
  }

  @Override
  public void onSurfaceCreated(GL10 unused, EGLConfig config) {
    int program = GLES20.glCreateProgram();
    addShaderTo(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_STRING, program);
    addShaderTo(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_STRING, program);

    GLES20.glLinkProgram(program);
    int[] result = new int[] { GLES20.GL_FALSE };
    result[0] = GLES20.GL_FALSE;
    GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, result, 0);
    abortUnless(result[0] == GLES20.GL_TRUE,
        GLES20.glGetProgramInfoLog(program));
    GLES20.glUseProgram(program);

    GLES20.glUniform1i(GLES20.glGetUniformLocation(program, "y_tex"), 0);
    GLES20.glUniform1i(GLES20.glGetUniformLocation(program, "u_tex"), 1);
    GLES20.glUniform1i(GLES20.glGetUniformLocation(program, "v_tex"), 2);

    // Actually set in drawRectangle(), but queried only once here.
    posLocation = GLES20.glGetAttribLocation(program, "in_pos");

    int tcLocation = GLES20.glGetAttribLocation(program, "in_tc");
    GLES20.glEnableVertexAttribArray(tcLocation);
    GLES20.glVertexAttribPointer(
        tcLocation, 2, GLES20.GL_FLOAT, false, 0, textureCoords);

    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    checkNoGLES2Error();
  }

  // Wrap a float[] in a direct FloatBuffer using native byte order.
  private static FloatBuffer directNativeFloatBuffer(float[] array) {
    FloatBuffer buffer = ByteBuffer.allocateDirect(array.length * 4).order(
        ByteOrder.nativeOrder()).asFloatBuffer();
    buffer.put(array);
    buffer.flip();
    return buffer;
  }

  // Upload the YUV planes from |frame| to |textures|.
  private void texImage2D(I420Frame frame, int[] textures) {
    for (int i = 0; i < 3; ++i) {
      ByteBuffer plane = frame.yuvPlanes[i];
      GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[i]);
      int w = i == 0 ? frame.width : frame.width / 2;
      int h = i == 0 ? frame.height : frame.height / 2;
      abortUnless(w == frame.yuvStrides[i], frame.yuvStrides[i] + "!=" + w);
      GLES20.glTexImage2D(
          GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, w, h, 0,
          GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, plane);
    }
    checkNoGLES2Error();
  }

  // Draw |textures| using |vertices| (X,Y coordinates).
  private void drawRectangle(int[] textures, FloatBuffer vertices) {
    for (int i = 0; i < 3; ++i) {
      GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[i]);
    }

    GLES20.glVertexAttribPointer(
        posLocation, 2, GLES20.GL_FLOAT, false, 0, vertices);
    GLES20.glEnableVertexAttribArray(posLocation);

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

  // Compile & attach a |type| shader specified by |source| to |program|.
  private static void addShaderTo(
      int type, String source, int program) {
    int[] result = new int[] { GLES20.GL_FALSE };
    int shader = GLES20.glCreateShader(type);
    GLES20.glShaderSource(shader, source);
    GLES20.glCompileShader(shader);
    GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0);
    abortUnless(result[0] == GLES20.GL_TRUE,
        GLES20.glGetShaderInfoLog(shader) + ", source: " + source);
    GLES20.glAttachShader(program, shader);
    GLES20.glDeleteShader(shader);
    checkNoGLES2Error();
  }

  // Poor-man's assert(): die with |msg| unless |condition| is true.
  private static void abortUnless(boolean condition, String msg) {
    if (!condition) {
      throw new RuntimeException(msg);
    }
  }

  // Assert that no OpenGL ES 2.0 error has been raised.
  private static void checkNoGLES2Error() {
    int error = GLES20.glGetError();
    abortUnless(error == GLES20.GL_NO_ERROR, "GLES20 error: " + error);
  }

  // Remote image should span the full screen.
  private static final FloatBuffer remoteVertices = directNativeFloatBuffer(
      new float[] { -1, 1, -1, -1, 1, 1, 1, -1 });

  // Local image should be thumbnailish.
  private static final FloatBuffer localVertices = directNativeFloatBuffer(
      new float[] { 0.6f, 0.9f, 0.6f, 0.6f, 0.9f, 0.9f, 0.9f, 0.6f });

  // Texture Coordinates mapping the entire texture.
  private static final FloatBuffer textureCoords = directNativeFloatBuffer(
      new float[] { 0, 0, 0, 1, 1, 0, 1, 1 });

  // Pass-through vertex shader.
  private static final String VERTEX_SHADER_STRING =
      "varying vec2 interp_tc;\n" +
      "\n" +
      "attribute vec4 in_pos;\n" +
      "attribute vec2 in_tc;\n" +
      "\n" +
      "void main() {\n" +
      "  gl_Position = in_pos;\n" +
      "  interp_tc = in_tc;\n" +
      "}\n";

  // YUV to RGB pixel shader. Loads a pixel from each plane and pass through the
  // matrix.
  private static final String FRAGMENT_SHADER_STRING =
      "precision mediump float;\n" +
      "varying vec2 interp_tc;\n" +
      "\n" +
      "uniform sampler2D y_tex;\n" +
      "uniform sampler2D u_tex;\n" +
      "uniform sampler2D v_tex;\n" +
      "\n" +
      "void main() {\n" +
      "  float y = texture2D(y_tex, interp_tc).r;\n" +
      "  float u = texture2D(u_tex, interp_tc).r - .5;\n" +
      "  float v = texture2D(v_tex, interp_tc).r - .5;\n" +
      // CSC according to http://www.fourcc.org/fccyvrgb.php
      "  gl_FragColor = vec4(y + 1.403 * v, " +
      "                      y - 0.344 * u - 0.714 * v, " +
      "                      y + 1.77 * u, 1);\n" +
      "}\n";
}
