/*
 * Copyright (C) 2017 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.recommendation;

import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.text.TextUtils;
import android.util.Log;

import androidx.tvprovider.media.tv.TvContractCompat;

import com.android.tv.Starter;
import com.android.tv.TvSingletons;
import com.android.tv.data.PreviewDataManager;
import com.android.tv.data.PreviewProgramContent;
import com.android.tv.data.api.Channel;
import com.android.tv.data.api.Program;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.util.Utils;

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

/** Class for updating the preview programs for {@link Channel}. */
@RequiresApi(Build.VERSION_CODES.O)
public class ChannelPreviewUpdater {
    private static final String TAG = "ChannelPreviewUpdater";
    private static final boolean DEBUG = false;

    private static final int UPATE_PREVIEW_PROGRAMS_JOB_ID = 1000001;
    private static final long ROUTINE_INTERVAL_MS = TimeUnit.MINUTES.toMillis(10);
    // The left time of a program should meet the threshold so that it could be recommended.
    private static final long RECOMMENDATION_THRESHOLD_LEFT_TIME_MS = TimeUnit.MINUTES.toMillis(10);
    private static final int RECOMMENDATION_THRESHOLD_PROGRESS = 90; // 90%
    private static final int RECOMMENDATION_COUNT = 6;
    private static final int MIN_COUNT_TO_ADD_ROW = 4;

    private static ChannelPreviewUpdater sChannelPreviewUpdater;

    /** Creates and returns the {@link ChannelPreviewUpdater}. */
    public static ChannelPreviewUpdater getInstance(Context context) {
        if (sChannelPreviewUpdater == null) {
            sChannelPreviewUpdater = new ChannelPreviewUpdater(context.getApplicationContext());
        }
        return sChannelPreviewUpdater;
    }

    private final Context mContext;
    private final Recommender mRecommender;
    private final PreviewDataManager mPreviewDataManager;
    private JobService mJobService;
    private JobParameters mJobParams;

    private final ParentalControlSettings mParentalControlSettings;

    private boolean mNeedUpdateAfterRecommenderReady = false;

    private Recommender.Listener mRecommenderListener =
            new Recommender.Listener() {
                @Override
                public void onRecommenderReady() {
                    if (mNeedUpdateAfterRecommenderReady) {
                        if (DEBUG) Log.d(TAG, "Recommender is ready");
                        updatePreviewDataForChannelsImmediately();
                        mNeedUpdateAfterRecommenderReady = false;
                    }
                }

                @Override
                public void onRecommendationChanged() {
                    updatePreviewDataForChannelsImmediately();
                }
            };

