/*
 * 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/
 *  Copyright (C) 2006-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "UPnP.h"

#include "FileItem.h"
#include "GUIUserMessages.h"
#include "ServiceBroker.h"
#include "UPnPInternal.h"
#include "UPnPRenderer.h"
#include "UPnPServer.h"
#include "UPnPSettings.h"
#include "URL.h"
#include "Util.h"
#include "cores/playercorefactory/PlayerCoreFactory.h"
#include "guilib/GUIComponent.h"
#include "guilib/GUIWindowManager.h"
#include "messaging/ApplicationMessenger.h"
#include "network/Network.h"
#include "profiles/ProfileManager.h"
#include "settings/Settings.h"
#include "settings/SettingsComponent.h"
#include "utils/StaticLoggerBase.h"
#include "utils/SystemInfo.h"
#include "utils/TimeUtils.h"
#include "utils/URIUtils.h"
#include "utils/log.h"
#include "video/VideoInfoTag.h"

#include <set>

#include <Platinum/Source/Platinum/Platinum.h>

using namespace UPNP;
using namespace KODI::MESSAGING;

#define UPNP_DEFAULT_MAX_RETURNED_ITEMS 200
#define UPNP_DEFAULT_MIN_RETURNED_ITEMS 30

/*
# Play speed
#    1 normal
#    0 invalid
DLNA_ORG_PS = 'DLNA.ORG_PS'
DLNA_ORG_PS_VAL = '1'

# Conversion Indicator
#    1 transcoded
#    0 not transcoded
DLNA_ORG_CI = 'DLNA.ORG_CI'
DLNA_ORG_CI_VAL = '0'

# Operations
#    00 not time seek range, not range
#    01 range supported
#    10 time seek range supported
#    11 both supported
DLNA_ORG_OP = 'DLNA.ORG_OP'
DLNA_ORG_OP_VAL = '01'

# Flags
#    senderPaced                      80000000  31
#    lsopTimeBasedSeekSupported       40000000  30
#    lsopByteBasedSeekSupported       20000000  29
#    playcontainerSupported           10000000  28
#    s0IncreasingSupported            08000000  27
#    sNIncreasingSupported            04000000  26
#    rtspPauseSupported               02000000  25
#    streamingTransferModeSupported   01000000  24
#    interactiveTransferModeSupported 00800000  23
#    backgroundTransferModeSupported  00400000  22
#    connectionStallingSupported      00200000  21
#    dlnaVersion15Supported           00100000  20
DLNA_ORG_FLAGS = 'DLNA.ORG_FLAGS'
DLNA_ORG_FLAGS_VAL = '01500000000000000000000000000000'
*/

/*----------------------------------------------------------------------
|   NPT_Console::Output
+---------------------------------------------------------------------*/
void
NPT_Console::Output(const char* msg) { }

spdlog::level::level_enum ConvertLogLevel(int nptLogLevel)
{
    if (nptLogLevel >= NPT_LOG_LEVEL_FATAL)
      return spdlog::level::critical;
    if (nptLogLevel >= NPT_LOG_LEVEL_SEVERE)
      return spdlog::level::err;
    if (nptLogLevel >= NPT_LOG_LEVEL_WARNING)
      return spdlog::level::warn;
    if (nptLogLevel >= NPT_LOG_LEVEL_FINE)
      return spdlog::level::info;
    if (nptLogLevel >= NPT_LOG_LEVEL_FINER)
      return spdlog::level::debug;

    return spdlog::level::trace;
}

void
UPnPLogger(const NPT_LogRecord* record)
{
  static Logger logger = CServiceBroker::GetLogging().GetLogger("Platinum");
  if (CServiceBroker::GetLogging().CanLogComponent(LOGUPNP))
    logger->log(ConvertLogLevel(record->m_Level), "[{}]: {}", record->m_LoggerName,
                record->m_Message);
}

namespace UPNP
{

/*----------------------------------------------------------------------
|   static
+---------------------------------------------------------------------*/
CUPnP* CUPnP::upnp = NULL;
static NPT_List<void*> g_UserData;
static NPT_Mutex       g_UserDataLock;

/*----------------------------------------------------------------------
|   CDeviceHostReferenceHolder class
+---------------------------------------------------------------------*/
class CDeviceHostReferenceHolder
{
public:
    PLT_DeviceHostReference m_Device;
};

/*----------------------------------------------------------------------
|   CCtrlPointReferenceHolder class
+---------------------------------------------------------------------*/
class CCtrlPointReferenceHolder
{
public:
    PLT_CtrlPointReference m_CtrlPoint;
};

/*----------------------------------------------------------------------
|   CUPnPCleaner class
+---------------------------------------------------------------------*/
class CUPnPCleaner : public NPT_Thread
{
public:
    explicit CUPnPCleaner(CUPnP* upnp) : NPT_Thread(true), m_UPnP(upnp) {}
    void Run() override {
        delete m_UPnP;
    }

