/*
 * Copyright (C) 2011 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */


#include <syncevo/GLibSupport.h>

#include "bluez-manager.h"
#include "server.h"
#include "syncevo/SmartPtr.h"

#include <algorithm>

using namespace GDBusCXX;


SE_BEGIN_CXX

BluezManager::BluezManager(Server &server) :
    DBusRemoteObject(!strcmp(getEnv("DBUS_TEST_BLUETOOTH", ""), "none") ?
                     nullptr : /* simulate missing Bluez */
                     GDBusCXX::dbus_get_bus_connection(!strcmp(getEnv("DBUS_TEST_BLUETOOTH", ""), "session") ?
                                                       "SESSION" : /* use our own Bluez stub */
                                                       "SYSTEM" /* use real Bluez */,
                                                       nullptr, true, nullptr),
                     "/", "org.bluez.Manager", "org.bluez",
                     true),
    m_server(server),
    m_adapterChanged(*this, "DefaultAdapterChanged")
{
    if (getConnection()) {
        m_done = false;
        DBusClientCall<DBusObject_t> getAdapter(*this, "DefaultAdapter");
        getAdapter.start([this] (const DBusObject_t &adapter, const string &error) { defaultAdapterCb(adapter, error); });
        m_adapterChanged.activate([this] (const DBusObject_t &adapter) { defaultAdapterChanged(adapter); });
    } else {
        m_done = true;
    }
}

void BluezManager::defaultAdapterChanged(const DBusObject_t &adapter)
{
    m_done = false;
    //remove devices that belong to this original adapter
    if(m_adapter) {
        for (const auto &device: m_adapter->getDevices()) {
            m_server.removeDevice(device->getMac());
        }
    }
    string error;
    defaultAdapterCb(adapter, error);
}

void BluezManager::defaultAdapterCb(const DBusObject_t &adapter, const string &error)
{
    if(!error.empty()) {
        SE_LOG_DEBUG(NULL, "Error in calling DefaultAdapter of Interface org.bluez.Manager: %s", error.c_str());
        m_done = true;
        return;
    }
    m_adapter.reset(new BluezAdapter(*this, adapter));
}

BluezManager::BluezAdapter::BluezAdapter(BluezManager &manager, const string &path) :
    DBusRemoteObject(manager.getConnection(),
                     path, "org.bluez.Adapter", "org.bluez"),
    m_manager(manager), m_devNo(0), m_devReplies(0),
    m_deviceRemoved(*this,  "DeviceRemoved"), m_deviceAdded(*this, "DeviceCreated")
{
    DBusClientCall<std::vector<DBusObject_t> > listDevices(*this, "ListDevices");
    listDevices.start([this] (const std::vector<DBusObject_t> &devices, const string &error) { listDevicesCb(devices, error); });
    m_deviceRemoved.activate([this] (const DBusObject_t &object) { deviceRemoved(object); });
    m_deviceAdded.activate([this] (const DBusObject_t &object) { deviceCreated(object); });
}

void BluezManager::BluezAdapter::listDevicesCb(const std::vector<DBusObject_t> &devices, const string &error)
{
    if(!error.empty()) {
        SE_LOG_DEBUG(NULL, "Error in calling ListDevices of Interface org.bluez.Adapter: %s", error.c_str());
        checkDone(true);
        return;
    }
    m_devNo = devices.size();
    for (const auto &device: devices) {
        auto bluezDevice = std::make_shared<BluezDevice>(*this, device);
        m_devices.push_back(bluezDevice);
    }
    checkDone();
}

void BluezManager::BluezAdapter::deviceRemoved(const DBusObject_t &object)
{
    string address;
    std::vector<std::shared_ptr<BluezDevice> >::iterator devIt;
    for(devIt = m_devices.begin(); devIt != m_devices.end(); ++devIt) {
        if(boost::equals((*devIt)->getPath(), object)) {
            address = (*devIt)->m_mac;
            if((*devIt)->m_reply) {
                m_devReplies--;
            }
            m_devNo--;
            m_devices.erase(devIt);
            break;
        }
    }
    m_manager.m_server.removeDevice(address);
}

void BluezManager::BluezAdapter::deviceCreated(const DBusObject_t &object)
{
    m_devNo++;
    auto bluezDevice = std::make_shared<BluezDevice>(*this, object);
    m_devices.push_back(bluezDevice);
}

