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

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
import android.os.Message;
import android.os.Process;
import android.util.Log;

import java.io.IOException;
import java.util.UUID;

class BluetoothPbapSession implements Callback {
    //TODO consider cleaning file organization and naming.
    private static final String TAG = "com.android.bluetooth.pbapclient.BluetoothPbapSession";

    /* local use only */
    private static final int RFCOMM_CONNECTED = 1;
    private static final int RFCOMM_FAILED = 2;

    /* to BluetoothPbapClient */
    public static final int REQUEST_COMPLETED = 3;
    public static final int REQUEST_FAILED = 4;
    public static final int SESSION_CONNECTING = 5;
    public static final int SESSION_CONNECTED = 6;
    public static final int SESSION_DISCONNECTED = 7;
    public static final int AUTH_REQUESTED = 8;
    public static final int AUTH_TIMEOUT = 9;

    public static final int ACTION_LISTING = 14;
    public static final int ACTION_VCARD = 15;
    public static final int ACTION_PHONEBOOK_SIZE = 16;

    private static final String PBAP_UUID =
            "0000112f-0000-1000-8000-00805f9b34fb";

    private final BluetoothAdapter mAdapter;
    private final BluetoothDevice mDevice;

    private final Handler mParentHandler;

    private final HandlerThread mHandlerThread;
    private final Handler mSessionHandler;

    private RfcommConnectThread mConnectThread;
    private BluetoothPbapObexTransport mTransport;

    private BluetoothPbapObexSession mObexSession;

    private BluetoothPbapRequest mPendingRequest = null;

    public BluetoothPbapSession(BluetoothDevice device, Handler handler) {

        mAdapter = BluetoothAdapter.getDefaultAdapter();
        if (mAdapter == null) {
            throw new NullPointerException("No Bluetooth adapter in the system");
        }

        mDevice = device;
        mParentHandler = handler;
        mConnectThread = null;
        mTransport = null;
        mObexSession = null;

        mHandlerThread = new HandlerThread("PBAP session handler",
                Process.THREAD_PRIORITY_BACKGROUND);
        mHandlerThread.start();
        mSessionHandler = new Handler(mHandlerThread.getLooper(), this);
    }

    @Override
    public boolean handleMessage(Message msg) {
        Log.d(TAG, "Handler: msg: " + msg.what);

        switch (msg.what) {
            case RFCOMM_FAILED:
                mConnectThread = null;

                mParentHandler.obtainMessage(SESSION_DISCONNECTED).sendToTarget();

                if (mPendingRequest != null) {
                    mParentHandler.obtainMessage(REQUEST_FAILED, mPendingRequest).sendToTarget();
                    mPendingRequest = null;
                }
                break;

            case RFCOMM_CONNECTED:
                mConnectThread = null;
                mTransport = (BluetoothPbapObexTransport) msg.obj;
                startObexSession();
                break;

            case BluetoothPbapObexSession.OBEX_SESSION_FAILED:
                stopObexSession();

                mParentHandler.obtainMessage(SESSION_DISCONNECTED).sendToTarget();

                if (mPendingRequest != null) {
                    mParentHandler.obtainMessage(REQUEST_FAILED, mPendingRequest).sendToTarget();
                    mPendingRequest = null;
                }
                break;

            case BluetoothPbapObexSession.OBEX_SESSION_CONNECTED:
                mParentHandler.obtainMessage(SESSION_CONNECTED).sendToTarget();

                if (mPendingRequest != null) {
                    mObexSession.schedule(mPendingRequest);
                    mPendingRequest = null;
                }
                break;

            case BluetoothPbapObexSession.OBEX_SESSION_DISCONNECTED:
                mParentHandler.obtainMessage(SESSION_DISCONNECTED).sendToTarget();
                stopRfcomm();
                break;

            case BluetoothPbapObexSession.OBEX_SESSION_REQUEST_COMPLETED:
                /* send to parent, process there */
                mParentHandler.obtainMessage(REQUEST_COMPLETED, msg.obj).sendToTarget();
                break;

            case BluetoothPbapObexSession.OBEX_SESSION_REQUEST_FAILED:
                /* send to parent, process there */
                mParentHandler.obtainMessage(REQUEST_FAILED, msg.obj).sendToTarget();
                break;

            case BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_REQUEST:
                /* send to parent, process there */
                mParentHandler.obtainMessage(AUTH_REQUESTED).sendToTarget();

                mSessionHandler
                        .sendMessageDelayed(
                                mSessionHandler
                                        .obtainMessage(BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_TIMEOUT),
                                30000);
                break;

            case BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_TIMEOUT:
                /* stop authentication */
                setAuthResponse(null);

                mParentHandler.obtainMessage(AUTH_TIMEOUT).sendToTarget();
                break;

            default:
                return false;
        }

        return true;
    }

