/**
 * Copyright (C) 2019 LinkedIn Corp.
 * <p>
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.linkedin.android.testbutler;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Parcel;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.BundleCompat;

import com.linkedin.android.testbutler.shell.ShellButlerService;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;


/**
 * Manages ShellButlerService from the ButlerService process. Responsible for starting, binding to,
 * and stopping ShellButlerService.
 *
 * To start and 'bind' to a service running as the shell user, we use a number of tricks:
 *
 * 1) ShellButlerServiceBinder directly makes ADB calls.
 * ***To do this, you must run "adb reverse tcp:5038 tcp:5037"***, which lets this app connect
 * to the ADB server on the host.
 *
 * 2) ShellButlerService is started via 'app_process', which is a shell command which lets you
 * start Java processes. We pass in this APK itself as the classpath and use ShellButlerService
 * as the main class.
 *
 * 3) ShellButlerService isn't a real 'service' -- we can't register it in the manifest and bind
 * to it directly, because it needs to be started by the shell user... Luckily, Binders
 * (such as AIDL stubs) can be passed around via Intent. That means instead of ButlerService
 * (or TestButler) binding to ShellButlerService, ShellButlerService sends its ButlerApi stub
 * back to ButlerService. Just like 'bindService', the ButlerApi Stub is still executed in the
 * shell process, and the intent receiver gets a proxy for it. We just need to make sure that
 * ShellButlerService stays alive as long as ButlerService is bound.
 *
 * 4) Most intents come through on the main thread. Because TestButler starts ButlerService
 * first, its 'onBind' call would come through before any ShellButlerService intent containing
 * the ButlerApi to return. However, BroadcastReceivers can be run on a separate handler. Thus
 * ShellButlerService uses sendBroadcast to send the ButlerApi, and ButlerService's onCreate
 * blocks until that ButlerApi is received in a separate handler thread.
 *
 * 5) ShellButlerService is notified of shutdown via the binder. However, we didn't want to
 * change the AIDL that the user sees, so we call 'transact' explicitly with an unused
 * transaction code.
 * */
class ShellButlerServiceBinder {

    private static final String TAG = ShellButlerServiceBinder.class.getSimpleName();

    private static final String ADB_HOST = "localhost";
    private static final int ADB_REVERSE_PORT = 5038;

    private final Context context;

    private HandlerThread thread;
    private ButlerApiBroadcastReceiver receiver;
    private AdbDevice.AdbCommandTask shellProcessTask;

    private volatile ButlerApi butlerApi;

    ShellButlerServiceBinder(@NonNull Context context) {
        this.context = context;
    }

    @Nullable
    ButlerApi bind(long timeout, @NonNull TimeUnit unit) throws InterruptedException {
        AdbDevice adbDevice = AdbDevice.getCurrentDevice(ADB_HOST, ADB_REVERSE_PORT);

        // note: must use separate thread to receive, as we block ButlerService's main thread in
        // onCreate waiting for the butler api broadcast.
        thread = new HandlerThread("ButlerServiceStarted");
        thread.start();

        receiver = new ButlerApiBroadcastReceiver();
        IntentFilter filter = new IntentFilter(ShellButlerService.BROADCAST_BUTLER_API_ACTION);
        context.registerReceiver(receiver, filter, null, new Handler(thread.getLooper()));

        Log.d(TAG, "Registered ShellButlerService receiver, launching ShellButlerService");

        // Execute this apk itself, invoking main() in ShellButlerService
        String apkPath = context.getApplicationInfo().publicSourceDir;
        shellProcessTask = adbDevice.shellCommand("CLASSPATH=" + apkPath, "app_process", "/", ShellButlerService.class.getName());

        Log.d(TAG, "ShellButlerService launched, waiting for ButlerApi broadcast");

        if (!receiver.received.await(timeout, unit)) {
            Log.e(TAG, "Timed out waiting for ShellButlerService");
        } else {
            Log.d(TAG, "Received ButlerApi from ShellButlerService");
        }

        return butlerApi;
    }

    void unbind() {
        try {
            if (butlerApi == null) {
                if (shellProcessTask != null) {
                    // This is a rare case -- we started the shell process but never received the
                    // ButlerApi we use to stop it. Have to close the socket to kill the process.
                    shellProcessTask.closeSocket();
                }
            } else {
                Parcel data = Parcel.obtain();
                try {
                    butlerApi.asBinder().transact(ShellButlerService.KILL_CODE, data, null, 0);
                } finally {
                    data.recycle();
                }
                shellProcessTask.get();
            }
        } catch (InterruptedException e) {
            Log.e(TAG, "Interrupted while shutting down ShellButlerService, future tests may fail!", e);
            Thread.currentThread().interrupt();
        } catch (Exception e) {
            Log.e(TAG, "Failed to shut down ShellButlerService cleanly, future tests may fail!", e);
        }

        context.unregisterReceiver(receiver);
        receiver = null;
        thread.quit();
        thread = null;
        butlerApi = null;
    }

    private class ButlerApiBroadcastReceiver extends BroadcastReceiver {
        private final CountDownLatch received = new CountDownLatch(1);

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d(TAG, "ButlerApiBroadcastReceiver#onReceive was called");
            Bundle bundle = intent.getBundleExtra(ShellButlerService.BUTLER_API_BUNDLE_KEY);
            if (bundle != null) {
                IBinder binder = BundleCompat.getBinder(bundle, ShellButlerService.BUTLER_API_BUNDLE_KEY);
                butlerApi = ButlerApi.Stub.asInterface(binder);
                received.countDown();
            }
        }
    }
}