/*
 * 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.car;

import android.car.CarProjectionManager;
import android.car.ICarProjection;
import android.car.ICarProjectionListener;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.view.KeyEvent;

import java.io.PrintWriter;

/**
 * Car projection service allows to bound to projected app to boost it prioirity.
 * It also enables proejcted applications to handle voice action requests.
 */
class CarProjectionService extends ICarProjection.Stub implements CarServiceBase,
        BinderInterfaceContainer.BinderEventHandler<ICarProjectionListener> {
    private final ListenerHolder mAllListeners;
    private final CarInputService mCarInputService;
    private final Context mContext;

    private final CarInputService.KeyEventListener mVoiceAssistantKeyListener =
            new CarInputService.KeyEventListener() {
                @Override
                public void onKeyEvent(KeyEvent event) {
                    handleVoiceAssitantRequest(false);
                }
            };

    private final CarInputService.KeyEventListener mLongVoiceAssistantKeyListener =
            new CarInputService.KeyEventListener() {
                @Override
                public void onKeyEvent(KeyEvent event) {
                    handleVoiceAssitantRequest(true);
                }
            };

    private final ServiceConnection mConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName className, IBinder service) {
                synchronized (CarProjectionService.this) {
                    mBound = true;
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName className) {
                // Service has crashed.
                Log.w(CarLog.TAG_PROJECTION, "Service disconnected: " + className);
                synchronized (CarProjectionService.this) {
                    mRegisteredService = null;
                }
                unbindServiceIfBound();
            }
        };

    private boolean mBound;
    private Intent mRegisteredService;

    CarProjectionService(Context context, CarInputService carInputService) {
        mContext = context;
        mCarInputService = carInputService;
        mAllListeners = new ListenerHolder(this);
    }

    @Override
    public void registerProjectionRunner(Intent serviceIntent) {
        // We assume one active projection app running in the system at one time.
        synchronized (this) {
            if (serviceIntent.filterEquals(mRegisteredService)) {
                return;
            }
            if (mRegisteredService != null) {
                Log.w(CarLog.TAG_PROJECTION, "Registering new service[" + serviceIntent
                        + "] while old service[" + mRegisteredService + "] is still running");
            }
            unbindServiceIfBound();
        }
        bindToService(serviceIntent);
    }

    @Override
    public void unregisterProjectionRunner(Intent serviceIntent) {
        synchronized (this) {
            if (!serviceIntent.filterEquals(mRegisteredService)) {
                Log.w(CarLog.TAG_PROJECTION, "Request to unbind unregistered service["
                        + serviceIntent + "]. Registered service[" + mRegisteredService + "]");
                return;
            }
            mRegisteredService = null;
        }
        unbindServiceIfBound();
    }

    private void bindToService(Intent serviceIntent) {
        synchronized (this) {
            mRegisteredService = serviceIntent;
        }
        UserHandle userHandle = UserHandle.getUserHandleForUid(Binder.getCallingUid());
        mContext.startServiceAsUser(serviceIntent, userHandle);
        mContext.bindServiceAsUser(serviceIntent, mConnection, Context.BIND_IMPORTANT, userHandle);
    }

    private void unbindServiceIfBound() {
        synchronized (this) {
            if (!mBound) {
                return;
            }
            mBound = false;
        }
        mContext.unbindService(mConnection);
    }

    private synchronized void handleVoiceAssitantRequest(boolean isTriggeredByLongPress) {
        for (BinderInterfaceContainer.BinderInterface<ICarProjectionListener> listener :
                 mAllListeners.getInterfaces()) {
            ListenerInfo listenerInfo = (ListenerInfo) listener;
            if ((listenerInfo.hasFilter(CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH)
                    && isTriggeredByLongPress)
                    || (listenerInfo.hasFilter(CarProjectionManager.PROJECTION_VOICE_SEARCH)
                    && !isTriggeredByLongPress)) {
                dispatchVoiceAssistantRequest(listenerInfo.binderInterface, isTriggeredByLongPress);
            }
        }
    }

    @Override
    public void regsiterProjectionListener(ICarProjectionListener listener, int filter) {
        synchronized (this) {
            ListenerInfo info = (ListenerInfo) mAllListeners.getBinderInterface(listener);
            if (info == null) {
                info = new ListenerInfo(mAllListeners, listener, filter);
                mAllListeners.addBinderInterface(info);
            } else {
                info.setFilter(filter);
            }
        }
        updateCarInputServiceListeners();
    }

    @Override
    public void unregsiterProjectionListener(ICarProjectionListener listener) {
        synchronized (this) {
            mAllListeners.removeBinder(listener);
        }
        updateCarInputServiceListeners();
    }

    private void updateCarInputServiceListeners() {
        boolean listenShortPress = false;
        boolean listenLongPress = false;
        synchronized (this) {
            for (BinderInterfaceContainer.BinderInterface<ICarProjectionListener> listener :
                         mAllListeners.getInterfaces()) {
                ListenerInfo listenerInfo = (ListenerInfo) listener;
                listenShortPress |= listenerInfo.hasFilter(
                        CarProjectionManager.PROJECTION_VOICE_SEARCH);
                listenLongPress |= listenerInfo.hasFilter(
                        CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH);
            }
        }
        mCarInputService.setVoiceAssitantKeyListener(listenShortPress
                ? mVoiceAssistantKeyListener : null);
        mCarInputService.setLongVoiceAssitantKeyListener(listenLongPress
                ? mLongVoiceAssistantKeyListener : null);
    }

    @Override
    public void init() {
        // nothing to do
    }

    @Override
    public void release() {
        synchronized (this) {
            mAllListeners.clear();
        }
    }

    @Override
    public void onBinderDeath(
            BinderInterfaceContainer.BinderInterface<ICarProjectionListener> bInterface) {
        unregsiterProjectionListener(bInterface.binderInterface);
    }

    @Override
    public void dump(PrintWriter writer) {
        writer.println("**CarProjectionService**");
        synchronized (this) {
            for (BinderInterfaceContainer.BinderInterface<ICarProjectionListener> listener :
                         mAllListeners.getInterfaces()) {
                ListenerInfo listenerInfo = (ListenerInfo) listener;
                writer.println(listenerInfo.toString());
            }
        }
    }

    private void dispatchVoiceAssistantRequest(ICarProjectionListener listener,
            boolean fromLongPress) {
        try {
            listener.onVoiceAssistantRequest(fromLongPress);
        } catch (RemoteException e) {
        }
    }

    private static class ListenerHolder extends BinderInterfaceContainer<ICarProjectionListener> {
        private ListenerHolder(CarProjectionService service) {
            super(service);
        }
    }

    private static class ListenerInfo extends
            BinderInterfaceContainer.BinderInterface<ICarProjectionListener> {
        private int mFilter;

        private ListenerInfo(ListenerHolder holder, ICarProjectionListener binder, int filter) {
            super(holder, binder);
            this.mFilter = filter;
        }

        private synchronized int getFilter() {
            return mFilter;
        }

        private boolean hasFilter(int filter) {
            return (getFilter() & filter) != 0;
        }

        private synchronized void setFilter(int filter) {
            mFilter = filter;
        }

        @Override
        public String toString() {
            synchronized (this) {
                return "ListenerInfo{filter=" + Integer.toHexString(mFilter) + "}";
            }
        }
    }
}