    CUPnP* m_UPnP;
};

/*----------------------------------------------------------------------
|   CMediaBrowser class
+---------------------------------------------------------------------*/
class CMediaBrowser : public PLT_SyncMediaBrowser,
                      public PLT_MediaContainerChangesListener,
                      protected CStaticLoggerBase
{
public:
  explicit CMediaBrowser(PLT_CtrlPointReference& ctrlPoint)
    : PLT_SyncMediaBrowser(ctrlPoint, true), CStaticLoggerBase("UPNP::CMediaBrowser")
  {
    SetContainerListener(this);
    }

    // PLT_MediaBrowser methods
    bool OnMSAdded(PLT_DeviceDataReference& device) override
    {
        CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
        message.SetStringParam("upnp://");
        CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);

        return PLT_SyncMediaBrowser::OnMSAdded(device);
    }
    void OnMSRemoved(PLT_DeviceDataReference& device) override
    {
        PLT_SyncMediaBrowser::OnMSRemoved(device);

        CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
        message.SetStringParam("upnp://");
        CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);

        PLT_SyncMediaBrowser::OnMSRemoved(device);
    }

    // PLT_MediaContainerChangesListener methods
    void OnContainerChanged(PLT_DeviceDataReference& device,
                                    const char*              item_id,
                                    const char*              update_id) override
    {
        NPT_String path = "upnp://"+device->GetUUID()+"/";
        if (!NPT_StringsEqual(item_id, "0")) {
            std::string id(CURL::Encode(item_id));
            URIUtils::AddSlashAtEnd(id);
            path += id.c_str();
        }

        s_logger->debug("notified container update {}", (const char*)path);
        CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
        message.SetStringParam(path.GetChars());
        CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
    }

    bool MarkWatched(const CFileItem& item, const bool watched)
    {
        if (watched) {
            CFileItem temp(item);
            temp.SetProperty("original_listitem_url", item.GetPath());
            return SaveFileState(temp, CBookmark(), watched);
        }
        else {
          s_logger->debug("Marking video item {} as watched", item.GetPath());
          return InvokeUpdateObject(item.GetPath().c_str(), "<upnp:playCount>1</upnp:playCount>",
                                    "<upnp:playCount>0</upnp:playCount>");
        }
    }

    bool SaveFileState(const CFileItem& item, const CBookmark& bookmark, const bool updatePlayCount)
    {
        std::string path = item.GetProperty("original_listitem_url").asString();
        if (!item.HasVideoInfoTag() || path.empty())  {
          return false;
        }

        NPT_String curr_value;
        NPT_String new_value;

        if (item.GetVideoInfoTag()->GetResumePoint().timeInSeconds != bookmark.timeInSeconds) {
          s_logger->debug("Updating resume point for item {}", path);
          long time = (long)bookmark.timeInSeconds;
          if (time < 0)
            time = 0;
          curr_value.Append(
              NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
                                 (long)item.GetVideoInfoTag()->GetResumePoint().timeInSeconds));
          curr_value += "<xbmc:lastPlayerState>";
          PLT_Didl::AppendXmlEscape(curr_value,
                                    item.GetVideoInfoTag()->GetResumePoint().playerState.c_str());
          curr_value += "</xbmc:lastPlayerState>";
          new_value.Append(NPT_String::Format(
              "<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>", time));
          new_value += "<xbmc:lastPlayerState>";
          PLT_Didl::AppendXmlEscape(new_value, bookmark.playerState.c_str());
          new_value += "</xbmc:lastPlayerState>";
        }
        if (updatePlayCount) {
          s_logger->debug("Marking video item {} as watched", path);
          if (!curr_value.IsEmpty())
            curr_value.Append(",");
          if (!new_value.IsEmpty())
            new_value.Append(",");
          curr_value.Append("<upnp:playCount>0</upnp:playCount>");
          new_value.Append("<upnp:playCount>1</upnp:playCount>");
        }

        return InvokeUpdateObject(path.c_str(), (const char*)curr_value, (const char*)new_value);
    }

    bool InvokeUpdateObject(const char* id, const char* curr_value, const char* new_value)
    {
        CURL url(id);
        PLT_DeviceDataReference device;
        PLT_Service* cds;
        PLT_ActionReference action;

        s_logger->debug("attempting to invoke UpdateObject for {}", id);

        // check this server supports UpdateObject action
        NPT_CHECK_LABEL(FindServer(url.GetHostName().c_str(), device),failed);
        NPT_CHECK_LABEL(device->FindServiceById("urn:upnp-org:serviceId:ContentDirectory", cds),failed);

        NPT_CHECK_LABEL(m_CtrlPoint->CreateAction(
            device,
            "urn:schemas-upnp-org:service:ContentDirectory:1",
            "UpdateObject",
            action), failed);

        NPT_CHECK_LABEL(action->SetArgumentValue("ObjectID", url.GetFileName().c_str()), failed);
        NPT_CHECK_LABEL(action->SetArgumentValue("CurrentTagValue", curr_value), failed);
        NPT_CHECK_LABEL(action->SetArgumentValue("NewTagValue", new_value), failed);

        NPT_CHECK_LABEL(m_CtrlPoint->InvokeAction(action, NULL),failed);

        s_logger->debug("invoked UpdateObject successfully");
        return true;

    failed:
      s_logger->info("invoking UpdateObject failed");
      return false;
    }
};


