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

import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.tv.TvContract;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.IntDef;
import android.support.annotation.MainThread;
import android.util.Log;
import android.util.Pair;

import androidx.tvprovider.media.tv.ChannelLogoUtils;
import androidx.tvprovider.media.tv.PreviewProgram;

import com.android.tv.R;
import com.android.tv.common.util.PermissionUtils;
import com.android.tv.util.images.ImageLoader;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

/** Class to manage the preview data. */
@TargetApi(Build.VERSION_CODES.O)
@MainThread
public class PreviewDataManager {
    private static final String TAG = "PreviewDataManager";
    private static final boolean DEBUG = false;

    /** Invalid preview channel ID. */
    public static final long INVALID_PREVIEW_CHANNEL_ID = -1;

    @IntDef({TYPE_DEFAULT_PREVIEW_CHANNEL, TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL})
    @Retention(RetentionPolicy.SOURCE)
    public @interface PreviewChannelType {}

    /** Type of default preview channel */
    public static final int TYPE_DEFAULT_PREVIEW_CHANNEL = 1;
    /** Type of recorded program channel */
    public static final int TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2;

    private final Context mContext;
    private final ContentResolver mContentResolver;
    private boolean mLoadFinished;
    private PreviewData mPreviewData = new PreviewData();
    private final Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>();

    private QueryPreviewDataTask mQueryPreviewTask;
    private final Map<Long, CreatePreviewChannelTask> mCreatePreviewChannelTasks = new HashMap<>();
    private final Map<Long, UpdatePreviewProgramTask> mUpdatePreviewProgramTasks = new HashMap<>();

    private final int mPreviewChannelLogoWidth;
    private final int mPreviewChannelLogoHeight;

    public PreviewDataManager(Context context) {
        mContext = context.getApplicationContext();
        mContentResolver = context.getContentResolver();
        mPreviewChannelLogoWidth =
                mContext.getResources().getDimensionPixelSize(R.dimen.preview_channel_logo_width);
        mPreviewChannelLogoHeight =
                mContext.getResources().getDimensionPixelSize(R.dimen.preview_channel_logo_height);
    }

    /** Starts the preview data manager. */
    public void start() {
        if (mQueryPreviewTask == null) {
            mQueryPreviewTask = new QueryPreviewDataTask();
            mQueryPreviewTask.execute();
        }
    }

    /** Stops the preview data manager. */
    public void stop() {
        if (mQueryPreviewTask != null) {
            mQueryPreviewTask.cancel(true);
        }
        for (CreatePreviewChannelTask createPreviewChannelTask :
                mCreatePreviewChannelTasks.values()) {
            createPreviewChannelTask.cancel(true);
        }
        for (UpdatePreviewProgramTask updatePreviewProgramTask :
                mUpdatePreviewProgramTasks.values()) {
            updatePreviewProgramTask.cancel(true);
        }

        mQueryPreviewTask = null;
        mCreatePreviewChannelTasks.clear();
        mUpdatePreviewProgramTasks.clear();
    }

    /** Gets preview channel ID from the preview channel type. */
    public @PreviewChannelType long getPreviewChannelId(long previewChannelType) {
        return mPreviewData.getPreviewChannelId(previewChannelType);
    }

    /** Creates default preview channel. */
    public void createDefaultPreviewChannel(
            OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
        createPreviewChannel(TYPE_DEFAULT_PREVIEW_CHANNEL, onPreviewChannelCreationResultListener);
    }

    /** Creates a preview channel for specific channel type. */
    public void createPreviewChannel(
            @PreviewChannelType long previewChannelType,
            OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
        CreatePreviewChannelTask currentRunningCreateTask =
                mCreatePreviewChannelTasks.get(previewChannelType);
        if (currentRunningCreateTask == null) {
            CreatePreviewChannelTask createPreviewChannelTask =
                    new CreatePreviewChannelTask(previewChannelType);
            createPreviewChannelTask.addOnPreviewChannelCreationResultListener(
                    onPreviewChannelCreationResultListener);
            createPreviewChannelTask.execute();
            mCreatePreviewChannelTasks.put(previewChannelType, createPreviewChannelTask);
        } else {
            currentRunningCreateTask.addOnPreviewChannelCreationResultListener(
                    onPreviewChannelCreationResultListener);
        }
    }

    /** Returns {@code true} if the preview data is loaded. */
    public boolean isLoadFinished() {
        return mLoadFinished;
    }

