//
//  callbacks.cc
//  noble-mac-native
//
//  Created by Georg Vienna on 30.08.18.
//
#include "callbacks.h"

#include <napi-thread-safe-callback.hpp>

#define _s(val) Napi::String::New(env, val)
#define _b(val) Napi::Boolean::New(env, val)
#define _n(val) Napi::Number::New(env, val)
#define _u(str) toUuid(env, str)

Napi::String toUuid(Napi::Env& env, const std::string& uuid)
{
    std::string str(uuid);
    str.erase(std::remove(str.begin(), str.end(), '-'), str.end());
    std::transform(str.begin(), str.end(), str.begin(), ::tolower);
    return _s(str);
}

Napi::String toAddressType(Napi::Env& env, const AddressType& type)
{
    if (type == PUBLIC)
    {
        return _s("public");
    }
    else if (type == RANDOM)
    {
        return _s("random");
    }
    return _s("unknown");
}

Napi::Buffer<uint8_t> toBuffer(Napi::Env& env, const Data& data)
{
    if (data.empty())
    {
        return Napi::Buffer<uint8_t>::New(env, 0);
    }
    return Napi::Buffer<uint8_t>::Copy(env, &data[0], data.size());
}

Napi::Array toUuidArray(Napi::Env& env, const std::vector<std::string>& data)
{
    if (data.empty())
    {
        return Napi::Array::New(env);
    }
    auto arr = Napi::Array::New(env, data.size());
    for (size_t i = 0; i < data.size(); i++)
    {
        arr.Set(i, _u(data[i]));
    }
    return arr;
}

Napi::Array toArray(Napi::Env& env, const std::vector<std::string>& data)
{
    if (data.empty())
    {
        return Napi::Array::New(env);
    }
    auto arr = Napi::Array::New(env, data.size());
    for (size_t i = 0; i < data.size(); i++)
    {
        arr.Set(i, _s(data[i]));
    }
    return arr;
}

void Emit::Wrap(const Napi::Value& receiver, const Napi::Function& callback)
{
    mCallback = std::make_shared<ThreadSafeCallback>(receiver, callback);
}

void Emit::RadioState(const std::string& state)
{
    mCallback->call([state](Napi::Env env, std::vector<napi_value>& args) {
        // emit('stateChange', state);
        args = { _s("stateChange"), _s(state) };
    });
}

void Emit::ScanState(bool start)
{
    mCallback->call([start](Napi::Env env, std::vector<napi_value>& args) {
        // emit('scanStart') emit('scanStop')
        args = { _s(start ? "scanStart" : "scanStop") };
    });
}

void Emit::Scan(const std::string& uuid, int rssi, const Peripheral& peripheral)
{
    auto address = peripheral.address;
    auto addressType = peripheral.addressType;
    auto connectable = peripheral.connectable;
    auto name = peripheral.name;
    auto txPowerLevel = peripheral.txPowerLevel;
    auto manufacturerData = peripheral.manufacturerData;
    auto serviceData = peripheral.serviceData;
    auto serviceUuids = peripheral.serviceUuids;
    mCallback->call([uuid, rssi, address, addressType, connectable, name, txPowerLevel,
                     manufacturerData, serviceData,
                     serviceUuids](Napi::Env env, std::vector<napi_value>& args) {
        Napi::Object advertisment = Napi::Object::New(env);
        advertisment.Set(_s("localName"), _s(name));
        advertisment.Set(_s("txPowerLevel"), txPowerLevel);
        advertisment.Set(_s("manufacturerData"), toBuffer(env, manufacturerData));
        auto array =
            serviceData.empty() ? Napi::Array::New(env) : Napi::Array::New(env, serviceData.size());
        for (size_t i = 0; i < serviceData.size(); i++)
        {
            Napi::Object data = Napi::Object::New(env);
            data.Set(_s("uuid"), _u(serviceData[i].first));
            data.Set(_s("data"), toBuffer(env, serviceData[i].second));
            array.Set(i, data);
        }
        advertisment.Set(_s("serviceData"), array);
        advertisment.Set(_s("serviceUuids"), toUuidArray(env, serviceUuids));
        // emit('discover', deviceUuid, address, addressType, connectable, advertisement, rssi);
        args = { _s("discover"),  _u(uuid),     _s(address), toAddressType(env, addressType),
                 _b(connectable), advertisment, _n(rssi) };
    });
}

void Emit::Connected(const std::string& uuid, const std::string& error)
{
    mCallback->call([uuid, error](Napi::Env env, std::vector<napi_value>& args) {
        // emit('connect', deviceUuid) error added here
        args = { _s("connect"), _u(uuid), error.empty() ? env.Null() : _s(error) };
    });
}

void Emit::Disconnected(const std::string& uuid)
{
    mCallback->call([uuid](Napi::Env env, std::vector<napi_value>& args) {
        // emit('disconnect', deviceUuid);
        args = { _s("disconnect"), _u(uuid) };
    });
}

void Emit::RSSI(const std::string& uuid, int rssi)
{
    mCallback->call([uuid, rssi](Napi::Env env, std::vector<napi_value>& args) {
        // emit('rssiUpdate', deviceUuid, rssi);
        args = { _s("rssiUpdate"), _u(uuid), _n(rssi) };
    });
}

void Emit::ServicesDiscovered(const std::string& uuid, const std::vector<std::string>& serviceUuids)
{
    mCallback->call([uuid, serviceUuids](Napi::Env env, std::vector<napi_value>& args) {
        // emit('servicesDiscover', deviceUuid, serviceUuids)
        args = { _s("servicesDiscover"), _u(uuid), toUuidArray(env, serviceUuids) };
    });
}

