/*
 * Copyright (C) 2012 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.nfc.handover;

import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.OobData;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.Log;

import java.util.Set;

public class PeripheralHandoverService extends Service implements BluetoothPeripheralHandover.Callback {
    static final String TAG = "PeripheralHandoverService";
    static final boolean DBG = true;

    static final int MSG_PAUSE_POLLING = 0;

    public static final String BUNDLE_TRANSFER = "transfer";
    public static final String EXTRA_PERIPHERAL_DEVICE = "device";
    public static final String EXTRA_PERIPHERAL_NAME = "headsetname";
    public static final String EXTRA_PERIPHERAL_TRANSPORT = "transporttype";
    public static final String EXTRA_PERIPHERAL_OOB_DATA = "oobdata";
    public static final String EXTRA_PERIPHERAL_UUIDS = "uuids";
    public static final String EXTRA_PERIPHERAL_CLASS = "class";
    public static final String EXTRA_CLIENT = "client";
    public static final String EXTRA_BT_ENABLED = "bt_enabled";

    public static final int MSG_HEADSET_CONNECTED = 0;
    public static final int MSG_HEADSET_NOT_CONNECTED = 1;

    // Amount of time to pause polling when connecting to peripherals
    private static final int PAUSE_POLLING_TIMEOUT_MS = 35000;
    private static final int PAUSE_DELAY_MILLIS = 300;

    private static final Object sLock = new Object();

    // Variables below only accessed on main thread
    final Messenger mMessenger;

    int mStartId;

    BluetoothAdapter mBluetoothAdapter;
    NfcAdapter mNfcAdapter;
    Handler mHandler;
    BluetoothPeripheralHandover mBluetoothPeripheralHandover;
    BluetoothDevice mDevice;
    Messenger mClient;
    boolean mBluetoothHeadsetConnected;
    boolean mBluetoothEnabledByNfc;

    class MessageHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_PAUSE_POLLING:
                    mNfcAdapter.pausePolling(PAUSE_POLLING_TIMEOUT_MS);
                    break;
            }
        }
    }

    final BroadcastReceiver mBluetoothStatusReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                handleBluetoothStateChanged(intent);
            }
        }
    };

    public PeripheralHandoverService() {
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        mHandler = new MessageHandler();
        mMessenger = new Messenger(mHandler);
        mBluetoothHeadsetConnected = false;
        mBluetoothEnabledByNfc = false;
        mStartId = 0;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        synchronized (sLock) {
            if (mStartId != 0) {
                mStartId = startId;
                // already running
                return START_STICKY;
            }
            mStartId = startId;
        }

        if (intent == null) {
            if (DBG) Log.e(TAG, "Intent is null, can't do peripheral handover.");
            synchronized (sLock) {
                stopSelf(startId);
                mStartId = 0;
            }
            return START_NOT_STICKY;
        }

        if (doPeripheralHandover(intent.getExtras())) {
            return START_STICKY;
        } else {
            onBluetoothPeripheralHandoverComplete(false);
            return START_NOT_STICKY;
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());

        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        registerReceiver(mBluetoothStatusReceiver, filter);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mBluetoothStatusReceiver);
    }

    boolean doPeripheralHandover(Bundle msgData) {
        if (mBluetoothPeripheralHandover != null) {
            Log.d(TAG, "Ignoring pairing request, existing handover in progress.");
            return true;
        }

        if (msgData == null) {
            return false;
        }

        mDevice = msgData.getParcelable(EXTRA_PERIPHERAL_DEVICE);
        String name = msgData.getString(EXTRA_PERIPHERAL_NAME);
        int transport = msgData.getInt(EXTRA_PERIPHERAL_TRANSPORT);
        OobData oobData = msgData.getParcelable(EXTRA_PERIPHERAL_OOB_DATA);
        Parcelable[] parcelables = msgData.getParcelableArray(EXTRA_PERIPHERAL_UUIDS);
        BluetoothClass btClass = msgData.getParcelable(EXTRA_PERIPHERAL_CLASS);

        ParcelUuid[] uuids = null;
        if (parcelables != null) {
            uuids = new ParcelUuid[parcelables.length];
            for (int i = 0; i < parcelables.length; i++) {
                uuids[i] = (ParcelUuid)parcelables[i];
            }
        }

        mClient = msgData.getParcelable(EXTRA_CLIENT);
        mBluetoothEnabledByNfc = msgData.getBoolean(EXTRA_BT_ENABLED);

        mBluetoothPeripheralHandover = new BluetoothPeripheralHandover(
                this, mDevice, name, transport, oobData, uuids, btClass, this);

        if (transport == BluetoothDevice.TRANSPORT_LE) {
            mHandler.sendMessageDelayed(
                    mHandler.obtainMessage(MSG_PAUSE_POLLING), PAUSE_DELAY_MILLIS);
        }
        if (mBluetoothAdapter.isEnabled()) {
            if (!mBluetoothPeripheralHandover.start()) {
                return false;
            }
        } else {
            // Once BT is enabled, the headset pairing will be started
            if (!enableBluetooth()) {
                Log.e(TAG, "Error enabling Bluetooth.");
                return false;
            }
        }

        return true;
    }

    private void handleBluetoothStateChanged(Intent intent) {
        int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
                BluetoothAdapter.ERROR);
        if (state == BluetoothAdapter.STATE_ON) {
            // If there is a pending device pairing, start it
            if (mBluetoothPeripheralHandover != null &&
                    !mBluetoothPeripheralHandover.hasStarted()) {
                if (!mBluetoothPeripheralHandover.start()) {
                    onBluetoothPeripheralHandoverComplete(false);
                }
            }
        }
    }

    @Override
    public void onBluetoothPeripheralHandoverComplete(boolean connected) {
        // Called on the main thread
        int transport = mBluetoothPeripheralHandover.mTransport;
        mBluetoothPeripheralHandover = null;
        mBluetoothHeadsetConnected = connected;

        // <hack> resume polling immediately if the connection failed,
        // otherwise just wait for polling to come back up after the timeout
        // This ensures we don't disconnect if the user left the volantis
        // on the tag after pairing completed, which results in automatic
        // disconnection </hack>
        if (transport == BluetoothDevice.TRANSPORT_LE && !connected) {
            if (mHandler.hasMessages(MSG_PAUSE_POLLING)) {
                mHandler.removeMessages(MSG_PAUSE_POLLING);
            }

            // do this unconditionally as the polling could have been paused as we were removing
            // the message in the handler. It's a no-op if polling is already enabled.
            mNfcAdapter.resumePolling();
        }
        disableBluetoothIfNeeded();
        replyToClient(connected);

        synchronized (sLock) {
            stopSelf(mStartId);
            mStartId = 0;
        }
    }


    boolean enableBluetooth() {
        if (!mBluetoothAdapter.isEnabled()) {
            mBluetoothEnabledByNfc = true;
            return mBluetoothAdapter.enableNoAutoConnect();
        }
        return true;
    }

    void disableBluetoothIfNeeded() {
        if (!mBluetoothEnabledByNfc) return;
        if (hasConnectedBluetoothDevices()) return;

        if (!mBluetoothHeadsetConnected) {
            mBluetoothAdapter.disable();
            mBluetoothEnabledByNfc = false;
        }
    }

    boolean hasConnectedBluetoothDevices() {
        Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices();

        if (bondedDevices != null) {
            for (BluetoothDevice device : bondedDevices) {
                if (device.equals(mDevice)) {
                    // Not required to check the remote BT "target" device
                    // connection status, because sometimes the connection
                    // state is not yet been updated upon disconnection.
                    // It is enough to check the connection status for
                    // "other" remote BT device/s.
                    continue;
                }
                if (device.isConnected()) return true;
            }
        }
        return false;
    }

    void replyToClient(boolean connected) {
        if (mClient == null) {
            return;
        }

        final int msgId = connected ? MSG_HEADSET_CONNECTED : MSG_HEADSET_NOT_CONNECTED;
        final Message msg = Message.obtain(null, msgId);
        msg.arg1 = mBluetoothEnabledByNfc ? 1 : 0;
        try {
            mClient.send(msg);
        } catch (RemoteException e) {
            // Ignore
        }
    }

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

    @Override
    public boolean onUnbind(Intent intent) {
        // prevent any future callbacks to the client, no rebind call needed.
        return false;
    }
}