/*----------------------------------------------------------------------
|   CMediaController class
+---------------------------------------------------------------------*/
class CMediaController
  : public PLT_MediaControllerDelegate
  , public PLT_MediaController
{
public:
  explicit CMediaController(PLT_CtrlPointReference& ctrl_point)
    : PLT_MediaController(ctrl_point)
  {
    PLT_MediaController::SetDelegate(this);
  }

  ~CMediaController() override
  {
    for (const auto& itRenderer : m_registeredRenderers)
      unregisterRenderer(itRenderer);
    m_registeredRenderers.clear();
  }

#define CHECK_USERDATA_RETURN(userdata) do {     \
  if (!g_UserData.Contains(userdata))            \
      return;                                    \
  } while(0)

  void OnStopResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnStopResult(res, device, userdata);
  }

  void OnSetPlayModeResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSetPlayModeResult(res, device, userdata);
  }

  void OnSetAVTransportURIResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSetAVTransportURIResult(res, device, userdata);
  }

  void OnSeekResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSeekResult(res, device, userdata);
  }

  void OnPreviousResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPreviousResult(res, device, userdata);
  }

  void OnPlayResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPlayResult(res, device, userdata);
  }

  void OnPauseResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPauseResult(res, device, userdata);
  }

  void OnNextResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnNextResult(res, device, userdata);
  }

  void OnGetMediaInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_MediaInfo* info, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetMediaInfoResult(res, device, info, userdata);
  }

  void OnGetPositionInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_PositionInfo* info, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetPositionInfoResult(res, device, info, userdata);
  }

  void OnGetTransportInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_TransportInfo* info, void* userdata) override
  { CHECK_USERDATA_RETURN(userdata);
    static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetTransportInfoResult(res, device, info, userdata);
  }

  bool OnMRAdded(PLT_DeviceDataReference& device ) override
  {
    if (device->GetUUID().IsEmpty() || device->GetUUID().GetChars() == NULL)
      return false;

    CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();

    playerCoreFactory.OnPlayerDiscovered((const char*)device->GetUUID()
                                          ,(const char*)device->GetFriendlyName());

    m_registeredRenderers.insert(std::string(device->GetUUID().GetChars()));
    return true;
  }

  void OnMRRemoved(PLT_DeviceDataReference& device ) override
  {
    if (device->GetUUID().IsEmpty() || device->GetUUID().GetChars() == NULL)
      return;

    std::string uuid(device->GetUUID().GetChars());
    unregisterRenderer(uuid);
    m_registeredRenderers.erase(uuid);
  }

private:
  void unregisterRenderer(const std::string &deviceUUID)
  {
    CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();

    playerCoreFactory.OnPlayerRemoved(deviceUUID);
  }

  std::set<std::string> m_registeredRenderers;
};