    private ChannelPreviewUpdater(Context context) {
        mContext = context;
        mRecommender = new Recommender(context, mRecommenderListener, true);
        mRecommender.registerEvaluator(new RandomEvaluator(), 0.1, 0.1);
        mRecommender.registerEvaluator(new FavoriteChannelEvaluator(), 0.5, 0.5);
        mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0);
        TvSingletons tvSingleton = TvSingletons.getSingletons(context);
        mPreviewDataManager = tvSingleton.getPreviewDataManager();
        mParentalControlSettings =
                tvSingleton.getTvInputManagerHelper().getParentalControlSettings();
    }

    /** Starts the routine service for updating the preview programs. */
    public void startRoutineService() {
        JobScheduler jobScheduler =
                (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        if (jobScheduler.getPendingJob(UPATE_PREVIEW_PROGRAMS_JOB_ID) != null) {
            if (DEBUG) Log.d(TAG, "UPDATE_PREVIEW_JOB already exists");
            return;
        }
        JobInfo job =
                new JobInfo.Builder(
                                UPATE_PREVIEW_PROGRAMS_JOB_ID,
                                new ComponentName(mContext, ChannelPreviewUpdateService.class))
                        .setPeriodic(ROUTINE_INTERVAL_MS)
                        .setPersisted(true)
                        .build();
        if (jobScheduler.schedule(job) < 0) {
            Log.i(TAG, "JobScheduler failed to schedule the job");
        }
    }

    /** Called when {@link ChannelPreviewUpdateService} is started. */
    void onStartJob(JobService service, JobParameters params) {
        if (DEBUG) Log.d(TAG, "onStartJob");
        mJobService = service;
        mJobParams = params;
        updatePreviewDataForChannelsImmediately();
    }

    /** Updates the preview programs table. */
    public void updatePreviewDataForChannelsImmediately() {
        if (!mRecommender.isReady()) {
            mNeedUpdateAfterRecommenderReady = true;
            return;
        }

        if (!mPreviewDataManager.isLoadFinished()) {
            mPreviewDataManager.addListener(
                    new PreviewDataManager.PreviewDataListener() {
                        @Override
                        public void onPreviewDataLoadFinished() {
                            mPreviewDataManager.removeListener(this);
                            updatePreviewDataForChannels();
                        }

                        @Override
                        public void onPreviewDataUpdateFinished() {}
                    });
            return;
        }
        updatePreviewDataForChannels();
    }

    /** Called when {@link ChannelPreviewUpdateService} is stopped. */
    void onStopJob() {
        if (DEBUG) Log.d(TAG, "onStopJob");
        mJobService = null;
        mJobParams = null;
    }

    private void updatePreviewDataForChannels() {
        new AsyncTask<Void, Void, Set<Program>>() {
            @Override
            protected Set<Program> doInBackground(Void... params) {
                Set<Program> programs = new HashSet<>();
                try {
                    List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
                    for (Channel channel : channels) {
                        if (channel.isPhysicalTunerChannel()) {
                            final Program program =
                                    Utils.getCurrentProgram(mContext, channel.getId());
                            if (program != null
                                    && isChannelRecommendationApplicable(channel, program)) {
                                programs.add(program);
                                if (programs.size() >= RECOMMENDATION_COUNT) {
                                    break;
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    Log.w(TAG, "Can't update preview data", e);
                }
                return programs;
            }

            private boolean isChannelRecommendationApplicable(Channel channel, Program program) {
                final long programDurationMs =
                        program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis();
                if (programDurationMs <= 0) {
                    return false;
                }
                if (TextUtils.isEmpty(program.getPosterArtUri())) {
                    return false;
                }
                if (mParentalControlSettings.isParentalControlsEnabled()
                        && (channel.isLocked()
                                || mParentalControlSettings.isRatingBlocked(
                                        program.getContentRatings()))) {
                    return false;
                }
                long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis();
                final int programProgress =
                        (programDurationMs <= 0)
                                ? -1
                                : 100 - (int) (programLeftTimsMs * 100 / programDurationMs);

                // We recommend those programs that meet the condition only.
                return programProgress < RECOMMENDATION_THRESHOLD_PROGRESS
                        || programLeftTimsMs > RECOMMENDATION_THRESHOLD_LEFT_TIME_MS;
            }

            @Override
            protected void onPostExecute(Set<Program> programs) {
                updatePreviewDataForChannelsInternal(programs);
            }
        }.execute();
    }

    private void updatePreviewDataForChannelsInternal(Set<Program> programs) {
        long defaultPreviewChannelId =
                mPreviewDataManager.getPreviewChannelId(
                        PreviewDataManager.TYPE_DEFAULT_PREVIEW_CHANNEL);
        if (defaultPreviewChannelId == PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) {
            // Only create if there is enough programs
            if (programs.size() > MIN_COUNT_TO_ADD_ROW) {
                mPreviewDataManager.createDefaultPreviewChannel(
                        new PreviewDataManager.OnPreviewChannelCreationResultListener() {
                            @Override
                            public void onPreviewChannelCreationResult(
                                    long createdPreviewChannelId) {
                                if (createdPreviewChannelId
                                        != PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) {
                                    TvContractCompat.requestChannelBrowsable(
                                            mContext, createdPreviewChannelId);
                                    updatePreviewProgramsForPreviewChannel(
                                            createdPreviewChannelId,
                                            generatePreviewProgramContentsFromPrograms(
                                                    createdPreviewChannelId, programs));
                                }
                            }
                        });
            } else if (mJobService != null && mJobParams != null) {
                if (DEBUG) {
                    Log.d(
                            TAG,
                            "Preview channel not created because there is only "
                                    + programs.size()
                                    + " programs");
                }
                mJobService.jobFinished(mJobParams, false);
                mJobService = null;
                mJobParams = null;
            }
        } else {
            updatePreviewProgramsForPreviewChannel(
                    defaultPreviewChannelId,
                    generatePreviewProgramContentsFromPrograms(defaultPreviewChannelId, programs));
        }
    }

    private Set<PreviewProgramContent> generatePreviewProgramContentsFromPrograms(
            long previewChannelId, Set<Program> programs) {
        Set<PreviewProgramContent> result = new HashSet<>();
        for (Program program : programs) {
            PreviewProgramContent previewProgramContent =
                    PreviewProgramContent.createFromProgram(mContext, previewChannelId, program);
            if (previewProgramContent != null) {
                result.add(previewProgramContent);
            }
        }
        return result;
    }

    private void updatePreviewProgramsForPreviewChannel(
            long previewChannelId, Set<PreviewProgramContent> previewProgramContents) {
        PreviewDataManager.PreviewDataListener previewDataListener =
                new PreviewDataManager.PreviewDataListener() {
                    @Override
                    public void onPreviewDataLoadFinished() {}

                    @Override
                    public void onPreviewDataUpdateFinished() {
                        mPreviewDataManager.removeListener(this);
                        if (mJobService != null && mJobParams != null) {
                            if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute with JobService");
                            mJobService.jobFinished(mJobParams, false);
                            mJobService = null;
                            mJobParams = null;
                        } else {
                            if (DEBUG)
                                Log.d(TAG, "UpdateAsyncTask.onPostExecute without JobService");
                        }
                    }
                };
        mPreviewDataManager.updatePreviewProgramsForChannel(
                previewChannelId, previewProgramContents, previewDataListener);
    }

    /** Job to execute the update of preview programs. */
    public static class ChannelPreviewUpdateService extends JobService {
        private ChannelPreviewUpdater mChannelPreviewUpdater;

        @Override
        public void onCreate() {
            Starter.start(this);
            if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onCreate");
            mChannelPreviewUpdater = ChannelPreviewUpdater.getInstance(this);
        }

        @Override
        public boolean onStartJob(JobParameters params) {
            mChannelPreviewUpdater.onStartJob(this, params);
            return true;
        }

        @Override
        public boolean onStopJob(JobParameters params) {
            mChannelPreviewUpdater.onStopJob();
            return false;
        }

        @Override
        public void onDestroy() {
            if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onDestroy");
        }
    }
}
