/*
 * Copyright (C) 2019 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 android.car.usb.handler;

import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.car.AoapService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.hardware.usb.UsbDevice;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import android.util.SparseArray;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/** Manages connections to {@link android.car.AoapService} (AOAP handler apps). */
public class AoapServiceManager {
    private static final String TAG = AoapServiceManager.class.getSimpleName();

    // Keep in sync with android.car.AoapService.java.
    private static final String KEY_DEVICE = "usb-device";
    private static final String KEY_RESULT = "result";
    private static final int MSG_CAN_SWITCH_TO_AOAP = 3;
    private static final int MSG_CAN_SWITCH_TO_AOAP_RESPONSE = 4;
    private static final int MSG_NEW_DEVICE_ATTACHED = 1;
    private static final int MSG_NEW_DEVICE_ATTACHED_RESPONSE = 2;

    private static final int MSG_DISCONNECT = 1;
    private static final int DISCONNECT_DELAY_MS = 30000;
    private static final int INVOCATION_TIMEOUT_MS = 20000;

    private final HashMap<ComponentName, AoapServiceConnection> mConnections = new HashMap<>();
    private Context mContext;
    private final Object mLock = new Object();
    private final HandlerThread mHandlerThread;
    private final Handler mHandler;

    public AoapServiceManager(Context context) {
        mContext = context;

        mHandlerThread = new HandlerThread(TAG);
        mHandlerThread.start();

        mHandler = new Handler(mHandlerThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == MSG_DISCONNECT) {
                    removeConnection((AoapServiceConnection) msg.obj);
                } else {
                    Log.e(TAG, "Unexpected message " + msg.what);
                }
            }
        };
    }

    /**
     * Calls synchronously with timeout {@link #INVOCATION_TIMEOUT_MS} to the given service to check
     * if it supports the device.
     */
    @WorkerThread
    public boolean isDeviceSupported(UsbDevice device, ComponentName serviceName) {
        final AoapServiceConnection connection = getConnectionOrNull(serviceName);
        if (connection == null) {
            return false;
        }

        try {
            return connection.isDeviceSupported(device)
                    .get(INVOCATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (ExecutionException | InterruptedException | TimeoutException e) {
            Log.w(TAG, "Failed to get response isDeviceSupported from " + serviceName, e);
            return false;
        }
    }

    /**
     * Calls synchronously with timeout {@link #INVOCATION_TIMEOUT_MS} to the given service to check
     * if the device can be switched to AOAP mode now.
     */
    @WorkerThread
    public boolean canSwitchDeviceToAoap(UsbDevice device, ComponentName serviceName) {
        final AoapServiceConnection connection = getConnectionOrNull(serviceName);
        if (connection == null) {
            return false;
        }

        try {
            return connection.canSwitchDeviceToAoap(device)
                    .get(INVOCATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (ExecutionException | InterruptedException | TimeoutException e) {
            Log.w(TAG, "Failed to get response of canSwitchDeviceToAoap from " + serviceName, e);
            return false;
        }
    }

    @Nullable
    private AoapServiceConnection getConnectionOrNull(ComponentName name) {
        AoapServiceConnection connection;
        synchronized (mLock) {
            connection = mConnections.get(name);
            if (connection != null) {
                postponeServiceDisconnection(connection);
                return connection;
            }

            connection = new AoapServiceConnection(name, this, mHandlerThread.getLooper());
            boolean bound = mContext.bindService(
                    createIntent(name), connection, Context.BIND_AUTO_CREATE);
            if (bound) {
                mConnections.put(name, connection);
                postponeServiceDisconnection(connection);
            } else {
                Log.w(TAG, "Failed to bind to service " + name);
                return null;
            }
        }
        return connection;
    }

    private void postponeServiceDisconnection(AoapServiceConnection connection) {
        mHandler.removeMessages(MSG_DISCONNECT, connection);
        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DISCONNECT, connection),
                DISCONNECT_DELAY_MS);
    }

    private static Intent createIntent(ComponentName name) {
        Intent intent = new Intent();
        intent.setComponent(name);
        return intent;
    }

    private void removeConnection(AoapServiceConnection connection) {
        Log.i(TAG, "Removing connection to " + connection);
        synchronized (mLock) {
            mConnections.remove(connection.mComponentName);
            if (connection.mBound) {
                mContext.unbindService(connection);
                connection.mBound = false;
            }
        }
    }

    private static class AoapServiceConnection implements ServiceConnection {
        private Messenger mOutgoingMessenger;
        private boolean mBound;
        private final CompletableFuture<Void> mConnected = new CompletableFuture<>();
        private final SparseArray<CompletableFuture<Bundle>> mExpectedResponses =
                new SparseArray<>();
        private final ComponentName mComponentName;
        private final WeakReference<AoapServiceManager> mManagerRef;
        private final Messenger mIncomingMessenger;
        private final Object mLock = new Object();

        private AoapServiceConnection(ComponentName name, AoapServiceManager manager,
                Looper looper) {
            mComponentName = name;
            mManagerRef = new WeakReference<>(manager);
            mIncomingMessenger = new Messenger(new Handler(looper) {
                @Override
                public void handleMessage(Message msg) {
                    onResponse(msg);
                }
            });
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            if (service == null) {
                Log.e(TAG, "Binder object was not provided on service connection to " + name);
                return;
            }

            synchronized (mLock) {
                mBound = true;
                mOutgoingMessenger = new Messenger(service);
            }
            mConnected.complete(null);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            synchronized (mLock) {
                mOutgoingMessenger = null;
                mBound = false;
            }

            final AoapServiceManager mgr = mManagerRef.get();
            if (mgr != null) {
                mgr.removeConnection(this);
            }
        }

        private void onResponse(Message message) {
            final CompletableFuture<Bundle> response;
            synchronized (mLock) {
                response = mExpectedResponses.removeReturnOld(message.what);
            }
            if (response == null) {
                Log.e(TAG, "Received unexpected response " + message.what + ", expected: "
                        + mExpectedResponses);
                return;
            }

            if (message.getData() == null) {
                throw new IllegalArgumentException("Received response msg " + message.what
                        + " without data");
            }
            Log.i(TAG, "onResponse msg: " + message.what + ", data: " + message.getData());
            boolean res = response.complete(message.getData());
            if (!res) {
                Log.w(TAG, "Failed to complete future " + response);
            }
        }

        CompletableFuture<Boolean> isDeviceSupported(UsbDevice device) {
            return sendMessageForResult(
                    MSG_NEW_DEVICE_ATTACHED,
                    MSG_NEW_DEVICE_ATTACHED_RESPONSE,
                    createUsbDeviceData(device))
                    .thenApply(this::isResultOk);

        }

        CompletableFuture<Boolean> canSwitchDeviceToAoap(UsbDevice device) {
            return sendMessageForResult(
                    MSG_CAN_SWITCH_TO_AOAP,
                    MSG_CAN_SWITCH_TO_AOAP_RESPONSE,
                    createUsbDeviceData(device))
                    .thenApply(this::isResultOk);
        }

        private boolean isResultOk(Bundle data) {
            int result = data.getInt(KEY_RESULT);
            Log.i(TAG, "Got result: " + data);
            return AoapService.RESULT_OK == result;
        }

        private static Bundle createUsbDeviceData(UsbDevice device) {
            Bundle data = new Bundle(1);
            data.putParcelable(KEY_DEVICE, device);
            return data;
        }

        private CompletableFuture<Bundle> sendMessageForResult(
                int msgRequest, int msgResponse, Bundle data) {
            return mConnected.thenCompose(x -> {
                CompletableFuture<Bundle> responseFuture = new CompletableFuture<>();
                Messenger messenger;
                synchronized (mLock) {
                    mExpectedResponses.put(msgResponse, responseFuture);
                    messenger = mOutgoingMessenger;
                }
                send(messenger, msgRequest, data);

                return responseFuture;
            });
        }

        private void send(Messenger messenger, int req, Bundle data) {
            Message msg = Message.obtain(null, req, null);
            msg.replyTo = mIncomingMessenger;
            msg.setData(data);
            try {
                messenger.send(msg);
            } catch (RemoteException e) {
                throw new RuntimeException("Connection broken with " + mComponentName, e);
            }
        }

        @Override
        public String toString() {
            return "AoapServiceConnection{"
                    + "mBound=" + mBound
                    + ", mConnected=" + mConnected
                    + ", mExpectedResponses=" + mExpectedResponses
                    + ", mComponentName=" + mComponentName
                    + '}';
        }
    }
}
