/*
* UPnP Support for XBMC
* Copyright (c) 2006 c0diq (Sylvain Rebaud)
* Portions Copyright (c) by the authors of libPlatinum
*
* http://www.plutinosoft.com/blog/category/platinum/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "UPnPDirectory.h"
#include "URL.h"
#include "network/upnp/UPnP.h"
#include "network/upnp/UPnPInternal.h"
#include "Platinum.h"
#include "PltSyncMediaBrowser.h"
#include "video/VideoInfoTag.h"
#include "FileItem.h"
#include "utils/log.h"
#include "utils/URIUtils.h"

using namespace MUSIC_INFO;
using namespace XFILE;
using namespace UPNP;

namespace XFILE
{
/*----------------------------------------------------------------------
|   CResourceFinder
+---------------------------------------------------------------------*/
class CResourceFinder {
public:
    CResourceFinder(const char* protocol, const char* content = NULL) : m_Protocol(protocol), m_Content(content) {}
    bool operator()(const PLT_MediaItemResource& resource) const {
        if (m_Content.IsEmpty())
            return (resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0);
        else
            return ((resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0)
                  && resource.m_ProtocolInfo.GetContentType().StartsWith(m_Content, true));
    }
private:
    NPT_String m_Protocol;
    NPT_String m_Content;
};

static CStdString GetContentMapping(NPT_String& objectClass)
{
    struct SClassMapping
    {
        const char* ObjectClass;
        const char* Content;
    };
    static const SClassMapping mapping[] = {
          { "object.item.videoItem.videoBroadcast", "episodes"      }
        , { "object.item.videoItem.musicVideoClip", "musicvideos"  }
        , { "object.item.videoItem"               , "movies"       }
        , { "object.item.audioItem.musicTrack"    , "songs"        }
        , { "object.item.audioItem"               , "songs"        }
        , { "object.item.imageItem.photo"         , "photos"       }
        , { "object.item.imageItem"               , "photos"       }
        , { "object.container.album.videoAlbum"   , "tvshows"      }
        , { "object.container.album.musicAlbum"   , "albums"       }
        , { "object.container.album.photoAlbum"   , "photos"       }
        , { "object.container.album"              , "albums"       }
        , { "object.container.person"             , "artists"      }
        , { NULL                                  , NULL           }
    };
    for(const SClassMapping* map = mapping; map->ObjectClass; map++)
    {
        if(objectClass.StartsWith(map->ObjectClass, true))
        {
          return map->Content;
          break;
        }
    }
    return "unknown";
}

static bool FindDeviceWait(CUPnP* upnp, const char* uuid, PLT_DeviceDataReference& device)
{
    bool client_started = upnp->IsClientStarted();
    upnp->StartClient();

    // look for device in our list
    // (and wait for it to respond for 5 secs if we're just starting upnp client)
    NPT_TimeStamp watchdog;
    NPT_System::GetCurrentTimeStamp(watchdog);
    watchdog += 5.f;

    for (;;) {
        if (NPT_SUCCEEDED(upnp->m_MediaBrowser->FindServer(uuid, device)) && !device.IsNull())
            break;

        // fail right away if device not found and upnp client was already running
        if (client_started)
            return false;

        // otherwise check if we've waited long enough without success
        NPT_TimeStamp now;
        NPT_System::GetCurrentTimeStamp(now);
        if (now > watchdog)
            return false;

        // sleep a bit and try again
        NPT_System::Sleep(NPT_TimeInterval((double)1));
    }

    return !device.IsNull();
}

