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

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.bluetooth.SdpMasRecord;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import com.android.bluetooth.BluetoothObexTransport;
import com.android.bluetooth.statemachine.StateMachine;

import java.io.IOException;
import java.lang.ref.WeakReference;

import javax.obex.ClientSession;
import javax.obex.HeaderSet;
import javax.obex.ResponseCodes;

/* MasClient is a one time use connection to a server defined by the SDP record passed in at
 * construction.  After use shutdown() must be called to properly clean up.
 */
public class MasClient {
    private static final int CONNECT = 0;
    private static final int DISCONNECT = 1;
    private static final int REQUEST = 2;
    private static final String TAG = "MasClient";
    private static final boolean DBG = MapClientService.DBG;
    private static final boolean VDBG = MapClientService.VDBG;
    private static final byte[] BLUETOOTH_UUID_OBEX_MAS = new byte[]{
            (byte) 0xbb,
            0x58,
            0x2b,
            0x40,
            0x42,
            0x0c,
            0x11,
            (byte) 0xdb,
            (byte) 0xb0,
            (byte) 0xde,
            0x08,
            0x00,
            0x20,
            0x0c,
            (byte) 0x9a,
            0x66
    };
    private static final byte OAP_TAGID_MAP_SUPPORTED_FEATURES = 0x29;
    private static final int L2CAP_INVALID_PSM = -1;
    private static final int MAP_FEATURE_NOTIFICATION_REGISTRATION = 0x00000001;
    private static final int MAP_FEATURE_NOTIFICATION = 0x00000002;
    private static final int MAP_FEATURE_BROWSING = 0x00000004;
    private static final int MAP_FEATURE_UPLOADING = 0x00000008;
    private static final int MAP_FEATURE_EXTENDED_EVENT_REPORT_1_1 = 0x00000040;
    static final int MAP_SUPPORTED_FEATURES =
            MAP_FEATURE_NOTIFICATION_REGISTRATION | MAP_FEATURE_NOTIFICATION
            | MAP_FEATURE_BROWSING | MAP_FEATURE_UPLOADING
            | MAP_FEATURE_EXTENDED_EVENT_REPORT_1_1;

    private final StateMachine mCallback;
    private Handler mHandler;
    private BluetoothSocket mSocket;
    private BluetoothObexTransport mTransport;
    private BluetoothDevice mRemoteDevice;
    private ClientSession mSession;
    private HandlerThread mThread;
    private boolean mConnected = false;
    SdpMasRecord mSdpMasRecord;

    public MasClient(BluetoothDevice remoteDevice, StateMachine callback,
            SdpMasRecord sdpMasRecord) {
        if (remoteDevice == null) {
            throw new NullPointerException("Obex transport is null");
        }
        mRemoteDevice = remoteDevice;
        mCallback = callback;
        mSdpMasRecord = sdpMasRecord;
        mThread = new HandlerThread("Client");
        mThread.start();
        /* This will block until the looper have started, hence it will be safe to use it,
           when the constructor completes */
        Looper looper = mThread.getLooper();
        mHandler = new MasClientHandler(looper, this);

        mHandler.obtainMessage(CONNECT).sendToTarget();
    }

    private void connect() {
        try {
            int l2capSocket = mSdpMasRecord.getL2capPsm();

            if (l2capSocket != L2CAP_INVALID_PSM) {
                if (DBG) {
                    Log.d(TAG, "Connecting to OBEX on L2CAP channel " + l2capSocket);
                }
                mSocket = mRemoteDevice.createL2capSocket(l2capSocket);
            } else {
                if (DBG) {
                    Log.d(TAG, "Connecting to OBEX on RFCOM channel "
                            + mSdpMasRecord.getRfcommCannelNumber());
                }
                mSocket = mRemoteDevice.createRfcommSocket(mSdpMasRecord.getRfcommCannelNumber());
            }
            if (DBG) Log.d(TAG, mRemoteDevice.toString() + "Socket: " + mSocket.toString());
            mSocket.connect();
            mTransport = new BluetoothObexTransport(mSocket);

            mSession = new ClientSession(mTransport);
            HeaderSet headerset = new HeaderSet();
            headerset.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_OBEX_MAS);
            ObexAppParameters oap = new ObexAppParameters();

            oap.add(OAP_TAGID_MAP_SUPPORTED_FEATURES, MAP_SUPPORTED_FEATURES);

            oap.addToHeaderSet(headerset);

            headerset = mSession.connect(headerset);
            if (DBG) Log.d(TAG, "Connection results" + headerset.getResponseCode());

            if (headerset.getResponseCode() == ResponseCodes.OBEX_HTTP_OK) {
                if (DBG) {
                    Log.d(TAG, "Connection Successful");
                }
                mConnected = true;
                mCallback.sendMessage(MceStateMachine.MSG_MAS_CONNECTED);
            } else {
                disconnect();
            }

        } catch (IOException e) {
            Log.e(TAG, "Caught an exception " + e.toString());
            disconnect();
        }
    }

    private void disconnect() {
        if (mSession != null) {
            try {
                mSession.disconnect(null);
            } catch (IOException e) {
                Log.e(TAG, "Caught an exception while disconnecting:" + e.toString());
            }

            try {
                mSession.close();
            } catch (IOException e) {
                Log.e(TAG, "Caught an exception while closing:" + e.toString());
            }
        }

        mConnected = false;
        mCallback.sendMessage(MceStateMachine.MSG_MAS_DISCONNECTED);
    }

    private void executeRequest(Request request) {
        try {
            request.execute(mSession);
            mCallback.sendMessage(MceStateMachine.MSG_MAS_REQUEST_COMPLETED, request);
        } catch (IOException e) {
            if (DBG) {
                Log.d(TAG, "Request failed: " + request);
            }
            // Disconnect to cleanup.
            disconnect();
        }
    }

    public boolean makeRequest(Request request) {
        if (DBG) {
            Log.d(TAG, "makeRequest called with: " + request);
        }

        boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request));
        if (!status) {
            Log.e(TAG, "Adding messages failed, state: " + mConnected);
            return false;
        }
        return true;
    }

    public void shutdown() {
        mHandler.obtainMessage(DISCONNECT).sendToTarget();
        mThread.quitSafely();
    }

    public enum CharsetType {
        NATIVE, UTF_8;
    }

    SdpMasRecord getSdpMasRecord() {
        return mSdpMasRecord;
    }

    private static class MasClientHandler extends Handler {
        WeakReference<MasClient> mInst;

        MasClientHandler(Looper looper, MasClient inst) {
            super(looper);
            mInst = new WeakReference<>(inst);
        }

        @Override
        public void handleMessage(Message msg) {
            MasClient inst = mInst.get();
            switch (msg.what) {
                case CONNECT:
                    if (!inst.mConnected) {
                        inst.connect();
                    }
                    break;

                case DISCONNECT:
                    if (inst.mConnected) {
                        inst.disconnect();
                    }
                    break;

                case REQUEST:
                    if (inst.mConnected) {
                        inst.executeRequest((Request) msg.obj);
                    }
                    break;
            }
        }
    }

}
