/*
 *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package com.ss.video.rtc.newdemo.common.utils;

import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.support.annotation.Nullable;
import android.util.Log;
import java.nio.FloatBuffer;
import org.webrtc.FilterType;
import org.webrtc.GlShader;
import org.webrtc.GlUtil;
import org.webrtc.RendererCommon;

/**
 * Helper class to implement an instance of RendererCommon.GlDrawer that can accept multiple input
 * sources (OES, RGB, or YUV) using a generic fragment shader as input. The generic fragment shader
 * should sample pixel values from the function "sample" that will be provided by this class and
 * provides an abstraction for the input source type (OES, RGB, or YUV). The texture coordinate
 * variable name will be "tc" and the texture matrix in the vertex shader will be "tex_mat". The
 * simplest possible generic shader that just draws pixel from the frame unmodified looks like:
 * void main() {
 *   gl_FragColor = sample(tc);
 * }
 * This class covers the cases for most simple shaders and generates the necessary boiler plate.
 * Advanced shaders can always implement RendererCommon.GlDrawer directly.
 */
public class GlGenericDrawer implements RendererCommon.GlDrawer {
  /**
   * The different shader types representing different input sources. YUV here represents three
   * separate Y, U, V textures.
   */
  public static enum ShaderType { OES, RGB, YUV }

  /**
   * The shader callbacks is used to customize behavior for a GlDrawer. It provides a hook to set
   * uniform variables in the shader before a frame is drawn.
   */
  public static interface ShaderCallbacks {
    /**
     * This callback is called when a new shader has been compiled and created. It will be
     * called for the first frame as well as when the shader type is changed. This callback can
     * be used to do custom initialization of the shader that only needs to happen once.
     */
    void onNewShader(GlShader shader);

    /**
     * This callback is called before rendering a frame. It can be used to do custom preparation
     * of the shader that needs to happen every frame.
     */
    void onPrepareShader(GlShader shader, float[] texMatrix, int frameWidth, int frameHeight,
        int viewportWidth, int viewportHeight);
  }

  private static final String INPUT_VERTEX_COORDINATE_NAME = "in_pos";
  private static final String INPUT_TEXTURE_COORDINATE_NAME = "in_tc";
  private static final String TEXTURE_MATRIX_NAME = "tex_mat";
  private static final String TAR_TEX_SIZE_NAME = "tar_tex_size";
  private static final String ORI_TEX_SIZE_NAME = "ori_tex_size";
  private static final String DEFAULT_VERTEX_SHADER_STRING = "varying vec2 tc;\n"
      + "varying vec2 otc;\n"
      + "attribute vec4 in_pos;\n"
      + "attribute vec4 in_tc;\n"
      + "uniform mat4 tex_mat;\n"
      + "void main() {\n"
      + "  gl_Position = in_pos;\n"
      + "  otc = in_tc.xy;\n"
      + "  tc = (tex_mat * in_tc).xy;\n"
      + "}\n";

  // Vertex coordinates in Normalized Device Coordinates, i.e. (-1, -1) is bottom-left and (1, 1)
  // is top-right.
  private static final FloatBuffer FULL_RECTANGLE_BUFFER = GlUtil.createFloatBuffer(new float[] {
      -1.0f, -1.0f, // Bottom left.
      1.0f, -1.0f, // Bottom right.
      -1.0f, 1.0f, // Top left.
      1.0f, 1.0f, // Top right.
  });

  // Texture coordinates - (0, 0) is bottom-left and (1, 1) is top-right.
  private static final FloatBuffer FULL_RECTANGLE_TEXTURE_BUFFER =
      GlUtil.createFloatBuffer(new float[] {
          0.0f, 0.0f, // Bottom left.
          1.0f, 0.0f, // Bottom right.
          0.0f, 1.0f, // Top left.
          1.0f, 1.0f, // Top right.
      });

