#include "CanonBLERemote.h"
#include <Arduino.h>
#include <BLEDevice.h>

static const char *LOG_TAG = "MySecurity";

advdCallback::advdCallback(BLEUUID service_uuid, bool *ready_to_connect, BLEAddress *address_to_connect)
{
    service_uuid_wanted = service_uuid;
    pready_to_connect = ready_to_connect;
    paddress_to_connect = address_to_connect;
}

void advdCallback::onResult(BLEAdvertisedDevice advertisedDevice)
{
    if (advertisedDevice.haveServiceUUID())
    { //Check if device has service UUID available.
        if (service_uuid_wanted.equals(advertisedDevice.getServiceUUID()))
        {
            *paddress_to_connect = advertisedDevice.getAddress();
            *pready_to_connect = true;
            advertisedDevice.getScan()->stop();
        }
    }
}

void ConnectivityState::onConnect(BLEClient *pclient)
{
    // Serial.println("Device is connected");
    connected = true;
}

void ConnectivityState::onDisconnect(BLEClient *pclient)
{
    // Serial.println("Device disconnnect");
    connected = false;
}

bool ConnectivityState::isConnected()
{
    return connected;
}

class SecurityCallback : public BLESecurityCallbacks
{

    uint32_t onPassKeyRequest()
    {
        return 123456;
    }
    void onPassKeyNotify(uint32_t pass_key)
    {
        ESP_LOGE(LOG_TAG, "The passkey Notify number:%d", pass_key);
    }
    bool onConfirmPIN(uint32_t pass_key)
    {
        ESP_LOGI(LOG_TAG, "The passkey YES/NO number:%d", pass_key);
        vTaskDelay(5000);
        return true;
    }
    bool onSecurityRequest()
    {
        ESP_LOGI(LOG_TAG, "Security Request");
        return true;
    }
    void onAuthenticationComplete(esp_ble_auth_cmpl_t auth_cmpl)
    {
        if (auth_cmpl.success)
        {
            ESP_LOGI(LOG_TAG, "remote BD_ADDR:");
            esp_log_buffer_hex(LOG_TAG, auth_cmpl.bd_addr, sizeof(auth_cmpl.bd_addr));
            ESP_LOGI(LOG_TAG, "address type = %d", auth_cmpl.addr_type);
        }
        ESP_LOGI(LOG_TAG, "pair status = %s", auth_cmpl.success ? "success" : "fail");
    }
};

CanonBLERemote::CanonBLERemote(String name) : SERVICE_UUID("00050000-0000-1000-0000-d8492fffa821"),
                                              PAIRING_SERVICE("00050002-0000-1000-0000-d8492fffa821"),
                                              SHUTTER_CONTROL_SERVICE("00050003-0000-1000-0000-d8492fffa821")
{
    device_name = name;
    // Add our connection callback for state tracking.
    pclient->setClientCallbacks(pconnection_state);
}

void CanonBLERemote::init()
{
    BLEDevice::init(device_name.c_str());
    BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_NO_MITM);
    BLEDevice::setSecurityCallbacks(new SecurityCallback());

    if (nvs.begin())
    {
        log_e("Initialize NVS Success");
        String address = nvs.getString("cameraaddr");

        if (address.length() == 17)
        {
            // Serial.printf("Paired camera address: %s\n", address.c_str());
            camera_address = BLEAddress(address.c_str());
        }
        else
        {
            // Serial.println("No camera has been paired yet.");
        }
    }
    else
    {
        log_e("Initialize NVS Failed");
    }
}

// Purpose : Scanning for new BLE devices around.
//           When found -> advdCallback::OnResult
void CanonBLERemote::scan(unsigned int scan_duration)
{

    log_i("Start BLE scan");
    BLEScan *pBLEScan = BLEDevice::getScan();
    advdCallback *advert_dev_callback = new advdCallback(SERVICE_UUID, &ready_to_connect, &camera_address);

    pBLEScan->setAdvertisedDeviceCallbacks(advert_dev_callback); // Retrieve a Scanner and set the callback we want to use to be informed when we have detected a new device.
    pBLEScan->setActiveScan(true);
    pBLEScan->start(scan_duration); // Specify that we want active scanning and start the scan to run for 30 seconds.
}

BLEAddress CanonBLERemote::getPairedAddress()
{
    return camera_address;
}

String CanonBLERemote::getPairedAddressString()
{
    return String(camera_address.toString().c_str());
}

