/*
 * Copyright (C) 2017 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.googlecode.android_scripting.facade.bluetooth;

import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseData.Builder;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.os.Bundle;
import android.os.ParcelUuid;

import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.MainThread;
import com.googlecode.android_scripting.facade.EventFacade;
import com.googlecode.android_scripting.facade.FacadeManager;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.rpc.Rpc;
import com.googlecode.android_scripting.rpc.RpcParameter;

import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;

/**
 * BluetoothLe Advertise functions.
 */

public class BluetoothLeAdvertiseFacade extends RpcReceiver {

    private final EventFacade mEventFacade;
    private BluetoothAdapter mBluetoothAdapter;
    private static int BleAdvertiseCallbackCount;
    private static int BleAdvertiseSettingsCount;
    private static int BleAdvertiseDataCount;
    private final HashMap<Integer, MyAdvertiseCallback> mAdvertiseCallbackList;
    private final BluetoothLeAdvertiser mAdvertise;
    private final Service mService;
    private Builder mAdvertiseDataBuilder;
    private android.bluetooth.le.AdvertiseSettings.Builder mAdvertiseSettingsBuilder;
    private final HashMap<Integer, AdvertiseData> mAdvertiseDataList;
    private final HashMap<Integer, AdvertiseSettings> mAdvertiseSettingsList;

    public BluetoothLeAdvertiseFacade(FacadeManager manager) {
        super(manager);
        mService = manager.getService();
        mBluetoothAdapter = MainThread.run(mService,
                new Callable<BluetoothAdapter>() {
                    @Override
                    public BluetoothAdapter call() throws Exception {
                        return BluetoothAdapter.getDefaultAdapter();
                    }
                });
        mEventFacade = manager.getReceiver(EventFacade.class);
        mAdvertiseCallbackList = new HashMap<Integer, MyAdvertiseCallback>();
        mAdvertise = mBluetoothAdapter.getBluetoothLeAdvertiser();
        mAdvertiseDataList = new HashMap<Integer, AdvertiseData>();
        mAdvertiseSettingsList = new HashMap<Integer, AdvertiseSettings>();
        mAdvertiseDataBuilder = new Builder();
        mAdvertiseSettingsBuilder =
                new android.bluetooth.le.AdvertiseSettings.Builder();
    }

    /**
     * Constructs a MyAdvertiseCallback obj and returns its index
     *
     * @return MyAdvertiseCallback.index
     */
    @Rpc(description = "Generate a new myAdvertisement Object")
    public Integer bleGenBleAdvertiseCallback() {
        BleAdvertiseCallbackCount += 1;
        int index = BleAdvertiseCallbackCount;
        MyAdvertiseCallback mCallback = new MyAdvertiseCallback(index);
        mAdvertiseCallbackList.put(mCallback.index,
                mCallback);
        return mCallback.index;
    }

    /**
     * Constructs a AdvertiseData obj and returns its index
     *
     * @return index
     */
    @Rpc(description =
            "Constructs a new Builder obj for AdvertiseData and returns its index")
    public Integer bleBuildAdvertiseData() {
        BleAdvertiseDataCount += 1;
        int index = BleAdvertiseDataCount;
        mAdvertiseDataList.put(index,
                mAdvertiseDataBuilder.build());
        mAdvertiseDataBuilder = new Builder();
        return index;
    }

    /**
     * Constructs a Advertise Settings obj and returns its index
     *
     * @return index
     */
    @Rpc(description =
            "Constructs a new Builder obj for AdvertiseData and returns its index")
    public Integer bleBuildAdvertiseSettings() {
        BleAdvertiseSettingsCount += 1;
        int index = BleAdvertiseSettingsCount;
        mAdvertiseSettingsList.put(index,
                mAdvertiseSettingsBuilder.build());
        mAdvertiseSettingsBuilder =
                new android.bluetooth.le.AdvertiseSettings.Builder();
        return index;
    }

    /**
     * Stops a ble advertisement
     *
     * @param index the id of the advertisement to stop advertising on
     * @throws Exception
     */
    @Rpc(description = "Stops an ongoing ble advertisement")
    public void bleStopBleAdvertising(
            @RpcParameter(name = "index")
            Integer index) throws Exception {
        if (mAdvertiseCallbackList.get(index) != null) {
            Log.d("bluetooth_le mAdvertise " + index);
            mAdvertise.stopAdvertising(mAdvertiseCallbackList
                    .get(index));
        } else {
            throw new Exception("Invalid index input:"
                    + Integer.toString(index));
        }
    }

