// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chromecast.shell;

import android.content.Context;
import android.graphics.Canvas;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;

import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
import org.chromium.base.VisibleForTesting;
import org.chromium.content.browser.ContainerViewObserver;
import org.chromium.content.browser.ContentViewCore;
import org.chromium.content.browser.RenderCoordinates;

import java.lang.ref.WeakReference;

//TODO(gunsch): componentize this class.
/**
 * This is a container for external video surfaces.
 * The object is owned by the native peer and it is owned by WebContents.
 *
 * The expected behavior of the media player on the video hole punching is as follows.
 * 1) If it requests the surface, it will call requestExternalVideoSurface().
 *    When the resolution of the video is changed, it'll call requestExternalVideoSurface().
 * 2) Whenever the size or the position of the video element is changed, it'll notify through
 *    onExternalVideoSurfacePositionChanged().
 * 3) Whenever the page that contains the video element is scrolled or zoomed,
 *    onFrameInfoUpdated() will be called.
 * 4) Usually steps 1) ~ 3) are repeated during the playback.
 * 5) If the player no longer needs the surface any more, it'll call
 *    releaseExternalVideoSurface().
 *
 * Please contact ycheo@chromium.org or wonsik@chromium.org if you have any
 * questions or issues for this class.
 *
 * Note(gunsch): This class was copied wholesale from android_webview/native
 * for the use of chromecast/. Owners of content/ have expressed not wanting
 * this code in content/ to discourage usage, since they don't plan to support
 * hole-punching long term. In the meantime, this copy is a workaround to avoid
 * a dependency on android_webview.
 */
@JNINamespace("chromecast::shell")
public class ExternalVideoSurfaceContainer implements SurfaceHolder.Callback {
    protected static final int INVALID_PLAYER_ID = -1;

    /**
     * Because WebView does hole-punching by itself, instead, the hole-punching logic
     * in SurfaceView can clear out some web elements like media control or subtitle.
     * So we need to disable its hole-punching logic.
     */
    @VisibleForTesting
    public static class NoPunchingSurfaceView extends SurfaceView {
        public NoPunchingSurfaceView(Context context) {
            super(context);
        }
        // SurfaceView.dispatchDraw implementation punches a hole in the view hierarchy.
        // Disable this by making this a no-op.
        @Override
        protected void dispatchDraw(Canvas canvas) {}
    }

    // There can be at most 1 external video surface for now.
    // If there are the multiple requests for the surface, then the second video will
    // kick the first one off.
    // To support the mulitple video surfaces seems impractical, because z-order between
    // the multiple SurfaceViews is non-deterministic.
    private static WeakReference<ExternalVideoSurfaceContainer> sActiveContainer =
            new WeakReference<ExternalVideoSurfaceContainer>(null);

    private final long mNativeExternalVideoSurfaceContainer;
    private final ContentViewCore mContentViewCore;
    private ViewGroup mContainerView;
    private ContainerViewObserver mContainerViewObserver;
    private int mPlayerId = INVALID_PLAYER_ID;
    private SurfaceView mSurfaceView;

    // The absolute CSS coordinates of the video element.
    private float mLeft;
    private float mTop;
    private float mRight;
    private float mBottom;

    // The physical location/size of the external video surface in pixels.
    private int mX;
    private int mY;
    private int mWidth;
    private int mHeight;

    /**
     * Factory class to facilitate dependency injection.
     */
    public static class Factory {
        public ExternalVideoSurfaceContainer create(
                long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) {
            return new ExternalVideoSurfaceContainer(
                    nativeExternalVideoSurfaceContainer, contentViewCore);
        }
    }
    private static Factory sFactory = new Factory();

    @VisibleForTesting
    public static void setFactory(Factory factory) {
        sFactory = factory;
    }

    @CalledByNative
    private static ExternalVideoSurfaceContainer create(
            long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) {
        return sFactory.create(nativeExternalVideoSurfaceContainer, contentViewCore);
    }

    protected ExternalVideoSurfaceContainer(
            long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) {
        assert contentViewCore != null;
        mNativeExternalVideoSurfaceContainer = nativeExternalVideoSurfaceContainer;
        mContentViewCore = contentViewCore;
        initializeCurrentPositionOfSurfaceView();
    }

    /**
     * Called when a media player wants to request an external video surface.
     * @param playerId The ID of the media player.
     */
    @CalledByNative
    protected void requestExternalVideoSurface(int playerId) {
        assert playerId != INVALID_PLAYER_ID;

        if (mPlayerId == playerId) return;

        setActiveContainer(this);

        mPlayerId = playerId;
        initializeCurrentPositionOfSurfaceView();

        createSurfaceView();
    }

    /**
     * Returns id of player currently using the external video surface.
     */
    @CalledByNative
    protected int getCurrentPlayerId() {
        return mPlayerId;
    }

    /**
     * Called when a media player wants to release an external video surface.
     * @param playerId The ID of the media player.
     */
    @CalledByNative
    protected void releaseExternalVideoSurface(int playerId) {
        if (mPlayerId != playerId) return;

        releaseIfActiveContainer(this);

        mPlayerId = INVALID_PLAYER_ID;
    }

    @CalledByNative
    protected void destroy() {
        releaseExternalVideoSurface(mPlayerId);
    }