/*----------------------------------------------------------------------
|   CUPnP::CUPnP
+---------------------------------------------------------------------*/
CUPnP::CUPnP() :
    m_MediaBrowser(NULL),
    m_MediaController(NULL),
    m_LogHandler(NULL),
    m_ServerHolder(new CDeviceHostReferenceHolder()),
    m_RendererHolder(new CRendererReferenceHolder()),
    m_CtrlPointHolder(new CCtrlPointReferenceHolder())
{
    NPT_LogManager::GetDefault().Configure("plist:.level=FINE;.handlers=CustomHandler;");
    NPT_LogHandler::Create("xbmc", "CustomHandler", m_LogHandler);
    m_LogHandler->SetCustomHandlerFunction(&UPnPLogger);

    // initialize upnp context
    m_UPnP = new PLT_UPnP();

    // keep main IP around
    if (CServiceBroker::GetNetwork().GetFirstConnectedInterface()) {
        m_IP = CServiceBroker::GetNetwork().GetFirstConnectedInterface()->GetCurrentIPAddress().c_str();
    }
    NPT_List<NPT_IpAddress> list;
    if (NPT_SUCCEEDED(PLT_UPnPMessageHelper::GetIPAddresses(list)) && list.GetItemCount()) {
        m_IP = (*(list.GetFirstItem())).ToString();
    }
    else if(m_IP.empty())
        m_IP = "localhost";

    // start upnp monitoring
    m_UPnP->Start();
}

/*----------------------------------------------------------------------
|   CUPnP::~CUPnP
+---------------------------------------------------------------------*/
CUPnP::~CUPnP()
{
    m_UPnP->Stop();
    StopClient();
    StopController();
    StopServer();

    delete m_UPnP;
    delete m_LogHandler;
    delete m_ServerHolder;
    delete m_RendererHolder;
    delete m_CtrlPointHolder;
}

/*----------------------------------------------------------------------
|   CUPnP::GetInstance
+---------------------------------------------------------------------*/
CUPnP*
CUPnP::GetInstance()
{
    if (!upnp) {
        upnp = new CUPnP();
    }

    return upnp;
}

/*----------------------------------------------------------------------
|   CUPnP::ReleaseInstance
+---------------------------------------------------------------------*/
void
CUPnP::ReleaseInstance(bool bWait)
{
    if (upnp) {
        CUPnP* _upnp = upnp;
        upnp = NULL;

        if (bWait) {
            delete _upnp;
        } else {
            // since it takes a while to clean up
            // starts a detached thread to do this
            CUPnPCleaner* cleaner = new CUPnPCleaner(_upnp);
            cleaner->Start();
        }
    }
}

/*----------------------------------------------------------------------
|   CUPnP::GetServer
+---------------------------------------------------------------------*/
CUPnPServer* CUPnP::GetServer()
{
  if(upnp)
    return static_cast<CUPnPServer*>(upnp->m_ServerHolder->m_Device.AsPointer());
  return NULL;
}

/*----------------------------------------------------------------------
|   CUPnP::MarkWatched
+---------------------------------------------------------------------*/
bool
CUPnP::MarkWatched(const CFileItem& item, const bool watched)
{
    if (upnp && upnp->m_MediaBrowser) {
        // dynamic_cast is safe here, avoids polluting CUPnP.h header file
        CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
        return browser->MarkWatched(item, watched);
    }
    return false;
}

/*----------------------------------------------------------------------
|   CUPnP::SaveFileState
+---------------------------------------------------------------------*/
bool
CUPnP::SaveFileState(const CFileItem& item, const CBookmark& bookmark, const bool updatePlayCount)
{
    if (upnp && upnp->m_MediaBrowser) {
        // dynamic_cast is safe here, avoids polluting CUPnP.h header file
        CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
        return browser->SaveFileState(item, bookmark, updatePlayCount);
    }
    return false;
}

/*----------------------------------------------------------------------
|   CUPnP::CreateControlPoint
+---------------------------------------------------------------------*/
void
CUPnP::CreateControlPoint()
{
    if (!m_CtrlPointHolder->m_CtrlPoint.IsNull())
        return;

    // create controlpoint
    m_CtrlPointHolder->m_CtrlPoint = new PLT_CtrlPoint();

    // start it
    m_UPnP->AddCtrlPoint(m_CtrlPointHolder->m_CtrlPoint);
}

/*----------------------------------------------------------------------
|   CUPnP::DestroyControlPoint
+---------------------------------------------------------------------*/
void
CUPnP::DestroyControlPoint()
{
    if (m_CtrlPointHolder->m_CtrlPoint.IsNull())
        return;

    m_UPnP->RemoveCtrlPoint(m_CtrlPointHolder->m_CtrlPoint);
    m_CtrlPointHolder->m_CtrlPoint = NULL;
}