    /**
     * Starts ble advertising
     *
     * @param callbackIndex The advertisementCallback index
     * @param dataIndex the AdvertiseData index
     * @param settingsIndex the advertisementsettings index
     * @throws Exception
     */
    @Rpc(description = "Starts ble advertisement")
    public void bleStartBleAdvertising(
            @RpcParameter(name = "callbackIndex")
            Integer callbackIndex,
            @RpcParameter(name = "dataIndex")
            Integer dataIndex,
            @RpcParameter(name = "settingsIndex")
            Integer settingsIndex
            ) throws Exception {
        AdvertiseData mData = new AdvertiseData.Builder().build();
        AdvertiseSettings mSettings = new AdvertiseSettings.Builder().build();
        if (mAdvertiseDataList.get(dataIndex) != null) {
            mData = mAdvertiseDataList.get(dataIndex);
        } else {
            throw new Exception("Invalid dataIndex input:"
                    + Integer.toString(dataIndex));
        }
        if (mAdvertiseSettingsList.get(settingsIndex) != null) {
            mSettings = mAdvertiseSettingsList.get(settingsIndex);
        } else {
            throw new Exception("Invalid settingsIndex input:"
                    + Integer.toString(settingsIndex));
        }
        if (mAdvertiseCallbackList.get(callbackIndex) != null) {
            Log.d("bluetooth_le starting a background advertisement on callback index: "
                    + Integer.toString(callbackIndex));
            mAdvertise.startAdvertising(
                    mSettings, mData, mAdvertiseCallbackList.get(callbackIndex));
        } else {
            throw new Exception("Invalid callbackIndex input"
                    + Integer.toString(callbackIndex));
        }
    }

    /**
     * Starts ble advertising with a scanResponse. ScanResponses are created in
     *        the same way
     * AdvertiseData is created since they share the same object type.
     *
     * @param callbackIndex The advertisementCallback index
     * @param dataIndex the AdvertiseData index
     * @param settingsIndex the advertisementsettings index
     * @param scanResponseIndex the scanResponse index
     * @throws Exception
     */
    @Rpc(description = "Starts ble advertisement")
    public void bleStartBleAdvertisingWithScanResponse(
            @RpcParameter(name = "callbackIndex")
            Integer callbackIndex,
            @RpcParameter(name = "dataIndex")
            Integer dataIndex,
            @RpcParameter(name = "settingsIndex")
            Integer settingsIndex,
            @RpcParameter(name = "scanResponseIndex")
            Integer scanResponseIndex
            ) throws Exception {
        AdvertiseData mData = new AdvertiseData.Builder().build();
        AdvertiseSettings mSettings = new AdvertiseSettings.Builder().build();
        AdvertiseData mScanResponse = new AdvertiseData.Builder().build();

        if (mAdvertiseDataList.get(dataIndex) != null) {
            mData = mAdvertiseDataList.get(dataIndex);
        } else {
            throw new Exception("Invalid dataIndex input:"
                    + Integer.toString(dataIndex));
        }
        if (mAdvertiseSettingsList.get(settingsIndex) != null) {
            mSettings = mAdvertiseSettingsList.get(settingsIndex);
        } else {
            throw new Exception("Invalid settingsIndex input:"
                    + Integer.toString(settingsIndex));
        }
        if (mAdvertiseDataList.get(scanResponseIndex) != null) {
            mScanResponse = mAdvertiseDataList.get(scanResponseIndex);
        } else {
            throw new Exception("Invalid scanResponseIndex input:"
                    + Integer.toString(settingsIndex));
        }
        if (mAdvertiseCallbackList.get(callbackIndex) != null) {
            Log.d("bluetooth_le starting a background advertise on callback index: "
                    + Integer.toString(callbackIndex));
            mAdvertise
                    .startAdvertising(mSettings, mData, mScanResponse,
                            mAdvertiseCallbackList.get(callbackIndex));
        } else {
            throw new Exception("Invalid callbackIndex input"
                    + Integer.toString(callbackIndex));
        }
    }

    /**
     * Get ble advertisement settings mode
     *
     * @param index the advertise settings object to use
     * @return the mode of the advertise settings object
     * @throws Exception
     */
    @Rpc(description = "Get ble advertisement settings mode")
    public int bleGetAdvertiseSettingsMode(
            @RpcParameter(name = "index")
            Integer index) throws Exception {
        if (mAdvertiseSettingsList.get(index) != null) {
            AdvertiseSettings mSettings = mAdvertiseSettingsList.get(index);
            return mSettings.getMode();
        } else {
            throw new Exception("Invalid index input:"
                    + Integer.toString(index));
        }
    }