  static String createFragmentShaderString(String genericFragmentSource,
                                           ShaderType shaderType,
                                           FilterType scaleFilter) {
    final StringBuilder stringBuilder = new StringBuilder();
    if (shaderType == ShaderType.OES) {
      stringBuilder.append("#extension GL_OES_EGL_image_external : require\n");
    }
    stringBuilder.append("precision mediump float;\n");
    stringBuilder.append("varying vec2 tc;\n");
    stringBuilder.append("varying vec2 otc;\n");

    if (scaleFilter == FilterType.Bilinear) {
      stringBuilder.append("uniform vec2 tar_tex_size;\n");
      stringBuilder.append("uniform vec2 ori_tex_size;\n");
    } else if (scaleFilter == FilterType.Bicubic) {
      stringBuilder.append("uniform vec2 tar_tex_size;\n");
      stringBuilder.append("uniform vec2 ori_tex_size;\n");
    }

    if (shaderType == ShaderType.YUV) {
      stringBuilder.append("uniform sampler2D y_tex;\n");
      stringBuilder.append("uniform sampler2D u_tex;\n");
      stringBuilder.append("uniform sampler2D v_tex;\n");

      if (scaleFilter == FilterType.Bilinear) {
        stringBuilder.append(
            "float bilinearTextureSample(sampler2D tex, vec2 p) {\n");
        stringBuilder.append(
            "        vec2 c_onePixel =  vec2(1.0 / ori_tex_size.y - tar_tex_size.x + tar_tex_size.x, 1.0 / ori_tex_size.y);\n");
        stringBuilder.append("    vec2 frac = vec2(0.5, 0.5);\n");
        stringBuilder.append(
            "    float C11 = texture2D(tex, p - c_onePixel).r;\n");
        stringBuilder.append(
            "    float C21 = texture2D(tex, p + vec2( c_onePixel.x , -c_onePixel.y)).r;\n");
        stringBuilder.append(
            "    float C12 = texture2D(tex, p + vec2( -c_onePixel.x , c_onePixel.y)).r;\n");
        stringBuilder.append(
            "    float C22 = texture2D(tex, p + c_onePixel).r;\n");
        stringBuilder.append("    float x1 = mix(C11, C21, frac.x);\n");
        stringBuilder.append("    float x2 = mix(C12, C22, frac.x);\n");
        stringBuilder.append("    return mix(x1, x2, frac.y);\n");
        stringBuilder.append("}\n");
        stringBuilder.append("vec4 sample(vec2 p) {\n");
        stringBuilder.append(
            "  float y = bilinearTextureSample(y_tex, p) * 1.16438;\n");
        stringBuilder.append("  float u = bilinearTextureSample(u_tex, p);\n");
        stringBuilder.append("  float v = bilinearTextureSample(v_tex, p);\n");
        stringBuilder.append("  return vec4(y + 1.59603 * v - 0.874202,\n");
        stringBuilder.append(
            "    y - 0.391762 * u - 0.812968 * v + 0.531668,\n");
        stringBuilder.append("    y + 2.01723 * u - 1.08563, 1);\n");
        stringBuilder.append("}\n");
      } else if (scaleFilter == FilterType.Bicubic) {
        stringBuilder.append(
            "vec3 cubic_hermite (vec3 A, vec3 B, vec3 C, vec3 D, float t) {\n");
        stringBuilder.append("	  float t2 = t*t;\n");
        stringBuilder.append("    float t3 = t*t*t;\n");
        stringBuilder.append(
            "    vec3 a = -A/2.0 + (3.0*B)/2.0 - (3.0*C)/2.0 + D/2.0;\n");
        stringBuilder.append(
            "    vec3 b = A - (5.0*B)/2.0 + 2.0*C - D / 2.0;\n");
        stringBuilder.append("    vec3 c = -A/2.0 + C/2.0;\n");
        stringBuilder.append("   	vec3 d = B;\n");
        stringBuilder.append("    return a*t3 + b*t2 + c*t + d;\n");
        stringBuilder.append("}\n");

        stringBuilder.append(
            "vec4 bicubic_sample (sampler2D tex, , vec2 P) {\n");
        stringBuilder.append(
            "    float c_onePixel_x = 1.0 / ori_tex_size.x - tar_tex_size.x + tar_tex_size.x;\n");
        stringBuilder.append(
            "    float c_onePixel_y = 1.0 / ori_tex_size.y;\n");
        stringBuilder.append(
            "    float c_twoPixels_x = 2.0 / ori_tex_size.x;\n");
        stringBuilder.append(
            "    float c_twoPixels_y = 2.0 / ori_tex_size.y;\n");
        stringBuilder.append(
            "    vec2 pixel = vec2(P.x * ori_tex_size.x, P.y * ori_tex_size.y) + 0.5;\n");
        stringBuilder.append("    vec2 frac = fract(pixel);\n");
        stringBuilder.append("    pixel = floor(pixel);\n");
        stringBuilder.append(
            "    pixel = vec2(pixel.x / ori_tex_size.x, pixel.y / ori_tex_size.y) - vec2(c_onePixel_x, c_onePixel_y) / 2.0;\n");
        stringBuilder.append(
            "    vec3 C00 = texture2D(tex, pixel + vec2(-c_onePixel_x ,-c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C10 = texture2D(tex, pixel + vec2( 0.0        ,-c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C20 = texture2D(tex, pixel + vec2( c_onePixel_x ,-c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C30 = texture2D(tex, pixel + vec2( c_twoPixels_x,-c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C01 = texture2D(tex, pixel + vec2(-c_onePixel_x , 0.0)).rgb;\n");
        stringBuilder.append(
            "    vec3 C11 = texture2D(tex, pixel + vec2( 0.0        , 0.0)).rgb;\n");
        stringBuilder.append(
            "    vec3 C21 = texture2D(tex, pixel + vec2( c_onePixel_x , 0.0)).rgb;\n");
        stringBuilder.append(
            "    vec3 C31 = texture2D(tex, pixel + vec2( c_twoPixels_x, 0.0)).rgb;\n");
        stringBuilder.append(
            "    vec3 C02 = texture2D(tex, pixel + vec2(-c_onePixel_x , c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C12 = texture2D(tex, pixel + vec2( 0.0        , c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C22 = texture2D(tex, pixel + vec2( c_onePixel_x , c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C32 = texture2D(tex, pixel + vec2( c_twoPixels_x, c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C03 = texture2D(tex, pixel + vec2(-c_onePixel_x , c_twoPixels_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C13 = texture2D(tex, pixel + vec2( 0.0        , c_twoPixels_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C23 = texture2D(tex, pixel + vec2( c_onePixel_x , c_twoPixels_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C33 = texture2D(tex, pixel + vec2( c_twoPixels_x, c_twoPixels_y)).rgb;  \n");
        stringBuilder.append(
            "    vec3 CP0X = cubic_hermite(C00, C10, C20, C30, frac.x);\n");
        stringBuilder.append(
            "    vec3 CP1X = cubic_hermite(C01, C11, C21, C31, frac.x);\n");
        stringBuilder.append(
            "    vec3 CP2X = cubic_hermite(C02, C12, C22, C32, frac.x);\n");
        stringBuilder.append(
            "    vec3 CP3X = cubic_hermite(C03, C13, C23, C33, frac.x);\n");
        stringBuilder.append(
            "    return vec4(cubic_hermite(CP0X, CP1X, CP2X, CP3X, frac.y), 1.0);");
        stringBuilder.append("}\n");
        stringBuilder.append("vec4 sample(vec2 p) {\n");
        stringBuilder.append(
            "  float y = bicubic_sample(y_tex, p) * 1.16438;\n");
        stringBuilder.append("  float u = bicubic_sample(u_tex, p);\n");
        stringBuilder.append("  float v = bicubic_sample(v_tex, p);\n");
        stringBuilder.append("  return vec4(y + 1.59603 * v - 0.874202,\n");
        stringBuilder.append(
            "    y - 0.391762 * u - 0.812968 * v + 0.531668,\n");
        stringBuilder.append("    y + 2.01723 * u - 1.08563, 1);\n");
        stringBuilder.append("}\n");
      } else {
        // Add separate function for sampling texture.
        // yuv_to_rgb_mat is inverse of the matrix defined in YuvConverter.
        stringBuilder.append("vec4 sample(vec2 p) {\n");
        stringBuilder.append("  float y = texture2D(y_tex, p).r * 1.16438;\n");
        stringBuilder.append("  float u = texture2D(u_tex, p).r;\n");
        stringBuilder.append("  float v = texture2D(v_tex, p).r;\n");
        stringBuilder.append("  return vec4(y + 1.59603 * v - 0.874202,\n");
        stringBuilder.append(
            "    y - 0.391762 * u - 0.812968 * v + 0.531668,\n");
        stringBuilder.append("    y + 2.01723 * u - 1.08563, 1);\n");
        stringBuilder.append("}\n");
      }
      stringBuilder.append(genericFragmentSource);
    } else {
      final String samplerName = shaderType == ShaderType.OES ? "samplerExternalOES" : "sampler2D";
      stringBuilder.append("uniform ").append(samplerName).append(" tex;\n");

      if (scaleFilter == FilterType.Bilinear) {
        stringBuilder.append("vec4 sample(vec2 p) {\n");
        stringBuilder.append(
            "    vec2 c_onePixel =  vec2(1.0 / ori_tex_size.y - tar_tex_size.x + tar_tex_size.x, 1.0 / ori_tex_size.y);\n");
        stringBuilder.append("    vec2 frac = vec2(0.5, 0.5);\n");
        stringBuilder.append(
            "    vec3 C11 = texture2D(tex, p - c_onePixel).rgb;\n");
        stringBuilder.append(
            "    vec3 C21 = texture2D(tex, p + vec2( c_onePixel.x , -c_onePixel.y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C12 = texture2D(tex, p + vec2( -c_onePixel.x , c_onePixel.y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C22 = texture2D(tex, p + c_onePixel).rgb;\n");
        stringBuilder.append("    vec3 x1 = mix(C11, C21, frac.x);\n");
        stringBuilder.append("    vec3 x2 = mix(C12, C22, frac.x);\n");
        stringBuilder.append("    return vec4(mix(x1, x2, frac.y), 1);\n");
        stringBuilder.append("}\n");
        stringBuilder.append(genericFragmentSource);
      } else if (scaleFilter == FilterType.Bicubic) {
        stringBuilder.append(
            "vec3 cubic_hermite (vec3 A, vec3 B, vec3 C, vec3 D, float t) {\n");
        stringBuilder.append("	  float t2 = t*t;\n");
        stringBuilder.append("    float t3 = t*t*t;\n");
        stringBuilder.append(
            "    vec3 a = -A/2.0 + (3.0*B)/2.0 - (3.0*C)/2.0 + D/2.0;\n");
        stringBuilder.append(
            "    vec3 b = A - (5.0*B)/2.0 + 2.0*C - D / 2.0;\n");
        stringBuilder.append("    vec3 c = -A/2.0 + C/2.0;\n");
        stringBuilder.append("   	vec3 d = B;\n");
        stringBuilder.append("    return a*t3 + b*t2 + c*t + d;\n");
        stringBuilder.append("}\n");
        stringBuilder.append("vec4 sample (vec2 P) {\n");
        stringBuilder.append(
            "    float c_onePixel_x = 1.0 / ori_tex_size.x - tar_tex_size.x + tar_tex_size.x;\n");
        stringBuilder.append(
            "    float c_onePixel_y = 1.0 / ori_tex_size.y;\n");
        stringBuilder.append(
            "    float c_twoPixels_x = 2.0 / ori_tex_size.x;\n");
        stringBuilder.append(
            "    float c_twoPixels_y = 2.0 / ori_tex_size.y;\n");

        stringBuilder.append(
            "    vec2 pixel = vec2(P.x * ori_tex_size.x, P.y * ori_tex_size.y) + 0.5;\n");
        stringBuilder.append("    vec2 frac = fract(pixel);\n");
        stringBuilder.append("    pixel = floor(pixel);\n");
        stringBuilder.append(
            "    pixel = vec2(pixel.x / ori_tex_size.x, pixel.y / ori_tex_size.y) - vec2(c_onePixel_x, c_onePixel_y) / 2.0;\n");

        stringBuilder.append(
            "    vec3 C00 = texture2D(tex, pixel + vec2(-c_onePixel_x ,-c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C10 = texture2D(tex, pixel + vec2( 0.0        ,-c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C20 = texture2D(tex, pixel + vec2( c_onePixel_x ,-c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C30 = texture2D(tex, pixel + vec2( c_twoPixels_x,-c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C01 = texture2D(tex, pixel + vec2(-c_onePixel_x , 0.0)).rgb;\n");
        stringBuilder.append(
            "    vec3 C11 = texture2D(tex, pixel + vec2( 0.0        , 0.0)).rgb;\n");
        stringBuilder.append(
            "    vec3 C21 = texture2D(tex, pixel + vec2( c_onePixel_x , 0.0)).rgb;\n");
        stringBuilder.append(
            "    vec3 C31 = texture2D(tex, pixel + vec2( c_twoPixels_x, 0.0)).rgb;\n");
        stringBuilder.append(
            "    vec3 C02 = texture2D(tex, pixel + vec2(-c_onePixel_x , c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C12 = texture2D(tex, pixel + vec2( 0.0        , c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C22 = texture2D(tex, pixel + vec2( c_onePixel_x , c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C32 = texture2D(tex, pixel + vec2( c_twoPixels_x, c_onePixel_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C03 = texture2D(tex, pixel + vec2(-c_onePixel_x , c_twoPixels_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C13 = texture2D(tex, pixel + vec2( 0.0        , c_twoPixels_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C23 = texture2D(tex, pixel + vec2( c_onePixel_x , c_twoPixels_y)).rgb;\n");
        stringBuilder.append(
            "    vec3 C33 = texture2D(tex, pixel + vec2( c_twoPixels_x, c_twoPixels_y)).rgb;  \n");
        stringBuilder.append(
            "    vec3 CP0X = cubic_hermite(C00, C10, C20, C30, frac.x);\n");
        stringBuilder.append(
            "    vec3 CP1X = cubic_hermite(C01, C11, C21, C31, frac.x);\n");
        stringBuilder.append(
            "    vec3 CP2X = cubic_hermite(C02, C12, C22, C32, frac.x);\n");
        stringBuilder.append(
            "    vec3 CP3X = cubic_hermite(C03, C13, C23, C33, frac.x);\n");
        stringBuilder.append(
            "    return vec4(cubic_hermite(CP0X, CP1X, CP2X, CP3X, frac.y), 1.0);");
        stringBuilder.append("}\n");
        stringBuilder.append(genericFragmentSource);
      } else {
        // Update the sampling function in-place.
        stringBuilder.append(
            genericFragmentSource.replace("sample(", "texture2D(tex, "));
      }
    }

    return stringBuilder.toString();
  }