BluezManager::BluezDevice::BluezDevice (BluezAdapter &adapter, const string &path) :
    GDBusCXX::DBusRemoteObject(adapter.m_manager.getConnection(),
                               path, "org.bluez.Device", "org.bluez"),
    m_adapter(adapter), m_reply(false), m_propertyChanged(*this, "PropertyChanged")
{
    DBusClientCall<PropDict> getProperties(*this, "GetProperties");
    getProperties.start([this] (const PropDict &props, const string &error) { getPropertiesCb(props, error); });

    m_propertyChanged.activate([this] (const string &name,
                                       const boost::variant<vector<string>, string> &prop) { propertyChanged(name, prop); });
}

/**
 * check whether the current device has the PnP Information attribute.
 */
static bool hasPnpInfoService(const std::vector<std::string> &uuids)
{
    // The UUID that indicates the PnPInformation attribute is available.
    static const char * PNPINFOMATION_ATTRIBUTE_UUID = "00001200-0000-1000-8000-00805f9b34fb";

    // Note: GetProperties appears to return this list sorted which binary_search requires.
    if(std::binary_search(uuids.begin(), uuids.end(), PNPINFOMATION_ATTRIBUTE_UUID)) {
        return true;
    }

    return false;
}

void BluezManager::BluezDevice::checkSyncService(const std::vector<std::string> &uuids)
{
    static const char * SYNCML_CLIENT_UUID = "00000002-0000-1000-8000-0002ee000002";
    bool hasSyncService = false;
    Server &server = m_adapter.m_manager.m_server;
    for (const string &uuid: uuids) {
        //if the device has sync service, add it to the device list
        if(boost::iequals(uuid, SYNCML_CLIENT_UUID)) {
            hasSyncService = true;
            if(!m_mac.empty()) {
                SyncConfig::DeviceDescription deviceDesc(m_mac, m_name,
                                                         SyncConfig::MATCH_FOR_SERVER_MODE);
                server.addDevice(deviceDesc);
                if(hasPnpInfoService(uuids)) {
                    DBusClientCall<ServiceDict> discoverServices(*this,
                                                                  "DiscoverServices");
                    static const std::string PNP_INFO_UUID("0x1200");
                    discoverServices.start([this] (const ServiceDict &serviceDict,
                                                   const string &error) { discoverServicesCb(serviceDict, error); },
                                           PNP_INFO_UUID);
                }
            }
            break;
        }
    }
    // if sync service is not available now, possible to remove device
    if(!hasSyncService && !m_mac.empty()) {
        server.removeDevice(m_mac);
    }
}

/*
 * Parse the XML-formatted service record.
 */
bool extractValuefromServiceRecord(const std::string &serviceRecord,
                                   const std::string &attributeId,
                                   std::string &attributeValue)
{
    // Find atribute
    size_t pos  = serviceRecord.find(attributeId);

    // Only proceed if the attribute id was found.
    if(pos != std::string::npos) {
        pos = serviceRecord.find("value", pos + attributeId.size());
        pos = serviceRecord.find("\"", pos) + 1;
        int valLen = serviceRecord.find("\"", pos) - pos;
        attributeValue = serviceRecord.substr(pos, valLen);
        return true;
    }

    return false;
}

void BluezManager::loadBluetoothDeviceLookupTable()
{
    GError *err = nullptr;
    string filePath(SyncEvolutionDataDir() + "/bluetooth_products.ini");
    if(!g_key_file_load_from_file(m_lookupTable.bt_key_file, filePath.c_str(),
                                  G_KEY_FILE_NONE, &err)) {
        SE_LOG_DEBUG(NULL, "%s[%d]: %s - filePath = %s, error = %s",
                     __FILE__, __LINE__, "Bluetooth products file not loaded",
                     filePath.c_str(), err->message);
        m_lookupTable.isLoaded = false;
    } else {
        m_lookupTable.isLoaded = true;
    }
}

/*
 * Get the names of the PnpInformation vendor and product from their
 * respective ids. At a minimum we need a matching vendor id for this
 * function to return true. If the product id is not found then we set
 * it to "", an empty string.
 */