    /**
     * Get ble advertisement settings tx power level
     *
     * @param index the advertise settings object to use
     * @return the tx power level of the advertise settings object
     * @throws Exception
     */
    @Rpc(description = "Get ble advertisement settings tx power level")
    public int bleGetAdvertiseSettingsTxPowerLevel(
            @RpcParameter(name = "index")
            Integer index) throws Exception {
        if (mAdvertiseSettingsList.get(index) != null) {
            AdvertiseSettings mSettings = mAdvertiseSettingsList.get(index);
            return mSettings.getTxPowerLevel();
        } else {
            throw new Exception("Invalid index input:" + Integer.toString(index));
        }
    }

    /**
     * Get ble advertisement settings isConnectable value
     *
     * @param index the advertise settings object to use
     * @return the boolean value whether the advertisement will indicate
     * connectable.
     * @throws Exception
     */
    @Rpc(description = "Get ble advertisement settings isConnectable value")
    public boolean bleGetAdvertiseSettingsIsConnectable(
            @RpcParameter(name = "index")
            Integer index) throws Exception {
        if (mAdvertiseSettingsList.get(index) != null) {
            AdvertiseSettings mSettings = mAdvertiseSettingsList.get(index);
            return mSettings.isConnectable();
        } else {
            throw new Exception("Invalid index input:" + Integer.toString(index));
        }
    }

    /**
     * Get ble advertisement settings own address type
     *
     * @param index the advertise settings object to use
     * @return the own address type of the advertise settings object
     * @throws Exception
     */
    @Rpc(description = "Get ble advertisement settings tx power level")
    public int bleGetAdvertiseSettingsOwnAddressType(
            @RpcParameter(name = "index")
            Integer index) throws Exception {
        if (mAdvertiseSettingsList.get(index) != null) {
            AdvertiseSettings mSettings = mAdvertiseSettingsList.get(index);
            return mSettings.getOwnAddressType();
        } else {
            throw new Exception("Invalid index input:" + Integer.toString(index));
        }
    }

    /**
     * Get ble advertisement data include tx power level
     *
     * @param index the advertise data object to use
     * @return True if include tx power level, false otherwise
     * @throws Exception
     */
    @Rpc(description = "Get ble advertisement data include tx power level")
    public Boolean bleGetAdvertiseDataIncludeTxPowerLevel(
            @RpcParameter(name = "index")
            Integer index) throws Exception {
        if (mAdvertiseDataList.get(index) != null) {
            AdvertiseData mData = mAdvertiseDataList.get(index);
            return mData.getIncludeTxPowerLevel();
        } else {
            throw new Exception("Invalid index input:"
                    + Integer.toString(index));
        }
    }

    /**
     * Get ble advertisement data manufacturer specific data
     *
     * @param index the advertise data object to use
     * @param manufacturerId the id that corresponds to the manufacturer specific data.
     * @return the corresponding manufacturer specific data to the manufacturer id.
     * @throws Exception
     */
    @Rpc(description = "Get ble advertisement data manufacturer specific data")
    public byte[] bleGetAdvertiseDataManufacturerSpecificData(
            @RpcParameter(name = "index")
            Integer index,
            @RpcParameter(name = "manufacturerId")
            Integer manufacturerId) throws Exception {
        if (mAdvertiseDataList.get(index) != null) {
            AdvertiseData mData = mAdvertiseDataList.get(index);
            if (mData.getManufacturerSpecificData() != null) {
                return mData.getManufacturerSpecificData().get(manufacturerId);
            } else {
                throw new Exception("Invalid manufacturerId input:"
                        + Integer.toString(manufacturerId));
            }
        } else {
            throw new Exception("Invalid index input:"
                    + Integer.toString(index));

        }
    }

    /**
     * Get ble advertisement data include device name
     *
     * @param index the advertise data object to use
     * @return the advertisement data's include device name
     * @throws Exception
     */
    @Rpc(description = "Get ble advertisement include device name")
    public Boolean bleGetAdvertiseDataIncludeDeviceName(
            @RpcParameter(name = "index")
            Integer index) throws Exception {
        if (mAdvertiseDataList.get(index) != null) {
            AdvertiseData mData = mAdvertiseDataList.get(index);
            return mData.getIncludeDeviceName();
        } else {
            throw new Exception("Invalid index input:"
                    + Integer.toString(index));
        }
    }