void Emit::IncludedServicesDiscovered(const std::string& uuid, const std::string& serviceUuid,
                                      const std::vector<std::string>& serviceUuids)
{
    mCallback->call(
        [uuid, serviceUuid, serviceUuids](Napi::Env env, std::vector<napi_value>& args) {
            // emit('includedServicesDiscover', deviceUuid, serviceUuid, includedServiceUuids)
            args = { _s("includedServicesDiscover"), _u(uuid), _u(serviceUuid),
                     toUuidArray(env, serviceUuids) };
        });
}

void Emit::CharacteristicsDiscovered(
    const std::string& uuid, const std::string& serviceUuid,
    const std::vector<std::pair<std::string, std::vector<std::string>>>& characteristics)
{
    mCallback->call(
        [uuid, serviceUuid, characteristics](Napi::Env env, std::vector<napi_value>& args) {
            auto arr = characteristics.empty() ? Napi::Array::New(env)
                                               : Napi::Array::New(env, characteristics.size());
            for (size_t i = 0; i < characteristics.size(); i++)
            {
                Napi::Object characteristic = Napi::Object::New(env);
                characteristic.Set(_s("uuid"), _u(characteristics[i].first));
                characteristic.Set(_s("properties"), toArray(env, characteristics[i].second));
                arr.Set(i, characteristic);
            }
            // emit('characteristicsDiscover', deviceUuid, serviceUuid, { uuid, properties:
            // ['broadcast', 'read', ...]})
            args = { _s("characteristicsDiscover"), _u(uuid), _u(serviceUuid), arr };
        });
}

void Emit::Read(const std::string& uuid, const std::string& serviceUuid,
                const std::string& characteristicUuid, const Data& data, bool isNotification)
{
    mCallback->call([uuid, serviceUuid, characteristicUuid, data,
                     isNotification](Napi::Env env, std::vector<napi_value>& args) {
        // emit('read', deviceUuid, serviceUuid, characteristicsUuid, data, isNotification);
        args = { _s("read"),          _u(uuid),          _u(serviceUuid), _u(characteristicUuid),
                 toBuffer(env, data), _b(isNotification) };
    });
}

void Emit::Write(const std::string& uuid, const std::string& serviceUuid,
                 const std::string& characteristicUuid)
{
    mCallback->call(
        [uuid, serviceUuid, characteristicUuid](Napi::Env env, std::vector<napi_value>& args) {
            // emit('write', deviceUuid, servicesUuid, characteristicsUuid)
            args = { _s("write"), _u(uuid), _u(serviceUuid), _u(characteristicUuid) };
        });
}

void Emit::Notify(const std::string& uuid, const std::string& serviceUuid,
                  const std::string& characteristicUuid, bool state)
{
    mCallback->call([uuid, serviceUuid, characteristicUuid, state](Napi::Env env,
                                                                   std::vector<napi_value>& args) {
        // emit('notify', deviceUuid, servicesUuid, characteristicsUuid, state)
        args = { _s("notify"), _u(uuid), _u(serviceUuid), _u(characteristicUuid), _b(state) };
    });
}

void Emit::DescriptorsDiscovered(const std::string& uuid, const std::string& serviceUuid,
                                 const std::string& characteristicUuid,
                                 const std::vector<std::string>& descriptorUuids)
{
    mCallback->call([uuid, serviceUuid, characteristicUuid,
                     descriptorUuids](Napi::Env env, std::vector<napi_value>& args) {
        // emit('descriptorsDiscover', deviceUuid, servicesUuid, characteristicsUuid, descriptors:
        // [uuids])
        args = { _s("descriptorsDiscover"), _u(uuid), _u(serviceUuid), _u(characteristicUuid),
                 toUuidArray(env, descriptorUuids) };
    });
}

void Emit::ReadValue(const std::string& uuid, const std::string& serviceUuid,
                     const std::string& characteristicUuid, const std::string& descriptorUuid,
                     const Data& data)
{
    mCallback->call([uuid, serviceUuid, characteristicUuid, descriptorUuid,
                     data](Napi::Env env, std::vector<napi_value>& args) {
        // emit('valueRead', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, data)
        args = { _s("valueRead"),        _u(uuid),           _u(serviceUuid),
                 _u(characteristicUuid), _u(descriptorUuid), toBuffer(env, data) };
    });
}

void Emit::WriteValue(const std::string& uuid, const std::string& serviceUuid,
                      const std::string& characteristicUuid, const std::string& descriptorUuid)
{
    mCallback->call([uuid, serviceUuid, characteristicUuid,
                     descriptorUuid](Napi::Env env, std::vector<napi_value>& args) {
        // emit('valueWrite', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid);
        args = { _s("valueWrite"), _u(uuid), _u(serviceUuid), _u(characteristicUuid),
                 _u(descriptorUuid) };
    });
}

void Emit::ReadHandle(const std::string& uuid, int descriptorHandle, const Data& data)
{
    mCallback->call([uuid, descriptorHandle, data](Napi::Env env, std::vector<napi_value>& args) {
        // emit('handleRead', deviceUuid, descriptorHandle, data);
        args = { _s("handleRead"), _u(uuid), _n(descriptorHandle), toBuffer(env, data) };
    });
}

void Emit::WriteHandle(const std::string& uuid, int descriptorHandle)
{
    mCallback->call([uuid, descriptorHandle](Napi::Env env, std::vector<napi_value>& args) {
        // emit('handleWrite', deviceUuid, descriptorHandle);
        args = { _s("handleWrite"), _u(uuid), _n(descriptorHandle) };
    });
}