bool BluezManager::getPnpInfoNamesFromValues(const std::string &vendorValue, std::string &vendorName,
                                             const std::string &productValue, std::string &productName)
{
    if(!m_lookupTable.bt_key_file) {
        // If this is the first invocation we then we need to start watching the loopup table.
        if (!m_watchedFile) {
            m_lookupTable.bt_key_file = g_key_file_new();
            string filePath(SyncEvolutionDataDir() + "/bluetooth_products.ini");
            m_watchedFile = std::shared_ptr<SyncEvo::GLibNotify>(
                new GLibNotify(filePath.c_str(),
                               [this] (GFile *, GFile *, GFileMonitorEvent) { loadBluetoothDeviceLookupTable(); }));
        }
        loadBluetoothDeviceLookupTable();
        // Make sure the file was actually loaded
        if(!m_lookupTable.isLoaded) {
            return false;
        }
    }

    const char *VENDOR_GROUP  = "Vendors";
    const char *PRODUCT_GROUP = "Products";

    GStringPtr vendor(g_key_file_get_string(m_lookupTable.bt_key_file, VENDOR_GROUP,
                                            vendorValue.c_str(), nullptr));
    if(vendor) {
        vendorName = vendor.get();
    } else {
        // We at least need a vendor id match.
        return false;
    }

    GStringPtr product(g_key_file_get_string(m_lookupTable.bt_key_file, PRODUCT_GROUP,
                                             productValue.c_str(), nullptr));
    if(product)  {
        productName = product.get();
    } else {
        // If the product is not in the look-up table, the product is
        // set to an empty string.
        productName = "";
    }

    return true;
}

void BluezManager::BluezDevice::discoverServicesCb(const ServiceDict &serviceDict,
                                                   const string &error)
{
    auto iter = serviceDict.begin();

    if(iter != serviceDict.end()) {
        std::string serviceRecord = (*iter).second;

        if(!serviceRecord.empty()) {
            static const std::string SOURCE_ATTRIBUTE_ID("0x0205");
            std::string sourceId;
            extractValuefromServiceRecord(serviceRecord, SOURCE_ATTRIBUTE_ID, sourceId);

            // A sourceId of 0x001 indicates that the vendor ID was
            // assigned by the Bluetooth SIG.
            // TODO: A sourceId of 0x002, means the vendor id was assigned by
            // the USB Implementor's forum. We do nothing in this case but
            // should do that look up as well.
            if(!boost::iequals(sourceId, "0x0001")) { return; }

            std::string vendorId, productId;
            static const std::string VENDOR_ATTRIBUTE_ID ("0x0201");
            static const std::string PRODUCT_ATTRIBUTE_ID("0x0202");
            extractValuefromServiceRecord(serviceRecord, VENDOR_ATTRIBUTE_ID,  vendorId);
            extractValuefromServiceRecord(serviceRecord, PRODUCT_ATTRIBUTE_ID, productId);

            std::string vendorName, productName;
            if (!m_adapter.m_manager.getPnpInfoNamesFromValues(vendorId,                   vendorName,
                                                               vendorId + "_" + productId, productName)) {
                return;
            }

            Server &server = m_adapter.m_manager.m_server;
            SyncConfig::DeviceDescription devDesc;
            if (server.getDevice(m_mac, devDesc)) {
                devDesc.m_pnpInformation =
                    std::shared_ptr<SyncConfig::PnpInformation>(
                        new SyncConfig::PnpInformation(vendorName, productName));
                server.updateDevice(m_mac, devDesc);
            }
        }
    }
}

void BluezManager::BluezDevice::getPropertiesCb(const PropDict &props, const string &error)
{
    m_adapter.m_devReplies++;
    m_reply = true;
    if(!error.empty()) {
        SE_LOG_DEBUG(NULL, "Error in calling GetProperties of Interface org.bluez.Device: %s", error.c_str());
    } else {
        auto it = props.find("Name");
        if(it != props.end()) {
            m_name = boost::get<string>(it->second);
        }
        it = props.find("Address");
        if(it != props.end()) {
            m_mac = boost::get<string>(it->second);
        }

        auto uuids = props.find("UUIDs");
        if(uuids != props.end()) {
            const std::vector<std::string> uuidVec = boost::get<std::vector<std::string> >(uuids->second);
            checkSyncService(uuidVec);
        }
    }
    m_adapter.checkDone();
}

void BluezManager::BluezDevice::propertyChanged(const string &name,
                                                const boost::variant<vector<string>, string> &prop)
{
    Server &server = m_adapter.m_manager.m_server;
    if(boost::iequals(name, "Name")) {
        m_name = boost::get<std::string>(prop);
        SyncConfig::DeviceDescription device;
        if(server.getDevice(m_mac, device)) {
            device.m_deviceName = m_name;
            server.updateDevice(m_mac, device);
        }
    } else if(boost::iequals(name, "UUIDs")) {
        const std::vector<std::string> uuidVec = boost::get<std::vector<std::string> >(prop);
        checkSyncService(uuidVec);
    } else if(boost::iequals(name, "Address")) {
        string mac = boost::get<std::string>(prop);
        SyncConfig::DeviceDescription device;
        if(server.getDevice(m_mac, device)) {
            device.m_deviceId = mac;
            server.updateDevice(m_mac, device);
        }
        m_mac = mac;
    }
}

SE_END_CXX
