package cn.tee3.avd;

import android.text.TextUtils;
import android.util.Log;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.Nullable;
import tee3.webrtc.EglRenderer;
import tee3.webrtc.JavaI420Buffer;
import tee3.webrtc.Logging;
import tee3.webrtc.RendererCommon;
import tee3.webrtc.TextureBufferImpl;
import tee3.webrtc.TextureViewRenderer;
import tee3.webrtc.VideoFrame;

/**
 * 视频显示类
 *
 * 视频显示类定义了视频显示相关操作。
 */
public class VideoRenderer {
  private static final String TAG = "VideoRenderer";
  private boolean isBusying;

  /**
   * 视频显示方式
   */
  public static enum ScalingType{
    Scale_Aspect_Fit,             // 宽或高顶住窗口，若窗口不顶住的宽或高两边填黑，视频不变形；
    Scale_Aspect_Full,            // 铺满窗口，视频不变形，视频的宽或高被裁剪；
    Scale_Fill,                   // 铺满窗口，可能会视频拉伸变形；
  }

  public static interface FirstFrameCallback {
    public void onFirstFrameArrived(VideoRenderer render);
  }

  public static final int Left_to_the_edge =  1 << 1;
  public static final int Top_to_the_edge =  1 << 2;
  public static final int Right_to_the_edge =  1 << 3;
  public static final int Bottom_to_the_edge =  1 << 4;

  @Nullable public TextureViewRenderer viewRenderer = null;
  @Nullable private String videoId = null;

  /** 创建视频显示对象
   * @param[in] glView layout 中的 SurfaceViewRenderer。
   * @return 返回视频显示对象。
   */
  public VideoRenderer(TextureViewRenderer textureViewRenderer){
    Tlog.i(TAG, "VideoRenderer, build: " + hashCode() + ", glView: " + textureViewRenderer.hashCode());
    this.viewRenderer = textureViewRenderer;
    this.viewRenderer.init(AVDEngine.instance().getEglBaseContext(), null);
    this.viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
    this.viewRenderer.setEnableHardwareScaler(false /* enabled */);
    this.nativeobj = nativeCreateVideoRendererWrapper(this.vcb_all);
  }

  public boolean frameSampleCapture(EglRenderer.FrameListener listener) {
    if (isBusying && viewRenderer != null) {
      viewRenderer.addFrameListener(listener, 1.0f);
      return true;
    }
    return false;
  }

  public void dispose() {
    Tlog.i(TAG, "VideoRenderer, dispose:" + hashCode());
    nativeFreeVideoRendererWrapper(nativeobj);
    if (null != viewRenderer) {
      viewRenderer.release();
      viewRenderer = null;
    }
    nativeobj = 0;
  }

  /** 设置当前视频显示方式
   * @param[in] value 前视频显示方式。
   * @sa ScalingType
   */
  public void setScalingType(RendererCommon.ScalingType value) {
    Tlog.d(TAG, "setScalingType:" + value);
    if (null != viewRenderer) {
      viewRenderer.setScalingType(value);
    }
  }
  /** 视频显示界面填黑
   */
  public void setMirror(boolean mirror) {
    if (null == this.viewRenderer) {
      return ;
    }
    this.viewRenderer.setMirror(mirror);
  }
  public void setAutoRotation(boolean rotation) {
  }

  public void setVideoId(String videoId) {
    if (TextUtils.isEmpty(videoId) || TextUtils.isEmpty(this.videoId) || !this.videoId.equals(videoId)) {
      isBusying = false;
    }
    this.videoId = videoId;
  }

  @Nullable public String getVideoId() {
    return this.videoId;
  }

  /**
   * 设置偏移量
   *
   * @param offsetX
   * @param offsetY
   */
  public int setOffset(float offsetX, float offsetY) {
    if (null == viewRenderer) {
      return 0;
    }
    viewRenderer.offsetLeftAndRight((int) offsetX);
    viewRenderer.offsetTopAndBottom((int) offsetY);
    return 0;
  }
  /**
   * 设置缩放的改变量
   *
   * @param scale
   */
  public void setScale(float scale) {
    if (null == viewRenderer) {
      return;
    }
    viewRenderer.setScaleX(scale);
    viewRenderer.setScaleY(scale);
  }