  private final String genericFragmentSource;
  private final String vertexShader;
  private final ShaderCallbacks shaderCallbacks;
  @Nullable private ShaderType currentShaderType;
  @Nullable private GlShader currentShader;
  private int inPosLocation;
  private int inTcLocation;
  private int texMatrixLocation;
  private int tarTexSizeLocation;
  private int oriTexSizeLocation;
  @Nullable private FilterType currentScaleFilter;

  public GlGenericDrawer(String genericFragmentSource, ShaderCallbacks shaderCallbacks) {
    this(DEFAULT_VERTEX_SHADER_STRING, genericFragmentSource, shaderCallbacks);
  }

  public GlGenericDrawer(
      String vertexShader, String genericFragmentSource, ShaderCallbacks shaderCallbacks) {
    this.vertexShader = vertexShader;
    this.genericFragmentSource = genericFragmentSource;
    this.shaderCallbacks = shaderCallbacks;
  }

  // Visible for testing.
  GlShader createShader(ShaderType shaderType, FilterType scaleFilter) {
    return new GlShader(vertexShader,
                        createFragmentShaderString(genericFragmentSource,
                                                   shaderType, scaleFilter));
  }

  /**
   * Draw an OES texture frame with specified texture transformation matrix. Required resources
   * are allocated at the first call to this function.
   */
  @Override
  public void drawOes(int oesTextureId, float[] texMatrix, int frameWidth, int frameHeight,
      int viewportX, int viewportY, int viewportWidth, int viewportHeight) {
    drawOes(oesTextureId, texMatrix, frameWidth, frameHeight, viewportX,
            viewportY, viewportWidth, viewportHeight, FilterType.Origin, 0, 0);
  }

