/* 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 <cstring>

#include <string>
#include <vector>
#include <iostream>

#include "libupnpp/control/cdircontent.hxx"
#include "libupnpp/log.hxx"
#include "libupnpp/upnpp_p.hxx"
#include "libupnpp/soaphelp.hxx"
#include "libupnpp/smallut.h"

#ifdef UPNPP_USE_EXPAT
#include "libupnpp/expatmm.h"
#define XMLPARSERTP inputRefXMLParser
#else
#define PICOXML_EXPAT_STARTELEMENT_COMPAT
#include "libupnpp/picoxml.h"
#define XMLPARSERTP PicoXMLParser
#endif

using namespace UPnPP;

namespace UPnPClient {

std::string UPnPDirObject::nullstr;

static std::string roledkey(const std::string& nm, const std::string& role)
{
    if (role.empty()) {
        return nm;
    } else {
        return nm + std::string(" role=\"") + SoapHelp::xmlQuote(role) + "\"";
    }
}

static const std::map<std::string, UPnPDirObject::ItemClass> okitems{
    {"object.item.audioItem", UPnPDirObject::ITC_audioItem},
    {"object.item.audioItem.musicTrack", UPnPDirObject::ITC_audioItem},
    {"object.item.audioItem.audioBroadcast", UPnPDirObject::ITC_audioItem},
    {"object.item.audioItem.audioBook", UPnPDirObject::ITC_audioItem},
    {"object.item.playlistItem", UPnPDirObject::ITC_playlist},
    {"object.item.videoItem", UPnPDirObject::ITC_videoItem},
};

// An XML parser which builds directory contents from DIDL-lite input.
class UPnPDirParser : public XMLPARSERTP {
public:
    UPnPDirParser(UPnPDirContent& dir, const std::string& input)
        : XMLPARSERTP(input), m_dir(dir) {
        //LOGDEB("UPnPDirParser: input: " << input << '\n');
    }
    UPnPDirContent& m_dir;
protected:
    /* !! Beware: if changed to use the attrs, and using picoxml, need to define
       PICOXML_EXPAT_STARTELEMENT_COMPAT before the picoxml.h include */
    void StartElement(const XML_Char* name, const XML_Char**) override {
        //LOGDEB("startElement: name [" << name << "]" << " bpos " << getCurrentByteIndex() << '\n');
        auto& mapattrs = m_path.back().attributes;
        switch (name[0]) {
        case 'c':
            if (!strcmp(name, "container")) {
                m_vendorcollect = false;
                m_tobj.clear();
                m_tobj.m_type = UPnPDirObject::container;
                m_tobj.m_id = mapattrs["id"];
                m_tobj.m_pid = mapattrs["parentID"];
            }
            break;
        case 'd':
            if (!strcmp(name, "desc")) {
                auto it = mapattrs.find("nameSpace");
                if (it != mapattrs.end() && it->second == "urn:schemas-upmpdcli-com:upnpdesc") {
                    m_vendorcollect = true;
                }
            }
            break;
        case 'i':
            if (!strcmp(name, "item")) {
                m_vendorcollect = false;
                m_tobj.clear();
                m_tobj.m_type = UPnPDirObject::item;
                m_tobj.m_id = mapattrs["id"];
                m_tobj.m_pid = mapattrs["parentID"];
            }
            break;
        default:
            break;
        }
    }

    virtual bool checkobjok() {
        // We used to check id and pid not empty, but this is ok if we
        // are parsing a didl fragment sent from a control point. So
        // lets all ok and hope for the best.
        bool ok =  true; /*!m_tobj.m_id.empty() && !m_tobj.m_pid.empty() &&
                           !m_tobj.m_title.empty();*/

        if (ok && m_tobj.m_type == UPnPDirObject::item) {
            const auto it = okitems.find(m_tobj.getupropref("upnp:class"));
            if (it == okitems.end()) {
                // Only log this if the record comes from an MS as e.g. naims
                // send records with empty classes (and empty id/pid)
                if (!m_tobj.m_id.empty()) {
                    LOGINF("checkobjok: found object of unknown class: [" <<
                           m_tobj.getupropref("upnp:class") << "]" << '\n');
                }
                m_tobj.m_iclass = UPnPDirObject::ITC_unknown;
            } else {
                m_tobj.m_iclass = it->second;
            }
        }

        if (!ok) {
            LOGINF("checkobjok:skip: id [" << m_tobj.m_id << "] pid [" << m_tobj.m_pid <<
                   "] clss [" << m_tobj.getupropref("upnp:class") << "] tt [" << m_tobj.m_title <<
                   "]" << '\n');
        }
        return ok;
    }

    void storeFrag(bool isContainer) {
        auto len = getCurrentByteIndex() - m_path.back().start_index;
        if (len > 0) {
            m_tobj.m_didlfrag = m_input.substr(m_path.back().start_index, len);
#ifdef UPNPP_USE_EXPAT
            // Expat returns the offset for the starting byte of the closing tag
            // We could mimick this in picoxml, but why?
            m_tobj.m_didlfrag += (isContainer ? "</container>" : "</item>");
#endif
            LOGDEB1("DIDLFRAG [" << m_tobj.m_didlfrag << "]\n");
        }
    }
    void EndElement(const XML_Char* name) override {
        std::string parentname;
        if (m_path.size() == 1) {
            parentname = "root";
        } else {
            parentname = m_path[m_path.size()-2].name;
        }
        LOGDEB1("Closing element " << name << " inside element " <<  parentname << 
                " data [" << m_path.back().data << "]\n");
        auto ordervalue = std::to_string(m_dir.m_containers.size() + m_dir.m_items.size());
        if (!strcmp(name, "container")) {
            if (checkobjok()) {
                m_tobj.m_props.insert({"upnpporder", ordervalue});
                storeFrag(true);
                m_dir.m_containers.push_back(m_tobj);
            }
        } else if (!strcmp(name, "item")) {
            if (checkobjok()) {
                m_tobj.m_props.insert({"upnpporder", ordervalue});
                storeFrag(false);
                m_dir.m_items.push_back(m_tobj);
            }
        } else if (parentname == "item" || parentname == "container" ||
                   (m_vendorcollect && parentname == "desc")) {
            switch (name[0]) {
            case 'd':
                if (!strcmp(name, "dc:title")) {
                    m_tobj.m_title = m_path.back().data;
                } else {
                    addprop(name, m_path.back().data);
                }
                break;
            case 'r':
                if (!strcmp(name, "res")) {
                    // <res protocolInfo="http-get:*:audio/mpeg:*" size="517149" bitrate="24576"
                    // duration="00:03:35" sampleFrequency="44100" nrAudioChannels="2">
                    UPnPResource res;
                    if (LibUPnP::getLibUPnP()->m->reSanitizeURLs()) {
                        res.m_uri = reSanitizeURL(m_path.back().data);
                    } else {
                        res.m_uri = m_path.back().data;
                    }
                    res.m_props = m_path.back().attributes;
                    m_tobj.m_resources.push_back(res);
                } else {
                    addprop(name, m_path.back().data);
                }
                break;
            case 'u':
                if (!strcmp(name, "upnp:albumArtURI")) {
                    if (LibUPnP::getLibUPnP()->m->reSanitizeURLs()) {
                        addprop(name, reSanitizeURL(m_path.back().data));
                    } else {
                        addprop(name, m_path.back().data);
                    }
                } else {
                    addprop(name, m_path.back().data);
                }
                break;
            default:
                addprop(name, m_path.back().data);
                break;
            }
        }
    }

    void CharacterData(const XML_Char* s, int len) override {
        if (s == 0 || *s == 0)
            return;
        std::string str(s, len);
        m_path.back().data += str;
    }