    /**
     * Get ble advertisement Service Data
     *
     * @param index the advertise data object to use
     * @param serviceUuid the uuid corresponding to the service data.
     * @return the advertisement data's service data
     * @throws Exception
     */
    @Rpc(description = "Get ble advertisement Service Data")
    public byte[] bleGetAdvertiseDataServiceData(
            @RpcParameter(name = "index")
            Integer index,
            @RpcParameter(name = "serviceUuid")
            String serviceUuid) throws Exception {
        ParcelUuid uuidKey = ParcelUuid.fromString(serviceUuid);
        if (mAdvertiseDataList.get(index) != null) {
            AdvertiseData mData = mAdvertiseDataList.get(index);
            if (mData.getServiceData().containsKey(uuidKey)) {
                return mData.getServiceData().get(uuidKey);
            } else {
                throw new Exception("Invalid serviceUuid input:" + serviceUuid);
            }
        } else {
            throw new Exception("Invalid index input:"
                    + Integer.toString(index));
        }
    }

    /**
     * Get ble advertisement Service Uuids
     *
     * @param index the advertise data object to use
     * @return the advertisement data's Service Uuids
     * @throws Exception
     */
    @Rpc(description = "Get ble advertisement Service Uuids")
    public List<ParcelUuid> bleGetAdvertiseDataServiceUuids(
            @RpcParameter(name = "index")
            Integer index) throws Exception {
        if (mAdvertiseDataList.get(index) != null) {
            AdvertiseData mData = mAdvertiseDataList.get(index);
            return mData.getServiceUuids();
        } else {
            throw new Exception("Invalid index input:"
                    + Integer.toString(index));
        }
    }

    /**
     * Set ble advertisement data service uuids
     *
     * @param uuidList
     * @throws Exception
     */
    @Rpc(description = "Set ble advertisement data service uuids")
    public void bleSetAdvertiseDataSetServiceUuids(
            @RpcParameter(name = "uuidList")
            String[] uuidList
            ) {
        for (String uuid : uuidList) {
            mAdvertiseDataBuilder.addServiceUuid(ParcelUuid.fromString(uuid));
        }
    }

    /**
     * Set ble advertise data service uuids
     *
     * @param serviceDataUuid
     * @param serviceData
     * @throws Exception
     */
    @Rpc(description = "Set ble advertise data service uuids")
    public void bleAddAdvertiseDataServiceData(
            @RpcParameter(name = "serviceDataUuid")
            String serviceDataUuid,
            @RpcParameter(name = "serviceData")
            byte[] serviceData
            ) {
        mAdvertiseDataBuilder.addServiceData(
                ParcelUuid.fromString(serviceDataUuid),
                serviceData);
    }

    /**
     * Set ble advertise data manufacturer id
     *
     * @param manufacturerId the manufacturer id to set
     * @param manufacturerSpecificData the manufacturer specific data to set
     * @throws Exception
     */
    @Rpc(description = "Set ble advertise data manufacturerId")
    public void bleAddAdvertiseDataManufacturerId(
            @RpcParameter(name = "manufacturerId")
            Integer manufacturerId,
            @RpcParameter(name = "manufacturerSpecificData")
            byte[] manufacturerSpecificData
            ) {
        mAdvertiseDataBuilder.addManufacturerData(manufacturerId,
                manufacturerSpecificData);
    }

    /**
     * Set ble advertise settings advertise mode
     *
     * @param advertiseMode
     * @throws Exception
     */
    @Rpc(description = "Set ble advertise settings advertise mode")
    public void bleSetAdvertiseSettingsAdvertiseMode(
            @RpcParameter(name = "advertiseMode")
            Integer advertiseMode
            ) {
        mAdvertiseSettingsBuilder.setAdvertiseMode(advertiseMode);
    }

    /**
     * Set ble advertise settings tx power level
     *
     * @param txPowerLevel the tx power level to set
     * @throws Exception
     */
    @Rpc(description = "Set ble advertise settings tx power level")
    public void bleSetAdvertiseSettingsTxPowerLevel(
            @RpcParameter(name = "txPowerLevel")
            Integer txPowerLevel
            ) {
        mAdvertiseSettingsBuilder.setTxPowerLevel(txPowerLevel);
    }

    /**
     * Set ble advertise settings the isConnectable value
     *
     * @param type the isConnectable value
     * @throws Exception
     */
    @Rpc(description = "Set ble advertise settings isConnectable value")
    public void bleSetAdvertiseSettingsIsConnectable(
            @RpcParameter(name = "value")
            Boolean value
            ) {
        mAdvertiseSettingsBuilder.setConnectable(value);
    }

