package tee3.webrtc;

import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.SurfaceTexture;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.TextureView;
import java.util.concurrent.CountDownLatch;
import cn.tee3.avd.ErrorCode;
import cn.tee3.avd.Tlog;

public class TextureViewRenderer extends TextureView implements VideoSink, TextureView.SurfaceTextureListener {
    private static final String TAG = "TextureViewRenderer";
    // Cached resource name.
    private final String resourceName;
    private final RendererCommon.VideoLayoutMeasure videoLayoutMeasure = new RendererCommon.VideoLayoutMeasure();
    private final EglRenderer eglRenderer;

    // Callback for reporting renderer events. Read-only after initilization so no lock required.
    private RendererCommon.RendererEvents rendererEvents;

    private int rotatedFrameWidth;
    private int rotatedFrameHeight;
    private boolean enableFixedSize;
    private boolean isEnableMeasure = true;
    private final Object layoutLock = new Object();
    private boolean isRenderingPaused = false;
    private boolean isFirstFrameRendered;
    private int frameRotation;
    public static Activity activity;
    private EglRenderer.RenderZone renderZone;

    /**
     * Standard View constructor. In order to render something, you must first call init().
     */
    public TextureViewRenderer(Context context) {
        super(context);
        TextureViewRenderer.activity = (Activity) context;
        this.resourceName = getResourceName();
        eglRenderer = new EglRenderer(resourceName);
        setSurfaceTextureListener(this);
    }

    /**
     * Standard View constructor. In order to render something, you must first call init().
     */
    public TextureViewRenderer(Context context, AttributeSet attrs) {
        super(context, attrs);
        TextureViewRenderer.activity = (Activity) context;
        this.resourceName = getResourceName();
        eglRenderer = new EglRenderer(resourceName);
        setSurfaceTextureListener(this);
    }