private:
    UPnPDirObject m_tobj;
    // Collect properties from vendor extension block. We only do this with upmpdcli
    bool m_vendorcollect{false}; 
    
    void addprop(const std::string& nm, const std::string& data) {
        // e.g <upnp:artist role="AlbumArtist">Jojo</upnp:artist>
        std::string rolevalue;
        auto& mapattrs = m_path.back().attributes;
        auto roleit = mapattrs.find("role");
        if (roleit != mapattrs.end())
            rolevalue = roleit->second;
        m_tobj.m_props.insert({roledkey(nm, rolevalue), data});
    }
};

bool UPnPDirObject::getprop(const std::string& name, std::string& value) const
{
    value.clear();
    auto it = m_props.lower_bound(name);
    if (it == m_props.end() || !beginswith(it->first, name))
        return false;
    for (;it != m_props.end() && beginswith(it->first, name); it++) {
        std::string role;
        if (it->first.size() != name.size()) {
            auto space = it->first.find(' ');
            if (space != name.size() || space == it->first.size()-1)
                break;
            if (it->first.find("role=", space+1) == space+1) {
                auto firstquote = space+1+6;
                auto secondquote = it->first.find('"', firstquote+1);
                role = SoapHelp::xmlUnquote(it->first.substr(firstquote, secondquote - firstquote));
            }
        }
        value += it->second + (role.empty() ? std::string(", ") : std::string(" (") + role + ")" +
                               ", ");
    }
    rtrimstring(value, ", ");
    return true;
}

