/*
 * 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.messaging.datamodel.action;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;

import androidx.core.app.JobIntentService;

import com.android.messaging.Factory;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.LoggingTimer;
import com.google.common.annotations.VisibleForTesting;

/**
 * ActionService used to perform background processing for data model
 */
public class ActionServiceImpl extends JobIntentService {
    private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
    private static final boolean VERBOSE = false;

    /**
     * Unique job ID for this service.
     */
    public static final int JOB_ID = 1000;

    public ActionServiceImpl() {
        super();
    }

    /**
     * Start action by sending intent to the service
     * @param action - action to start
     */
    protected static void startAction(final Action action) {
        final Intent intent = makeIntent(OP_START_ACTION);
        final Bundle actionBundle = new Bundle();
        actionBundle.putParcelable(BUNDLE_ACTION, action);
        intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
        action.markStart();
        startServiceWithIntent(intent);
    }

    /**
     * Schedule an action to run after specified delay using alarm manager to send pendingintent
     * @param action - action to start
     * @param requestCode - request code used to collapse requests
     * @param delayMs - delay in ms (from now) before action will start
     */
    protected static void scheduleAction(final Action action, final int requestCode,
            final long delayMs) {
        final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION);
        final Bundle actionBundle = new Bundle();
        actionBundle.putParcelable(BUNDLE_ACTION, action);
        intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);

        PendingActionReceiver.scheduleAlarm(intent, requestCode, delayMs);
    }

    /**
     * Handle response returned by BackgroundWorker
     * @param request - request generating response
     * @param response - response from service
     */
    protected static void handleResponseFromBackgroundWorker(final Action action,
            final Bundle response) {
        final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_RESPONSE);

        final Bundle actionBundle = new Bundle();
        actionBundle.putParcelable(BUNDLE_ACTION, action);
        intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
        intent.putExtra(EXTRA_WORKER_RESPONSE, response);

        startServiceWithIntent(intent);
    }

    /**
     * Handle response returned by BackgroundWorker
     * @param request - request generating failure
     */
    protected static void handleFailureFromBackgroundWorker(final Action action,
            final Exception exception) {
        final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_FAILURE);

        final Bundle actionBundle = new Bundle();
        actionBundle.putParcelable(BUNDLE_ACTION, action);
        intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
        intent.putExtra(EXTRA_WORKER_EXCEPTION, exception);

        startServiceWithIntent(intent);
    }

    // ops
    @VisibleForTesting
    protected static final int OP_START_ACTION = 200;
    @VisibleForTesting
    protected static final int OP_RECEIVE_BACKGROUND_RESPONSE = 201;
    @VisibleForTesting
    protected static final int OP_RECEIVE_BACKGROUND_FAILURE = 202;

    // extras
    @VisibleForTesting
    protected static final String EXTRA_OP_CODE = "op";
    @VisibleForTesting
    protected static final String EXTRA_ACTION_BUNDLE = "datamodel_action_bundle";
    @VisibleForTesting
    protected static final String EXTRA_WORKER_EXCEPTION = "worker_exception";
    @VisibleForTesting
    protected static final String EXTRA_WORKER_RESPONSE = "worker_response";
    @VisibleForTesting
    protected static final String EXTRA_WORKER_UPDATE = "worker_update";
    @VisibleForTesting
    protected static final String BUNDLE_ACTION = "bundle_action";

    private BackgroundWorker mBackgroundWorker;

    /**
     * Allocate an intent with a specific opcode.
     */
    private static Intent makeIntent(final int opcode) {
        final Intent intent = new Intent(Factory.get().getApplicationContext(),
                ActionServiceImpl.class);
        intent.putExtra(EXTRA_OP_CODE, opcode);
        return intent;
    }

    /**
     * Broadcast receiver for alarms scheduled through ActionService.
     */
    public static class PendingActionReceiver extends BroadcastReceiver {
        static final String ACTION = "com.android.messaging.datamodel.PENDING_ACTION";

        /**
         * Allocate an intent with a specific opcode and alarm action.
         */
        public static Intent makeIntent(final int opcode) {
            final Intent intent = new Intent(Factory.get().getApplicationContext(),
                    PendingActionReceiver.class);
            intent.setAction(ACTION);
            intent.putExtra(EXTRA_OP_CODE, opcode);
            return intent;
        }

        public static void scheduleAlarm(final Intent intent, final int requestCode,
                final long delayMs) {
            final Context context = Factory.get().getApplicationContext();
            final PendingIntent pendingIntent = PendingIntent.getBroadcast(
                    context, requestCode, intent, PendingIntent.FLAG_CANCEL_CURRENT);

            final AlarmManager mgr =
                    (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

            if (delayMs < Long.MAX_VALUE) {
                mgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                        SystemClock.elapsedRealtime() + delayMs, pendingIntent);
            } else {
                mgr.cancel(pendingIntent);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onReceive(final Context context, final Intent intent) {
            ActionServiceImpl.startServiceWithIntent(intent);
        }
    }

    /**
     * Creates a pending intent that will trigger a data model action when the intent is
     * triggered
     */
    public static PendingIntent makeStartActionPendingIntent(final Context context,
            final Action action, final int requestCode, final boolean launchesAnActivity) {
        final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION);
        final Bundle actionBundle = new Bundle();
        actionBundle.putParcelable(BUNDLE_ACTION, action);
        intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
        if (launchesAnActivity) {
            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        }
        return PendingIntent.getBroadcast(context, requestCode, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCreate() {
        super.onCreate();
        mBackgroundWorker = DataModel.get().getBackgroundWorkerForActionService();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    /**
     * Queue intent to the ActionService.
     */
    private static void startServiceWithIntent(final Intent intent) {
        final Context context = Factory.get().getApplicationContext();
        final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
        intent.setClass(context, ActionServiceImpl.class);
        enqueueWork(context, intent);
    }

    public static void enqueueWork(Context context, Intent work) {
        enqueueWork(context, ActionServiceImpl.class, JOB_ID, work);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onHandleWork(final Intent intent) {
        if (intent == null) {
            // Shouldn't happen but sometimes does following another crash.
            LogUtil.w(TAG, "ActionService.onHandleIntent: Called with null intent");
            return;
        }
        final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);

        Action action;
        final Bundle actionBundle = intent.getBundleExtra(EXTRA_ACTION_BUNDLE);
        actionBundle.setClassLoader(getClassLoader());
        switch(opcode) {
            case OP_START_ACTION: {
                action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
                executeAction(action);
                break;
            }

            case OP_RECEIVE_BACKGROUND_RESPONSE: {
                action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
                final Bundle response = intent.getBundleExtra(EXTRA_WORKER_RESPONSE);
                processBackgroundResponse(action, response);
                break;
            }

            case OP_RECEIVE_BACKGROUND_FAILURE: {
                action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
                processBackgroundFailure(action);
                break;
            }

            default:
                throw new RuntimeException("Unrecognized opcode in ActionServiceImpl");
        }

        action.sendBackgroundActions(mBackgroundWorker);
    }

    private static final long EXECUTION_TIME_WARN_LIMIT_MS = 1000; // 1 second
    /**
     * Local execution of action on ActionService thread
     */
    private void executeAction(final Action action) {
        action.markBeginExecute();

        final LoggingTimer timer = createLoggingTimer(action, "#executeAction");
        timer.start();

        final Object result = action.executeAction();

        timer.stopAndLog();

        action.markEndExecute(result);
    }

    /**
     * Process response on ActionService thread
     */
    private void processBackgroundResponse(final Action action, final Bundle response) {
        final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundResponse");
        timer.start();

        action.processBackgroundWorkResponse(response);

        timer.stopAndLog();
    }

    /**
     * Process failure on ActionService thread
     */
    private void processBackgroundFailure(final Action action) {
        final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundFailure");
        timer.start();

        action.processBackgroundWorkFailure();

        timer.stopAndLog();
    }

    private static LoggingTimer createLoggingTimer(
            final Action action, final String methodName) {
        return new LoggingTimer(TAG, action.getClass().getSimpleName() + methodName,
                EXECUTION_TIME_WARN_LIMIT_MS);
    }
}
