/* 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
 */
#ifndef _UPNPDIRCONTENT_H_X_INCLUDED_
#define _UPNPDIRCONTENT_H_X_INCLUDED_

#include <map>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

#include "libupnpp/upnpavutils.hxx"

namespace UPnPClient {

/**
 * UPnP resource. A resource describes one of the entities associated with a directory entry. This
 * would be typically the audio file URI, with its characteristics (sample rate etc.) as attributes.
 * There can be several resource elements in a directory entry, e.g. for different encoding formats.
 */
class UPNPP_API UPnPResource {
public:
    /// URI Value
    std::string m_uri;

    /// Resource attributes
    std::map<std::string, std::string> m_props;

    /// Get the protocolinfo attribute in cooked form
    bool protoInfo(UPnPP::ProtocolinfoEntry& e) const {
        const auto it = m_props.find("protocolInfo");
        if (it == m_props.end()) {
            return false;
        }
        return UPnPP::parseProtoInfEntry(it->second, e);
    }
};

/**
 * UPnP Media Server directory entry, converted from XML data.
 *
 * This is a dumb data holder class, a struct with helpers.
 */
class UPNPP_API UPnPDirObject {
public:

    /////////
    //  Types

    enum ObjType {objtnone = -1, item, container};
    // There are actually several kinds of containers: object.container.storageFolder,
    // object.container.person, object.container.playlistContainer etc., but they all seem to behave
    // the same as far as we're concerned. Otoh, musicTrack items are special to us, and so should
    // playlists, but I've not seen one of the latter yet (servers seem to use containers for
    // playlists).
    enum ItemClass {ITC_audioItem = 0, ITC_playlist = 1,
                    ITC_unknown = 2,
                    ITC_videoItem = 3,
                    ITC_audioItem_musicTrack = ITC_audioItem, // hist compat
                    ITC_audioItem_playlist = ITC_playlist // hist compat
                   };

    ///////////////
    // Data fields
    
    /// Object Id
    std::string m_id;
    /// Parent Object Id
    std::string m_pid;
    /// Value of dc:title.
    std::string m_title;
    /// Item or container
    ObjType m_type;
    /// Item type details
    ItemClass m_iclass;

    /// Storage for the properties.
    ///
    /// Properties as gathered from the XML document (album, artist, etc.).
    /// In general, the multimap keys are the XML tag names, with exceptions:
    ///  - dc:title is stored in a proper field, m_title.
    ///  - upnp:artist fields with an an additional role are stored with the following key:
    ///      upnp:artist role="rolename"
    ///    There are utility functions to deal with this. upnp:artist is the only
    ///    property in UPnP 1 with a significant attribute (role).
    std::multimap<std::string, std::string> m_props;

    /// Resources: there may be several, for example for different audio formats of the same track,
    /// each with an URI and descriptor fields.
    std::vector<UPnPResource> m_resources;


    ///////////////////
    // Methods
    
    /** Get named property. Discriminate non-existing from empty values
     * @param name (dc:description, upnp:album...). Use m_title instead for dc:title, and the
     *  specialized artist methods for upnp:artist.
     * @param[out] value the parameter value if found
     * @return true if found.
     */
    bool getprop(const std::string& name, std::string& value) const;

    /** Get named property. Return empty value if it does not exist.
     * @param name (e.g. dc:description, upnp:album...). Use m_title instead for dc:title, and the
     *  specialized artist methods for upnp:artist.
     * @return value if found, empty string if not found.
     */
    const std::string getprop(const std::string& name) const;

    /** Get reference to first (supposed unique) instance of property. Used for e.g. upnp:class */
    const std::string& getupropref(const std::string& name) const;

    /** Specialized methods for artists, dealing with the role attribute and the special AlbumArtist
     * role value */
    /// return vector of {role, artist} pairs (role can be empty).
    bool getRoledArtists(std::vector<std::pair<std::string, std::string>>& out) const;
    /// @return the value of upnp:artist role="AlbumArtist"
    std::string getAlbumArtist() const;
    /// @return concatenate all upnp:artist values, appending a role in parentheses if set,
    /// excluding AlbumArtist.
    std::string getArtists() const;
    /// @return albumartist if set, else artists
    std::string getAlbumArtistElseArtists() const;