const std::string& UPnPDirObject::getupropref(const std::string& name) const
{
    auto it = m_props.lower_bound(name);
    if (it == m_props.end() || name != it->first)
        return nullstr;
    return it->second;
}

const std::string UPnPDirObject::getprop(const std::string& name) const
{
    std::string value;
    getprop(name, value);
    return value;
}

std::string UPnPDirObject::getAlbumArtist() const
{
    static const std::string albumartistprop{roledkey("upnp:artist", "AlbumArtist")};

    auto it = m_props.lower_bound(albumartistprop);
    if (it == m_props.end() || it->first != albumartistprop) {
        return "";
    } else {
        return it->second;
    }
}

std::string UPnPDirObject::getArtists() const
{
    std::string out;
    std::vector<std::pair<std::string, std::string>> artists;
    getRoledArtists(artists);
    for (const auto& [role, value] : artists) {
        if (role == "AlbumArtist")
            continue;
        // Do we need to avoid dups ? We could use an std::set and join at the end
        out += value;
        if (!role.empty())
            out += std::string(" (") + role + ")";
        out += ", ";
    }
    rtrimstring(out, ", ");
    return out;
}

std::string UPnPDirObject::getAlbumArtistElseArtists() const
{
    std::string ret = getAlbumArtist();
    if (!ret.empty()) {
        return ret;
    }
    return getArtists();
}

bool UPnPDirObject::getRoledArtists(std::vector<std::pair<std::string, std::string>>& out) const
{
    out.clear();
    static const std::string name{"upnp:artist"};
    auto it = m_props.lower_bound(name);
    if (it == m_props.end() || !beginswith(it->first, name))
        return false;
    for (;it != m_props.end() && beginswith(it->first, name); it++) {
        std::string role;
        if (it->first.size() != name.size()) {
            auto space = it->first.find(' ');
            if (space != name.size() || space == it->first.size()-1)
                break;
            if (it->first.find("role=", space+1) == space+1) {
                auto firstquote = space+1+6;
                auto secondquote = it->first.find('"', firstquote+1);
                role = SoapHelp::xmlUnquote(it->first.substr(firstquote, secondquote - firstquote));
            }
        }
        out.push_back({role, it->second});
    }
    return true;
}

bool UPnPDirContent::parse(const std::string& input)
{
    if (input.empty()) {
        return false;
    }
    const std::string *ipp = &input;

    // Double-quoting happens. Just deal with it...
    std::string unquoted;
    if (input[0] == '&') {
        LOGDEB0("UPnPDirContent::parse: unquoting over-quoted input: " << input << '\n');
        unquoted = SoapHelp::xmlUnquote(input);
        ipp = &unquoted;
    }

    UPnPDirParser parser(*this, *ipp);
    bool ret = parser.Parse();
    if (!ret) {
        LOGERR("UPnPDirContent::parse: parser failed: " << parser.getLastErrorMessage() <<
               " for:\n" << *ipp << '\n');
    }
    return ret;
}

static const std::string didl_header(
    "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
    "<DIDL-Lite xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\""
    " xmlns:dc=\"http://purl.org/dc/elements/1.1/\""
    " xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\""
    " xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\">");
static const std::string didl_close("</DIDL-Lite>");

// Maybe we'll do something about building didl from scratch if this proves necessary.
std::string UPnPDirObject::getdidl() const
{
    return didl_header + m_didlfrag + didl_close;
}

} // namespace