    private void initializeCurrentPositionOfSurfaceView() {
        mX = Integer.MIN_VALUE;
        mY = Integer.MIN_VALUE;
        mWidth = 0;
        mHeight = 0;
    }

    private static void setActiveContainer(ExternalVideoSurfaceContainer container) {
        ExternalVideoSurfaceContainer activeContainer = sActiveContainer.get();
        if (activeContainer != null) {
            activeContainer.removeSurfaceView();
        }
        sActiveContainer = new WeakReference<ExternalVideoSurfaceContainer>(container);
    }

    private static void releaseIfActiveContainer(ExternalVideoSurfaceContainer container) {
        ExternalVideoSurfaceContainer activeContainer = sActiveContainer.get();
        if (activeContainer == container) {
            setActiveContainer(null);
        }
    }

    private void createSurfaceView() {
        assert mSurfaceView == null;
        assert mContainerView == null;
        assert mContainerViewObserver == null;

        mSurfaceView = new NoPunchingSurfaceView(mContentViewCore.getContext());
        mSurfaceView.getHolder().addCallback(this);
        // SurfaceHoder.surfaceCreated() will be called after the SurfaceView is attached to
        // the Window and becomes visible.
        mContainerView = mContentViewCore.getContainerView();
        mContainerView.addView(mSurfaceView);
        mContainerViewObserver = new ContainerViewObserver() {
            @Override
            public void onContainerViewChanged(ViewGroup newContainerView) {
                mContainerView.removeView(mSurfaceView);
                mContainerView = newContainerView;
                mContainerView.addView(mSurfaceView);
            }
        };
        mContentViewCore.addContainerViewObserver(mContainerViewObserver);
    }

    private void removeSurfaceView() {
        assert mSurfaceView != null;
        assert mContainerView != null;
        assert mContainerViewObserver != null;

        // SurfaceHoder.surfaceDestroyed() will be called in ViewGroup.removeView()
        // as soon as the SurfaceView is detached from the Window.
        mContentViewCore.removeContainerViewObserver(mContainerViewObserver);
        mContainerView.removeView(mSurfaceView);

        mContainerViewObserver = null;
        mSurfaceView = null;
        mContainerView = null;
    }

    /**
     * Called when the position of the video element which uses the external
     * video surface is changed.
     * @param playerId The ID of the media player.
     * @param left The absolute CSS X coordinate of the left side of the video element.
     * @param top The absolute CSS Y coordinate of the top side of the video element.
     * @param right The absolute CSS X coordinate of the right side of the video element.
     * @param bottom The absolute CSS Y coordinate of the bottom side of the video element.
     */
    @CalledByNative
    protected void onExternalVideoSurfacePositionChanged(
            int playerId, float left, float top, float right, float bottom) {
        if (mPlayerId != playerId) return;

        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;

        layOutSurfaceView();
    }

    /**
     * Called when the page that contains the video element is scrolled or zoomed.
     */
    @CalledByNative
    protected void onFrameInfoUpdated() {
        if (mPlayerId == INVALID_PLAYER_ID) return;

        layOutSurfaceView();
    }

    private void layOutSurfaceView() {
        RenderCoordinates renderCoordinates = mContentViewCore.getRenderCoordinates();
        RenderCoordinates.NormalizedPoint topLeft = renderCoordinates.createNormalizedPoint();
        RenderCoordinates.NormalizedPoint bottomRight = renderCoordinates.createNormalizedPoint();
        topLeft.setAbsoluteCss(mLeft, mTop);
        bottomRight.setAbsoluteCss(mRight, mBottom);
        float top = topLeft.getYPix();
        float left = topLeft.getXPix();
        float bottom = bottomRight.getYPix();
        float right = bottomRight.getXPix();

        int x = Math.round(left + renderCoordinates.getScrollXPix());
        int y = Math.round(top + renderCoordinates.getScrollYPix());
        int width = Math.round(right - left);
        int height = Math.round(bottom - top);
        if (mX == x && mY == y && mWidth == width && mHeight == height) return;
        mX = x;
        mY = y;
        mWidth = width;
        mHeight = height;

        mSurfaceView.setX(x);
        mSurfaceView.setY(y);
        ViewGroup.LayoutParams layoutParams = mSurfaceView.getLayoutParams();
        layoutParams.width = width;
        layoutParams.height = height;
        mSurfaceView.requestLayout();
    }

    // SurfaceHolder.Callback methods.
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}

    @Override
    // surfaceCreated() callback can be called regardless of requestExternalVideoSurface,
    // if the activity comes back from the background and becomes visible.
    public void surfaceCreated(SurfaceHolder holder) {
        if (mPlayerId != INVALID_PLAYER_ID) {
            nativeSurfaceCreated(
                    mNativeExternalVideoSurfaceContainer, mPlayerId, holder.getSurface());
        }
    }

    // surfaceDestroyed() callback can be called regardless of releaseExternalVideoSurface,
    // if the activity moves to the backgound and becomes invisible.
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (mPlayerId != INVALID_PLAYER_ID) {
            nativeSurfaceDestroyed(mNativeExternalVideoSurfaceContainer, mPlayerId);
        }
    }

    private native void nativeSurfaceCreated(
            long nativeExternalVideoSurfaceContainerImpl, int playerId, Surface surface);

    private native void nativeSurfaceDestroyed(
            long nativeExternalVideoSurfaceContainerImpl, int playerId);
}