    public void start() {
        Log.d(TAG, "start");

        startRfcomm();
    }

    public void stop() {
        Log.d(TAG, "Stop");

        stopObexSession();
        stopRfcomm();
    }

    public void abort() {
        Log.d(TAG, "abort");

        /* fail pending request immediately */
        if (mPendingRequest != null) {
            mParentHandler.obtainMessage(REQUEST_FAILED, mPendingRequest).sendToTarget();
            mPendingRequest = null;
        }

        if (mObexSession != null) {
            mObexSession.abort();
        }
    }

    public boolean makeRequest(BluetoothPbapRequest request) {
        Log.v(TAG, "makeRequest: " + request.getClass().getSimpleName());

        if (mPendingRequest != null) {
            Log.w(TAG, "makeRequest: request already queued, exiting");
            return false;
        }

        if (mObexSession == null) {
            mPendingRequest = request;

            /*
             * since there is no pending request and no session it's safe to
             * assume that RFCOMM does not exist either and we should start
             * connecting it
             */
            startRfcomm();

            return true;
        }

        return mObexSession.schedule(request);
    }

    public boolean setAuthResponse(String key) {
        Log.d(TAG, "setAuthResponse key=" + key);

        mSessionHandler
                .removeMessages(BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_TIMEOUT);

        /* does not make sense to set auth response when OBEX session is down */
        if (mObexSession == null) {
            return false;
        }

        return mObexSession.setAuthReply(key);
    }

    private void startRfcomm() {
        Log.d(TAG, "startRfcomm");

        if (mConnectThread == null && mObexSession == null) {
            mParentHandler.obtainMessage(SESSION_CONNECTING).sendToTarget();

            mConnectThread = new RfcommConnectThread();
            mConnectThread.start();
        }

        /*
         * don't care if mConnectThread is not null - it means RFCOMM is being
         * connected anyway
         */
    }

    private void stopRfcomm() {
        Log.d(TAG, "stopRfcomm");

        if (mConnectThread != null) {
            try {
                // Force close the socket in case the thread is stuck doing the connect()
                // call.
                mConnectThread.closeSocket();
                // TODO: Add timed join if closeSocket does not clean up the state.
                mConnectThread.join();
            } catch (InterruptedException e) {
            }

            mConnectThread = null;
        }

        if (mTransport != null) {
            try {
                mTransport.close();
            } catch (IOException e) {
            }

            mTransport = null;
        }
    }

    private void startObexSession() {
        Log.d(TAG, "startObexSession");

        mObexSession = new BluetoothPbapObexSession(mTransport);
        mObexSession.start(mSessionHandler);
    }

    private void stopObexSession() {
        Log.d(TAG, "stopObexSession");

        if (mObexSession != null) {
            mObexSession.stop();
            mObexSession = null;
        }
    }

    private class RfcommConnectThread extends Thread {
        private static final String TAG = "RfcommConnectThread";

        private BluetoothSocket mSocket;

        public RfcommConnectThread() {
            super("RfcommConnectThread");
        }

        @Override
        public void run() {
            if (mAdapter.isDiscovering()) {
                mAdapter.cancelDiscovery();
            }

            try {
                mSocket = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(PBAP_UUID));
                mSocket.connect();

                BluetoothPbapObexTransport transport;
                transport = new BluetoothPbapObexTransport(mSocket);

                mSessionHandler.obtainMessage(RFCOMM_CONNECTED, transport).sendToTarget();
            } catch (IOException e) {
                closeSocket();
                mSessionHandler.obtainMessage(RFCOMM_FAILED).sendToTarget();
            }

        }

        // This method may be called from outside the thread if the connect() call above is stuck.
        public void closeSocket() {
            try {
                if (mSocket != null) {
                    mSocket.close();
                }
            } catch (IOException e) {
                Log.e(TAG, "Error when closing socket", e);
            }
        }
    }
}
