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

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Programs;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.util.Log;
import android.util.Range;

import com.android.tv.TvSingletons;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.ChannelImpl;
import com.android.tv.data.ProgramImpl;
import com.android.tv.data.api.Channel;
import com.android.tv.data.api.Program;
import com.android.tv.dvr.data.RecordedProgram;

import com.google.common.base.Predicate;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

import javax.inject.Qualifier;

/**
 * {@link AsyncTask} that defaults to executing on its own single threaded Executor Service.
 *
 * @param <Params> the type of the parameters sent to the task upon execution.
 * @param <Progress> the type of the progress units published during the background computation.
 * @param <Result> the type of the result of the background computation.
 */
public abstract class AsyncDbTask<Params, Progress, Result>
        extends AsyncTask<Params, Progress, Result> {
    private static final String TAG = "AsyncDbTask";
    private static final boolean DEBUG = false;

    /** Annotation for requesting the {@link Executor} for data base access. */
    @Qualifier
    public @interface DbExecutor {}

    private final Executor mExecutor;
    boolean mCalledExecuteOnDbThread;

    protected AsyncDbTask(Executor mExecutor) {
        this.mExecutor = mExecutor;
    }

    /**
     * Returns the result of a {@link ContentResolver#query(Uri, String[], String, String[],
     * String)}.
     *
     * <p>{@link #doInBackground(Void...)} executes the query on call {@link #onQuery(Cursor)} which
     * is implemented by subclasses.
     *
     * @param <Result> the type of result returned by {@link #onQuery(Cursor)}
     */
    public abstract static class AsyncQueryTask<Result> extends AsyncDbTask<Void, Void, Result> {
        private final WeakReference<Context> mContextReference;
        private final Uri mUri;
        private final String mSelection;
        private final String[] mSelectionArgs;
        private final String mOrderBy;
        private String[] mProjection;

        public AsyncQueryTask(
                @DbExecutor Executor executor,
                Context context,
                Uri uri,
                String[] projection,
                String selection,
                String[] selectionArgs,
                String orderBy) {
            super(executor);
            mContextReference = new WeakReference<>(context);
            mUri = uri;
            mProjection = projection;
            mSelection = selection;
            mSelectionArgs = selectionArgs;
            mOrderBy = orderBy;
        }

        @Override
        protected final Result doInBackground(Void... params) {
            if (!mCalledExecuteOnDbThread) {
                IllegalStateException e =
                        new IllegalStateException(
                                this
                                        + " should only be executed using executeOnDbThread, "
                                        + "but it was called on thread "
                                        + Thread.currentThread());
                Log.w(TAG, e);
                if (BuildConfig.ENG) {
                    throw e;
                }
            }

            if (isCancelled()) {
                // This is guaranteed to never call onPostExecute because the task is canceled.
                return null;
            }
            Context context = mContextReference.get();
            if (context == null) {
                return null;
            }
            if (Utils.isProgramsUri(mUri)
                    && TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
                mProjection =
                        TvProviderUtils.addExtraColumnsToProjection(
                                mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
            } else if (Utils.isRecordedProgramsUri(mUri)) {
                if (TvProviderUtils.checkSeriesIdColumn(
                        context, TvContract.RecordedPrograms.CONTENT_URI)) {
                    mProjection =
                            TvProviderUtils.addExtraColumnsToProjection(
                                    mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
                }
                if (TvProviderUtils.checkStateColumn(
                        context, TvContract.RecordedPrograms.CONTENT_URI)) {
                    mProjection =
                            TvProviderUtils.addExtraColumnsToProjection(
                                    mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE);
                }
            }
            if (DEBUG) {
                Log.v(TAG, "Starting query for " + this);
            }
            try (Cursor c =
                    context.getContentResolver()
                            .query(mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) {
                if (c != null && !isCancelled()) {
                    Result result = onQuery(c);
                    if (DEBUG) {
                        Log.v(TAG, "Finished query for " + this);
                    }
                    return result;
                } else {
                    if (c == null) {
                        Log.e(TAG, "Unknown query error for " + this);
                    } else {
                        if (DEBUG) {
                            Log.d(TAG, "Canceled query for " + this);
                        }
                    }
                    return null;
                }
            } catch (Exception e) {
                SoftPreconditions.warn(TAG, null, e, "Error querying " + this);
                return null;
            }
        }

        /**
         * Return the result from the cursor.
         *
         * <p><b>Note</b> This is executed on the DB thread by {@link #doInBackground(Void...)}
         */
        @WorkerThread
        protected abstract Result onQuery(Cursor c);

        @Override
        public String toString() {
            return this.getClass().getName() + "(" + mUri + ")";
        }
    }

    /**
     * Returns the result of a query as an {@link List} of {@code T}.
     *
     * <p>Subclasses must implement {@link #fromCursor(Cursor)}.
     *
     * @param <T> the type of result returned in a list by {@link #onQuery(Cursor)}
     */
    public abstract static class AsyncQueryListTask<T> extends AsyncQueryTask<List<T>> {
        private final CursorFilter mFilter;

        public AsyncQueryListTask(
                Executor executor,
                Context context,
                Uri uri,
                String[] projection,
                String selection,
                String[] selectionArgs,
                String orderBy) {
            this(executor, context, uri, projection, selection, selectionArgs, orderBy, null);
        }

        public AsyncQueryListTask(
                Executor executor,
                Context context,
                Uri uri,
                String[] projection,
                String selection,
                String[] selectionArgs,
                String orderBy,
                CursorFilter filter) {
            super(executor, context, uri, projection, selection, selectionArgs, orderBy);
            mFilter = filter;
        }

        @Override
        protected final List<T> onQuery(Cursor c) {
            List<T> result = new ArrayList<>();
            while (c.moveToNext()) {
                if (isCancelled()) {
                    // This is guaranteed to never call onPostExecute because the task is canceled.
                    return null;
                }
                if (mFilter != null && !mFilter.apply(c)) {
                    continue;
                }
                T t = fromCursor(c);
                result.add(t);
            }
            if (DEBUG) {
                Log.v(TAG, "Found " + result.size() + " for  " + this);
            }
            return result;
        }

        /**
         * Return a single instance of {@code T} from the cursor.
         *
         * <p><b>NOTE</b> Do not move the cursor or close it, that is handled by {@link
         * #onQuery(Cursor)}.
         *
         * <p><b>Note</b> This is executed on the DB thread by {@link #onQuery(Cursor)}
         *
         * @param c The cursor with the values to create T from.
         */
        @WorkerThread
        protected abstract T fromCursor(Cursor c);
    }

    /**
     * Returns the result of a query as a single instance of {@code T}.
     *
     * <p>Subclasses must implement {@link #fromCursor(Cursor)}.
     */
    public abstract static class AsyncQueryItemTask<T> extends AsyncQueryTask<T> {

        public AsyncQueryItemTask(
                Executor executor,
                Context context,
                Uri uri,
                String[] projection,
                String selection,
                String[] selectionArgs,
                String orderBy) {
            super(executor, context, uri, projection, selection, selectionArgs, orderBy);
        }

        @Override
        protected final T onQuery(Cursor c) {
            if (c.moveToNext()) {
                if (isCancelled()) {
                    // This is guaranteed to never call onPostExecute because the task is canceled.
                    return null;
                }
                T result = fromCursor(c);
                if (c.moveToNext()) {
                    Log.w(TAG, "More than one result for found for  " + this);
                }
                return result;
            } else {
                if (DEBUG) {
                    Log.v(TAG, "No result for found  for  " + this);
                }
                return null;
            }
        }

        /**
         * Return a single instance of {@code T} from the cursor.
         *
         * <p><b>NOTE</b> Do not move the cursor or close it, that is handled by {@link
         * #onQuery(Cursor)}.
         *
         * <p><b>Note</b> This is executed on the DB thread by {@link #onQuery(Cursor)}
         *
         * @param c The cursor with the values to create T from.
         */
        @WorkerThread
        protected abstract T fromCursor(Cursor c);
    }

    /** Gets an {@link List} of {@link Channel}s from {@link TvContract.Channels#CONTENT_URI}. */
    public abstract static class AsyncChannelQueryTask extends AsyncQueryListTask<Channel> {

        public AsyncChannelQueryTask(Executor executor, Context context) {
            super(
                    executor,
                    context,
                    TvContract.Channels.CONTENT_URI,
                    ChannelImpl.PROJECTION,
                    null,
                    null,
                    null);
        }

        @Override
        protected final Channel fromCursor(Cursor c) {
            return ChannelImpl.fromCursor(c);
        }
    }

    /**
     * Gets an {@link List} of {@link ProgramImpl}s from {@link TvContract.Programs#CONTENT_URI}.
     */
    public abstract static class AsyncProgramQueryTask extends AsyncQueryListTask<Program> {
        public AsyncProgramQueryTask(Executor executor, Context context) {
            super(
                    executor,
                    context,
                    Programs.CONTENT_URI,
                    ProgramImpl.PROJECTION,
                    null,
                    null,
                    null);
        }

        public AsyncProgramQueryTask(
                Executor executor,
                Context context,
                Uri uri,
                String selection,
                String[] selectionArgs,
                String sortOrder,
                CursorFilter filter) {
            super(
                    executor,
                    context,
                    uri,
                    ProgramImpl.PROJECTION,
                    selection,
                    selectionArgs,
                    sortOrder,
                    filter);
        }

        @Override
        protected final Program fromCursor(Cursor c) {
            return ProgramImpl.fromCursor(c);
        }
    }

    /** Gets an {@link List} of {@link TvContract.RecordedPrograms}s. */
    public abstract static class AsyncRecordedProgramQueryTask
            extends AsyncQueryListTask<RecordedProgram> {
        public AsyncRecordedProgramQueryTask(Executor executor, Context context, Uri uri) {
            super(executor, context, uri, RecordedProgram.PROJECTION, null, null, null);
        }

        @Override
        protected final RecordedProgram fromCursor(Cursor c) {
            return RecordedProgram.fromCursor(c);
        }
    }

    /** Execute the task on {@link TvSingletons#getDbExecutor()}. */
    @SafeVarargs
    @MainThread
    public final void executeOnDbThread(Params... params) {
        mCalledExecuteOnDbThread = true;
        executeOnExecutor(mExecutor, params);
    }

    /**
     * Gets an {@link List} of {@link ProgramImpl}s for a given channel and period {@link
     * TvContract#buildProgramsUriForChannel(long, long, long)}. If the {@code period} is {@code
     * null}, then all the programs is queried.
     */
    public static class LoadProgramsForChannelTask extends AsyncProgramQueryTask {
        protected final Range<Long> mPeriod;
        protected final long mChannelId;

        public LoadProgramsForChannelTask(
                Executor executor, Context context, long channelId, @Nullable Range<Long> period) {
            super(
                    executor,
                    context,
                    period == null
                            ? TvContract.buildProgramsUriForChannel(channelId)
                            : TvContract.buildProgramsUriForChannel(
                                    channelId, period.getLower(), period.getUpper()),
                    null,
                    null,
                    null,
                    null);
            mPeriod = period;
            mChannelId = channelId;
        }

        public long getChannelId() {
            return mChannelId;
        }

        public final Range<Long> getPeriod() {
            return mPeriod;
        }
    }

    /** Gets a single {@link ProgramImpl} from {@link TvContract.Programs#CONTENT_URI}. */
    public static class AsyncQueryProgramTask extends AsyncQueryItemTask<Program> {

        public AsyncQueryProgramTask(Executor executor, Context context, long programId) {
            super(
                    executor,
                    context,
                    TvContract.buildProgramUri(programId),
                    ProgramImpl.PROJECTION,
                    null,
                    null,
                    null);
        }

        @Override
        protected Program fromCursor(Cursor c) {
            return ProgramImpl.fromCursor(c);
        }
    }

    /** An interface which filters the row. */
    public interface CursorFilter extends Predicate<Cursor> {}
}