/*----------------------------------------------------------------------
|   CUPnPDirectory::GetFriendlyName
+---------------------------------------------------------------------*/
const char*
CUPnPDirectory::GetFriendlyName(const char* url)
{
    NPT_String path = url;
    if (!path.EndsWith("/")) path += "/";

    if (path.Left(7).Compare("upnp://", true) != 0) {
        return NULL;
    } else if (path.Compare("upnp://", true) == 0) {
        return "UPnP Media Servers (Auto-Discover)";
    }

    // look for nextslash
    int next_slash = path.Find('/', 7);
    if (next_slash == -1)
        return NULL;

    NPT_String uuid = path.SubString(7, next_slash-7);
    NPT_String object_id = path.SubString(next_slash+1, path.GetLength()-next_slash-2);

    // look for device
    PLT_DeviceDataReference device;
    if(!FindDeviceWait(CUPnP::GetInstance(), uuid, device))
        return NULL;

    return (const char*)device->GetFriendlyName();
}

/*----------------------------------------------------------------------
|   CUPnPDirectory::GetDirectory
+---------------------------------------------------------------------*/
bool CUPnPDirectory::GetResource(const CURL& path, CFileItem &item)
{
    if(path.GetProtocol() != "upnp")
      return false;

    CUPnP* upnp = CUPnP::GetInstance();
    if(!upnp)
        return false;

    CStdString uuid   = path.GetHostName();
    CStdString object = path.GetFileName();
    object.TrimRight("/");
    CURL::Decode(object);

    PLT_DeviceDataReference device;
    if(!FindDeviceWait(upnp, uuid.c_str(), device)) {
        CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - unable to find uuid %s", uuid.c_str());
        return false;
    }

    PLT_MediaObjectListReference list;
    if (NPT_FAILED(upnp->m_MediaBrowser->BrowseSync(device, object.c_str(), list, true))) {
        CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - unable to find object %s", object.c_str());
        return false;
    }

    if (list.IsNull() || !list->GetItemCount()) {
      CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - no items returned for object %s", object.c_str());
      return false;
    }

    PLT_MediaObjectList::Iterator entry = list->GetFirstItem();
    if (entry == 0)
        return false;

    PLT_MediaItemResource resource;

    // look for a resource with "xbmc-get" protocol
    // if we can't find one, try to find a valid resource
    if(NPT_FAILED(NPT_ContainerFind((*entry)->m_Resources,
                      CResourceFinder("xbmc-get"), resource))) {
        const char* content = NULL;
        if ((*entry)->m_ObjectClass.type.StartsWith("object.item.audioItem"))
             content = "audio";
        else if ((*entry)->m_ObjectClass.type.StartsWith("object.item.imageItem"))
             content = "image";
        else if ((*entry)->m_ObjectClass.type.StartsWith("object.item.videoItem"))
             content = "video";

        if(NPT_FAILED(NPT_ContainerFind((*entry)->m_Resources,
                          CResourceFinder("http-get", content), resource))) {
            if((*entry)->m_Resources.GetItemCount()) {
                // last attempt to find something suitable
                resource = (*entry)->m_Resources[0];
            }
            else {
                CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - no resources returned for object %s", object.c_str());
                return false;
            }
        }
    }

    // store original path so we remember it
    item.SetProperty("original_listitem_url",  item.GetPath());
    item.SetProperty("original_listitem_mime", item.GetMimeType(false));

    // if it's an item, path is the first url to the item
    // we hope the server made the first one reachable for us
    // (it could be a format we dont know how to play however)
    item.SetPath((const char*) resource.m_Uri);

    // look for content type in protocol info
    if (resource.m_ProtocolInfo.IsValid()) {
        CLog::Log(LOGDEBUG, "CUPnPDirectory::GetResource - resource protocol info '%s'",
            (const char*)(resource.m_ProtocolInfo.ToString()));

        if (resource.m_ProtocolInfo.GetContentType().Compare("application/octet-stream") != 0) {
            item.SetMimeType((const char*)resource.m_ProtocolInfo.GetContentType());
        }
    } else {
        CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - invalid protocol info '%s'",
            (const char*)(resource.m_ProtocolInfo.ToString()));
    }

    // look for subtitles
    unsigned subs = 0;
    for(unsigned r = 0; r < (*entry)->m_Resources.GetItemCount(); r++)
    {
        PLT_MediaItemResource& res  = (*entry)->m_Resources[r];
        PLT_ProtocolInfo&      info = res.m_ProtocolInfo;
        static const char* allowed[] = { "text/srt"
                                       , "text/ssa"
                                       , "text/sub"
                                       , "text/idx" };
        for(unsigned type = 0; type < sizeof(allowed)/sizeof(allowed[0]); type++)
        {
            if(info.Match(PLT_ProtocolInfo("*", "*", allowed[type], "*")))
            {
                CStdString prop;
                prop.Format("upnp:subtitle:%d", ++subs);
                item.SetProperty(prop, (const char*)res.m_Uri);
                break;
            }
        }
    }

    return true;
}