    /** Adds listener. */
    public void addListener(PreviewDataListener previewDataListener) {
        mPreviewDataListeners.add(previewDataListener);
    }

    /** Removes listener. */
    public void removeListener(PreviewDataListener previewDataListener) {
        mPreviewDataListeners.remove(previewDataListener);
    }

    /** Updates the preview programs table for a specific preview channel. */
    public void updatePreviewProgramsForChannel(
            long previewChannelId,
            Set<PreviewProgramContent> programs,
            PreviewDataListener previewDataListener) {
        UpdatePreviewProgramTask currentRunningUpdateTask =
                mUpdatePreviewProgramTasks.get(previewChannelId);
        if (currentRunningUpdateTask != null
                && currentRunningUpdateTask.getPrograms().equals(programs)) {
            currentRunningUpdateTask.addPreviewDataListener(previewDataListener);
            return;
        }
        UpdatePreviewProgramTask updatePreviewProgramTask =
                new UpdatePreviewProgramTask(previewChannelId, programs);
        updatePreviewProgramTask.addPreviewDataListener(previewDataListener);
        if (currentRunningUpdateTask != null) {
            currentRunningUpdateTask.cancel(true);
            currentRunningUpdateTask.saveStatus();
            updatePreviewProgramTask.addPreviewDataListeners(
                    currentRunningUpdateTask.getPreviewDataListeners());
        }
        updatePreviewProgramTask.execute();
        mUpdatePreviewProgramTasks.put(previewChannelId, updatePreviewProgramTask);
    }

    private void notifyPreviewDataLoadFinished() {
        for (PreviewDataListener l : mPreviewDataListeners) {
            l.onPreviewDataLoadFinished();
        }
    }

    public interface PreviewDataListener {
        /** Called when the preview data is loaded. */
        void onPreviewDataLoadFinished();

        /** Called when the preview data is updated. */
        void onPreviewDataUpdateFinished();
    }

    public interface OnPreviewChannelCreationResultListener {
        /**
         * Called when the creation of preview channel is finished.
         *
         * @param createdPreviewChannelId The preview channel ID if created successfully, otherwise
         *     it's {@value #INVALID_PREVIEW_CHANNEL_ID}.
         */
        void onPreviewChannelCreationResult(long createdPreviewChannelId);
    }

    private final class QueryPreviewDataTask extends AsyncTask<Void, Void, PreviewData> {
        private final String PARAM_PREVIEW = "preview";
        private final String mChannelSelection = TvContract.Channels.COLUMN_PACKAGE_NAME + "=?";