/*----------------------------------------------------------------------
|   CUPnP::StartClient
+---------------------------------------------------------------------*/
void
CUPnP::StartClient()
{
    if (m_MediaBrowser != NULL)
        return;

    CreateControlPoint();

    // start browser
    m_MediaBrowser = new CMediaBrowser(m_CtrlPointHolder->m_CtrlPoint);
}

/*----------------------------------------------------------------------
|   CUPnP::StopClient
+---------------------------------------------------------------------*/
void
CUPnP::StopClient()
{
    if (m_MediaBrowser == NULL)
        return;

    delete m_MediaBrowser;
    m_MediaBrowser = NULL;

    if (!IsControllerStarted())
        DestroyControlPoint();
}

/*----------------------------------------------------------------------
|   CUPnP::StartController
+---------------------------------------------------------------------*/
void
CUPnP::StartController()
{
    if (m_MediaController != NULL)
        return;

    CreateControlPoint();

    m_MediaController = new CMediaController(m_CtrlPointHolder->m_CtrlPoint);
}

/*----------------------------------------------------------------------
|   CUPnP::StopController
+---------------------------------------------------------------------*/
void
CUPnP::StopController()
{
  if (m_MediaController == NULL)
      return;

  delete m_MediaController;
  m_MediaController = NULL;

  if (!IsClientStarted())
      DestroyControlPoint();
}

/*----------------------------------------------------------------------
|   CUPnP::CreateServer
+---------------------------------------------------------------------*/
CUPnPServer*
CUPnP::CreateServer(int port /* = 0 */)
{
    CUPnPServer* device =
        new CUPnPServer(CSysInfo::GetDeviceName().c_str(),
                        CUPnPSettings::GetInstance().GetServerUUID().length() ? CUPnPSettings::GetInstance().GetServerUUID().c_str() : NULL,
                        port);

    // trying to set optional upnp values for XP UPnP UI Icons to detect us
    // but it doesn't work anyways as it requires multicast for XP to detect us
    device->m_PresentationURL =
        NPT_HttpUrl(m_IP.c_str(),
                    CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SERVICES_WEBSERVERPORT),
                    "/").ToString();

    device->m_ModelName        = "Kodi";
    device->m_ModelNumber      = CSysInfo::GetVersion().c_str();
    device->m_ModelDescription = "Kodi - Media Server";
    device->m_ModelURL         = "http://kodi.tv/";
    device->m_Manufacturer     = "XBMC Foundation";
    device->m_ManufacturerURL  = "http://kodi.tv/";

    device->SetDelegate(device);
    return device;
}

/*----------------------------------------------------------------------
|   CUPnP::StartServer
+---------------------------------------------------------------------*/
bool
CUPnP::StartServer()
{
    if (!m_ServerHolder->m_Device.IsNull()) return false;

  const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();

    // load upnpserver.xml
    std::string filename = URIUtils::AddFileToFolder(profileManager->GetUserDataFolder(), "upnpserver.xml");
    CUPnPSettings::GetInstance().Load(filename);

    // create the server with a XBox compatible friendlyname and UUID from upnpserver.xml if found
    m_ServerHolder->m_Device = CreateServer(CUPnPSettings::GetInstance().GetServerPort());

    // start server
    NPT_Result res = m_UPnP->AddDevice(m_ServerHolder->m_Device);
    if (NPT_FAILED(res)) {
        // if the upnp device port was not 0, it could have failed because
        // of port being in used, so restart with a random port
        if (CUPnPSettings::GetInstance().GetServerPort() > 0) m_ServerHolder->m_Device = CreateServer(0);

        res = m_UPnP->AddDevice(m_ServerHolder->m_Device);
    }

    // save port but don't overwrite saved settings if port was random
    if (NPT_SUCCEEDED(res)) {
        if (CUPnPSettings::GetInstance().GetServerPort() == 0) {
            CUPnPSettings::GetInstance().SetServerPort(m_ServerHolder->m_Device->GetPort());
        }
        CUPnPServer::m_MaxReturnedItems = UPNP_DEFAULT_MAX_RETURNED_ITEMS;
        if (CUPnPSettings::GetInstance().GetMaximumReturnedItems() > 0) {
            // must be > UPNP_DEFAULT_MIN_RETURNED_ITEMS
            CUPnPServer::m_MaxReturnedItems = std::max(UPNP_DEFAULT_MIN_RETURNED_ITEMS, CUPnPSettings::GetInstance().GetMaximumReturnedItems());
        }
        CUPnPSettings::GetInstance().SetMaximumReturnedItems(CUPnPServer::m_MaxReturnedItems);
    }

    // save UUID
    CUPnPSettings::GetInstance().SetServerUUID(m_ServerHolder->m_Device->GetUUID().GetChars());
    return CUPnPSettings::GetInstance().Save(filename);
}