  /** 视频显示界面在activity的stop中操作
   */
  public void stop() {
    if (null == viewRenderer) {
      return;
    }
    viewRenderer.pauseVideo();
  }
  /** 视频显示界面在activity的resume中操作
   */
  public void resume() {
    if (null == viewRenderer) {
      return;
    }
  }
  /** 视频显示界面填黑
   */
  public void fillBlack() {
    if (null == viewRenderer) {
      return;
    }
    viewRenderer.clearImage();
  }
  /** 启停图像抓取功能
   *
   * @param[in] enable 开启或者停止。
   * @return 返回错误代码。
   */
  public int enableCapture(boolean enable) {
    return nativeenableCapture(this.nativeobj, enable);
  }
  /** 抓取图像
   *
   * @param[in] fileName 图像保存地址，绝对地址, 支持".bmp和.jpg"。
   * @param[in] quality  图像质量：0-100, 0最差，100最好。
   * @return 返回错误代码。
   */
  public int captureImage(String fileName, int quality) {
    Log.i(TAG, "fileName = " + fileName + ", quality = " + quality);
    return nativecaptureImage(this.nativeobj, fileName, quality);
  }

  public static interface Callbacks {
    void setSize(int width, int height);
    // |frame| might have pending rotation and implementation of Callbacks
    // should handle that by applying rotation during rendering. The callee
    // is responsible for signaling when it is done with |frame| by calling
    // renderFrameDone(frame).
    void renderFrame(I420Frame frame);
  }

  /**
   * 更新指定区域局部渲染
   * @param zone 需要局部显示的视频坐标 （startX, startY, width, height）
   *  startX、startY 指整个视频的起始坐标
   *  width、height 指要裁剪视频分辨率的宽高
   * @return -1代表区域内含无效值，0代表成功。
   */
  public int setRenderZone(EglRenderer.RenderZone zone) {
    Tlog.i(TAG, "setRenderZone: " + zone.toString());
    if (zone == null || zone.getWidth() <= 0 || zone.getHeight() <= 0) {
      return -1;
    }
    if (viewRenderer != null) {
      viewRenderer.requestLayout();
      Tlog.i(TAG, "setRenderZone done.");
      return viewRenderer.setRenderZone(zone);
    }
    return -1;
  }

  class NativeCallBack implements Callbacks {
    @Override
    public void setSize(int width, int height) {
      Tlog.v(TAG, "Render.setSize, wh:" + width +"x"+height);
    }

    private long prev_frameMs = 0;
    private long prev_fpsMs = 0;
    private int curr_fps = 0;
    private int frameCount_fps = 0;
    private int frameCount_all = 0;
    private void log_renderFrame(I420Frame avdframe) {
      long curMs = System.currentTimeMillis();
      if (0 == prev_fpsMs) {
        prev_fpsMs = curMs;
      }
      ++ frameCount_all;
      ++ frameCount_fps;
      boolean logcurr = false;
      if ((curMs-prev_fpsMs)>3000) {
        curr_fps = frameCount_fps / 3;
        prev_fpsMs = curMs;
        frameCount_fps = 1;
        logcurr = true;
      }
      if (logcurr || (curMs-prev_frameMs)> 100 ) {
        Logging.d(TAG, "Render.renderFrame, span:" + (curMs -prev_frameMs)  + " fps:" + curr_fps + " cnt:" + frameCount_all + " frame:" + avdframe.toString());
      }
      prev_frameMs = curMs;
    }
    @Override
    public void renderFrame(I420Frame avdframe) {
      //log_renderFrame(avdframe);
      VideoFrame rtcframe = avdframe.toVideoFrame();
      if (TextUtils.isEmpty(videoId)) {
        rtcframe.release();
        return;
      }
      if (!isBusying) {
        onFirstFrameArrived();
        isBusying = true;
      }
      if (null != viewRenderer) {
        viewRenderer.onFrame(rtcframe);
      }
      rtcframe.release();
    }
  }
  private final NativeCallBack vcb_all = new NativeCallBack();
  @Nullable private List<FirstFrameCallback> mFirstFrameCallbackList = new ArrayList<>();

  public void setFirstFrameCallback(FirstFrameCallback callback) {
    if (callback == null) {
      return;
    }
    if (mFirstFrameCallbackList == null) {
      mFirstFrameCallbackList = new ArrayList<>();
    }
    if (!mFirstFrameCallbackList.contains(callback)) {
      mFirstFrameCallbackList.add(callback);
    }
  }

  public void removeFirstFrameCallback(FirstFrameCallback callback) {
    if (mFirstFrameCallbackList != null && callback != null) {
      mFirstFrameCallbackList.remove(callback);
    }
  }

