/*
 * 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.guide;

import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Point;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;

import androidx.leanback.widget.OnChildSelectedListener;
import androidx.leanback.widget.SearchOrbView;
import androidx.leanback.widget.VerticalGridView;

import com.android.tv.ChannelTuner;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.util.DurationTimer;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.GenreItems;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.features.TvFeatures;
import com.android.tv.perf.EventNames;
import com.android.tv.perf.PerformanceMonitor;
import com.android.tv.perf.TimerEvent;
import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
import com.android.tv.ui.ViewUtils;
import com.android.tv.ui.hideable.AutoHideScheduler;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;

import com.android.tv.common.flags.UiFlags;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/** The program guide. */
public class ProgramGuide
        implements ProgramGrid.ChildFocusListener, AccessibilityStateChangeListener {
    private static final String TAG = "ProgramGuide";
    private static final boolean DEBUG = false;

    // Whether we should show the guide partially. The first time the user enters the program guide,
    // we show the grid partially together with the genre side panel on the left. Next time
    // the program guide is entered, we recover the previous state (partial or full).
    private static final String KEY_SHOW_GUIDE_PARTIAL = "show_guide_partial";
    private static final long TIME_INDICATOR_UPDATE_FREQUENCY = TimeUnit.SECONDS.toMillis(1);
    private static final long HOUR_IN_MILLIS = TimeUnit.HOURS.toMillis(1);
    private static final long HALF_HOUR_IN_MILLIS = HOUR_IN_MILLIS / 2;
    // We keep the duration between mStartTime and the current time larger than this value.
    // We clip out the first program entry in ProgramManager, if it does not have enough width.
    // In order to prevent from clipping out the current program, this value need be larger than
    // or equal to ProgramManager.FIRST_ENTRY_MIN_DURATION.
    private static final long MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME =
            ProgramManager.FIRST_ENTRY_MIN_DURATION;

    private static final int MSG_PROGRAM_TABLE_FADE_IN_ANIM = 1000;

    private static final String SCREEN_NAME = "EPG";

    private final MainActivity mActivity;
    private final ProgramManager mProgramManager;
    private final AccessibilityManager mAccessibilityManager;
    private final ChannelTuner mChannelTuner;
    private final Tracker mTracker;
    private final DurationTimer mVisibleDuration = new DurationTimer();
    private final Runnable mPreShowRunnable;
    private final Runnable mPostHideRunnable;

    private final int mWidthPerHour;
    private final long mViewPortMillis;
    private final int mRowHeight;
    private final int mDetailHeight;
    private final int mSelectionRow; // Row that is focused
    private final int mTableFadeAnimDuration;
    private final int mAnimationDuration;
    private final int mDetailPadding;
    private final SearchOrbView mSearchOrb;
    private final UiFlags mUiFlags;
    private int mCurrentTimeIndicatorWidth;

    private final View mContainer;
    private final View mSidePanel;
    private final VerticalGridView mSidePanelGridView;
    private final View mTable;
    private final TimelineRow mTimelineRow;
    private final ProgramGrid mGrid;
    private final TimeListAdapter mTimeListAdapter;
    private final View mCurrentTimeIndicator;

    private final Animator mShowAnimatorFull;
    private final Animator mShowAnimatorPartial;
    // mHideAnimatorFull and mHideAnimatorPartial are created from the same animation xmls.
    // When we share the one animator for two different animations, the starting value
    // is broken, even though the starting value is not defined in XML.
    private final Animator mHideAnimatorFull;
    private final Animator mHideAnimatorPartial;
    private final Animator mPartialToFullAnimator;
    private final Animator mFullToPartialAnimator;
    private final Animator mProgramTableFadeOutAnimator;
    private final Animator mProgramTableFadeInAnimator;

    // When the program guide is popped up, we keep the previous state of the guide.
    private boolean mShowGuidePartial;
    private final SharedPreferences mSharedPreference;
    private View mSelectedRow;
    private Animator mDetailOutAnimator;
    private Animator mDetailInAnimator;

    private long mStartUtcTime;
    private boolean mTimelineAnimation;
    private int mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS;
    private boolean mIsDuringResetRowSelection;
    private final Handler mHandler = new ProgramGuideHandler(this);
    private boolean mActive;

    private final AutoHideScheduler mAutoHideScheduler;
    private final long mShowDurationMillis;
    private ViewTreeObserver.OnGlobalLayoutListener mOnLayoutListenerForShow;

    private final ProgramManagerListener mProgramManagerListener = new ProgramManagerListener();

    private final PerformanceMonitor mPerformanceMonitor;
    private TimerEvent mTimerEvent;

    private final Runnable mUpdateTimeIndicator =
            new Runnable() {
                @Override
                public void run() {
                    positionCurrentTimeIndicator();
                    mHandler.postAtTime(
                            this,
                            Utils.ceilTime(
                                    SystemClock.uptimeMillis(), TIME_INDICATOR_UPDATE_FREQUENCY));
                }
            };

    @SuppressWarnings("RestrictTo")
    public ProgramGuide(
            MainActivity activity,
            ChannelTuner channelTuner,
            TvInputManagerHelper tvInputManagerHelper,
            ChannelDataManager channelDataManager,
            ProgramDataManager programDataManager,
            @Nullable DvrDataManager dvrDataManager,
            @Nullable DvrScheduleManager dvrScheduleManager,
            Tracker tracker,
            Runnable preShowRunnable,
            Runnable postHideRunnable) {
        mActivity = activity;
        TvSingletons singletons = TvSingletons.getSingletons(mActivity);
        mPerformanceMonitor = singletons.getPerformanceMonitor();
        mUiFlags = singletons.getUiFlags();
        mProgramManager =
                new ProgramManager(
                        tvInputManagerHelper,
                        channelDataManager,
                        programDataManager,
                        dvrDataManager,
                        dvrScheduleManager);
        mChannelTuner = channelTuner;
        mTracker = tracker;
        mPreShowRunnable = preShowRunnable;
        mPostHideRunnable = postHideRunnable;

        Resources res = activity.getResources();

        mWidthPerHour = res.getDimensionPixelSize(R.dimen.program_guide_table_width_per_hour);
        GuideUtils.setWidthPerHour(mWidthPerHour);

        Point displaySize = new Point();
        mActivity.getWindowManager().getDefaultDisplay().getSize(displaySize);
        int gridWidth =
                displaySize.x
                        - res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start)
                        - res.getDimensionPixelSize(
                                R.dimen.program_guide_table_header_column_width);
        mViewPortMillis = (gridWidth * HOUR_IN_MILLIS) / mWidthPerHour;

        mRowHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_item_row_height);
        mDetailHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_height);
        mSelectionRow = res.getInteger(R.integer.program_guide_selection_row);
        mTableFadeAnimDuration =
                res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration);
        mShowDurationMillis = res.getInteger(R.integer.program_guide_show_duration);
        mAnimationDuration =
                res.getInteger(R.integer.program_guide_table_detail_toggle_anim_duration);
        mDetailPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_detail_padding);

        mContainer = mActivity.findViewById(R.id.program_guide);
        ViewTreeObserver.OnGlobalFocusChangeListener globalFocusChangeListener =
                new GlobalFocusChangeListener();
        mContainer.getViewTreeObserver().addOnGlobalFocusChangeListener(globalFocusChangeListener);

        GenreListAdapter genreListAdapter = new GenreListAdapter(mActivity, mProgramManager, this);
        mSidePanel = mContainer.findViewById(R.id.program_guide_side_panel);
        mSidePanelGridView =
                (VerticalGridView) mContainer.findViewById(R.id.program_guide_side_panel_grid_view);
        mSidePanelGridView
                .getRecycledViewPool()
                .setMaxRecycledViews(
                        R.layout.program_guide_side_panel_row,
                        res.getInteger(R.integer.max_recycled_view_pool_epg_side_panel_row));
        mSidePanelGridView.setAdapter(genreListAdapter);
        mSidePanelGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
        mSidePanelGridView.setWindowAlignmentOffset(
                mActivity
                        .getResources()
                        .getDimensionPixelOffset(R.dimen.program_guide_side_panel_alignment_y));
        mSidePanelGridView.setWindowAlignmentOffsetPercent(
                VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);

        if (TvFeatures.EPG_SEARCH.isEnabled(mActivity)) {
            mSearchOrb =
                    (SearchOrbView)
                            mContainer.findViewById(R.id.program_guide_side_panel_search_orb);
            mSearchOrb.setVisibility(View.VISIBLE);

            mSearchOrb.setOnOrbClickedListener(
                    new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            hide();
                            mActivity.showProgramGuideSearchFragment();
                        }
                    });
            mSidePanelGridView.setOnChildSelectedListener(
                    new androidx.leanback.widget.OnChildSelectedListener() {
                        @Override
                        public void onChildSelected(ViewGroup viewGroup, View view, int i, long l) {
                            mSearchOrb.animate().alpha(i == 0 ? 1.0f : 0.0f);
                        }
                    });
        } else {
            mSearchOrb = null;
        }

        mTable = mContainer.findViewById(R.id.program_guide_table);

        mTimelineRow = (TimelineRow) mTable.findViewById(R.id.time_row);
        mTimeListAdapter = new TimeListAdapter(res);
        mTimelineRow
                .getRecycledViewPool()
                .setMaxRecycledViews(
                        R.layout.program_guide_table_header_row_item,
                        res.getInteger(R.integer.max_recycled_view_pool_epg_header_row_item));
        mTimelineRow.setAdapter(mTimeListAdapter);

        ProgramTableAdapter programTableAdapter =
                new ProgramTableAdapter(mActivity, this, mUiFlags);
        programTableAdapter.registerAdapterDataObserver(
                new RecyclerView.AdapterDataObserver() {
                    @Override
                    public void onChanged() {
                        // It is usually called when Genre is changed.
                        // Reset selection of ProgramGrid
                        resetRowSelection();
                        updateGuidePosition();
                    }
                });

        mGrid = (ProgramGrid) mTable.findViewById(R.id.grid);
        mGrid.initialize(mProgramManager);
        mGrid.getRecycledViewPool()
                .setMaxRecycledViews(
                        R.layout.program_guide_table_row,
                        res.getInteger(R.integer.max_recycled_view_pool_epg_table_row));
        mGrid.setAdapter(programTableAdapter);

        mGrid.setChildFocusListener(this);
        mGrid.setOnChildSelectedListener(
                new OnChildSelectedListener() {
                    @Override
                    public void onChildSelected(
                            ViewGroup parent, View view, int position, long id) {
                        if (mIsDuringResetRowSelection) {
                            // Ignore if it's during the first resetRowSelection, because
                            // onChildSelected
                            // will be called again when rows are bound to the program table. if
                            // selectRow
                            // is called here, mSelectedRow is set and the second selectRow call
                            // doesn't
                            // work as intended.
                            mIsDuringResetRowSelection = false;
                            return;
                        }
                        selectRow(view);
                    }
                });
        mGrid.setFocusScrollStrategy(ProgramGrid.FOCUS_SCROLL_ALIGNED);
        mGrid.setWindowAlignmentOffset(mSelectionRow * mRowHeight);
        mGrid.setWindowAlignmentOffsetPercent(ProgramGrid.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
        mGrid.setItemAlignmentOffset(0);
        mGrid.setItemAlignmentOffsetPercent(ProgramGrid.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);

        mGrid.addOnScrollListener(
                new RecyclerView.OnScrollListener() {
                    @Override
                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                        if (DEBUG) {
                            Log.d(TAG, "ProgramGrid onScrollStateChanged. newState=" + newState);
                        }
                        if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
                            mPerformanceMonitor.startJankRecorder(
                                    EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY);
                        } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                            mPerformanceMonitor.stopJankRecorder(
                                    EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY);
                        }
                    }
                });

        RecyclerView.OnScrollListener onScrollListener =
                new RecyclerView.OnScrollListener() {
                    @Override
                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                        onHorizontalScrolled(dx);
                    }

                    @Override
                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                        if (DEBUG) {
                            Log.d(TAG, "TimelineRow onScrollStateChanged. newState=" + newState);
                        }
                        if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
                            mPerformanceMonitor.startJankRecorder(
                                    EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY);
                        } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                            mPerformanceMonitor.stopJankRecorder(
                                    EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY);
                        }
                    }
                };
        mTimelineRow.addOnScrollListener(onScrollListener);

        mCurrentTimeIndicator = mTable.findViewById(R.id.current_time_indicator);

        mShowAnimatorFull =
                createAnimator(
                        R.animator.program_guide_side_panel_enter_full,
                        0,
                        R.animator.program_guide_table_enter_full);
        mShowAnimatorFull.addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        if (mTimerEvent != null) {
                            mPerformanceMonitor.stopTimer(
                                    mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW);
                            mTimerEvent = null;
                        }
                        mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
                    }
                });

        mShowAnimatorPartial =
                createAnimator(
                        R.animator.program_guide_side_panel_enter_partial,
                        0,
                        R.animator.program_guide_table_enter_partial);
        mShowAnimatorPartial.addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        mSidePanelGridView.setVisibility(View.VISIBLE);
                        mSidePanelGridView.setAlpha(1.0f);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        if (mTimerEvent != null) {
                            mPerformanceMonitor.stopTimer(
                                    mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW);
                            mTimerEvent = null;
                        }
                        mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
                    }
                });

        mHideAnimatorFull =
                createAnimator(
                        R.animator.program_guide_side_panel_exit,
                        0,
                        R.animator.program_guide_table_exit);
        mHideAnimatorFull.addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        mContainer.setVisibility(View.GONE);
                    }
                });
        mHideAnimatorPartial =
                createAnimator(
                        R.animator.program_guide_side_panel_exit,
                        0,
                        R.animator.program_guide_table_exit);
        mHideAnimatorPartial.addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        mContainer.setVisibility(View.GONE);
                    }
                });

        mPartialToFullAnimator =
                createAnimator(
                        R.animator.program_guide_side_panel_hide,
                        R.animator.program_guide_side_panel_grid_fade_out,
                        R.animator.program_guide_table_partial_to_full);
        mFullToPartialAnimator =
                createAnimator(
                        R.animator.program_guide_side_panel_reveal,
                        R.animator.program_guide_side_panel_grid_fade_in,
                        R.animator.program_guide_table_full_to_partial);

        mProgramTableFadeOutAnimator =
                AnimatorInflater.loadAnimator(mActivity, R.animator.program_guide_table_fade_out);
        mProgramTableFadeOutAnimator.setTarget(mTable);
        mProgramTableFadeOutAnimator.addListener(
                new HardwareLayerAnimatorListenerAdapter(mTable) {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);

                        if (!isActive()) {
                            return;
                        }
                        mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId);
                        resetTimelineScroll();
                        if (!mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) {
                            mHandler.sendEmptyMessage(MSG_PROGRAM_TABLE_FADE_IN_ANIM);
                        }
                    }
                });
        mProgramTableFadeInAnimator =
                AnimatorInflater.loadAnimator(mActivity, R.animator.program_guide_table_fade_in);
        mProgramTableFadeInAnimator.setTarget(mTable);
        mProgramTableFadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable));
        mSharedPreference = PreferenceManager.getDefaultSharedPreferences(mActivity);
        mAccessibilityManager =
                (AccessibilityManager) mActivity.getSystemService(Context.ACCESSIBILITY_SERVICE);
        mShowGuidePartial =
                mAccessibilityManager.isEnabled()
                        || mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true);
        mAutoHideScheduler = new AutoHideScheduler(activity, this::hide);
    }

    @Override
    public void onRequestChildFocus(View oldFocus, View newFocus) {
        if (oldFocus != null && newFocus != null) {
            int selectionRowOffset = mSelectionRow * mRowHeight;
            if (oldFocus.getTop() < newFocus.getTop()) {
                // Selection moves downwards
                // Adjust scroll offset to be at the bottom of the target row and to expand up. This
                // will set the scroll target to be one row height up from its current position.
                mGrid.setWindowAlignmentOffset(selectionRowOffset + mRowHeight + mDetailHeight);
                mGrid.setItemAlignmentOffsetPercent(100);
            } else if (oldFocus.getTop() > newFocus.getTop()) {
                // Selection moves upwards
                // Adjust scroll offset to be at the top of the target row and to expand down. This
                // will set the scroll target to be one row height down from its current position.
                mGrid.setWindowAlignmentOffset(selectionRowOffset);
                mGrid.setItemAlignmentOffsetPercent(0);
            }
        }
    }

    /**
     * Show the program guide. This reveals the side panel, and the program guide table is shown
     * partially.
     *
     * <p>Note: the animation which starts together with ProgramGuide showing animation needs to be
     * initiated in {@code runnableAfterAnimatorReady}. If the animation starts together with
     * show(), the animation may drop some frames.
     */
    public void show(final Runnable runnableAfterAnimatorReady) {
        if (mContainer.getVisibility() == View.VISIBLE) {
            return;
        }
        mTimerEvent = mPerformanceMonitor.startTimer();
        mPerformanceMonitor.startJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
        mTracker.sendShowEpg();
        mTracker.sendScreenView(SCREEN_NAME);
        if (mPreShowRunnable != null) {
            mPreShowRunnable.run();
        }
        mVisibleDuration.start();

        mProgramManager.programGuideVisibilityChanged(true);
        mStartUtcTime =
                Utils.floorTime(
                        System.currentTimeMillis() - MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME,
                        HALF_HOUR_IN_MILLIS);
        mProgramManager.updateInitialTimeRange(mStartUtcTime, mStartUtcTime + mViewPortMillis);
        mProgramManager.addListener(mProgramManagerListener);
        mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS;
        mTimeListAdapter.update(mStartUtcTime);
        mTimelineRow.resetScroll();

        mContainer.setVisibility(View.VISIBLE);
        mActive = true;
        if (!mShowGuidePartial) {
            mTable.requestFocus();
        }
        positionCurrentTimeIndicator();
        mSidePanelGridView.setSelectedPosition(0);
        if (DEBUG) {
            Log.d(TAG, "show()");
        }
        mOnLayoutListenerForShow =
                new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                        mTable.setLayerType(View.LAYER_TYPE_HARDWARE, null);
                        mSidePanelGridView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
                        mTable.buildLayer();
                        mSidePanelGridView.buildLayer();
                        mOnLayoutListenerForShow = null;
                        mTimelineAnimation = true;
                        // Make sure that time indicator update starts after animation is finished.
                        startCurrentTimeIndicator(TIME_INDICATOR_UPDATE_FREQUENCY);
                        if (DEBUG) {
                            mContainer
                                    .getViewTreeObserver()
                                    .addOnDrawListener(
                                            new ViewTreeObserver.OnDrawListener() {
                                                long time = System.currentTimeMillis();
                                                int count = 0;

                                                @Override
                                                public void onDraw() {
                                                    long curtime = System.currentTimeMillis();
                                                    Log.d(
                                                            TAG,
                                                            "onDraw "
                                                                    + count++
                                                                    + " "
                                                                    + (curtime - time)
                                                                    + "ms");
                                                    time = curtime;
                                                    if (count > 10) {
                                                        mContainer
                                                                .getViewTreeObserver()
                                                                .removeOnDrawListener(this);
                                                    }
                                                }
                                            });
                        }
                        updateGuidePosition();
                        runnableAfterAnimatorReady.run();
                        if (mShowGuidePartial) {
                            mShowAnimatorPartial.start();
                        } else {
                            mShowAnimatorFull.start();
                        }
                    }
                };
        mContainer.getViewTreeObserver().addOnGlobalLayoutListener(mOnLayoutListenerForShow);
        scheduleHide();
    }

    /** Hide the program guide. */
    public void hide() {
        if (!isActive()) {
            return;
        }
        if (mOnLayoutListenerForShow != null) {
            mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(mOnLayoutListenerForShow);
            mOnLayoutListenerForShow = null;
        }
        mTracker.sendHideEpg(mVisibleDuration.reset());
        cancelHide();
        mProgramManager.programGuideVisibilityChanged(false);
        mProgramManager.removeListener(mProgramManagerListener);
        mActive = false;
        if (!mShowGuidePartial) {
            mHideAnimatorFull.start();
        } else {
            mHideAnimatorPartial.start();
        }

        // Clears fade-out/in animation for genre change
        if (mProgramTableFadeOutAnimator.isRunning()) {
            mProgramTableFadeOutAnimator.cancel();
        }
        if (mProgramTableFadeInAnimator.isRunning()) {
            mProgramTableFadeInAnimator.cancel();
        }
        mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM);
        mTable.setAlpha(1.0f);

        mTimelineAnimation = false;
        stopCurrentTimeIndicator();
        if (mPostHideRunnable != null) {
            mPostHideRunnable.run();
        }
    }

    /** Schedules hiding the program guide. */
    public void scheduleHide() {
        mAutoHideScheduler.schedule(mShowDurationMillis);
    }

    /** Cancels hiding the program guide. */
    public void cancelHide() {
        mAutoHideScheduler.cancel();
    }

    /** Process the {@code KEYCODE_BACK} key event. */
    public void onBackPressed() {
        hide();
    }

    /** Returns {@code true} if the program guide should process the input events. */
    public boolean isActive() {
        return mActive;
    }

    /**
     * Returns {@code true} if the program guide is shown, i.e. showing animation is done and hiding
     * animation is not started yet.
     */
    public boolean isRunningAnimation() {
        return mShowAnimatorPartial.isStarted()
                || mShowAnimatorFull.isStarted()
                || mHideAnimatorPartial.isStarted()
                || mHideAnimatorFull.isStarted();
    }

    /** Returns if program table is in full screen mode. * */
    boolean isFull() {
        return !mShowGuidePartial;
    }

    /** Requests change genre to {@code genreId}. */
    void requestGenreChange(int genreId) {
        if (mLastRequestedGenreId == genreId) {
            // When Recycler.onLayout() removes its children to recycle,
            // View tries to find next focus candidate immediately
            // so GenreListAdapter can take focus back while it's hiding.
            // Returns early here to prevent re-entrance.
            return;
        }
        mLastRequestedGenreId = genreId;
        if (mProgramTableFadeOutAnimator.isStarted()) {
            // When requestGenreChange is called repeatedly in short time, we keep the fade-out
            // state for mTableFadeAnimDuration from now. Without it, we'll see blinks.
            mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM);
            mHandler.sendEmptyMessageDelayed(
                    MSG_PROGRAM_TABLE_FADE_IN_ANIM, mTableFadeAnimDuration);
            return;
        }
        if (mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) {
            mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId);
            mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM);
            mHandler.sendEmptyMessageDelayed(
                    MSG_PROGRAM_TABLE_FADE_IN_ANIM, mTableFadeAnimDuration);
            return;
        }
        if (mProgramTableFadeInAnimator.isStarted()) {
            mProgramTableFadeInAnimator.cancel();
        }

        mProgramTableFadeOutAnimator.start();
    }

    /** Returns the scroll offset of the time line row in pixels. */
    int getTimelineRowScrollOffset() {
        return mTimelineRow.getScrollOffset();
    }

    /** Returns the program grid view that hold all component views. */
    ProgramGrid getProgramGrid() {
        return mGrid;
    }

    /** Returns if Accessibility is enabled. */
    boolean isAccessibilityEnabled() {
        return mAccessibilityManager.isEnabled();
    }

    /** Gets {@link VerticalGridView} for "genre select" side panel. */
    VerticalGridView getSidePanel() {
        return mSidePanelGridView;
    }

    /** Returns the program manager the program guide is using to provide program information. */
    ProgramManager getProgramManager() {
        return mProgramManager;
    }

    private void updateGuidePosition() {
        // Align EPG at vertical center, if EPG table height is less than the screen size.
        Resources res = mActivity.getResources();
        int screenHeight = mContainer.getHeight();
        if (screenHeight <= 0) {
            // mContainer is not initialized yet.
            return;
        }
        int startPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start);
        int topPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_top);
        int bottomPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom);
        int tableHeight =
                res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height)
                        + mDetailHeight
                        + mRowHeight * mGrid.getAdapter().getItemCount()
                        + topPadding
                        + bottomPadding;
        if (tableHeight > screenHeight) {
            // EPG height is longer that the screen height.
            mTable.setPaddingRelative(startPadding, topPadding, 0, 0);
            LayoutParams layoutParams = mTable.getLayoutParams();
            layoutParams.height = LayoutParams.WRAP_CONTENT;
            mTable.setLayoutParams(layoutParams);
        } else {
            mTable.setPaddingRelative(startPadding, topPadding, 0, bottomPadding);
            LayoutParams layoutParams = mTable.getLayoutParams();
            layoutParams.height = tableHeight;
            mTable.setLayoutParams(layoutParams);
        }
    }

    private Animator createAnimator(
            int sidePanelAnimResId, int sidePanelGridAnimResId, int tableAnimResId) {
        List<Animator> animatorList = new ArrayList<>();

        Animator sidePanelAnimator = AnimatorInflater.loadAnimator(mActivity, sidePanelAnimResId);
        sidePanelAnimator.setTarget(mSidePanel);
        animatorList.add(sidePanelAnimator);

        if (sidePanelGridAnimResId != 0) {
            Animator sidePanelGridAnimator =
                    AnimatorInflater.loadAnimator(mActivity, sidePanelGridAnimResId);
            sidePanelGridAnimator.setTarget(mSidePanelGridView);
            sidePanelGridAnimator.addListener(
                    new HardwareLayerAnimatorListenerAdapter(mSidePanelGridView));
            animatorList.add(sidePanelGridAnimator);
        }
        Animator tableAnimator = AnimatorInflater.loadAnimator(mActivity, tableAnimResId);
        tableAnimator.setTarget(mTable);
        tableAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable));
        animatorList.add(tableAnimator);

        AnimatorSet set = new AnimatorSet();
        set.playTogether(animatorList);
        return set;
    }

    private void startFull() {
        if (!mShowGuidePartial) {
            return;
        }
        mShowGuidePartial = false;
        mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply();
        mPartialToFullAnimator.start();
    }

    private void startPartial() {
        if (mShowGuidePartial) {
            return;
        }
        mShowGuidePartial = true;
        mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply();
        mFullToPartialAnimator.start();
    }

    private void startCurrentTimeIndicator(long initialDelay) {
        mHandler.postDelayed(mUpdateTimeIndicator, initialDelay);
    }

    private void stopCurrentTimeIndicator() {
        mHandler.removeCallbacks(mUpdateTimeIndicator);
    }

    private void positionCurrentTimeIndicator() {
        int offset =
                GuideUtils.convertMillisToPixel(mStartUtcTime, System.currentTimeMillis())
                        - mTimelineRow.getScrollOffset();
        if (offset < 0) {
            mCurrentTimeIndicator.setVisibility(View.GONE);
        } else {
            if (mCurrentTimeIndicatorWidth == 0) {
                mCurrentTimeIndicator.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
                mCurrentTimeIndicatorWidth = mCurrentTimeIndicator.getMeasuredWidth();
            }
            mCurrentTimeIndicator.setPaddingRelative(
                    offset - mCurrentTimeIndicatorWidth / 2, 0, 0, 0);
            mCurrentTimeIndicator.setVisibility(View.VISIBLE);
        }
    }

    private void resetTimelineScroll() {
        if (mProgramManager.getFromUtcMillis() != mStartUtcTime) {
            boolean timelineAnimation = mTimelineAnimation;
            mTimelineAnimation = false;
            // mProgramManagerListener.onTimeRangeUpdated() will be called by shiftTime().
            mProgramManager.shiftTime(mStartUtcTime - mProgramManager.getFromUtcMillis());
            mTimelineAnimation = timelineAnimation;
        }
    }

    private void onHorizontalScrolled(int dx) {
        if (DEBUG) Log.d(TAG, "onHorizontalScrolled(dx=" + dx + ")");
        positionCurrentTimeIndicator();
        for (int i = 0, n = mGrid.getChildCount(); i < n; ++i) {
            mGrid.getChildAt(i).findViewById(R.id.row).scrollBy(dx, 0);
        }
    }

    private void resetRowSelection() {
        if (mDetailOutAnimator != null) {
            mDetailOutAnimator.end();
        }
        if (mDetailInAnimator != null) {
            mDetailInAnimator.cancel();
        }
        mSelectedRow = null;
        mIsDuringResetRowSelection = true;
        mGrid.setSelectedPosition(
                Math.max(mProgramManager.getChannelIndex(mChannelTuner.getCurrentChannel()), 0));
        mGrid.resetFocusState();
        mGrid.onItemSelectionReset();
        mIsDuringResetRowSelection = false;
    }

    private void selectRow(View row) {
        if (row == null || row == mSelectedRow) {
            return;
        }
        if (mSelectedRow == null
                || mGrid.getChildAdapterPosition(mSelectedRow) == RecyclerView.NO_POSITION) {
            if (mSelectedRow != null) {
                View oldDetailView = mSelectedRow.findViewById(R.id.detail);
                oldDetailView.setVisibility(View.GONE);
            }
            View detailView = row.findViewById(R.id.detail);
            detailView.findViewById(R.id.detail_content_full).setAlpha(1);
            detailView.findViewById(R.id.detail_content_full).setTranslationY(0);
            ViewUtils.setLayoutHeight(detailView, mDetailHeight);
            detailView.setVisibility(View.VISIBLE);

            final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row);
            programRow.post(programRow::focusCurrentProgram);
        } else {
            animateRowChange(mSelectedRow, row);
        }
        mSelectedRow = row;
    }

    private void animateRowChange(View outRow, View inRow) {
        if (mDetailOutAnimator != null) {
            mDetailOutAnimator.end();
        }
        if (mDetailInAnimator != null) {
            mDetailInAnimator.cancel();
        }

        int operationDirection = mGrid.getLastUpDownDirection();
        int animationPadding = 0;
        if (operationDirection == View.FOCUS_UP) {
            animationPadding = mDetailPadding;
        } else if (operationDirection == View.FOCUS_DOWN) {
            animationPadding = -mDetailPadding;
        }

        View outDetail = outRow != null ? outRow.findViewById(R.id.detail) : null;
        if (outDetail != null && outDetail.isShown()) {
            final View outDetailContent = outDetail.findViewById(R.id.detail_content_full);

            Animator fadeOutAnimator =
                    ObjectAnimator.ofPropertyValuesHolder(
                            outDetailContent,
                            PropertyValuesHolder.ofFloat(View.ALPHA, outDetail.getAlpha(), 0f),
                            PropertyValuesHolder.ofFloat(
                                    View.TRANSLATION_Y,
                                    outDetailContent.getTranslationY(),
                                    animationPadding));
            fadeOutAnimator.setStartDelay(0);
            fadeOutAnimator.setDuration(mAnimationDuration);
            fadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(outDetailContent));

            Animator collapseAnimator =
                    ViewUtils.createHeightAnimator(
                            outDetail, ViewUtils.getLayoutHeight(outDetail), 0);
            collapseAnimator.setStartDelay(mAnimationDuration);
            collapseAnimator.setDuration(mTableFadeAnimDuration);
            collapseAnimator.addListener(
                    new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationStart(Animator animator) {
                            outDetailContent.setVisibility(View.GONE);
                        }

                        @Override
                        public void onAnimationEnd(Animator animator) {
                            outDetailContent.setVisibility(View.VISIBLE);
                        }
                    });

            AnimatorSet outAnimator = new AnimatorSet();
            outAnimator.playTogether(fadeOutAnimator, collapseAnimator);
            outAnimator.addListener(
                    new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animator) {
                            mDetailOutAnimator = null;
                        }
                    });
            mDetailOutAnimator = outAnimator;
            outAnimator.start();
        }

        View inDetail = inRow != null ? inRow.findViewById(R.id.detail) : null;
        if (inDetail != null) {
            final View inDetailContent = inDetail.findViewById(R.id.detail_content_full);

            Animator expandAnimator = ViewUtils.createHeightAnimator(inDetail, 0, mDetailHeight);
            expandAnimator.setStartDelay(mAnimationDuration);
            expandAnimator.setDuration(mTableFadeAnimDuration);
            expandAnimator.addListener(
                    new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationStart(Animator animator) {
                            inDetailContent.setVisibility(View.GONE);
                        }

                        @Override
                        public void onAnimationEnd(Animator animator) {
                            inDetailContent.setVisibility(View.VISIBLE);
                            inDetailContent.setAlpha(0);
                        }
                    });
            Animator fadeInAnimator =
                    ObjectAnimator.ofPropertyValuesHolder(
                            inDetailContent,
                            PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f),
                            PropertyValuesHolder.ofFloat(
                                    View.TRANSLATION_Y, -animationPadding, 0f));
            fadeInAnimator.setDuration(mAnimationDuration);
            fadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(inDetailContent));

            AnimatorSet inAnimator = new AnimatorSet();
            inAnimator.playSequentially(expandAnimator, fadeInAnimator);
            inAnimator.addListener(
                    new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animator) {
                            mDetailInAnimator = null;
                        }
                    });
            mDetailInAnimator = inAnimator;
            inAnimator.start();
        }
    }

    @Override
    public void onAccessibilityStateChanged(boolean enabled) {
        mAutoHideScheduler.onAccessibilityStateChanged(enabled);
    }

    private class GlobalFocusChangeListener
            implements ViewTreeObserver.OnGlobalFocusChangeListener {
        private static final int UNKNOWN = 0;
        private static final int SIDE_PANEL = 1;
        private static final int PROGRAM_TABLE = 2;
        private static final int CHANNEL_COLUMN = 3;

        @Override
        public void onGlobalFocusChanged(View oldFocus, View newFocus) {
            if (DEBUG) Log.d(TAG, "onGlobalFocusChanged " + oldFocus + " -> " + newFocus);
            if (!isActive()) {
                return;
            }
            int fromLocation = getLocation(oldFocus);
            int toLocation = getLocation(newFocus);
            if (fromLocation == SIDE_PANEL && toLocation == PROGRAM_TABLE) {
                startFull();
            } else if (fromLocation == PROGRAM_TABLE && toLocation == SIDE_PANEL) {
                startPartial();
            } else if (fromLocation == CHANNEL_COLUMN && toLocation == PROGRAM_TABLE) {
                startFull();
            } else if (fromLocation == PROGRAM_TABLE && toLocation == CHANNEL_COLUMN) {
                startPartial();
            }
        }

        private int getLocation(View view) {
            if (view == null) {
                return UNKNOWN;
            }
            for (Object obj = view; obj instanceof View; obj = ((View) obj).getParent()) {
                if (obj == mSidePanel) {
                    return SIDE_PANEL;
                } else if (obj == mGrid) {
                    if (view instanceof ProgramItemView) {
                        return PROGRAM_TABLE;
                    } else {
                        return CHANNEL_COLUMN;
                    }
                }
            }
            return UNKNOWN;
        }
    }

    private class ProgramManagerListener extends ProgramManager.ListenerAdapter {
        @Override
        public void onTimeRangeUpdated() {
            int scrollOffset =
                    (int) (mWidthPerHour * mProgramManager.getShiftedTime() / HOUR_IN_MILLIS);
            if (DEBUG) {
                Log.d(
                        TAG,
                        "Horizontal scroll to "
                                + scrollOffset
                                + " pixels ("
                                + mProgramManager.getShiftedTime()
                                + " millis)");
            }
            mTimelineRow.scrollTo(scrollOffset, mTimelineAnimation);
        }
    }

    private static class ProgramGuideHandler extends WeakHandler<ProgramGuide> {
        ProgramGuideHandler(ProgramGuide ref) {
            super(ref);
        }

        @Override
        public void handleMessage(Message msg, @NonNull ProgramGuide programGuide) {
            if (msg.what == MSG_PROGRAM_TABLE_FADE_IN_ANIM) {
                programGuide.mProgramTableFadeInAnimator.start();
            }
        }
    }
}