/*----------------------------------------------------------------------
|   CUPnP::StopServer
+---------------------------------------------------------------------*/
void
CUPnP::StopServer()
{
    if (m_ServerHolder->m_Device.IsNull()) return;

    m_UPnP->RemoveDevice(m_ServerHolder->m_Device);
    m_ServerHolder->m_Device = NULL;
}

/*----------------------------------------------------------------------
|   CUPnP::CreateRenderer
+---------------------------------------------------------------------*/
CUPnPRenderer*
CUPnP::CreateRenderer(int port /* = 0 */)
{
    CUPnPRenderer* device =
        new CUPnPRenderer(CSysInfo::GetDeviceName().c_str(),
                          false,
                          (CUPnPSettings::GetInstance().GetRendererUUID().length() ? CUPnPSettings::GetInstance().GetRendererUUID().c_str() : NULL),
                          port);

    device->m_PresentationURL =
        NPT_HttpUrl(m_IP.c_str(),
                    CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SERVICES_WEBSERVERPORT),
                    "/").ToString();
    device->m_ModelName        = "Kodi";
    device->m_ModelNumber      = CSysInfo::GetVersion().c_str();
    device->m_ModelDescription = "Kodi - Media Renderer";
    device->m_ModelURL         = "http://kodi.tv/";
    device->m_Manufacturer     = "XBMC Foundation";
    device->m_ManufacturerURL  = "http://kodi.tv/";

    return device;
}

/*----------------------------------------------------------------------
|   CUPnP::StartRenderer
+---------------------------------------------------------------------*/
bool CUPnP::StartRenderer()
{
    if (!m_RendererHolder->m_Device.IsNull())
      return false;

    const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();

    std::string filename = URIUtils::AddFileToFolder(profileManager->GetUserDataFolder(), "upnpserver.xml");
    CUPnPSettings::GetInstance().Load(filename);

    m_RendererHolder->m_Device = CreateRenderer(CUPnPSettings::GetInstance().GetRendererPort());

    NPT_Result res = m_UPnP->AddDevice(m_RendererHolder->m_Device);

    // failed most likely because port is in use, try again with random port now
    if (NPT_FAILED(res) && CUPnPSettings::GetInstance().GetRendererPort() != 0) {
        m_RendererHolder->m_Device = CreateRenderer(0);

        res = m_UPnP->AddDevice(m_RendererHolder->m_Device);
    }

    // save port but don't overwrite saved settings if random
    if (NPT_SUCCEEDED(res) && CUPnPSettings::GetInstance().GetRendererPort() == 0) {
        CUPnPSettings::GetInstance().SetRendererPort(m_RendererHolder->m_Device->GetPort());
    }

    // save UUID
    CUPnPSettings::GetInstance().SetRendererUUID(m_RendererHolder->m_Device->GetUUID().GetChars());
    return CUPnPSettings::GetInstance().Save(filename);
}

/*----------------------------------------------------------------------
|   CUPnP::StopRenderer
+---------------------------------------------------------------------*/
void CUPnP::StopRenderer()
{
    if (m_RendererHolder->m_Device.IsNull()) return;

    m_UPnP->RemoveDevice(m_RendererHolder->m_Device);
    m_RendererHolder->m_Device = NULL;
}

/*----------------------------------------------------------------------
|   CUPnP::UpdateState
+---------------------------------------------------------------------*/
void CUPnP::UpdateState()
{
  if (!m_RendererHolder->m_Device.IsNull())
      static_cast<CUPnPRenderer*>(m_RendererHolder->m_Device.AsPointer())->UpdateState();
}

void CUPnP::RegisterUserdata(void* ptr)
{
  NPT_AutoLock lock(g_UserDataLock);
  g_UserData.Add(ptr);
}

void CUPnP::UnregisterUserdata(void* ptr)
{
  NPT_AutoLock lock(g_UserDataLock);
  g_UserData.Remove(ptr);
}

} /* namespace UPNP */
