/*
 * Copyright (C) 2016 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.phone.vvm;

import android.annotation.Nullable;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telephony.CarrierConfigManager;
import android.telephony.VisualVoicemailService;
import android.telephony.VisualVoicemailSms;
import android.text.TextUtils;

import com.android.internal.telephony.util.TelephonyUtils;
import com.android.phone.Assert;
import com.android.phone.R;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
 * Service to manage tasks issued to the {@link VisualVoicemailService}. This service will bind to
 * the default dialer on a visual voicemail event if it implements the VisualVoicemailService. The
 * service will hold all resource for the VisualVoicemailService until {@link
 * VisualVoicemailService.VisualVoicemailTask#finish()} has been called on all issued tasks.
 *
 * If the service is already running it will be reused for new events. The service will stop itself
 * after all events are handled.
 */
public class RemoteVvmTaskManager extends Service {

    private static final String TAG = "RemoteVvmTaskManager";

    private static final String ACTION_START_CELL_SERVICE_CONNECTED =
            "ACTION_START_CELL_SERVICE_CONNECTED";
    private static final String ACTION_START_SMS_RECEIVED = "ACTION_START_SMS_RECEIVED";
    private static final String ACTION_START_SIM_REMOVED = "ACTION_START_SIM_REMOVED";

    // TODO(b/35766990): Remove after VisualVoicemailService API is stabilized.
    private static final String ACTION_VISUAL_VOICEMAIL_SERVICE_EVENT =
            "com.android.phone.vvm.ACTION_VISUAL_VOICEMAIL_SERVICE_EVENT";
    private static final String EXTRA_WHAT = "what";

    private static final String EXTRA_TARGET_PACKAGE = "target_package";

    // TODO(twyen): track task individually to have time outs.
    private int mTaskReferenceCount;

    private RemoteServiceConnection mConnection;

    /**
     * Handles incoming messages from the VisualVoicemailService.
     */
    private Messenger mMessenger;

