/*
 * 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.dvr.ui.browse;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import androidx.leanback.app.BrowseFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ClassPresenterSelector;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.TitleViewAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.data.GenreItems;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.SortedArrayAdapter;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;

/** {@link BrowseFragment} for DVR functions. */
@TargetApi(Build.VERSION_CODES.N)
@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
public class DvrBrowseFragment extends BrowseFragment
        implements RecordedProgramListener,
                ScheduledRecordingListener,
                SeriesRecordingListener,
                OnDvrScheduleLoadFinishedListener,
                OnRecordedProgramLoadFinishedListener {
    private static final String TAG = "DvrBrowseFragment";
    private static final boolean DEBUG = false;

    private static final int MAX_RECENT_ITEM_COUNT = 4;
    private static final int MAX_SCHEDULED_ITEM_COUNT = 4;

    private boolean mShouldShowScheduleRow;
    private boolean mEntranceTransitionEnded;

    private RecentRowAdapter mRecentAdapter;
    private ScheduleAdapter mScheduleAdapter;
    private SeriesAdapter mSeriesAdapter;
    private RecordedProgramAdapter[] mGenreAdapters =
            new RecordedProgramAdapter[GenreItems.getGenreCount() + 1];
    private ListRow mRecentRow;
    private ListRow mScheduledRow;
    private ListRow mSeriesRow;
    private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1];
    private List<String> mGenreLabels;
    private DvrDataManager mDvrDataManager;
    private DvrScheduleManager mDvrScheudleManager;
    private ArrayObjectAdapter mRowsAdapter;
    private ClassPresenterSelector mPresenterSelector;
    private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>();
    private final Handler mHandler = new Handler();
    private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener =
            new OnGlobalFocusChangeListener() {
                @Override
                public void onGlobalFocusChanged(View oldFocus, View newFocus) {
                    if (oldFocus instanceof RecordingCardView) {
                        ((RecordingCardView) oldFocus).expandTitle(false, true);
                    }
                    if (newFocus instanceof RecordingCardView) {
                        // If the header transition is ongoing, expand cards immediately without
                        // animation to make a smooth transition.
                        ((RecordingCardView) newFocus).expandTitle(true, !isInHeadersTransition());
                    }
                }
            };

    private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR =
            (Object lhs, Object rhs) -> {
                if (lhs instanceof SeriesRecording) {
                    lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId());
                }
                if (rhs instanceof SeriesRecording) {
                    rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId());
                }
                if (lhs instanceof RecordedProgram) {
                    if (rhs instanceof RecordedProgram) {
                        return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
                                .reversed()
                                .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
                    } else {
                        return -1;
                    }
                } else if (rhs instanceof RecordedProgram) {
                    return 1;
                } else {
                    return 0;
                }
            };

    private static final Comparator<Object> SCHEDULE_COMPARATOR =
            (Object lhs, Object rhs) -> {
                if (lhs instanceof ScheduledRecording) {
                    if (rhs instanceof ScheduledRecording) {
                        return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
                                .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
                    } else {
                        return -1;
                    }
                } else if (rhs instanceof ScheduledRecording) {
                    return 1;
                } else {
                    return 0;
                }
            };

    static final Comparator<Object> RECENT_ROW_COMPARATOR =
            (Object lhs, Object rhs) -> {
                if (lhs instanceof ScheduledRecording) {
                    if (rhs instanceof ScheduledRecording) {
                        return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
                                .reversed()
                                .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
                    } else if (rhs instanceof RecordedProgram) {
                        ScheduledRecording scheduled = (ScheduledRecording) lhs;
                        RecordedProgram recorded = (RecordedProgram) rhs;
                        int compare =
                                Long.compare(
                                        recorded.getStartTimeUtcMillis(),
                                        scheduled.getStartTimeMs());
                        // recorded program first when the start times are the same
                        return compare == 0 ? 1 : compare;
                    } else {
                        return -1;
                    }
                } else if (lhs instanceof RecordedProgram) {
                    if (rhs instanceof RecordedProgram) {
                        return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
                                .reversed()
                                .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
                    } else if (rhs instanceof ScheduledRecording) {
                        RecordedProgram recorded = (RecordedProgram) lhs;
                        ScheduledRecording scheduled = (ScheduledRecording) rhs;
                        int compare =
                                Long.compare(
                                        scheduled.getStartTimeMs(),
                                        recorded.getStartTimeUtcMillis());
                        // recorded program first when the start times are the same
                        return compare == 0 ? -1 : compare;
                    } else {
                        return -1;
                    }
                } else {
                    return !(rhs instanceof RecordedProgram) && !(rhs instanceof ScheduledRecording)
                            ? 0
                            : 1;
                }
            };

    private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener =
            new DvrScheduleManager.OnConflictStateChangeListener() {
                @Override
                public void onConflictStateChange(
                        boolean conflict, ScheduledRecording... schedules) {
                    if (mScheduleAdapter != null) {
                        for (ScheduledRecording schedule : schedules) {
                            onScheduledRecordingConflictStatusChanged(schedule);
                        }
                    }
                }
            };

    private final Runnable mUpdateRowsRunnable = this::updateRows;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        if (DEBUG) Log.d(TAG, "onCreate");
        super.onCreate(savedInstanceState);
        Context context = getContext();
        TvSingletons singletons = TvSingletons.getSingletons(context);
        mDvrDataManager = singletons.getDvrDataManager();
        mDvrScheudleManager = singletons.getDvrScheduleManager();
        mPresenterSelector =
                new ClassPresenterSelector()
                        .addClassPresenter(
                                ScheduledRecording.class, new ScheduledRecordingPresenter(context))
                        .addClassPresenter(
                                RecordedProgram.class, new RecordedProgramPresenter(context))
                        .addClassPresenter(
                                SeriesRecording.class, new SeriesRecordingPresenter(context))
                        .addClassPresenter(
                                FullScheduleCardHolder.class,
                                new FullSchedulesCardPresenter(context))
                        .addClassPresenter(
                                DvrHistoryCardHolder.class, new DvrHistoryCardPresenter(context));

        mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context)));
        mGenreLabels.add(getString(R.string.dvr_main_others));
        prepareUiElements();
        if (!startBrowseIfDvrInitialized()) {
            if (!mDvrDataManager.isDvrScheduleLoadFinished()) {
                mDvrDataManager.addDvrScheduleLoadFinishedListener(this);
            }
            if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
                mDvrDataManager.addRecordedProgramLoadFinishedListener(this);
            }
        }
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        view.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
    }

    @Override
    public void onDestroyView() {
        getView()
                .getViewTreeObserver()
                .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
        super.onDestroyView();
    }

    @Override
    public void onDestroy() {
        if (DEBUG) Log.d(TAG, "onDestroy");
        mHandler.removeCallbacks(mUpdateRowsRunnable);
        mDvrScheudleManager.removeOnConflictStateChangeListener(mOnConflictStateChangeListener);
        mDvrDataManager.removeRecordedProgramListener(this);
        mDvrDataManager.removeScheduledRecordingListener(this);
        mDvrDataManager.removeSeriesRecordingListener(this);
        mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
        mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
        mRowsAdapter.clear();
        mSeriesId2LatestProgram.clear();
        for (Presenter presenter : mPresenterSelector.getPresenters()) {
            if (presenter instanceof DvrItemPresenter) {
                ((DvrItemPresenter) presenter).unbindAllViewHolders();
            }
        }
        super.onDestroy();
    }

    @Override
    public void onDvrScheduleLoadFinished() {
        startBrowseIfDvrInitialized();
        mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
    }

    @Override
    public void onRecordedProgramLoadFinished() {
        startBrowseIfDvrInitialized();
        mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
    }

    @Override
    public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
        for (RecordedProgram recordedProgram : recordedPrograms) {
            handleRecordedProgramAdded(recordedProgram, true);
        }
        postUpdateRows();
    }

    @Override
    public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
        for (RecordedProgram recordedProgram : recordedPrograms) {
            if (recordedProgram.isVisible()) {
                handleRecordedProgramChanged(recordedProgram);
            }
        }
        postUpdateRows();
    }

    @Override
    public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
        for (RecordedProgram recordedProgram : recordedPrograms) {
            handleRecordedProgramRemoved(recordedProgram);
        }
        postUpdateRows();
    }

    // No need to call updateRows() during ScheduledRecordings' change because
    // the row for ScheduledRecordings is always displayed.
    @Override
    public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
        for (ScheduledRecording scheduleRecording : scheduledRecordings) {
            if (needToShowScheduledRecording(scheduleRecording)) {
                mScheduleAdapter.add(scheduleRecording);
            } else if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
                mRecentAdapter.add(scheduleRecording);
            }
        }
    }

    @Override
    public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
        for (ScheduledRecording scheduleRecording : scheduledRecordings) {
            mScheduleAdapter.remove(scheduleRecording);
            if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
                mRecentAdapter.remove(scheduleRecording);
            }
        }
    }

    @Override
    public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
        for (ScheduledRecording scheduleRecording : scheduledRecordings) {
            if (needToShowScheduledRecording(scheduleRecording)) {
                mScheduleAdapter.change(scheduleRecording);
            } else {
                mScheduleAdapter.removeWithId(scheduleRecording);
            }
            if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
                mRecentAdapter.change(scheduleRecording);
            }
        }
    }

    private void onScheduledRecordingConflictStatusChanged(ScheduledRecording... schedules) {
        for (ScheduledRecording schedule : schedules) {
            if (needToShowScheduledRecording(schedule)) {
                if (mScheduleAdapter.contains(schedule)) {
                    mScheduleAdapter.change(schedule);
                }
            } else {
                mScheduleAdapter.removeWithId(schedule);
            }
        }
    }

    @Override
    public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {
        handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings));
        postUpdateRows();
    }

    @Override
    public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
        handleSeriesRecordingsRemoved(Arrays.asList(seriesRecordings));
        postUpdateRows();
    }

    @Override
    public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
        handleSeriesRecordingsChanged(Arrays.asList(seriesRecordings));
        postUpdateRows();
    }

    // Workaround of b/29108300
    @Override
    public void showTitle(int flags) {
        flags &= ~TitleViewAdapter.SEARCH_VIEW_VISIBLE;
        super.showTitle(flags);
    }

    @Override
    protected void onEntranceTransitionEnd() {
        super.onEntranceTransitionEnd();
        if (mShouldShowScheduleRow) {
            showScheduledRowInternal();
        }
        mEntranceTransitionEnded = true;
    }

    void showScheduledRow() {
        if (!mEntranceTransitionEnded) {
            setHeadersState(HEADERS_HIDDEN);
            mShouldShowScheduleRow = true;
        } else {
            showScheduledRowInternal();
        }
    }

    private void showScheduledRowInternal() {
        setSelectedPosition(mRowsAdapter.indexOf(mScheduledRow), true, null);
        if (getHeadersState() == HEADERS_ENABLED) {
            startHeadersTransition(false);
        }
        mShouldShowScheduleRow = false;
    }

    private void prepareUiElements() {
        setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge));
        setHeadersState(HEADERS_ENABLED);
        setHeadersTransitionOnBackEnabled(false);
        setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null));
        mRowsAdapter = new ArrayObjectAdapter(new DvrListRowPresenter(getContext()));
        setAdapter(mRowsAdapter);
        prepareEntranceTransition();
    }

    private boolean startBrowseIfDvrInitialized() {
        if (mDvrDataManager.isInitialized()) {
            // Setup rows
            mRecentAdapter = new RecentRowAdapter(MAX_RECENT_ITEM_COUNT);
            mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT);
            mSeriesAdapter = new SeriesAdapter();
            for (int i = 0; i < mGenreAdapters.length; i++) {
                mGenreAdapters[i] = new RecordedProgramAdapter();
            }
            // Schedule Recordings.
            // only get not started or in progress recordings
            List<ScheduledRecording> schedules = mDvrDataManager.getAvailableScheduledRecordings();
            onScheduledRecordingAdded(ScheduledRecording.toArray(schedules));
            mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
            // Recorded Programs.
            for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
                if (recordedProgram.isVisible()) {
                    handleRecordedProgramAdded(recordedProgram, false);
                }
            }
            // only get failed recordings
            for (ScheduledRecording scheduledRecording :
                    mDvrDataManager.getFailedScheduledRecordings()) {
                onScheduledRecordingAdded(scheduledRecording);
            }
            mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER);

            // Series Recordings. Series recordings should be added after recorded programs, because
            // we build series recordings' latest program information while adding recorded
            // programs.
            List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings();
            handleSeriesRecordingsAdded(recordings);
            mRecentRow =
                    new ListRow(
                            new HeaderItem(getString(R.string.dvr_main_recent)), mRecentAdapter);
            mScheduledRow =
                    new ListRow(
                            new HeaderItem(getString(R.string.dvr_main_scheduled)),
                            mScheduleAdapter);
            mSeriesRow =
                    new ListRow(
                            new HeaderItem(getString(R.string.dvr_main_series)), mSeriesAdapter);
            mRowsAdapter.add(mScheduledRow);
            updateRows();
            // Initialize listeners
            mDvrDataManager.addRecordedProgramListener(this);
            mDvrDataManager.addScheduledRecordingListener(this);
            mDvrDataManager.addSeriesRecordingListener(this);
            mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener);
            startEntranceTransition();
            return true;
        }
        return false;
    }

    private void handleRecordedProgramAdded(
            RecordedProgram recordedProgram, boolean updateSeriesRecording) {
        mRecentAdapter.add(recordedProgram);
        String seriesId = recordedProgram.getSeriesId();
        SeriesRecording seriesRecording = null;
        if (!TextUtils.isEmpty(seriesId)) {
            seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
            RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId);
            if (latestProgram == null
                    || RecordedProgram.START_TIME_THEN_ID_COMPARATOR.compare(
                                    latestProgram, recordedProgram)
                            < 0) {
                mSeriesId2LatestProgram.put(seriesId, recordedProgram);
                if (updateSeriesRecording && seriesRecording != null) {
                    onSeriesRecordingChanged(seriesRecording);
                }
            }
        }
        if (seriesRecording == null) {
            for (RecordedProgramAdapter adapter :
                    getGenreAdapters(recordedProgram.getCanonicalGenres())) {
                adapter.add(recordedProgram);
            }
        }
    }

    private void handleRecordedProgramRemoved(RecordedProgram recordedProgram) {
        mRecentAdapter.remove(recordedProgram);
        String seriesId = recordedProgram.getSeriesId();
        if (!TextUtils.isEmpty(seriesId)) {
            SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
            RecordedProgram latestProgram =
                    mSeriesId2LatestProgram.get(recordedProgram.getSeriesId());
            if (latestProgram != null && latestProgram.getId() == recordedProgram.getId()) {
                if (seriesRecording != null) {
                    updateLatestRecordedProgram(seriesRecording);
                    onSeriesRecordingChanged(seriesRecording);
                }
            }
        }
        for (RecordedProgramAdapter adapter :
                getGenreAdapters(recordedProgram.getCanonicalGenres())) {
            adapter.remove(recordedProgram);
        }
    }

    private void handleRecordedProgramChanged(RecordedProgram recordedProgram) {
        mRecentAdapter.change(recordedProgram);
        String seriesId = recordedProgram.getSeriesId();
        SeriesRecording seriesRecording = null;
        if (!TextUtils.isEmpty(seriesId)) {
            seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
            RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId);
            if (latestProgram == null
                    || RecordedProgram.START_TIME_THEN_ID_COMPARATOR.compare(
                                    latestProgram, recordedProgram)
                            <= 0) {
                mSeriesId2LatestProgram.put(seriesId, recordedProgram);
                if (seriesRecording != null) {
                    onSeriesRecordingChanged(seriesRecording);
                }
            } else if (latestProgram.getId() == recordedProgram.getId()) {
                if (seriesRecording != null) {
                    updateLatestRecordedProgram(seriesRecording);
                    onSeriesRecordingChanged(seriesRecording);
                }
            }
        }
        if (seriesRecording == null) {
            updateGenreAdapters(
                    getGenreAdapters(recordedProgram.getCanonicalGenres()), recordedProgram);
        } else {
            updateGenreAdapters(new ArrayList<>(), recordedProgram);
        }
    }

    private void handleSeriesRecordingsAdded(List<SeriesRecording> seriesRecordings) {
        for (SeriesRecording seriesRecording : seriesRecordings) {
            mSeriesAdapter.add(seriesRecording);
            if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) {
                for (RecordedProgramAdapter adapter :
                        getGenreAdapters(seriesRecording.getCanonicalGenreIds())) {
                    adapter.add(seriesRecording);
                }
            }
        }
    }

    private void handleSeriesRecordingsRemoved(List<SeriesRecording> seriesRecordings) {
        for (SeriesRecording seriesRecording : seriesRecordings) {
            mSeriesAdapter.remove(seriesRecording);
            for (RecordedProgramAdapter adapter :
                    getGenreAdapters(seriesRecording.getCanonicalGenreIds())) {
                adapter.remove(seriesRecording);
            }
        }
    }

    private void handleSeriesRecordingsChanged(List<SeriesRecording> seriesRecordings) {
        for (SeriesRecording seriesRecording : seriesRecordings) {
            mSeriesAdapter.change(seriesRecording);
            if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) {
                updateGenreAdapters(
                        getGenreAdapters(seriesRecording.getCanonicalGenreIds()), seriesRecording);
            } else {
                // Remove series recording from all genre rows if it has no recorded program
                updateGenreAdapters(new ArrayList<>(), seriesRecording);
            }
        }
    }

    private List<RecordedProgramAdapter> getGenreAdapters(ImmutableList<String> genres) {
        List<RecordedProgramAdapter> result = new ArrayList<>();
        if (genres == null || genres.isEmpty()) {
            result.add(mGenreAdapters[mGenreAdapters.length - 1]);
        } else {
            for (String genre : genres) {
                int genreId = GenreItems.getId(genre);
                if (genreId >= mGenreAdapters.length) {
                    Log.d(TAG, "Wrong Genre ID: " + genreId);
                } else {
                    result.add(mGenreAdapters[genreId]);
                }
            }
        }
        return result;
    }

    private List<RecordedProgramAdapter> getGenreAdapters(int[] genreIds) {
        List<RecordedProgramAdapter> result = new ArrayList<>();
        if (genreIds == null || genreIds.length == 0) {
            result.add(mGenreAdapters[mGenreAdapters.length - 1]);
        } else {
            for (int genreId : genreIds) {
                if (genreId >= mGenreAdapters.length) {
                    Log.d(TAG, "Wrong Genre ID: " + genreId);
                } else {
                    result.add(mGenreAdapters[genreId]);
                }
            }
        }
        return result;
    }

    private void updateGenreAdapters(List<RecordedProgramAdapter> adapters, Object r) {
        for (RecordedProgramAdapter adapter : mGenreAdapters) {
            if (adapters.contains(adapter)) {
                adapter.change(r);
            } else {
                adapter.remove(r);
            }
        }
    }

    private void postUpdateRows() {
        mHandler.removeCallbacks(mUpdateRowsRunnable);
        mHandler.post(mUpdateRowsRunnable);
    }

    private void updateRows() {
        int visibleRowsCount = 1; // Schedule's Row will never be empty
        if (mRecentAdapter.size() <= 1) {
            // remove the row if there is only the DVR history card
            mRowsAdapter.remove(mRecentRow);
        } else {
            if (mRowsAdapter.indexOf(mRecentRow) < 0) {
                mRowsAdapter.add(0, mRecentRow);
            }
            visibleRowsCount++;
        }
        if (mSeriesAdapter.isEmpty()) {
            mRowsAdapter.remove(mSeriesRow);
        } else {
            if (mRowsAdapter.indexOf(mSeriesRow) < 0) {
                mRowsAdapter.add(visibleRowsCount, mSeriesRow);
            }
            visibleRowsCount++;
        }
        for (int i = 0; i < mGenreAdapters.length; i++) {
            RecordedProgramAdapter adapter = mGenreAdapters[i];
            if (adapter != null) {
                if (adapter.isEmpty()) {
                    mRowsAdapter.remove(mGenreRows[i]);
                } else {
                    if (mGenreRows[i] == null || mRowsAdapter.indexOf(mGenreRows[i]) < 0) {
                        mGenreRows[i] = new ListRow(new HeaderItem(mGenreLabels.get(i)), adapter);
                        mRowsAdapter.add(visibleRowsCount, mGenreRows[i]);
                    }
                    visibleRowsCount++;
                }
            }
        }
        if (getSelectedPosition() >= mRowsAdapter.size()) {
            setSelectedPosition(mRowsAdapter.size() - 1);
        }
    }

    private boolean needToShowScheduledRecording(ScheduledRecording recording) {
        int state = recording.getState();
        return state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS
                || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED;
    }

    private void updateLatestRecordedProgram(SeriesRecording seriesRecording) {
        RecordedProgram latestProgram = null;
        for (RecordedProgram program :
                mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) {
            if (latestProgram == null
                    || RecordedProgram.START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program)
                            < 0) {
                latestProgram = program;
            }
        }
        mSeriesId2LatestProgram.put(seriesRecording.getSeriesId(), latestProgram);
    }

    private class ScheduleAdapter extends SortedArrayAdapter<Object> {
        ScheduleAdapter(int maxItemCount) {
            super(mPresenterSelector, SCHEDULE_COMPARATOR, maxItemCount);
        }

        @Override
        public long getId(Object item) {
            if (item instanceof ScheduledRecording) {
                return ((ScheduledRecording) item).getId();
            } else {
                return -1;
            }
        }
    }

    private class SeriesAdapter extends SortedArrayAdapter<SeriesRecording> {
        SeriesAdapter() {
            super(
                    mPresenterSelector,
                    (SeriesRecording lhs, SeriesRecording rhs) -> {
                        if (lhs.isStopped() && !rhs.isStopped()) {
                            return 1;
                        } else if (!lhs.isStopped() && rhs.isStopped()) {
                            return -1;
                        }
                        return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs);
                    });
        }

        @Override
        public long getId(SeriesRecording item) {
            return item.getId();
        }
    }

    private class RecordedProgramAdapter extends SortedArrayAdapter<Object> {
        RecordedProgramAdapter() {
            this(Integer.MAX_VALUE);
        }

        RecordedProgramAdapter(int maxItemCount) {
            super(mPresenterSelector, RECORDED_PROGRAM_COMPARATOR, maxItemCount);
        }

        @Override
        public long getId(Object item) {
            // We takes the inverse number for the ID of recorded programs to make the ID stable.
            if (item instanceof SeriesRecording) {
                return ((SeriesRecording) item).getId();
            } else if (item instanceof RecordedProgram) {
                return -((RecordedProgram) item).getId() - 1;
            } else {
                return -1;
            }
        }
    }

    private class RecentRowAdapter extends SortedArrayAdapter<Object> {
        RecentRowAdapter(int maxItemCount) {
            super(mPresenterSelector, RECENT_ROW_COMPARATOR, maxItemCount);
        }

        @Override
        public long getId(Object item) {
            // We takes the inverse number for the ID of scheduled recordings to make the ID stable.
            if (item instanceof ScheduledRecording) {
                return -((ScheduledRecording) item).getId() - 1;
            } else if (item instanceof RecordedProgram) {
                return ((RecordedProgram) item).getId();
            } else {
                return -1;
            }
        }
    }
}