  private void onFirstFrameArrived() {
    Tlog.i(TAG, "render: " + hashCode() + ", onFirstFrameArrived, videoId:" + videoId);
    if (mFirstFrameCallbackList != null) {
      for (FirstFrameCallback callback : mFirstFrameCallbackList) {
        callback.onFirstFrameArrived(this);
      }
    }
  }

  public static class I420Frame {
    public final int width;
    public final int height;
    @Nullable
    public final int[] yuvStrides;
    @Nullable public ByteBuffer[] yuvPlanes;
    public final boolean yuvFrame;
    // Matrix that transforms standard coordinates to their proper sampling locations in
    // the texture. This transform compensates for any properties of the video source that
    // cause it to appear different from a normalized texture. This matrix does not take
    // |rotationDegree| into account.
    @Nullable public final float[] samplingMatrix;
    public int textureId;
    // Frame pointer in C++.
    private long nativeFramePointer;

    // rotationDegree is the degree that the frame must be rotated clockwisely
    // to be rendered correctly.
    public int rotationDegree;

    // If this I420Frame was constructed from VideoFrame.Buffer, this points to
    // the backing buffer.
    @Nullable private final VideoFrame.Buffer backingBuffer;

    /**
     * Construct a frame of the given dimensions with the specified planar data.
     */
    public I420Frame(int width, int height, int rotationDegree, int[] yuvStrides,
                     ByteBuffer[] yuvPlanes, long nativeFramePointer) {
      this.width = width;
      this.height = height;
      this.yuvStrides = yuvStrides;
      this.yuvPlanes = yuvPlanes;
      this.yuvFrame = true;
      this.rotationDegree = rotationDegree;
      this.nativeFramePointer = nativeFramePointer;
      backingBuffer = null;
//      if (rotationDegree % 90 != 0) {
//        throw new IllegalArgumentException("Rotation degree not multiple of 90: " + rotationDegree);
//      }
      // The convention in WebRTC is that the first element in a ByteBuffer corresponds to the
      // top-left corner of the image, but in glTexImage2D() the first element corresponds to the
      // bottom-left corner. This discrepancy is corrected by setting a vertical flip as sampling
      // matrix.
      samplingMatrix = RendererCommon.verticalFlipMatrix();
    }

    /**
     * Construct a texture frame of the given dimensions with data in SurfaceTexture
     */
    public I420Frame(int width, int height, int rotationDegree, int textureId,
                     float[] samplingMatrix, long nativeFramePointer) {
      this.width = width;
      this.height = height;
      this.yuvStrides = null;
      this.yuvPlanes = null;
      this.samplingMatrix = samplingMatrix;
      this.textureId = textureId;
      this.yuvFrame = false;
      this.rotationDegree = rotationDegree;
      this.nativeFramePointer = nativeFramePointer;
      backingBuffer = null;
//      if (rotationDegree % 90 != 0) {
//        throw new IllegalArgumentException("Rotation degree not multiple of 90: " + rotationDegree);
//      }
    }

    /**
     * Construct a frame from VideoFrame.Buffer.
     */
    //@CalledByNative("I420Frame")
    public I420Frame(int rotationDegree, VideoFrame.Buffer buffer, long nativeFramePointer) {
      this.width = buffer.getWidth();
      this.height = buffer.getHeight();
      this.rotationDegree = rotationDegree;
//      if (rotationDegree % 90 != 0) {
//        throw new IllegalArgumentException("Rotation degree not multiple of 90: " + rotationDegree);
//      }
      if (buffer instanceof VideoFrame.TextureBuffer
              && ((VideoFrame.TextureBuffer) buffer).getType() == VideoFrame.TextureBuffer.Type.OES) {
        VideoFrame.TextureBuffer textureBuffer = (VideoFrame.TextureBuffer) buffer;
        this.yuvFrame = false;
        this.textureId = textureBuffer.getTextureId();
        this.samplingMatrix = RendererCommon.convertMatrixFromAndroidGraphicsMatrix(
                textureBuffer.getTransformMatrix());

        this.yuvStrides = null;
        this.yuvPlanes = null;
        backingBuffer = buffer;
      } else if (buffer instanceof VideoFrame.I420Buffer) {
        VideoFrame.I420Buffer i420Buffer = (VideoFrame.I420Buffer) buffer;
        this.yuvFrame = true;
        this.yuvStrides =
                new int[] {i420Buffer.getStrideY(), i420Buffer.getStrideU(), i420Buffer.getStrideV()};
        this.yuvPlanes =
                new ByteBuffer[] {i420Buffer.getDataY(), i420Buffer.getDataU(), i420Buffer.getDataV()};
        // The convention in WebRTC is that the first element in a ByteBuffer corresponds to the
        // top-left corner of the image, but in glTexImage2D() the first element corresponds to the
        // bottom-left corner. This discrepancy is corrected by multiplying the sampling matrix with
        // a vertical flip matrix.
        this.samplingMatrix = RendererCommon.verticalFlipMatrix();

        this.textureId = 0;
        backingBuffer = null;
      } else {
        this.yuvFrame = false;
        this.textureId = 0;
        this.samplingMatrix = null;
        this.yuvStrides = null;
        this.yuvPlanes = null;
        backingBuffer = null;
      }
      this.nativeFramePointer = nativeFramePointer;
    }