/**
 *  Scan and pair camera 
 * 
 *  This function should be called only when you want to pair with the new camera, and the camera is in remote paring screen.
 *  After paired, camera mac address will be stored in ESP32 NVS for later connection use.
 * 
 *  @param  scan_duration : Scan duration in seconds
 */

bool CanonBLERemote::pair(unsigned int scan_duration)
{
    BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_MITM);
    log_i("Scanning for camera...");
    scan(scan_duration);
    unsigned long start_ms = millis();
    while (!ready_to_connect && millis() - start_ms < scan_duration * 1000)
    {
    }

    if (ready_to_connect)
    {
        log_i("Canon device found");
        // Serial.println(camera_address.toString().c_str());
    }
    else
    {
        log_i("Camera not found");
        return false;
    }

    // Pair camera
    log_i("Pairing..");
    if (pclient->connect(camera_address))
    {
        // Acquire reference to main service
        pRemoteService = pclient->getService(SERVICE_UUID);
        if (pRemoteService != nullptr)
        {
            // Acquire reference to BLE characteristics
            pRemoteCharacteristic_Pairing = pRemoteService->getCharacteristic(PAIRING_SERVICE);
            if ((pRemoteCharacteristic_Pairing != nullptr))
            {
                // Send request on pairing service from external device
                String device_name_ = " " + device_name + " ";          //Pairing message to send
                byte cmdPress[device_name_.length()];                   // Stocking list of Bytes char of the message
                device_name_.getBytes(cmdPress, device_name_.length()); // message Parser
                cmdPress[0] = {0x03};
                pRemoteCharacteristic_Pairing->writeValue(cmdPress, sizeof(cmdPress), false); // Writing to Canon_pairing_service
                log_e("Camera paring success");
                delay(200);
                disconnect();
                BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_NO_MITM);
                delay(200);
                connect();
                nvs.setString("cameraaddr", String(camera_address.toString().c_str()));
                if (nvs.commit())
                {
                    log_i("Saving camera's address to NVS success");
                    return true;
                }
                else
                {
                    log_e("Storing camera's address to NVS failed");
                    return false;
                }
            }
            else
            {
                log_e("Couldn't acquire the pairing or shutter service");
            }
        }
        else
        {
            log_e("Couldn't acquire the remote main service");
        }
    }
    else
    {
        log_e("Couldn't connect the BLEClient to the device");
    }
    return false;
}

bool CanonBLERemote::connect()
{
    if (pclient->connect(camera_address))
    {
        pRemoteService = pclient->getService(SERVICE_UUID);
        if (pRemoteService != nullptr)
        {
            // Serial.println("Get remote service OK");
            pRemoteCharacteristic_Trigger = pRemoteService->getCharacteristic(SHUTTER_CONTROL_SERVICE);
            if (pRemoteCharacteristic_Trigger != nullptr)
            {
                log_i("Camera connection Success");
                // disconnect();       // Disconnect remote from the camera every time after action, as the real canon remote did.
                return true;
            }
            else
            {
                log_e("Get trigger service failed");
            }
        }
        else
        {
            log_e("Couldn't acquire the remote main service");
        }
        disconnect();
    }
    return false;
}

void CanonBLERemote::disconnect()
{
    pclient->disconnect();
}

bool CanonBLERemote::isConnected()
{
    return pconnection_state->isConnected();
}

/** Trigger Camera 
 *  If the camera is in photo mode, it will take a single picture.
 *  If the camera is in movie mode, it will start/stop movie recording. 
 */

bool CanonBLERemote::trigger()
{

    if (!isConnected())
    {
        if (!connect())
        {
            return false;
        }
    }

    byte cmdByte = {MODE_IMMEDIATE | BUTTON_RELEASE};          // Binary OR : Concatenate Mode and Button
    pRemoteCharacteristic_Trigger->writeValue(cmdByte, false); // Set the characteristic's value to be the array of bytes that is actually a string.
    delay(200);
    pRemoteCharacteristic_Trigger->writeValue(MODE_IMMEDIATE, false);
    delay(50);
    return true;
}

bool CanonBLERemote::focus()
{
    if (!isConnected())
    {
        if (!connect())
        {
            return false;
        }
    }
    byte cmdByte[] = {MODE_IMMEDIATE | BUTTON_FOCUS};                    // Binary OR : Concatenate Mode and Button
    pRemoteCharacteristic_Trigger->writeValue(cmdByte, sizeof(cmdByte)); // Set the characteristic's value to be the array of bytes that is actually a string.
    delay(200);
    pRemoteCharacteristic_Trigger->writeValue(MODE_IMMEDIATE, sizeof(MODE_IMMEDIATE));
    return true;
}
