/* Copyright (C) 2006-2024 J.F.Dockes
 *
 * 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) any later version.
 *
 * 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 "config.h"

#include "libupnpp/control/typedservice.hxx"

#include <string>
#include <utility>
#include <functional>
#include <condition_variable>
#include <chrono>

#include <upnp.h>

#include "libupnpp/control/discovery.hxx"
#include "libupnpp/control/description.hxx"
#include "libupnpp/log.hxx"
#include "libupnpp/smallut.h"
#include "libupnpp/soaphelp.hxx"


using namespace std;
using namespace std::placeholders;
using namespace UPnPP;

namespace UPnPClient {
class TypedService::Internal {
public:
    string servicetype;
    int version;
    UPnPServiceDesc::Parsed proto;
};

TypedService::TypedService(const string& tp)
    : m(new Internal())
{
    string::size_type colon = tp.find_last_of(':');
    m->servicetype = tp.substr(0, colon);
    if (colon != string::npos && colon != tp.size() -1) {
        m->version = atoi(tp.substr(colon+1).c_str());
    } else {
        m->version = 0;
    }
    LOGDEB2("TypedService::TypedService: tp " << m->servicetype <<" version " << m->version <<'\n');
};

TypedService::~TypedService()
{
    delete m;
}

bool TypedService::serviceTypeMatch(const string& _tp)
{
    LOGDEB2("TypedService::serviceTypeMatch: [" << _tp << "]\n");
    string::size_type colon = _tp.find_last_of(':');
    string tp = _tp.substr(0, colon);
    int version = 0;
    if (colon != string::npos && colon != _tp.size() -1) {
        version = atoi(_tp.substr(colon+1).c_str());
    }
    return m->servicetype == tp && m->version >= version;
}


bool TypedService::serviceInit(const UPnPDeviceDesc& device, const UPnPServiceDesc& service)
{
    return service.fetchAndParseDesc(device.URLBase, m->proto);
}

int TypedService::runAction(const string& actnm, vector<string> args, map<string, string>& data)
{
    auto it = m->proto.actionList.find(actnm);
    if (it == m->proto.actionList.end()) {
        LOGERR("TypedService::runAction: action [" << actnm << "] not found\n");
        return UPNP_E_INVALID_ACTION;
    }
    const auto& action = it->second;
    
    unsigned int outargcnt = 0;
    for (const auto& arg : action.argList) {
        if (arg.todevice) {
            outargcnt++;
        }
    }
    if (outargcnt != args.size()) {
        LOGERR("TypedService::runAction: expected " << outargcnt << " outgoing arguments, got " <<
               args.size() << '\n');
        return UPNP_SOAP_E_INVALID_ARGS;
    }
    SoapOutgoing soapargs(getServiceType(), actnm);
    for (unsigned int i = 0; i < args.size(); i++) {
        soapargs.addarg(action.argList[i].name, args[i]);
    }
    SoapIncoming sdata;
    int ret = Service::runAction(soapargs, sdata);
    if (ret != 0) {
        return ret;
    }
    unordered_map<string,string> retdata;
    sdata.getMap(retdata);
    data = map<string,string>(retdata.begin(), retdata.end());
    return UPNP_E_SUCCESS;
}

void TypedService::evtCallback(const std::unordered_map<std::string, std::string>& props)
{
    VarEventReporter *reporter = getReporter();
    LOGDEB1("TypedService::evtCallback: reporter: " << reporter << '\n');
    for (const auto& ent : props) {
        if (!reporter) {
            LOGDEB1("TypedService::evtCallback: " << ent.first << " -> " << ent.second << '\n');
        } else {
            reporter->changed(ent.first.c_str(), ent.second.c_str());
        }
    }
}

void TypedService::registerCallback()
{
    Service::registerCallback(bind(&TypedService::evtCallback, this, _1));
}

// Callback for the discovery service to inform us about the devices
// and services it knows or finds.
class DirCB {
public:
    DirCB(const string& dv, const string& tp, bool fz,
          std::condition_variable& cv)
        : dvname(dv),
          ldvname(stringtolower(dv)),
          stype((fz? stringtolower(tp) : tp)),
          fuzzy(fz), discocv(cv) {
    }
    string dvname;
    string ldvname;
    string stype;
    bool fuzzy;
    std::condition_variable& discocv;
    UPnPDeviceDesc founddev;
    UPnPServiceDesc foundserv;
    
    bool visit(const UPnPDeviceDesc& dev, const UPnPServiceDesc& serv) {
        LOGDEB2("findTypedService:visit: got " << dev.friendlyName << " " <<
               dev.UDN << " " << serv.serviceType << '\n');
        bool matched = dev.UDN == dvname || !stringlowercmp(ldvname, dev.friendlyName);
        if (matched) {
            if (fuzzy) {
                string ltp = stringtolower(serv.serviceType);
                matched = matched && (ltp.find(stype) != string::npos);
            } else {
                matched =  matched && stype == serv.serviceType;
            }
        }
        if (matched) {
            founddev = dev;
            foundserv = serv;
            discocv.notify_all();
        }
        // We return false if found, to stop the traversal. Success is
        // indicated by our data.
        return !matched;
    }
};

TypedService *findTypedService(
    const std::string& devname, const std::string& servicetype, bool fuzzy)
{
    UPnPDeviceDirectory *superdir = UPnPDeviceDirectory::getTheDir();
    if (superdir == 0) {
        LOGERR("Discovery init failed\n");
        return 0;
    }
    std::mutex discolock;
    std::condition_variable discocv;

    DirCB cb(devname, servicetype, fuzzy, discocv);
    UPnPDeviceDirectory::Visitor vis = bind(&DirCB::visit, &cb, _1, _2);

    {
        std::unique_lock<std::mutex> mylock(discolock);
        // Calls to vis() may occur *during* the addCallback()
        // call. We need to check if the device was found before going
        // into the wait loop.
        int callbackidx = superdir->addCallback(vis);
        if (cb.founddev.UDN.empty()) {
            time_t ms;
            while ((ms = superdir->getRemainingDelayMs()) > 100) {
                discocv.wait_for(mylock, std::chrono::milliseconds(ms));
                if (!cb.founddev.UDN.empty()) {
                    break;
                }
            }
        }
        superdir->delCallback(callbackidx);
    }

    if (cb.founddev.UDN.empty()) {
        LOGDEB("findTypedService: no luck with CB, traversing\n");
        // Not found during the timeout. Let's traverse as a last
        // ditch effort
        superdir->traverse(vis);
    }

    if (!cb.founddev.UDN.empty()) {
        TypedService *service = new TypedService(cb.foundserv.serviceType);
        service->initFromDescription(cb.founddev);
        // string sdesc = cb.founddev.dump();
        return service;
    }
    LOGDEB("Service not found: " << devname << "/" << servicetype << " fuzzy " << fuzzy << '\n');
    return 0;
}

}