  @Override
  public void drawRgb(int textureId, float[] texMatrix, int frameWidth,
                      int frameHeight, int viewportX, int viewportY,
                      int viewportWidth, int viewportHeight) {
    drawRgb(textureId, texMatrix, frameWidth, frameHeight, viewportX, viewportY,
            viewportWidth, viewportHeight, FilterType.Origin, 0, 0);
  }

  @Override
  public void drawYuv(int[] yuvTextures, float[] texMatrix, int frameWidth,
                      int frameHeight, int viewportX, int viewportY,
                      int viewportWidth, int viewportHeight) {
    drawYuv(yuvTextures, texMatrix, frameWidth, frameHeight, viewportX,
            viewportY, viewportWidth, viewportHeight, FilterType.Origin, 0, 0);
  }

  @Override
  public void drawOes(int oesTextureId, float[] texMatrix, int frameWidth, int frameHeight,
                      int viewportX, int viewportY, int viewportWidth, int viewportHeight,
                      FilterType scaleFilter, int unscaleWidth, int unscaleHeight) {
    prepareShader(ShaderType.OES, texMatrix, frameWidth, frameHeight,
                  viewportWidth, viewportHeight, scaleFilter, unscaleWidth,
                  unscaleHeight);
    // Bind the texture.
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId);