    /**
     * Initialize this class, sharing resources with |sharedContext|. It is allowed to call init() to
     * reinitialize the renderer after a previous init()/release() cycle.
     */
    public void init(EglBase.Context sharedContext, RendererCommon.RendererEvents rendererEvents) {
        init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer());
    }

    /**
     * Initialize this class, sharing resources with |sharedContext|. The custom |drawer| will be used
     * for drawing frames on the EGLSurface. This class is responsible for calling release() on
     * |drawer|. It is allowed to call init() to reinitialize the renderer after a previous
     * init()/release() cycle.
     */
    public void init(final EglBase.Context sharedContext,
                     RendererCommon.RendererEvents rendererEvents, final int[] configAttributes,
                     RendererCommon.GlDrawer drawer) {
        ThreadUtils.checkIsOnMainThread();
        this.rendererEvents = rendererEvents;
        synchronized (layoutLock) {
            isFirstFrameRendered = false;
            rotatedFrameWidth = 0;
            rotatedFrameHeight = 0;
            frameRotation = 0;
        }
        eglRenderer.init(sharedContext, configAttributes, drawer);
    }

    /**
     * Block until any pending frame is returned and all GL resources released, even if an interrupt
     * occurs. If an interrupt occurs during release(), the interrupt flag will be set. This function
     * should be called before the Activity is destroyed and the EGLContext is still valid. If you
     * don't call this function, the GL resources might leak.
     */
    public void release() {
        clearRenderZone();
        eglRenderer.release();
        activity = null;
    }

    /**
     * Register a callback to be invoked when a new video frame has been received.
     *
     * @param listener The callback to be invoked. The callback will be invoked on the render thread.
     *                 It should be lightweight and must not call removeFrameListener.
     * @param scale    The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
     *                 required.
     * @param drawer   Custom drawer to use for this frame listener.
     */
    public void addFrameListener(
            EglRenderer.FrameListener listener, float scale, RendererCommon.GlDrawer drawerParam) {
        eglRenderer.addFrameListener(listener, scale, drawerParam);
    }

    /**
     * Register a callback to be invoked when a new video frame has been received. This version uses
     * the drawer of the EglRenderer that was passed in init.
     *
     * @param listener The callback to be invoked. The callback will be invoked on the render thread.
     *                 It should be lightweight and must not call removeFrameListener.
     * @param scale    The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
     *                 required.
     */
    public void addFrameListener(EglRenderer.FrameListener listener, float scale) {
        eglRenderer.addFrameListener(listener, scale);
    }

    public void removeFrameListener(EglRenderer.FrameListener listener) {
        eglRenderer.removeFrameListener(listener);
    }

    /**
     * Enables fixed size for the surface. This provides better performance but might be buggy on some
     * devices. By default this is turned off.
     */
    public void setEnableHardwareScaler(boolean enabled) {
        ThreadUtils.checkIsOnMainThread();
        enableFixedSize = enabled;
    }

    /**
     * Set if the video stream should be mirrored or not.
     */
    public void setMirror(final boolean mirror) {
        eglRenderer.setMirror(mirror);
    }

    /**
     * Set how the video will fill the allowed layout area.
     */
    public void setScalingType(RendererCommon.ScalingType scalingType) {
        ThreadUtils.checkIsOnMainThread();
        videoLayoutMeasure.setScalingType(scalingType);
        requestLayout();
    }

    public void setScalingType(RendererCommon.ScalingType scalingTypeMatchOrientation,
                               RendererCommon.ScalingType scalingTypeMismatchOrientation) {
        ThreadUtils.checkIsOnMainThread();
        videoLayoutMeasure.setScalingType(scalingTypeMatchOrientation, scalingTypeMismatchOrientation);
        requestLayout();
    }

    /**
     * Limit render framerate.
     *
     * @param fps Limit render framerate to this value, or use Float.POSITIVE_INFINITY to disable fps
     *            reduction.
     */
    public void setFpsReduction(float fps) {
        synchronized (layoutLock) {
            isRenderingPaused = fps == 0f;
        }
        eglRenderer.setFpsReduction(fps);
    }

    public void disableFpsReduction() {
        synchronized (layoutLock) {
            isRenderingPaused = false;
        }
        eglRenderer.disableFpsReduction();
    }

    public void pauseVideo() {
        synchronized (layoutLock) {
            isRenderingPaused = true;
        }
        eglRenderer.pauseVideo();
    }

    // VideoSink interface.
    @Override
    public void onFrame(VideoFrame frame) {
        updateFrameDimensionsAndReportEvents(frame);
        eglRenderer.onFrame(frame);
    }

    // View layout interface.
    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        ThreadUtils.checkIsOnMainThread();
        if (renderZone == null) {
            final Point size;
            synchronized (layoutLock) {
                size = videoLayoutMeasure.measure(widthSpec, heightSpec, rotatedFrameWidth, rotatedFrameHeight);
            }
            setMeasuredDimension(size.x, size.y);
        } else {
            int widthSpecSize = MeasureSpec.getSize(widthSpec);
            int heightSpecSize = MeasureSpec.getSize(heightSpec);
            int width, height;

            if ((float) widthSpecSize / (float) heightSpecSize < (float) renderZone.getWidth() / (float) renderZone.getHeight()) {
                width = widthSpecSize;
                height = (int) ((float) renderZone.getHeight() / (float) renderZone.getWidth() * widthSpecSize);
            } else {
                width = (int) ((float) renderZone.getWidth() / (float) renderZone.getHeight() * heightSpecSize);
                height = heightSpecSize;
            }

            setMeasuredDimension(width, height);
        }
        logD("onMeasure(): New size: " + getMeasuredWidthAndState() + "x" + getMeasuredHeightAndState());
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        ThreadUtils.checkIsOnMainThread();
        eglRenderer.setLayoutAspectRatio((right - left) / (float) (bottom - top));
    }

    @Override
    public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width,
                                          final int height) {
        logD("onSurfaceTextureAvailable: size: " + width + "x" + height);
        ThreadUtils.checkIsOnMainThread();
        eglRenderer.createEglSurface(surface);
        eglRenderer.clearImage();
    }

    @Override
    public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, final int width,
                                            final int height) {
        logD("onSurfaceTextureSizeChanged: size: " + width + "x" + height);
        ThreadUtils.checkIsOnMainThread();
        eglRenderer.clearImage();
    }

    @Override
    public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) {
        logD("onSurfaceTextureDestroyed");
        ThreadUtils.checkIsOnMainThread();
        final CountDownLatch completionLatch = new CountDownLatch(1);
        eglRenderer.releaseEglSurface(new Runnable() {
            @Override
            public void run() {
                completionLatch.countDown();
            }
        });
        ThreadUtils.awaitUninterruptibly(completionLatch);
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(final SurfaceTexture surface) {
    }

    public int setRenderZone(EglRenderer.RenderZone zone) {
        if (eglRenderer != null) {
            renderZone = zone;
            eglRenderer.setRenderZone(zone);
            Tlog.i(TAG, "setRenderZone done.");
            return ErrorCode.AVD_OK;
        }
        return -1;
    }

    public void clearRenderZone() {
        renderZone = null;
    }

    public EglRenderer getEglRenderer() {
        return eglRenderer;
    }

    private String getResourceName() {
        try {
            return getResources().getResourceEntryName(getId()) + ": ";
        } catch (Resources.NotFoundException e) {
            return "";
        }
    }

    /**
     * Post a task to clear the SurfaceView to a transparent uniform color.
     */
    public void clearImage() {
        eglRenderer.clearImage();
    }

    // Update frame dimensions and report any changes to |rendererEvents|.
    private void updateFrameDimensionsAndReportEvents(VideoFrame frame) {
        synchronized (layoutLock) {
            if (isRenderingPaused) {
                return;
            }
            if (!isFirstFrameRendered) {
                isFirstFrameRendered = true;
                logD("Reporting first rendered frame.");
                if (rendererEvents != null) {
                    rendererEvents.onFirstFrameRendered();
                }
            }
            if (rotatedFrameWidth != frame.getRotatedWidth() || rotatedFrameHeight != frame.getRotatedHeight()
                    || frameRotation != frame.getRotation()) {
                logD("Reporting frame resolution changed to " + frame.getRotatedWidth() + "x" + frame.getRotatedHeight() + " with rotation " + frame.getRotation());
                if (rendererEvents != null) {
                    rendererEvents.onFrameResolutionChanged(frame.getBuffer().getWidth(), frame.getBuffer().getHeight(), frame.getRotation());
                }
                rotatedFrameWidth = frame.getRotatedWidth();
                rotatedFrameHeight = frame.getRotatedHeight();
                frameRotation = frame.getRotation();
                post(new Runnable() {
                    @Override
                    public void run() {
                        requestLayout();
                    }
                });
            }
        }
    }

    private void postOrRun(Runnable r) {
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            r.run();
        } else {
            post(r);
        }
    }

    private void logD(String string) {
        Logging.d(TAG, resourceName + hashCode() + ", " + string);
    }

    public void enableMeasure(boolean enableMeasure) {
        isEnableMeasure = enableMeasure;
    }
}