/*
 * Copyright (C) 2023 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.companion.virtual;

import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.companion.virtual.audio.VirtualAudioDevice;
import android.companion.virtual.sensor.VirtualSensor;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.IVirtualDisplayCallback;
import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.hardware.input.VirtualDpad;
import android.hardware.input.VirtualDpadConfig;
import android.hardware.input.VirtualKeyboard;
import android.hardware.input.VirtualKeyboardConfig;
import android.hardware.input.VirtualMouse;
import android.hardware.input.VirtualMouseConfig;
import android.hardware.input.VirtualNavigationTouchpad;
import android.hardware.input.VirtualNavigationTouchpadConfig;
import android.hardware.input.VirtualTouchscreen;
import android.hardware.input.VirtualTouchscreenConfig;
import android.media.AudioManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.util.ArrayMap;

import com.android.internal.annotations.GuardedBy;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.IntConsumer;

/**
 * An internal representation of a virtual device.
 *
 * @hide
 */
public class VirtualDeviceInternal {

    private final Context mContext;
    private final IVirtualDeviceManager mService;
    private final IVirtualDevice mVirtualDevice;
    private final Object mActivityListenersLock = new Object();
    @GuardedBy("mActivityListenersLock")
    private final ArrayMap<VirtualDeviceManager.ActivityListener, ActivityListenerDelegate>
            mActivityListeners =
            new ArrayMap<>();
    private final Object mIntentInterceptorListenersLock = new Object();
    @GuardedBy("mIntentInterceptorListenersLock")
    private final ArrayMap<VirtualDeviceManager.IntentInterceptorCallback,
            IntentInterceptorDelegate> mIntentInterceptorListeners =
            new ArrayMap<>();
    private final Object mSoundEffectListenersLock = new Object();
    @GuardedBy("mSoundEffectListenersLock")
    private final ArrayMap<VirtualDeviceManager.SoundEffectListener, SoundEffectListenerDelegate>
            mSoundEffectListeners = new ArrayMap<>();
    private final IVirtualDeviceActivityListener mActivityListenerBinder =
            new IVirtualDeviceActivityListener.Stub() {

                @Override
                public void onTopActivityChanged(int displayId, ComponentName topActivity,
                        @UserIdInt int userId) {
                    final long token = Binder.clearCallingIdentity();
                    try {
                        synchronized (mActivityListenersLock) {
                            for (int i = 0; i < mActivityListeners.size(); i++) {
                                mActivityListeners.valueAt(i)
                                        .onTopActivityChanged(displayId, topActivity);
                                mActivityListeners.valueAt(i)
                                        .onTopActivityChanged(displayId, topActivity, userId);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }

                @Override
                public void onDisplayEmpty(int displayId) {
                    final long token = Binder.clearCallingIdentity();
                    try {
                        synchronized (mActivityListenersLock) {
                            for (int i = 0; i < mActivityListeners.size(); i++) {
                                mActivityListeners.valueAt(i).onDisplayEmpty(displayId);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }
            };
    private final IVirtualDeviceSoundEffectListener mSoundEffectListener =
            new IVirtualDeviceSoundEffectListener.Stub() {
                @Override
                public void onPlaySoundEffect(int soundEffect) {
                    final long token = Binder.clearCallingIdentity();
                    try {
                        synchronized (mSoundEffectListenersLock) {
                            for (int i = 0; i < mSoundEffectListeners.size(); i++) {
                                mSoundEffectListeners.valueAt(i).onPlaySoundEffect(soundEffect);
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }
            };
    @Nullable
    private VirtualAudioDevice mVirtualAudioDevice;

    VirtualDeviceInternal(
            IVirtualDeviceManager service,
            Context context,
            int associationId,
            VirtualDeviceParams params) throws RemoteException {
        mService = service;
        mContext = context.getApplicationContext();
        mVirtualDevice = service.createVirtualDevice(
                new Binder(),
                mContext.getPackageName(),
                associationId,
                params,
                mActivityListenerBinder,
                mSoundEffectListener);
    }

    int getDeviceId() {
        try {
            return mVirtualDevice.getDeviceId();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull Context createContext() {
        try {
            return mContext.createDeviceContext(mVirtualDevice.getDeviceId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    List<VirtualSensor> getVirtualSensorList() {
        try {
            return mVirtualDevice.getVirtualSensorList();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void launchPendingIntent(
            int displayId,
            @NonNull PendingIntent pendingIntent,
            @NonNull Executor executor,
            @NonNull IntConsumer listener) {
        try {
            mVirtualDevice.launchPendingIntent(
                    displayId,
                    pendingIntent,
                    new ResultReceiver(new Handler(Looper.getMainLooper())) {
                        @Override
                        protected void onReceiveResult(int resultCode, Bundle resultData) {
                            super.onReceiveResult(resultCode, resultData);
                            executor.execute(() -> listener.accept(resultCode));
                        }
                    });
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    @Nullable
    VirtualDisplay createVirtualDisplay(
            @NonNull VirtualDisplayConfig config,
            @Nullable @CallbackExecutor Executor executor,
            @Nullable VirtualDisplay.Callback callback) {
        IVirtualDisplayCallback callbackWrapper =
                new DisplayManagerGlobal.VirtualDisplayCallback(callback, executor);
        final int displayId;
        try {
            displayId = mService.createVirtualDisplay(config, callbackWrapper, mVirtualDevice,
                    mContext.getPackageName());
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
        DisplayManagerGlobal displayManager = DisplayManagerGlobal.getInstance();
        return displayManager.createVirtualDisplayWrapper(config, callbackWrapper,
                displayId);
    }

    void close() {
        try {
            // This also takes care of unregistering all virtual sensors.
            mVirtualDevice.close();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        if (mVirtualAudioDevice != null) {
            mVirtualAudioDevice.close();
            mVirtualAudioDevice = null;
        }
    }

    @NonNull
    VirtualDpad createVirtualDpad(@NonNull VirtualDpadConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualDpad:" + config.getInputDeviceName());
            mVirtualDevice.createVirtualDpad(config, token);
            return new VirtualDpad(mVirtualDevice, token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualKeyboard createVirtualKeyboard(@NonNull VirtualKeyboardConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualKeyboard:" + config.getInputDeviceName());
            mVirtualDevice.createVirtualKeyboard(config, token);
            return new VirtualKeyboard(mVirtualDevice, token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualMouse createVirtualMouse(@NonNull VirtualMouseConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualMouse:" + config.getInputDeviceName());
            mVirtualDevice.createVirtualMouse(config, token);
            return new VirtualMouse(mVirtualDevice, token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualTouchscreen createVirtualTouchscreen(
            @NonNull VirtualTouchscreenConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualTouchscreen:" + config.getInputDeviceName());
            mVirtualDevice.createVirtualTouchscreen(config, token);
            return new VirtualTouchscreen(mVirtualDevice, token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualNavigationTouchpad createVirtualNavigationTouchpad(
            @NonNull VirtualNavigationTouchpadConfig config) {
        try {
            final IBinder token = new Binder(
                    "android.hardware.input.VirtualNavigationTouchpad:"
                            + config.getInputDeviceName());
            mVirtualDevice.createVirtualNavigationTouchpad(config, token);
            return new VirtualNavigationTouchpad(mVirtualDevice, token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    VirtualAudioDevice createVirtualAudioDevice(
            @NonNull VirtualDisplay display,
            @Nullable Executor executor,
            @Nullable VirtualAudioDevice.AudioConfigurationChangeCallback callback) {
        if (mVirtualAudioDevice == null) {
            mVirtualAudioDevice = new VirtualAudioDevice(mContext, mVirtualDevice, display,
                    executor, callback, () -> mVirtualAudioDevice = null);
        }
        return mVirtualAudioDevice;
    }

    @NonNull
    void setShowPointerIcon(boolean showPointerIcon) {
        try {
            mVirtualDevice.setShowPointerIcon(showPointerIcon);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    void addActivityListener(
            @CallbackExecutor @NonNull Executor executor,
            @NonNull VirtualDeviceManager.ActivityListener listener) {
        final ActivityListenerDelegate delegate = new ActivityListenerDelegate(
                Objects.requireNonNull(listener), Objects.requireNonNull(executor));
        synchronized (mActivityListenersLock) {
            mActivityListeners.put(listener, delegate);
        }
    }

    void removeActivityListener(@NonNull VirtualDeviceManager.ActivityListener listener) {
        synchronized (mActivityListenersLock) {
            mActivityListeners.remove(Objects.requireNonNull(listener));
        }
    }

    void addSoundEffectListener(@CallbackExecutor @NonNull Executor executor,
            @NonNull VirtualDeviceManager.SoundEffectListener soundEffectListener) {
        final SoundEffectListenerDelegate delegate =
                new SoundEffectListenerDelegate(Objects.requireNonNull(executor),
                        Objects.requireNonNull(soundEffectListener));
        synchronized (mSoundEffectListenersLock) {
            mSoundEffectListeners.put(soundEffectListener, delegate);
        }
    }

    void removeSoundEffectListener(
            @NonNull VirtualDeviceManager.SoundEffectListener soundEffectListener) {
        synchronized (mSoundEffectListenersLock) {
            mSoundEffectListeners.remove(Objects.requireNonNull(soundEffectListener));
        }
    }

    void registerIntentInterceptor(
            @NonNull IntentFilter interceptorFilter,
            @CallbackExecutor @NonNull Executor executor,
            @NonNull VirtualDeviceManager.IntentInterceptorCallback interceptorCallback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(interceptorFilter);
        Objects.requireNonNull(interceptorCallback);
        final IntentInterceptorDelegate delegate =
                new IntentInterceptorDelegate(executor, interceptorCallback);
        try {
            mVirtualDevice.registerIntentInterceptor(delegate, interceptorFilter);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        synchronized (mIntentInterceptorListenersLock) {
            mIntentInterceptorListeners.put(interceptorCallback, delegate);
        }
    }

    void unregisterIntentInterceptor(
            @NonNull VirtualDeviceManager.IntentInterceptorCallback interceptorCallback) {
        Objects.requireNonNull(interceptorCallback);
        final IntentInterceptorDelegate delegate;
        synchronized (mIntentInterceptorListenersLock) {
            delegate = mIntentInterceptorListeners.remove(interceptorCallback);
        }
        if (delegate != null) {
            try {
                mVirtualDevice.unregisterIntentInterceptor(delegate);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }

    /**
     * A wrapper for {@link VirtualDeviceManager.ActivityListener} that executes callbacks on the
     * given executor.
     */
    private static class ActivityListenerDelegate {
        @NonNull private final VirtualDeviceManager.ActivityListener mActivityListener;
        @NonNull private final Executor mExecutor;

        ActivityListenerDelegate(@NonNull VirtualDeviceManager.ActivityListener listener,
                @NonNull Executor executor) {
            mActivityListener = listener;
            mExecutor = executor;
        }

        public void onTopActivityChanged(int displayId, ComponentName topActivity) {
            mExecutor.execute(() -> mActivityListener.onTopActivityChanged(displayId, topActivity));
        }

        public void onTopActivityChanged(int displayId, ComponentName topActivity,
                @UserIdInt int userId) {
            mExecutor.execute(() ->
                    mActivityListener.onTopActivityChanged(displayId, topActivity, userId));
        }

        public void onDisplayEmpty(int displayId) {
            mExecutor.execute(() -> mActivityListener.onDisplayEmpty(displayId));
        }
    }

    /**
     * A wrapper for {@link VirtualDeviceManager.IntentInterceptorCallback} that executes callbacks
     * on the given executor.
     */
    private static class IntentInterceptorDelegate extends IVirtualDeviceIntentInterceptor.Stub {
        @NonNull private final VirtualDeviceManager.IntentInterceptorCallback
                mIntentInterceptorCallback;
        @NonNull private final Executor mExecutor;

        private IntentInterceptorDelegate(Executor executor,
                VirtualDeviceManager.IntentInterceptorCallback interceptorCallback) {
            mExecutor = executor;
            mIntentInterceptorCallback = interceptorCallback;
        }

        @Override
        public void onIntentIntercepted(Intent intent) {
            final long token = Binder.clearCallingIdentity();
            try {
                mExecutor.execute(() -> mIntentInterceptorCallback.onIntentIntercepted(intent));
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }
    }

    /**
     * A wrapper for {@link VirtualDeviceManager.SoundEffectListener} that executes callbacks on the
     * given executor.
     */
    private static class SoundEffectListenerDelegate {
        @NonNull private final VirtualDeviceManager.SoundEffectListener mSoundEffectListener;
        @NonNull private final Executor mExecutor;

        private SoundEffectListenerDelegate(Executor executor,
                VirtualDeviceManager.SoundEffectListener soundEffectCallback) {
            mSoundEffectListener = soundEffectCallback;
            mExecutor = executor;
        }

        public void onPlaySoundEffect(@AudioManager.SystemSoundEffect int effectType) {
            mExecutor.execute(() -> mSoundEffectListener.onPlaySoundEffect(effectType));
        }
    }
}