    /**
     * Set ble advertise settings own address type
     *
     * @param ownAddressType the own address type to set
     * @throws Exception
     */
    @Rpc(description = "Set ble advertise settings tx power level")
    public void bleSetAdvertiseSettingsOwnAddressType(
            @RpcParameter(name = "ownAddressType")
            Integer ownAddressType) {
        mAdvertiseSettingsBuilder.setOwnAddressType(ownAddressType);
    }

    /**
     * Set ble advertisement data include tx power level
     *
     * @param includeTxPowerLevel boolean whether to include the tx
     *         power level or not in the advertisement
     */
    @Rpc(description = "Set ble advertisement data include tx power level")
    public void bleSetAdvertiseDataIncludeTxPowerLevel(
            @RpcParameter(name = "includeTxPowerLevel")
            Boolean includeTxPowerLevel
            ) {
        mAdvertiseDataBuilder.setIncludeTxPowerLevel(includeTxPowerLevel);
    }

    /**
     * Set ble advertisement settings set timeout
     *
     * @param timeoutSeconds Limit advertising to a given amount of time.
     */
    @Rpc(description = "Set ble advertisement data include tx power level")
    public void bleSetAdvertiseSettingsTimeout(
            @RpcParameter(name = "timeoutSeconds")
            Integer timeoutSeconds
            ) {
        mAdvertiseSettingsBuilder.setTimeout(timeoutSeconds);
    }

    /**
     * Set ble advertisement data include device name
     *
     * @param includeDeviceName boolean whether to include device name or
     *        not in the advertisement
     */
    @Rpc(description = "Set ble advertisement data include device name")
    public void bleSetAdvertiseDataIncludeDeviceName(
            @RpcParameter(name = "includeDeviceName")
            Boolean includeDeviceName
            ) {
        mAdvertiseDataBuilder.setIncludeDeviceName(includeDeviceName);
    }

    private class MyAdvertiseCallback extends AdvertiseCallback {
        public Integer index;
        private final Bundle mResults;
        String mEventType;

        public MyAdvertiseCallback(int idx) {
            index = idx;
            mEventType = "BleAdvertise";
            mResults = new Bundle();
        }

        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
            Log.d("bluetooth_le_advertisement onSuccess " + mEventType + " "
                    + index);
            mResults.putString("Type", "onSuccess");
            mResults.putParcelable("SettingsInEffect", settingsInEffect);
            mEventFacade.postEvent(mEventType + index
                    + "onSuccess", mResults.clone());
            mResults.clear();
        }

        @Override
        public void onStartFailure(int errorCode) {
            String errorString = "UNKNOWN_ERROR_CODE";
            if (errorCode
                    == AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED) {
                errorString = "ADVERTISE_FAILED_ALREADY_STARTED";
            } else if (errorCode
                    == AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE) {
                errorString = "ADVERTISE_FAILED_DATA_TOO_LARGE";
            } else if (errorCode
                    == AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED) {
                errorString = "ADVERTISE_FAILED_FEATURE_UNSUPPORTED";
            } else if (errorCode
                    == AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR) {
                errorString = "ADVERTISE_FAILED_INTERNAL_ERROR";
            } else if (errorCode
                    == AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS) {
                errorString = "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS";
            }
            Log.d("bluetooth_le_advertisement onFailure " + mEventType + " "
                    + index + " error " + errorString);
            mResults.putString("Type", "onFailure");
            mResults.putInt("ErrorCode", errorCode);
            mResults.putString("Error", errorString);
            mEventFacade.postEvent(mEventType + index + "onFailure",
                    mResults.clone());
            mResults.clear();
        }
    }

    /**
     * Clear all advertise settings
     *
     * @param None
     */
    @Rpc(description = "Clear all advertise settings")
    public void bleAdvertiseClearAll() {
        Log.d("bleAdvertiseClearAll: called");
        if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
            for (MyAdvertiseCallback mAdvertise : mAdvertiseCallbackList
                .values()) {
                if (mAdvertise != null) {
                    try{
                        mBluetoothAdapter.getBluetoothLeAdvertiser()
                            .stopAdvertising(mAdvertise);
                    } catch (NullPointerException e) {
                        Log.e("Failed to stop ble advertising.", e);
                    }
                }
            }
        }
        mAdvertiseCallbackList.clear();
        mAdvertiseSettingsList.clear();
        mAdvertiseDataList.clear();
    }

    @Override
    public void shutdown() {
        bleAdvertiseClearAll();
    }
}