    static void startCellServiceConnected(Context context,
            PhoneAccountHandle phoneAccountHandle) {
        Intent intent = new Intent(ACTION_START_CELL_SERVICE_CONNECTED, null, context,
                RemoteVvmTaskManager.class);
        intent.putExtra(VisualVoicemailService.DATA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
        context.startService(intent);
    }

    static void startSmsReceived(Context context, VisualVoicemailSms sms,
            String targetPackage) {
        Intent intent = new Intent(ACTION_START_SMS_RECEIVED, null, context,
                RemoteVvmTaskManager.class);
        intent.putExtra(VisualVoicemailService.DATA_PHONE_ACCOUNT_HANDLE,
                sms.getPhoneAccountHandle());
        intent.putExtra(VisualVoicemailService.DATA_SMS, sms);
        intent.putExtra(EXTRA_TARGET_PACKAGE, targetPackage);
        context.startService(intent);
    }

    static void startSimRemoved(Context context, PhoneAccountHandle phoneAccountHandle) {
        Intent intent = new Intent(ACTION_START_SIM_REMOVED, null, context,
                RemoteVvmTaskManager.class);
        intent.putExtra(VisualVoicemailService.DATA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
        context.startService(intent);
    }

    static boolean hasRemoteService(Context context, int subId, String targetPackage) {
        return getRemotePackage(context, subId, targetPackage) != null;
    }

    /**
     * Return the {@link ComponentName} of the {@link VisualVoicemailService} which is active (the
     * current default dialer), or {@code null} if no implementation is found.
     */
    @Nullable
    public static ComponentName getRemotePackage(Context context, int subId) {
        return getRemotePackage(context, subId, null);
    }

    /**
     * Return the {@link ComponentName} of the {@link VisualVoicemailService} which is active (the
     * current default dialer), or {@code null} if no implementation is found.
     *
     * @param targetPackage the package that should be the active VisualVociemailService
     */
    @Nullable
    public static ComponentName getRemotePackage(Context context, int subId,
            @Nullable String targetPackage) {
        ComponentName broadcastPackage = getBroadcastPackage(context);
        if (broadcastPackage != null) {
            return broadcastPackage;
        }

        Intent bindIntent = newBindIntent(context);

        TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
        List<String> packages = new ArrayList<>();
        packages.add(telecomManager.getDefaultDialerPackage());
        // TODO(b/73136824): Check permissions in the calling function and avoid relying on the
        // binder caller's permissions to access the carrier config.
        PersistableBundle carrierConfig = context
                .getSystemService(CarrierConfigManager.class).getConfigForSubId(subId);
        packages.add(
                carrierConfig
                        .getString(CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING));
        String[] vvmPackages = carrierConfig
                .getStringArray(CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY);
        if (vvmPackages != null && vvmPackages.length > 0) {
            for (String packageName : vvmPackages) {
                packages.add(packageName);
            }
        }
        packages.add(context.getResources().getString(R.string.system_visual_voicemail_client));
        packages.add(telecomManager.getSystemDialerPackage());

        for (String packageName : packages) {
            if (TextUtils.isEmpty(packageName)) {
                continue;
            }
            bindIntent.setPackage(packageName);
            ResolveInfo info = context.getPackageManager().resolveService(bindIntent, 0);
            if (info == null) {
                continue;
            }
            if (info.serviceInfo == null) {
                VvmLog.w(TAG,
                        "Component " + TelephonyUtils.getComponentInfo(info)
                            + " is not a service, ignoring");
                continue;
            }
            if (!android.Manifest.permission.BIND_VISUAL_VOICEMAIL_SERVICE
                    .equals(info.serviceInfo.permission)) {
                VvmLog.w(TAG, "package " + info.serviceInfo.packageName
                        + " does not enforce BIND_VISUAL_VOICEMAIL_SERVICE, ignoring");
                continue;
            }
            if (targetPackage != null && !TextUtils.equals(packageName, targetPackage)) {
                VvmLog.w(TAG, "target package " + targetPackage
                        + " is no longer the active VisualVoicemailService, ignoring");
                continue;
            }
            ComponentInfo componentInfo = TelephonyUtils.getComponentInfo(info);
            return new ComponentName(componentInfo.packageName, componentInfo.name);

        }
        return null;
    }

    @Nullable
    private static ComponentName getBroadcastPackage(Context context) {
        Intent broadcastIntent = new Intent(ACTION_VISUAL_VOICEMAIL_SERVICE_EVENT);
        broadcastIntent.setPackage(
                context.getSystemService(TelecomManager.class).getDefaultDialerPackage());
        List<ResolveInfo> info = context.getPackageManager()
                .queryBroadcastReceivers(broadcastIntent, PackageManager.MATCH_ALL);
        if (info == null) {
            return null;
        }
        if (info.isEmpty()) {
            return null;
        }
        ComponentInfo componentInfo = TelephonyUtils.getComponentInfo(info.get(0));
        return new ComponentName(componentInfo.packageName, componentInfo.name);
    }

    @Override
    public void onCreate() {
        Assert.isMainThread();
        mMessenger = new Messenger(new Handler() {
            @Override
            public void handleMessage(Message msg) {
                Assert.isMainThread();
                switch (msg.what) {
                    case VisualVoicemailService.MSG_TASK_ENDED:
                        mTaskReferenceCount--;
                        checkReference();
                        break;
                    default:
                        VvmLog.wtf(TAG, "unexpected message " + msg.what);
                }
            }
        });
    }

    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
        Assert.isMainThread();
        mTaskReferenceCount++;

        if (intent == null) {
            VvmLog.i(TAG, "received intent is null");
            checkReference();
            return START_NOT_STICKY;
        }
        PhoneAccountHandle phoneAccountHandle = intent.getExtras()
                .getParcelable(VisualVoicemailService.DATA_PHONE_ACCOUNT_HANDLE);
        int subId = PhoneAccountHandleConverter.toSubId(phoneAccountHandle);
        UserHandle userHandle = phoneAccountHandle.getUserHandle();
        ComponentName remotePackage = getRemotePackage(this, subId,
                intent.getStringExtra(EXTRA_TARGET_PACKAGE));
        if (remotePackage == null) {
            VvmLog.i(TAG, "No service to handle " + intent.getAction() + ", ignoring");
            checkReference();
            return START_NOT_STICKY;
        }

        switch (intent.getAction()) {
            case ACTION_START_CELL_SERVICE_CONNECTED:
                send(remotePackage, VisualVoicemailService.MSG_ON_CELL_SERVICE_CONNECTED,
                        intent.getExtras(), userHandle);
                break;
            case ACTION_START_SMS_RECEIVED:
                send(remotePackage, VisualVoicemailService.MSG_ON_SMS_RECEIVED, intent.getExtras(),
                        userHandle);
                break;
            case ACTION_START_SIM_REMOVED:
                send(remotePackage, VisualVoicemailService.MSG_ON_SIM_REMOVED, intent.getExtras(),
                        userHandle);
                break;
            default:
                Assert.fail("Unexpected action +" + intent.getAction());
                break;
        }
        // Don't rerun service if processed is killed.
        return START_NOT_STICKY;
    }

    @Override
    @Nullable
    public IBinder onBind(Intent intent) {
        return null;
    }

    private int getTaskId() {
        // TODO(twyen): generate unique IDs. Reference counting is used now so it doesn't matter.
        return 1;
    }

    /**
     * Class for interacting with the main interface of the service.
     */
    private class RemoteServiceConnection implements ServiceConnection {

        private final Queue<Message> mTaskQueue = new LinkedList<>();

        private boolean mConnected;

        /**
         * A handler in the VisualVoicemailService
         */
        private Messenger mRemoteMessenger;

        public void enqueue(Message message) {
            mTaskQueue.add(message);
            if (mConnected) {
                runQueue();
            }
        }

        public boolean isConnected() {
            return mConnected;
        }

        public void onServiceConnected(ComponentName className,
                IBinder service) {
            mRemoteMessenger = new Messenger(service);
            mConnected = true;
            runQueue();
        }

        public void onServiceDisconnected(ComponentName className) {
            mConnection = null;
            mConnected = false;
            mRemoteMessenger = null;
            VvmLog.e(TAG, "Service disconnected, " + mTaskReferenceCount + " tasks dropped.");
            mTaskReferenceCount = 0;
            checkReference();
        }

        private void runQueue() {
            Assert.isMainThread();
            Message message = mTaskQueue.poll();
            while (message != null) {
                message.replyTo = mMessenger;
                message.arg1 = getTaskId();

                try {
                    mRemoteMessenger.send(message);
                } catch (RemoteException e) {
                    VvmLog.e(TAG, "Error sending message to remote service", e);
                }
                message = mTaskQueue.poll();
            }
        }
    }

    private void send(ComponentName remotePackage, int what, Bundle extras, UserHandle userHandle) {
        Assert.isMainThread();

        if (getBroadcastPackage(this) != null) {
            /*
             * Temporarily use a broadcast to notify dialer VVM events instead of using the
             * VisualVoicemailService.
             * b/35766990 The VisualVoicemailService is undergoing API changes. The dialer is in
             * a different repository so it can not be updated in sync with android SDK. It is also
             * hard to make a manifest service to work in the intermittent state.
             */
            VvmLog.i(TAG, "sending broadcast " + what + " to " + remotePackage);
            Intent intent = new Intent(ACTION_VISUAL_VOICEMAIL_SERVICE_EVENT);
            intent.putExtras(extras);
            intent.putExtra(EXTRA_WHAT, what);
            intent.setComponent(remotePackage);
            sendBroadcastAsUser(intent, userHandle);
            return;
        }

        Message message = Message.obtain();
        message.what = what;
        message.setData(new Bundle(extras));
        if (mConnection == null) {
            mConnection = new RemoteServiceConnection();
        }
        mConnection.enqueue(message);

        if (!mConnection.isConnected()) {
            Intent intent = newBindIntent(this);
            intent.setComponent(remotePackage);
            VvmLog.i(TAG, "Binding to " + intent.getComponent());
            bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE, userHandle);
        }
    }

    private void checkReference() {
        if (mConnection == null) {
            return;
        }
        if (mTaskReferenceCount == 0) {
            unbindService(mConnection);
            mConnection = null;
        }
    }

    private static Intent newBindIntent(Context context) {
        Intent intent = new Intent();
        intent.setAction(VisualVoicemailService.SERVICE_INTERFACE);
        return intent;
    }
}