/*----------------------------------------------------------------------
|   CUPnPDirectory::GetDirectory
+---------------------------------------------------------------------*/
bool
CUPnPDirectory::GetDirectory(const CStdString& strPath, CFileItemList &items)
{
    CUPnP* upnp = CUPnP::GetInstance();

    /* upnp should never be cached, it has internal cache */
    items.SetCacheToDisc(CFileItemList::CACHE_NEVER);

    // We accept upnp://devuuid/[item_id/]
    NPT_String path = strPath.c_str();
    if (!path.StartsWith("upnp://", true)) {
        return false;
    }

    if (path.Compare("upnp://", true) == 0) {
        upnp->StartClient();

        // root -> get list of devices
        const NPT_Lock<PLT_DeviceDataReferenceList>& devices = upnp->m_MediaBrowser->GetMediaServers();
        NPT_List<PLT_DeviceDataReference>::Iterator device = devices.GetFirstItem();
        while (device) {
            NPT_String name = (*device)->GetFriendlyName();
            NPT_String uuid = (*device)->GetUUID();

            CFileItemPtr pItem(new CFileItem((const char*)name));
            pItem->SetPath(CStdString((const char*) "upnp://" + uuid + "/"));
            pItem->m_bIsFolder = true;
            pItem->SetArt("thumb", (const char*)(*device)->GetIconUrl("image/png"));

            items.Add(pItem);

            ++device;
        }
    } else {
        if (!path.EndsWith("/")) path += "/";

        // look for nextslash
        int next_slash = path.Find('/', 7);

        NPT_String uuid = (next_slash==-1)?path.SubString(7):path.SubString(7, next_slash-7);
        NPT_String object_id = (next_slash==-1)?"":path.SubString(next_slash+1);
        object_id.TrimRight("/");
        if (object_id.GetLength()) {
            CStdString tmp = (char*) object_id;
            CURL::Decode(tmp);
            object_id = tmp;
        }

        // try to find the device with wait on startup
        PLT_DeviceDataReference device;
        if (!FindDeviceWait(upnp, uuid, device))
            goto failure;

        // issue a browse request with object_id
        // if object_id is empty use "0" for root
        object_id = object_id.IsEmpty()?"0":object_id;

        // remember a count of object classes
        std::map<NPT_String, int> classes;

        // just a guess as to what types of files we want
        bool video = true;
        bool audio = true;
        bool image = true;
        m_strFileMask.TrimLeft("/");
        if (!m_strFileMask.IsEmpty()) {
            video = m_strFileMask.Find(".wmv") >= 0;
            audio = m_strFileMask.Find(".wma") >= 0;
            image = m_strFileMask.Find(".jpg") >= 0;
        }

        // special case for Windows Media Connect and WMP11 when looking for root
        // We can target which root subfolder we want based on directory mask
        if (object_id == "0" && ((device->GetFriendlyName().Find("Windows Media Connect", 0, true) >= 0) ||
                                 (device->m_ModelName == "Windows Media Player Sharing"))) {

            // look for a specific type to differentiate which folder we want
            if (audio && !video && !image) {
                // music
                object_id = "1";
            } else if (!audio && video && !image) {
                // video
                object_id = "2";
            } else if (!audio && !video && image) {
                // pictures
                object_id = "3";
            }
        }

#ifdef DISABLE_SPECIALCASE
        // same thing but special case for XBMC
        if (object_id == "0" && ((device->m_ModelName.Find("XBMC", 0, true) >= 0) ||
                                 (device->m_ModelName.Find("Xbox Media Center", 0, true) >= 0))) {
            // look for a specific type to differentiate which folder we want
            if (audio && !video && !image) {
                // music
                object_id = "virtualpath://upnpmusic";
            } else if (!audio && video && !image) {
                // video
                object_id = "virtualpath://upnpvideo";
            } else if (!audio && !video && image) {
                // pictures
                object_id = "virtualpath://upnppictures";
            }
        }
#endif

        // if error, return now, the device could have gone away
        // this will make us go back to the sources list
        PLT_MediaObjectListReference list;
        NPT_Result res = upnp->m_MediaBrowser->BrowseSync(device, object_id, list);
        if (NPT_FAILED(res)) goto failure;

        // empty list is ok
        if (list.IsNull()) goto cleanup;

        PLT_MediaObjectList::Iterator entry = list->GetFirstItem();
        while (entry) {
            // disregard items with wrong class/type
            if( (!video && (*entry)->m_ObjectClass.type.CompareN("object.item.videoitem", 21,true) == 0)
             || (!audio && (*entry)->m_ObjectClass.type.CompareN("object.item.audioitem", 21,true) == 0)
             || (!image && (*entry)->m_ObjectClass.type.CompareN("object.item.imageitem", 21,true) == 0) )
            {
                ++entry;
                continue;
            }

            // never show empty containers in media views
            if((*entry)->IsContainer()) {
                if( (audio || video || image)
                 && ((PLT_MediaContainer*)(*entry))->m_ChildrenCount == 0) {
                    ++entry;
                    continue;
                }
            }

            NPT_String ObjectClass = (*entry)->m_ObjectClass.type.ToLowercase();

            // keep count of classes
            classes[(*entry)->m_ObjectClass.type]++;

            CFileItemPtr pItem(new CFileItem((const char*)(*entry)->m_Title));
            pItem->SetLabelPreformated(true);
            pItem->m_strTitle = (const char*)(*entry)->m_Title;
            pItem->m_bIsFolder = (*entry)->IsContainer();

            CStdString id = (char*) (*entry)->m_ObjectID;
            CURL::Encode(id);
            URIUtils::AddSlashAtEnd(id);
            pItem->SetPath(CStdString((const char*) "upnp://" + uuid + "/" + id.c_str()));

            // if it's a container, format a string as upnp://uuid/object_id
            if (pItem->m_bIsFolder) {
                pItem->SetPath(pItem->GetPath() + "/");

                // look for metadata
                if( ObjectClass.StartsWith("object.container.album.videoalbum") ) {
                    pItem->SetLabelPreformated(false);
                    UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *(*entry), NULL);

                } else if( ObjectClass.StartsWith("object.container.album.photoalbum")) {
                  //CPictureInfoTag* tag = pItem->GetPictureInfoTag();

                } else if( ObjectClass.StartsWith("object.container.album") ) {
                    pItem->SetLabelPreformated(false);
                    UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *(*entry), NULL);
                }

            } else {

                // set a general content type
                std::string content;
                if (ObjectClass.StartsWith("object.item.videoitem")) {
                    pItem->SetMimeType("video/octet-stream");
                    content = "video";
                }
                else if(ObjectClass.StartsWith("object.item.audioitem")) {
                    pItem->SetMimeType("audio/octet-stream");
                    content = "audio";
                }
                else if(ObjectClass.StartsWith("object.item.imageitem")) {
                    pItem->SetMimeType("image/octet-stream");
                    content = "image";
                }

                // attempt to find a valid resource (may be multiple)
                PLT_MediaItemResource resource;
                if(NPT_SUCCEEDED(NPT_ContainerFind((*entry)->m_Resources,
                                  CResourceFinder("http-get", content.c_str()), resource))) {

                    // set metadata
                    if (resource.m_Size != (NPT_LargeSize)-1) {
                        pItem->m_dwSize  = resource.m_Size;
                    }

                    // look for metadata
                    if(content.compare("video") == 0) {
                        pItem->SetLabelPreformated(false);
                        UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *(*entry), &resource);

                    } else if(content.compare("audio") == 0) {
                        pItem->SetLabelPreformated(false);
                        UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *(*entry), &resource);

                    } else if(content.compare("image") == 0) {
                      //CPictureInfoTag* tag = pItem->GetPictureInfoTag();

                    }
                }
            }

            // look for date?
            if((*entry)->m_Description.date.GetLength()) {
                SYSTEMTIME time = {};
                sscanf((*entry)->m_Description.date, "%hu-%hu-%huT%hu:%hu:%hu",
                       &time.wYear, &time.wMonth, &time.wDay, &time.wHour, &time.wMinute, &time.wSecond);
                pItem->m_dateTime = time;
            }

            // if there is a thumbnail available set it here
            if((*entry)->m_ExtraInfo.album_arts.GetItem(0))
                // only considers first album art
                pItem->SetArt("thumb", (const char*) (*entry)->m_ExtraInfo.album_arts.GetItem(0)->uri);
            else if((*entry)->m_Description.icon_uri.GetLength())
                pItem->SetArt("thumb", (const char*) (*entry)->m_Description.icon_uri);

            PLT_ProtocolInfo fanart_mask("xbmc.org", "*", "fanart", "*");
            for(unsigned i = 0; i < (*entry)->m_Resources.GetItemCount(); ++i) {
                PLT_MediaItemResource& res = (*entry)->m_Resources[i];
                if(res.m_ProtocolInfo.Match(fanart_mask)) {
                    pItem->SetArt("fanart", (const char*)res.m_Uri);
                    break;
                }
            }
            // set the watched overlay, as this will not be set later due to
            // content set on file item list
            if (pItem->HasVideoInfoTag()) {
                int episodes = pItem->GetVideoInfoTag()->m_iEpisode;
                int played   = pItem->GetVideoInfoTag()->m_playCount;
                const std::string& type = pItem->GetVideoInfoTag()->m_type;
                bool watched(false);
                if (type == "tvshow" || type == "season") {
                    pItem->SetProperty("totalepisodes", episodes);
                    pItem->SetProperty("numepisodes", episodes);
                    pItem->SetProperty("watchedepisodes", played);
                    pItem->SetProperty("unwatchedepisodes", episodes - played);
                    watched = (episodes && played == episodes);
                }
                else if (type == "episode" || type == "movie")
                    watched = (played > 0);
                pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, watched);
            }
            items.Add(pItem);

            ++entry;
        }

        NPT_String max_string = "";
        int        max_count  = 0;
        for(std::map<NPT_String, int>::iterator it = classes.begin(); it != classes.end(); it++)
        {
          if(it->second > max_count)
          {
            max_string = it->first;
            max_count  = it->second;
          }
        }
        std::string content = GetContentMapping(max_string);
        items.SetContent(content);
        if (content == "unknown")
        {
          items.AddSortMethod(SORT_METHOD_UNSORTED, 571, LABEL_MASKS("%L", "%I", "%L", ""));
          items.AddSortMethod(SORT_METHOD_LABEL_IGNORE_FOLDERS, 551, LABEL_MASKS("%L", "%I", "%L", ""));
          items.AddSortMethod(SORT_METHOD_SIZE, 553, LABEL_MASKS("%L", "%I", "%L", "%I"));
          items.AddSortMethod(SORT_METHOD_DATE, 552, LABEL_MASKS("%L", "%J", "%L", "%J"));
        }
    }

cleanup:
    return true;

failure:
    return false;
}
}