    int error = GLES20.glGetError();
    if (error != GLES20.GL_NO_ERROR) {
      // the incoming texture may not match its target sometimes.
      Log.e("GlGenericDrawer", "drawOes glBindTexture error: " + error);
      return;
    }

    // Draw the texture.
    GLES20.glViewport(viewportX, viewportY, viewportWidth, viewportHeight);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    // Unbind the texture as a precaution.
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);

    GlUtil.checkNoGLES2Error("drawOes done");
  }

  /**
   * Draw a RGB(A) texture frame with specified texture transformation matrix. Required resources
   * are allocated at the first call to this function.
   */
  @Override
  public void drawRgb(int textureId, float[] texMatrix, int frameWidth, int frameHeight,
                      int viewportX, int viewportY, int viewportWidth, int viewportHeight,
                      FilterType scaleFilter, int unscaleWidth, int unscaleHeight) {
    prepareShader(ShaderType.RGB, texMatrix, frameWidth, frameHeight,
                  viewportWidth, viewportHeight, scaleFilter, unscaleWidth,
                  unscaleHeight);
    // Bind the texture.
    GlUtil.checkNoGLES2Error("after prepare shader");
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);

    int error = GLES20.glGetError();
    if (error != GLES20.GL_NO_ERROR) {
      // the incoming texture may not match its target sometimes.
      Log.e("GlGenericDrawer", "drawRgb glBindTexture error: " + error);
      return;
    }

    // Draw the texture.
    GLES20.glViewport(viewportX, viewportY, viewportWidth, viewportHeight);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    // Unbind the texture as a precaution.
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    GlUtil.checkNoGLES2Error("drawRgb done");
  }

  /**
   * Draw a YUV frame with specified texture transformation matrix. Required resources are
   * allocated at the first call to this function.
   */
  @Override
  public void drawYuv(int[] yuvTextures, float[] texMatrix, int frameWidth, int frameHeight,
                      int viewportX, int viewportY, int viewportWidth, int viewportHeight,
                      FilterType scaleFilter, int unscaleWidth, int unscaleHeight) {
    prepareShader(ShaderType.YUV, texMatrix, frameWidth, frameHeight,
                  viewportWidth, viewportHeight, scaleFilter, unscaleWidth,
                  unscaleHeight);
    // Bind the textures.
    for (int i = 0; i < 3; ++i) {
      GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]);
    }

    int error = GLES20.glGetError();
    if (error != GLES20.GL_NO_ERROR) {
      // the incoming texture may not match its target sometimes.
      Log.e("GlGenericDrawer", "drawYuv glBindTexture error: " + error);
      return;
    }

    // Draw the textures.
    GLES20.glViewport(viewportX, viewportY, viewportWidth, viewportHeight);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    // Unbind the textures as a precaution.
    for (int i = 0; i < 3; ++i) {
      GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    }
    GlUtil.checkNoGLES2Error("drawYuv done");
  }

  private void prepareShader(ShaderType shaderType, float[] texMatrix,
                             int frameWidth, int frameHeight, int viewportWidth,
                             int viewportHeight, FilterType scaleFilter,
                             int unscaleWidth, int unscaleHeight) {
    final GlShader shader;

    // check input param
    if ((null == scaleFilter) || (0 == unscaleWidth) || (0 == unscaleHeight)) {
      scaleFilter = FilterType.Origin;
      unscaleWidth = 0;
      unscaleHeight = 0;
    }

    if (shaderType.equals(currentShaderType) &&
        scaleFilter.equals(currentScaleFilter)) {
      // Same shader type as before, reuse exising shader.
      shader = currentShader;
    } else {
      // Allocate new shader.
      currentShaderType = shaderType;
      if (currentShader != null) {
        currentShader.release();
      }
      shader = createShader(shaderType, scaleFilter);
      currentShader = shader;
      currentScaleFilter = scaleFilter;

      shader.useProgram();
      // Set input texture units.
      if (shaderType == ShaderType.YUV) {
        GLES20.glUniform1i(shader.getUniformLocation("y_tex"), 0);
        GLES20.glUniform1i(shader.getUniformLocation("u_tex"), 1);
        GLES20.glUniform1i(shader.getUniformLocation("v_tex"), 2);
      } else {
        GLES20.glUniform1i(shader.getUniformLocation("tex"), 0);
      }

      GlUtil.checkNoGLES2Error("Create shader");
      shaderCallbacks.onNewShader(shader);
      texMatrixLocation = shader.getUniformLocation(TEXTURE_MATRIX_NAME);
      if (scaleFilter != FilterType.Origin) {
        tarTexSizeLocation = shader.getUniformLocation(TAR_TEX_SIZE_NAME);
        oriTexSizeLocation = shader.getUniformLocation(ORI_TEX_SIZE_NAME);
      }
      inPosLocation = shader.getAttribLocation(INPUT_VERTEX_COORDINATE_NAME);
      inTcLocation = shader.getAttribLocation(INPUT_TEXTURE_COORDINATE_NAME);
    }

    GlUtil.checkNoGLES2Error("before useProgram ");
    shader.useProgram();

    // Upload the vertex coordinates.
    GLES20.glEnableVertexAttribArray(inPosLocation);
    GLES20.glVertexAttribPointer(inPosLocation, /* size= */ 2,
        /* type= */ GLES20.GL_FLOAT, /* normalized= */ false, /* stride= */ 0,
        FULL_RECTANGLE_BUFFER);

    // Upload the texture coordinates.
    GLES20.glEnableVertexAttribArray(inTcLocation);
    GLES20.glVertexAttribPointer(inTcLocation, /* size= */ 2,
        /* type= */ GLES20.GL_FLOAT, /* normalized= */ false, /* stride= */ 0,
        FULL_RECTANGLE_TEXTURE_BUFFER);

    // Upload the texture transformation matrix.
    GLES20.glUniformMatrix4fv(
        texMatrixLocation, 1 /* count= */, false /* transpose= */, texMatrix, 0 /* offset= */);

    if ((scaleFilter != FilterType.Origin) && (unscaleWidth > 0) &&
        (unscaleWidth > 0)) {
      // Upload the texture size used by bilinear.
      GLES20.glUniform2f(tarTexSizeLocation, frameWidth, frameHeight);

      // Upload the original texture size used by bilinear.
      GLES20.glUniform2f(oriTexSizeLocation, unscaleWidth, unscaleHeight);
    }

    // Do custom per-frame shader preparation.
    shaderCallbacks.onPrepareShader(
        shader, texMatrix, frameWidth, frameHeight, viewportWidth, viewportHeight);
    GlUtil.checkNoGLES2Error("Prepare shader");
  }

  /**
   * Release all GLES resources. This needs to be done manually, otherwise the resources are
   * leaked.
   */
  @Override
  public void release() {
    if (currentShader != null) {
      currentShader.release();
      currentShader = null;
      currentShaderType = null;
    }
  }
}