        @Override
        protected PreviewData doInBackground(Void... voids) {
            // Query preview channels and programs.
            if (DEBUG) Log.d(TAG, "QueryPreviewDataTask.doInBackground");
            PreviewData previewData = new PreviewData();
            try {
                Uri previewChannelsUri =
                        PreviewDataUtils.addQueryParamToUri(
                                TvContract.Channels.CONTENT_URI,
                                Pair.create(PARAM_PREVIEW, String.valueOf(true)));
                String packageName = mContext.getPackageName();
                if (PermissionUtils.hasAccessAllEpg(mContext)) {
                    try (Cursor cursor =
                            mContentResolver.query(
                                    previewChannelsUri,
                                    androidx.tvprovider.media.tv.Channel.PROJECTION,
                                    mChannelSelection,
                                    new String[] {packageName},
                                    null)) {
                        if (cursor != null) {
                            while (cursor.moveToNext()) {
                                androidx.tvprovider.media.tv.Channel previewChannel =
                                        androidx.tvprovider.media.tv.Channel.fromCursor(cursor);
                                Long previewChannelType = previewChannel.getInternalProviderFlag1();
                                if (previewChannelType != null) {
                                    previewData.addPreviewChannelId(
                                            previewChannelType, previewChannel.getId());
                                }
                            }
                        }
                    }
                } else {
                    try (Cursor cursor =
                            mContentResolver.query(
                                    previewChannelsUri,
                                    androidx.tvprovider.media.tv.Channel.PROJECTION,
                                    null,
                                    null,
                                    null)) {
                        if (cursor != null) {
                            while (cursor.moveToNext()) {
                                androidx.tvprovider.media.tv.Channel previewChannel =
                                        androidx.tvprovider.media.tv.Channel.fromCursor(cursor);
                                Long previewChannelType = previewChannel.getInternalProviderFlag1();
                                if (packageName.equals(previewChannel.getPackageName())
                                        && previewChannelType != null) {
                                    previewData.addPreviewChannelId(
                                            previewChannelType, previewChannel.getId());
                                }
                            }
                        }
                    }
                }

                for (long previewChannelId : previewData.getAllPreviewChannelIds().values()) {
                    Uri previewProgramsUriForPreviewChannel =
                            TvContract.buildPreviewProgramsUriForChannel(previewChannelId);
                    try (Cursor previewProgramCursor =
                            mContentResolver.query(
                                    previewProgramsUriForPreviewChannel,
                                    PreviewProgram.PROJECTION,
                                    null,
                                    null,
                                    null)) {
                        if (previewProgramCursor != null) {
                            while (previewProgramCursor.moveToNext()) {
                                PreviewProgram previewProgram =
                                        PreviewProgram.fromCursor(previewProgramCursor);
                                previewData.addPreviewProgram(previewProgram);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                Log.w(TAG, "Unable to get preview data", e);
            }
            return previewData;
        }

        @Override
        protected void onPostExecute(PreviewData result) {
            super.onPostExecute(result);
            if (mQueryPreviewTask == this) {
                mQueryPreviewTask = null;
                mPreviewData = new PreviewData(result);
                mLoadFinished = true;
                notifyPreviewDataLoadFinished();
            }
        }
    }

    private final class CreatePreviewChannelTask extends AsyncTask<Void, Void, Long> {
        private final long mPreviewChannelType;
        private Set<OnPreviewChannelCreationResultListener>
                mOnPreviewChannelCreationResultListeners = new CopyOnWriteArraySet<>();

        public CreatePreviewChannelTask(long previewChannelType) {
            mPreviewChannelType = previewChannelType;
        }

        public void addOnPreviewChannelCreationResultListener(
                OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
            if (onPreviewChannelCreationResultListener != null) {
                mOnPreviewChannelCreationResultListeners.add(
                        onPreviewChannelCreationResultListener);
            }
        }

        @Override
        protected Long doInBackground(Void... params) {
            if (DEBUG) Log.d(TAG, "CreatePreviewChannelTask.doInBackground");
            long previewChannelId;
            try {
                Uri channelUri =
                        mContentResolver.insert(
                                TvContract.Channels.CONTENT_URI,
                                PreviewDataUtils.createPreviewChannel(mContext, mPreviewChannelType)
                                        .toContentValues());
                if (channelUri != null) {
                    previewChannelId = ContentUris.parseId(channelUri);
                } else {
                    Log.e(TAG, "Fail to insert preview channel");
                    return INVALID_PREVIEW_CHANNEL_ID;
                }
            } catch (UnsupportedOperationException | NumberFormatException e) {
                Log.e(TAG, "Fail to get channel ID");
                return INVALID_PREVIEW_CHANNEL_ID;
            }
            Drawable appIcon = mContext.getApplicationInfo().loadIcon(mContext.getPackageManager());
            if (appIcon != null && appIcon instanceof BitmapDrawable) {
                ChannelLogoUtils.storeChannelLogo(
                        mContext,
                        previewChannelId,
                        Bitmap.createScaledBitmap(
                                ((BitmapDrawable) appIcon).getBitmap(),
                                mPreviewChannelLogoWidth,
                                mPreviewChannelLogoHeight,
                                false));
            }
            return previewChannelId;
        }

        @Override
        protected void onPostExecute(Long result) {
            super.onPostExecute(result);
            if (result != INVALID_PREVIEW_CHANNEL_ID) {
                mPreviewData.addPreviewChannelId(mPreviewChannelType, result);
            }
            for (OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener :
                    mOnPreviewChannelCreationResultListeners) {
                onPreviewChannelCreationResultListener.onPreviewChannelCreationResult(result);
            }
            mCreatePreviewChannelTasks.remove(mPreviewChannelType);
        }
    }

    /**
     * Updates the whole data which belongs to the package in preview programs table for a specific
     * preview channel with a set of {@link PreviewProgramContent}.
     */
    private final class UpdatePreviewProgramTask extends AsyncTask<Void, Void, Void> {
        private long mPreviewChannelId;
        private Set<PreviewProgramContent> mPrograms;
        private Map<Long, Long> mCurrentProgramId2PreviewProgramId;
        private Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>();

        public UpdatePreviewProgramTask(
                long previewChannelId, Set<PreviewProgramContent> programs) {
            mPreviewChannelId = previewChannelId;
            mPrograms = programs;
            if (mPreviewData.getPreviewProgramIds(previewChannelId) == null) {
                mCurrentProgramId2PreviewProgramId = new HashMap<>();
            } else {
                mCurrentProgramId2PreviewProgramId =
                        new HashMap<>(mPreviewData.getPreviewProgramIds(previewChannelId));
            }
        }

        public void addPreviewDataListener(PreviewDataListener previewDataListener) {
            if (previewDataListener != null) {
                mPreviewDataListeners.add(previewDataListener);
            }
        }

        public void addPreviewDataListeners(Set<PreviewDataListener> previewDataListeners) {
            if (previewDataListeners != null) {
                mPreviewDataListeners.addAll(previewDataListeners);
            }
        }

        public Set<PreviewProgramContent> getPrograms() {
            return mPrograms;
        }

        public Set<PreviewDataListener> getPreviewDataListeners() {
            return mPreviewDataListeners;
        }

        @Override
        protected Void doInBackground(Void... params) {
            if (DEBUG) Log.d(TAG, "UpdatePreviewProgamTask.doInBackground");
            Map<Long, Long> uncheckedPrograms = new HashMap<>(mCurrentProgramId2PreviewProgramId);
            for (PreviewProgramContent program : mPrograms) {
                if (isCancelled()) {
                    return null;
                }
                Long existingPreviewProgramId = uncheckedPrograms.remove(program.getId());
                if (existingPreviewProgramId != null) {
                    if (DEBUG)
                        Log.d(
                                TAG,
                                "Preview program "
                                        + existingPreviewProgramId
                                        + " "
                                        + "already exists for program "
                                        + program.getId());
                    continue;
                }
                try {
                    int aspectRatio =
                            ImageLoader.getAspectRatioFromPosterArtUri(
                                    mContext, program.getPosterArtUri().toString());
                    Uri programUri =
                            mContentResolver.insert(
                                    TvContract.PreviewPrograms.CONTENT_URI,
                                    PreviewDataUtils.createPreviewProgramFromContent(
                                                    program, aspectRatio)
                                            .toContentValues());
                    if (programUri != null) {
                        long previewProgramId = ContentUris.parseId(programUri);
                        mCurrentProgramId2PreviewProgramId.put(program.getId(), previewProgramId);
                        if (DEBUG) Log.d(TAG, "Add new preview program " + previewProgramId);
                    } else {
                        Log.e(TAG, "Fail to insert preview program");
                    }
                } catch (Exception e) {
                    Log.e(TAG, "Fail to get preview program ID");
                }
            }

            for (Long key : uncheckedPrograms.keySet()) {
                if (isCancelled()) {
                    return null;
                }
                try {
                    if (DEBUG) Log.d(TAG, "Remove preview program " + uncheckedPrograms.get(key));
                    mContentResolver.delete(
                            TvContract.buildPreviewProgramUri(uncheckedPrograms.get(key)),
                            null,
                            null);
                    mCurrentProgramId2PreviewProgramId.remove(key);
                } catch (Exception e) {
                    Log.e(TAG, "Fail to remove preview program " + uncheckedPrograms.get(key));
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            super.onPostExecute(result);
            mPreviewData.setPreviewProgramIds(
                    mPreviewChannelId, mCurrentProgramId2PreviewProgramId);
            mUpdatePreviewProgramTasks.remove(mPreviewChannelId);
            for (PreviewDataListener previewDataListener : mPreviewDataListeners) {
                previewDataListener.onPreviewDataUpdateFinished();
            }
        }

        public void saveStatus() {
            mPreviewData.setPreviewProgramIds(
                    mPreviewChannelId, mCurrentProgramId2PreviewProgramId);
        }
    }

    /** Class to store the query result of preview data. */
    private static final class PreviewData {
        private Map<Long, Long> mPreviewChannelType2Id = new HashMap<>();
        private Map<Long, Map<Long, Long>> mProgramId2PreviewProgramId = new HashMap<>();

        PreviewData() {
            mPreviewChannelType2Id = new HashMap<>();
            mProgramId2PreviewProgramId = new HashMap<>();
        }

        PreviewData(PreviewData previewData) {
            mPreviewChannelType2Id = new HashMap<>(previewData.mPreviewChannelType2Id);
            mProgramId2PreviewProgramId = new HashMap<>(previewData.mProgramId2PreviewProgramId);
        }

        public void addPreviewProgram(PreviewProgram previewProgram) {
            long previewChannelId = previewProgram.getChannelId();
            Map<Long, Long> programId2PreviewProgram =
                    mProgramId2PreviewProgramId.get(previewChannelId);
            if (programId2PreviewProgram == null) {
                programId2PreviewProgram = new HashMap<>();
            }
            mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgram);
            if (previewProgram.getInternalProviderId() != null) {
                programId2PreviewProgram.put(
                        Long.parseLong(previewProgram.getInternalProviderId()),
                        previewProgram.getId());
            }
        }

        public @PreviewChannelType long getPreviewChannelId(long previewChannelType) {
            Long result = mPreviewChannelType2Id.get(previewChannelType);
            return result == null ? INVALID_PREVIEW_CHANNEL_ID : result;
        }

        public Map<Long, Long> getAllPreviewChannelIds() {
            return mPreviewChannelType2Id;
        }

        public void addPreviewChannelId(long previewChannelType, long previewChannelId) {
            mPreviewChannelType2Id.put(previewChannelType, previewChannelId);
        }

        public void removePreviewChannelId(long previewChannelType) {
            mPreviewChannelType2Id.remove(previewChannelType);
        }

        public void removePreviewChannel(long previewChannelId) {
            removePreviewChannelId(previewChannelId);
            removePreviewProgramIds(previewChannelId);
        }

        public Map<Long, Long> getPreviewProgramIds(long previewChannelId) {
            return mProgramId2PreviewProgramId.get(previewChannelId);
        }

        public Map<Long, Map<Long, Long>> getAllPreviewProgramIds() {
            return mProgramId2PreviewProgramId;
        }

        public void setPreviewProgramIds(
                long previewChannelId, Map<Long, Long> programId2PreviewProgramId) {
            mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgramId);
        }

        public void removePreviewProgramIds(long previewChannelId) {
            mProgramId2PreviewProgramId.remove(previewChannelId);
        }
    }

    /** A utils class for preview data. */
    public static final class PreviewDataUtils {
        /** Creates a preview channel. */
        public static androidx.tvprovider.media.tv.Channel createPreviewChannel(
                Context context, @PreviewChannelType long previewChannelType) {
            if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) {
                return createRecordedProgramPreviewChannel(context, previewChannelType);
            }
            return createDefaultPreviewChannel(context, previewChannelType);
        }

        private static androidx.tvprovider.media.tv.Channel createDefaultPreviewChannel(
                Context context, @PreviewChannelType long previewChannelType) {
            androidx.tvprovider.media.tv.Channel.Builder builder =
                    new androidx.tvprovider.media.tv.Channel.Builder();
            CharSequence appLabel =
                    context.getApplicationInfo().loadLabel(context.getPackageManager());
            CharSequence appDescription =
                    context.getApplicationInfo().loadDescription(context.getPackageManager());
            builder.setType(TvContract.Channels.TYPE_PREVIEW)
                    .setDisplayName(appLabel == null ? null : appLabel.toString())
                    .setDescription(appDescription == null ? null : appDescription.toString())
                    .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI)
                    .setInternalProviderFlag1(previewChannelType);
            return builder.build();
        }

        private static androidx.tvprovider.media.tv.Channel createRecordedProgramPreviewChannel(
                Context context, @PreviewChannelType long previewChannelType) {
            androidx.tvprovider.media.tv.Channel.Builder builder =
                    new androidx.tvprovider.media.tv.Channel.Builder();
            builder.setType(TvContract.Channels.TYPE_PREVIEW)
                    .setDisplayName(
                            context.getResources()
                                    .getString(R.string.recorded_programs_preview_channel))
                    .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI)
                    .setInternalProviderFlag1(previewChannelType);
            return builder.build();
        }

        /** Creates a preview program. */
        public static PreviewProgram createPreviewProgramFromContent(
                PreviewProgramContent program, int aspectRatio) {
            PreviewProgram.Builder builder = new PreviewProgram.Builder();
            builder.setChannelId(program.getPreviewChannelId())
                    .setType(program.getType())
                    .setLive(program.getLive())
                    .setTitle(program.getTitle())
                    .setDescription(program.getDescription())
                    .setPosterArtAspectRatio(aspectRatio)
                    .setPosterArtUri(program.getPosterArtUri())
                    .setIntentUri(program.getIntentUri())
                    .setPreviewVideoUri(program.getPreviewVideoUri())
                    .setInternalProviderId(Long.toString(program.getId()))
                    .setContentId(program.getIntentUri().toString());
            return builder.build();
        }

        /** Appends query parameters to a Uri. */
        public static Uri addQueryParamToUri(Uri uri, Pair<String, String> param) {
            return uri.buildUpon().appendQueryParameter(param.first, param.second).build();
        }
    }
}
