/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tv.ui;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Point;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.Property;
import android.view.Display;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import com.android.tv.R;
import com.android.tv.TvOptionsManager;
import com.android.tv.data.DisplayMode;
import com.android.tv.features.TvFeatures;
import com.android.tv.util.TvSettings;

/**
 * The TvViewUiManager is responsible for handling UI layouting and animation of main TvView. It
 * also control the settings regarding TvView UI such as display mode.
 */
public class TvViewUiManager {
    private static final String TAG = "TvViewManager";
    private static final boolean DEBUG = false;

    private static final float DISPLAY_MODE_EPSILON = 0.001f;
    private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f;

    private static final int MSG_SET_LAYOUT_PARAMS = 1000;

    private final Context mContext;
    private final Resources mResources;
    private final FrameLayout mContentView;
    private final TunableTvView mTvView;
    private final TvOptionsManager mTvOptionsManager;
    private final int mTvViewShrunkenStartMargin;
    private final int mTvViewShrunkenEndMargin;
    private int mWindowWidth;
    private int mWindowHeight;
    private final SharedPreferences mSharedPreferences;
    private final TimeInterpolator mLinearOutSlowIn;
    private final TimeInterpolator mFastOutLinearIn;
    private final Handler mHandler =
            new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case MSG_SET_LAYOUT_PARAMS:
                            FrameLayout.LayoutParams layoutParams =
                                    (FrameLayout.LayoutParams) msg.obj;
                            if (DEBUG) {
                                Log.d(
                                        TAG,
                                        "setFixedSize: w="
                                                + layoutParams.width
                                                + " h="
                                                + layoutParams.height);
                            }
                            mTvView.setTvViewLayoutParams(layoutParams);
                            mTvView.setLayoutParams(mTvViewFrame);
                            // Smooth PIP size change, we don't change surface size when
                            // isInPictureInPictureMode is true.
                            if (!TvFeatures.PICTURE_IN_PICTURE.isEnabled(mContext)
                                    || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
                                            && !((Activity) mContext).isInPictureInPictureMode())) {
                                mTvView.setFixedSurfaceSize(
                                        layoutParams.width, layoutParams.height);
                            }
                            break;
                    }
                }
            };
    private int mDisplayMode;
    // Used to restore the previous state from ShrunkenTvView state.
    private int mTvViewStartMarginBeforeShrunken;
    private int mTvViewEndMarginBeforeShrunken;
    private int mDisplayModeBeforeShrunken;
    private boolean mIsUnderShrunkenTvView;
    private int mTvViewStartMargin;
    private int mTvViewEndMargin;
    private ObjectAnimator mTvViewAnimator;
    private FrameLayout.LayoutParams mTvViewLayoutParams;
    // TV view's position when the display mode is FULL. It is used to compute PIP location relative
    // to TV view's position.
    private FrameLayout.LayoutParams mTvViewFrame;
    private FrameLayout.LayoutParams mLastAnimatedTvViewFrame;
    private FrameLayout.LayoutParams mOldTvViewFrame;
    private ObjectAnimator mBackgroundAnimator;
    private int mBackgroundColor;
    private int mAppliedDisplayedMode = DisplayMode.MODE_NOT_DEFINED;
    private int mAppliedTvViewStartMargin;
    private int mAppliedTvViewEndMargin;
    private float mAppliedVideoDisplayAspectRatio;

    public TvViewUiManager(
            Context context,
            TunableTvView tvView,
            FrameLayout contentView,
            TvOptionsManager tvOptionManager) {
        mContext = context;
        mResources = mContext.getResources();
        mTvView = tvView;
        mContentView = contentView;
        mTvOptionsManager = tvOptionManager;

        DisplayManager displayManager =
                (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
        Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
        Point size = new Point();
        display.getSize(size);
        mWindowWidth = size.x;
        mWindowHeight = size.y;

        // Have an assumption that TvView Shrinking happens only in full screen.
        mTvViewShrunkenStartMargin =
                mResources.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start);
        mTvViewShrunkenEndMargin =
                mResources.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_end)
                        + mResources.getDimensionPixelSize(R.dimen.side_panel_width);
        mTvViewFrame = createMarginLayoutParams(0, 0, 0, 0);

        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);

        mLinearOutSlowIn =
                AnimationUtils.loadInterpolator(
                        mContext, android.R.interpolator.linear_out_slow_in);
        mFastOutLinearIn =
                AnimationUtils.loadInterpolator(
                        mContext, android.R.interpolator.fast_out_linear_in);
    }

    public void onConfigurationChanged(final int windowWidth, final int windowHeight) {
        if (windowWidth > 0 && windowHeight > 0) {
            if (mWindowWidth != windowWidth || mWindowHeight != windowHeight) {
                mWindowWidth = windowWidth;
                mWindowHeight = windowHeight;
                applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, true);
            }
        }
    }

    /**
     * Initializes animator in advance of using the animator to improve animation performance. For
     * fast first tune, it is not expected to be called in Activity.onCreate, but called a few
     * seconds later after onCreate.
     */
    public void initAnimatorIfNeeded() {
        initTvAnimatorIfNeeded();
        initBackgroundAnimatorIfNeeded();
    }

    /**
     * It is called when shrunken TvView is desired, such as EditChannelFragment and
     * ChannelsLockedFragment.
     */
    public void startShrunkenTvView() {
        mIsUnderShrunkenTvView = true;
        mTvView.setIsUnderShrunken(true);

        mTvViewStartMarginBeforeShrunken = mTvViewStartMargin;
        mTvViewEndMarginBeforeShrunken = mTvViewEndMargin;
        setTvViewMargin(mTvViewShrunkenStartMargin, mTvViewShrunkenEndMargin);
        mDisplayModeBeforeShrunken = setDisplayMode(DisplayMode.MODE_NORMAL, false, true);
    }

    /**
     * It is called when shrunken TvView is no longer desired, such as EditChannelFragment and
     * ChannelsLockedFragment.
     */
    public void endShrunkenTvView() {
        mIsUnderShrunkenTvView = false;
        mTvView.setIsUnderShrunken(false);
        setTvViewMargin(mTvViewStartMarginBeforeShrunken, mTvViewEndMarginBeforeShrunken);
        setDisplayMode(mDisplayModeBeforeShrunken, false, true);
    }

    /** Returns true, if TvView is shrunken. */
    public boolean isUnderShrunkenTvView() {
        return mIsUnderShrunkenTvView;
    }

    /**
     * Returns true, if {@code displayMode} is available now. If screen ratio is matched to video
     * ratio, other display modes than {@link DisplayMode#MODE_NORMAL} are not available.
     */
    public boolean isDisplayModeAvailable(int displayMode) {
        if (displayMode == DisplayMode.MODE_NORMAL) {
            return true;
        }

        int viewWidth = mContentView.getWidth();
        int viewHeight = mContentView.getHeight();

        float videoDisplayAspectRatio = mTvView.getVideoDisplayAspectRatio();
        if (viewWidth <= 0 || viewHeight <= 0 || videoDisplayAspectRatio <= 0f) {
            Log.w(TAG, "Video size is currently unavailable");
            if (DEBUG) {
                Log.d(
                        TAG,
                        "isDisplayModeAvailable: "
                                + "viewWidth="
                                + viewWidth
                                + ", viewHeight="
                                + viewHeight
                                + ", videoDisplayAspectRatio="
                                + videoDisplayAspectRatio);
            }
            return false;
        }

        float viewRatio = viewWidth / (float) viewHeight;
        return Math.abs(viewRatio - videoDisplayAspectRatio) >= DISPLAY_MODE_EPSILON;
    }

    /** Returns a constant defined in DisplayMode. */
    public int getDisplayMode() {
        if (isDisplayModeAvailable(mDisplayMode)) {
            return mDisplayMode;
        }
        return DisplayMode.MODE_NORMAL;
    }

    /**
     * Sets the display mode to the given value.
     *
     * @return the previous display mode.
     */
    public int setDisplayMode(int displayMode, boolean storeInPreference, boolean animate) {
        int prev = mDisplayMode;
        mDisplayMode = displayMode;
        if (storeInPreference) {
            mSharedPreferences.edit().putInt(TvSettings.PREF_DISPLAY_MODE, displayMode).apply();
        }
        applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), animate, false);
        return prev;
    }

    /** Restores the display mode to the display mode stored in preference. */
    public void restoreDisplayMode(boolean animate) {
        int displayMode =
                mSharedPreferences.getInt(TvSettings.PREF_DISPLAY_MODE, DisplayMode.MODE_NORMAL);
        setDisplayMode(displayMode, false, animate);
    }

    /** Updates TvView's aspect ratio. It should be called when video resolution is changed. */
    public void updateTvAspectRatio() {
        applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, false);
        if (mTvView.isVideoAvailable() && mTvView.isFadedOut()) {
            mTvView.fadeIn(
                    mResources.getInteger(R.integer.tvview_fade_in_duration),
                    mFastOutLinearIn,
                    null);
        }
    }

    /** Fades in TvView. */
    public void fadeInTvView() {
        if (mTvView.isFadedOut()) {
            mTvView.fadeIn(
                    mResources.getInteger(R.integer.tvview_fade_in_duration),
                    mFastOutLinearIn,
                    null);
        }
    }

    /** Fades out TvView. */
    public void fadeOutTvView(Runnable postAction) {
        if (!mTvView.isFadedOut()) {
            mTvView.fadeOut(
                    mResources.getInteger(R.integer.tvview_fade_out_duration),
                    mLinearOutSlowIn,
                    postAction);
        }
    }

    /** This margins will be applied when applyDisplayMode is called. */
    private void setTvViewMargin(int tvViewStartMargin, int tvViewEndMargin) {
        mTvViewStartMargin = tvViewStartMargin;
        mTvViewEndMargin = tvViewEndMargin;
    }

    private boolean isTvViewFullScreen() {
        return mTvViewStartMargin == 0 && mTvViewEndMargin == 0;
    }

    private void setBackgroundColor(
            int color, FrameLayout.LayoutParams targetLayoutParams, boolean animate) {
        if (animate) {
            initBackgroundAnimatorIfNeeded();
            if (mBackgroundAnimator.isStarted()) {
                // Cancel the current animation and start new one.
                mBackgroundAnimator.cancel();
            }

            int decorViewWidth = mContentView.getWidth();
            int decorViewHeight = mContentView.getHeight();
            boolean hasPillarBox =
                    mTvView.getWidth() != decorViewWidth || mTvView.getHeight() != decorViewHeight;
            boolean willHavePillarBox =
                    ((targetLayoutParams.width != LayoutParams.MATCH_PARENT)
                                    && targetLayoutParams.width != decorViewWidth)
                            || ((targetLayoutParams.height != LayoutParams.MATCH_PARENT)
                                    && targetLayoutParams.height != decorViewHeight);

            if (!isTvViewFullScreen() && !hasPillarBox) {
                // If there is no pillar box, no animation is needed.
                mContentView.setBackgroundColor(color);
            } else if (!isTvViewFullScreen() || willHavePillarBox) {
                mBackgroundAnimator.setIntValues(mBackgroundColor, color);
                mBackgroundAnimator.setEvaluator(new ArgbEvaluator());
                mBackgroundAnimator.setInterpolator(mFastOutLinearIn);
                mBackgroundAnimator.start();
            }
            // In the 'else' case (TV activity is getting out of the shrunken tv view mode and will
            // have a pillar box), we keep the background color and don't show the animation.
        } else {
            mContentView.setBackgroundColor(color);
        }
        mBackgroundColor = color;
    }

    private void setTvViewPosition(
            final FrameLayout.LayoutParams layoutParams,
            FrameLayout.LayoutParams tvViewFrame,
            boolean animate) {
        if (DEBUG) {
            Log.d(
                    TAG,
                    "setTvViewPosition: w="
                            + layoutParams.width
                            + " h="
                            + layoutParams.height
                            + " s="
                            + layoutParams.getMarginStart()
                            + " t="
                            + layoutParams.topMargin
                            + " e="
                            + layoutParams.getMarginEnd()
                            + " b="
                            + layoutParams.bottomMargin
                            + " animate="
                            + animate);
        }
        FrameLayout.LayoutParams oldTvViewFrame = mTvViewFrame;
        mTvViewLayoutParams = layoutParams;
        mTvViewFrame = tvViewFrame;
        if (animate) {
            initTvAnimatorIfNeeded();
            if (mTvViewAnimator.isStarted()) {
                // Cancel the current animation and start new one.
                mTvViewAnimator.cancel();
                mOldTvViewFrame = new FrameLayout.LayoutParams(mLastAnimatedTvViewFrame);
            } else {
                mOldTvViewFrame = new FrameLayout.LayoutParams(oldTvViewFrame);
            }
            mTvViewAnimator.setObjectValues(mTvView.getTvViewLayoutParams(), layoutParams);
            mTvViewAnimator.setEvaluator(
                    new TypeEvaluator<FrameLayout.LayoutParams>() {
                        FrameLayout.LayoutParams lp;

                        @Override
                        public FrameLayout.LayoutParams evaluate(
                                float fraction,
                                FrameLayout.LayoutParams startValue,
                                FrameLayout.LayoutParams endValue) {
                            if (lp == null) {
                                lp = new FrameLayout.LayoutParams(0, 0);
                                lp.gravity = startValue.gravity;
                            }
                            interpolateMargins(lp, startValue, endValue, fraction);
                            return lp;
                        }
                    });
            mTvViewAnimator.setInterpolator(
                    isTvViewFullScreen() ? mFastOutLinearIn : mLinearOutSlowIn);
            mTvViewAnimator.start();
        } else {
            if (mTvViewAnimator != null && mTvViewAnimator.isStarted()) {
                // Continue the current animation.
                // layoutParams will be applied when animation ends.
                return;
            }
            // This block is also called when animation ends.
            if (isTvViewFullScreen()) {
                // When this layout is for full screen, fix the surface size after layout to make
                // resize animation smooth. During PIP size change, the multiple messages can be
                // queued, if we don't remove MSG_SET_LAYOUT_PARAMS.
                mHandler.removeMessages(MSG_SET_LAYOUT_PARAMS);
                mHandler.obtainMessage(MSG_SET_LAYOUT_PARAMS, layoutParams).sendToTarget();
            } else {
                mTvView.setTvViewLayoutParams(layoutParams);
                mTvView.setLayoutParams(mTvViewFrame);
            }
        }
    }

    private void initTvAnimatorIfNeeded() {
        if (mTvViewAnimator != null) {
            return;
        }

        // TvViewAnimator animates TvView by repeatedly re-layouting TvView.
        // TvView includes a SurfaceView on which scale/translation effects do not work. Normally,
        // SurfaceView can be animated by changing left/top/right/bottom directly using
        // ObjectAnimator, although it would require calling getChildAt(0) against TvView (which is
        // supposed to be opaque). More importantly, this method does not work in case of TvView,
        // because TvView may request layout itself during animation and layout SurfaceView with
        // its own parameters when TvInputService requests to do so.
        mTvViewAnimator = new ObjectAnimator();
        mTvViewAnimator.setTarget(mTvView.getTvView());
        mTvViewAnimator.setProperty(
                Property.of(FrameLayout.class, ViewGroup.LayoutParams.class, "layoutParams"));
        mTvViewAnimator.setDuration(mResources.getInteger(R.integer.tvview_anim_duration));
        mTvViewAnimator.addListener(
                new AnimatorListenerAdapter() {
                    private boolean mCanceled = false;

                    @Override
                    public void onAnimationCancel(Animator animation) {
                        mCanceled = true;
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        if (mCanceled) {
                            mCanceled = false;
                            return;
                        }
                        mHandler.post(
                                () -> setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false));
                    }
                });
        mTvViewAnimator.addUpdateListener(
                new AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animator) {
                        float fraction = animator.getAnimatedFraction();
                        mLastAnimatedTvViewFrame =
                                (FrameLayout.LayoutParams) mTvView.getLayoutParams();
                        interpolateMargins(
                                mLastAnimatedTvViewFrame, mOldTvViewFrame, mTvViewFrame, fraction);
                        mTvView.setLayoutParams(mLastAnimatedTvViewFrame);
                    }
                });
    }

    private void initBackgroundAnimatorIfNeeded() {
        if (mBackgroundAnimator != null) {
            return;
        }

        mBackgroundAnimator = new ObjectAnimator();
        mBackgroundAnimator.setTarget(mContentView);
        mBackgroundAnimator.setPropertyName("backgroundColor");
        mBackgroundAnimator.setDuration(
                mResources.getInteger(R.integer.tvactivity_background_anim_duration));
        mBackgroundAnimator.addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        mHandler.post(() -> mContentView.setBackgroundColor(mBackgroundColor));
                    }
                });
    }

    private void applyDisplayMode(
            float videoDisplayAspectRatio, boolean animate, boolean forceUpdate) {
        if (videoDisplayAspectRatio <= 0f) {
            videoDisplayAspectRatio = (float) mWindowWidth / mWindowHeight;
        }
        if (mAppliedDisplayedMode == mDisplayMode
                && mAppliedTvViewStartMargin == mTvViewStartMargin
                && mAppliedTvViewEndMargin == mTvViewEndMargin
                && Math.abs(mAppliedVideoDisplayAspectRatio - videoDisplayAspectRatio)
                        < DISPLAY_ASPECT_RATIO_EPSILON) {
            if (!forceUpdate) {
                return;
            }
        } else {
            mAppliedDisplayedMode = mDisplayMode;
            mAppliedTvViewStartMargin = mTvViewStartMargin;
            mAppliedTvViewEndMargin = mTvViewEndMargin;
            mAppliedVideoDisplayAspectRatio = videoDisplayAspectRatio;
        }
        int availableAreaWidth = mWindowWidth - mTvViewStartMargin - mTvViewEndMargin;
        int availableAreaHeight = availableAreaWidth * mWindowHeight / mWindowWidth;
        int displayMode = mDisplayMode;
        float availableAreaRatio = 0;
        if (availableAreaWidth <= 0 || availableAreaHeight <= 0) {
            displayMode = DisplayMode.MODE_FULL;
            Log.w(
                    TAG,
                    "Some resolution info is missing during applyDisplayMode. ("
                            + "availableAreaWidth="
                            + availableAreaWidth
                            + ", availableAreaHeight="
                            + availableAreaHeight
                            + ")");
        } else {
            availableAreaRatio = (float) availableAreaWidth / availableAreaHeight;
        }
        FrameLayout.LayoutParams layoutParams =
                new FrameLayout.LayoutParams(
                        0, 0, ((FrameLayout.LayoutParams) mTvView.getTvViewLayoutParams()).gravity);
        switch (displayMode) {
            case DisplayMode.MODE_ZOOM:
                if (videoDisplayAspectRatio < availableAreaRatio) {
                    // Y axis will be clipped.
                    layoutParams.width = availableAreaWidth;
                    layoutParams.height = Math.round(availableAreaWidth / videoDisplayAspectRatio);
                } else {
                    // X axis will be clipped.
                    layoutParams.width = Math.round(availableAreaHeight * videoDisplayAspectRatio);
                    layoutParams.height = availableAreaHeight;
                }
                break;
            case DisplayMode.MODE_NORMAL:
                if (videoDisplayAspectRatio < availableAreaRatio) {
                    // X axis has black area.
                    layoutParams.width = Math.round(availableAreaHeight * videoDisplayAspectRatio);
                    layoutParams.height = availableAreaHeight;
                } else {
                    // Y axis has black area.
                    layoutParams.width = availableAreaWidth;
                    layoutParams.height = Math.round(availableAreaWidth / videoDisplayAspectRatio);
                }
                break;
            case DisplayMode.MODE_FULL:
            default:
                layoutParams.width = availableAreaWidth;
                layoutParams.height = availableAreaHeight;
                break;
        }
        // FrameLayout has an issue with centering when left and right margins differ.
        // So stick to Gravity.START | Gravity.CENTER_VERTICAL.
        int marginStart = (availableAreaWidth - layoutParams.width) / 2;
        layoutParams.setMarginStart(marginStart);
        int tvViewFrameTop = (mWindowHeight - availableAreaHeight) / 2;
        FrameLayout.LayoutParams tvViewFrame =
                createMarginLayoutParams(
                        mTvViewStartMargin, mTvViewEndMargin, tvViewFrameTop, tvViewFrameTop);
        setTvViewPosition(layoutParams, tvViewFrame, animate);
        setBackgroundColor(
                mResources.getColor(
                        isTvViewFullScreen()
                                ? R.color.tvactivity_background
                                : R.color.tvactivity_background_on_shrunken_tvview,
                        null),
                layoutParams,
                animate);

        // Update the current display mode.
        mTvOptionsManager.onDisplayModeChanged(displayMode);
    }

    private static int interpolate(int start, int end, float fraction) {
        return (int) (start + (end - start) * fraction);
    }

    private static void interpolateMargins(
            MarginLayoutParams out,
            MarginLayoutParams startValue,
            MarginLayoutParams endValue,
            float fraction) {
        out.topMargin = interpolate(startValue.topMargin, endValue.topMargin, fraction);
        out.bottomMargin = interpolate(startValue.bottomMargin, endValue.bottomMargin, fraction);
        out.setMarginStart(
                interpolate(startValue.getMarginStart(), endValue.getMarginStart(), fraction));
        out.setMarginEnd(interpolate(startValue.getMarginEnd(), endValue.getMarginEnd(), fraction));
        out.width = interpolate(startValue.width, endValue.width, fraction);
        out.height = interpolate(startValue.height, endValue.height, fraction);
    }

    private FrameLayout.LayoutParams createMarginLayoutParams(
            int startMargin, int endMargin, int topMargin, int bottomMargin) {
        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(0, 0);
        lp.setMarginStart(startMargin);
        lp.setMarginEnd(endMargin);
        lp.topMargin = topMargin;
        lp.bottomMargin = bottomMargin;
        lp.width = mWindowWidth - startMargin - endMargin;
        lp.height = mWindowHeight - topMargin - bottomMargin;
        return lp;
    }
}