    /** Get named resource attribute.
     * Field names: "bitrate", "duration" (H:mm:ss.ms), "nrAudioChannels",
     * "protocolInfo", "sampleFrequency" (Hz), "size" (bytes).
     * @param ridx index in resources array.
     * @param nm attribute name.
     * @param[output] value if found.
     * @return true if found, else false.
     */
    bool getrprop(unsigned int ridx, const std::string& nm, std::string& val) const {
        if (ridx >= m_resources.size())
            return false;
        const auto it = m_resources[ridx].m_props.find(nm);
        if (it == m_resources[ridx].m_props.end())
            return false;
        val = it->second;
        return true;
    }

    /** Simplified interface to retrieving values: we don't distinguish
     * between non-existing and empty, and we only use the first ressource
     */
    std::string f2s(const std::string& nm, bool isresfield) {
        std::string val;
        if (isresfield) {
            getrprop(0, nm, val);
        } else {
            getprop(nm, val);
        }
        return val;
    }

    /** Return resource duration in seconds. 
     * @param ridx resource index.
     * @return duration or 1 if the attribute is not found.
     */
    int getDurationSeconds(unsigned ridx = 0) const {
        std::string sdur;
        if (!getrprop(ridx, "duration", sdur)) {
            //?? Avoid returning 0, who knows...
            return 1;
        }
        return UPnPP::upnpdurationtos(sdur);
    }

    /**
     * Get a DIDL document suitable for sending to a mediaserver. 
     * We may have missed useful stuff while parsing the data from the content
     * directory, so we send the original if we can.
     */
    std::string getdidl() const;

    void clear() {
        m_id.clear();
        m_pid.clear();
        m_title.clear();
        m_type = objtnone;
        m_iclass = ITC_unknown;
        m_props.clear();
        m_resources.clear();
        m_didlfrag.clear();
    }

    std::string dump() const {
        std::ostringstream os;
        os << "UPnPDirObject: " << (m_type == item ? "item" : "container") << " id [" << m_id <<
            "] pid [" << m_pid << "] title [" << m_title << "]" << '\n';
        os << "Properties: " << '\n';
        for (const auto& m_prop : m_props) {
            os << "[" << m_prop.first << "]->[" << m_prop.second << "] " << '\n';
        }
        os << "Resources:" << '\n';
        for (const auto& m_resource : m_resources) {
            os << "  Uri [" << m_resource.m_uri << "]" << '\n';
            os << "  Resource attributes:" << '\n';
            for (const auto& m_prop : m_resource.m_props) {
                os << "    [" << m_prop.first << "]->[" << m_prop.second << "] " << '\n';
            }
        }
        os << '\n';
        return os.str();
    }

private:
    friend class UPnPDirParser;
    // DIDL text for element, sans header
    std::string m_didlfrag;
    static std::string nullstr;
};

/**
 * Image of a MediaServer Directory Service container (directory),
 * possibly containing items and subordinate containers.
 * We use separate lists for containers and items. This was a design mistake because we lose the
 * exact ordering from the media server if containers and items are originally interpersed, which
 * does happen (e.g. minimserver in some cases). As a workaround we now set an "upnpporder" property
 * to all item and container object, set to the index in the original list.
 */
class UPNPP_API UPnPDirContent {
public:
    std::vector<UPnPDirObject> m_containers;
    std::vector<UPnPDirObject> m_items;

    void clear() {
        m_containers.clear();
        m_items.clear();
    }

    /**
     * Parse from DIDL-Lite XML data.
     *
     * Normally only used by ContentDirectory::readDir() * This is cumulative: in general, the XML
     * data is obtained in several documents corresponding to (offset,count) slices of the directory
     * (container). parse() can be called repeatedly with the successive XML documents and will
     * accumulate entries in the item and container vectors. This makes more sense if the different
     * chunks are from the same container, but given that UPnP Ids are actually global, nothing
     * really bad will happen if you mix up...
     *
     * @param detailed if true, populate the m_allprops field.
     */
    bool parse(const std::string& didltext);
};

} // namespace

#endif /* _UPNPDIRCONTENT_H_X_INCLUDED_ */