    public int rotatedWidth() {
      return (rotationDegree % 180 == 0) ? width : height;
    }

    public int rotatedHeight() {
      return (rotationDegree % 180 == 0) ? height : width;
    }

    public byte[] getYuvData(I420Frame frame, int index) {
      if (index < 0 || index > 2) {
        return new byte[0];
      }
      ByteBuffer buffer = frame.yuvPlanes[index];
      byte[] data = new byte[buffer.remaining()];
      buffer.get(data);
      return data;
    }

    @Override
    public String toString() {
      final String type = yuvFrame
              ? "Y: " + yuvStrides[0] + ", U: " + yuvStrides[1] + ", V: " + yuvStrides[2]
              : "Texture: " + textureId;
      return  width + "x" + height + ", " + type;
    }

    /**
     * Convert the frame to VideoFrame. It is no longer safe to use the I420Frame after calling
     * this.
     */
    public VideoFrame toVideoFrame() {
      final VideoFrame.Buffer buffer;
      if (backingBuffer != null) {
        // We were construted from a VideoFrame.Buffer, just return it.
        // Make sure webrtc::VideoFrame object is released.
        backingBuffer.retain();
        VideoRenderer.renderFrameDone(this);
        buffer = backingBuffer;
      } else if (yuvFrame) {
        buffer = JavaI420Buffer.wrap(width, height, yuvPlanes[0], yuvStrides[0], yuvPlanes[1],
                yuvStrides[1], yuvPlanes[2], yuvStrides[2],
                () -> { VideoRenderer.renderFrameDone(this); });
      } else {
        // Note: No Handler or YuvConverter means calling toI420 will crash.
        buffer = new TextureBufferImpl(width, height, VideoFrame.TextureBuffer.Type.OES, textureId,
                RendererCommon.convertMatrixToAndroidGraphicsMatrix(samplingMatrix), null /* handler */,
                null /* yuvConverter */, () -> VideoRenderer.renderFrameDone(this));
      }
      return new VideoFrame(buffer, rotationDegree, 0 /* timestampNs */);
    }

    //@CalledByNative("I420Frame")
    static I420Frame createI420Frame(int width, int height, int rotationDegree,
      int y_stride, ByteBuffer y_buffer, int u_stride, ByteBuffer u_buffer, int v_stride, ByteBuffer v_buffer, long nativeFramePointer) {
      return new I420Frame(width, height, rotationDegree, new int[] {y_stride, u_stride, v_stride},
              new ByteBuffer[] {y_buffer, u_buffer, v_buffer}, nativeFramePointer);
    }
  }
  /**
   * This must be called after every renderFrame() to release the frame.
   */
  public static void renderFrameDone(I420Frame frame) {
    frame.yuvPlanes = null;
    frame.textureId = 0;
    if (frame.nativeFramePointer != 0) {
      nativeReleaseFrame(frame.nativeFramePointer);
      frame.nativeFramePointer = 0;
    }
  }

  // |this| either wraps a native (GUI) renderer or a client-supplied Callbacks
  // (Java) implementation; so exactly one of these will be non-0/null.
  private long nativeobj;
  protected long nativeRender() {
    return nativeobj;
  }
  private static native long nativeCreateVideoRendererWrapper(Callbacks callbacks);
  private static native void nativeFreeVideoRendererWrapper(long nativeobj);
  protected static native int nativeenableCapture(long nativeobj, boolean enable);
  protected static native int nativecaptureImage(long nativeobj, String FileName, int quality);

  protected static native void nativeReleaseFrame(long framePointer);
  protected static native void nativeClearCopyPlane2Buff();
}
