/*
 *  Copyright (C) 2005-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 "PlayListPlayer.h"

#include "FileItem.h"
#include "GUIUserMessages.h"
#include "PartyModeManager.h"
#include "ServiceBroker.h"
#include "URL.h"
#include "application/Application.h"
#include "application/ApplicationComponents.h"
#include "application/ApplicationPlayer.h"
#include "application/ApplicationPowerHandling.h"
#include "dialogs/GUIDialogKaiToast.h"
#include "filesystem/PluginDirectory.h"
#include "filesystem/VideoDatabaseFile.h"
#include "guilib/GUIComponent.h"
#include "guilib/GUIWindowManager.h"
#include "guilib/LocalizeStrings.h"
#include "input/actions/Action.h"
#include "input/actions/ActionIDs.h"
#include "interfaces/AnnouncementManager.h"
#include "messaging/ApplicationMessenger.h"
#include "messaging/helpers/DialogOKHelper.h"
#include "music/tags/MusicInfoTag.h"
#include "playlists/PlayList.h"
#include "settings/AdvancedSettings.h"
#include "settings/SettingsComponent.h"
#include "utils/StringUtils.h"
#include "utils/URIUtils.h"
#include "utils/Variant.h"
#include "utils/log.h"
#include "video/VideoDatabase.h"

using namespace PLAYLIST;
using namespace KODI::MESSAGING;

CPlayListPlayer::CPlayListPlayer(void)
{
  m_PlaylistMusic = new CPlayList(TYPE_MUSIC);
  m_PlaylistVideo = new CPlayList(TYPE_VIDEO);
  m_PlaylistEmpty = new CPlayList;
  m_iCurrentSong = -1;
  m_bPlayedFirstFile = false;
  m_bPlaybackStarted = false;
  m_iFailedSongs = 0;
  m_failedSongsStart = std::chrono::steady_clock::now();
}

CPlayListPlayer::~CPlayListPlayer(void)
{
  Clear();
  delete m_PlaylistMusic;
  delete m_PlaylistVideo;
  delete m_PlaylistEmpty;
}

bool CPlayListPlayer::OnAction(const CAction &action)
{
  if (action.GetID() == ACTION_PREV_ITEM && !IsSingleItemNonRepeatPlaylist())
  {
    PlayPrevious();
    return true;
  }
  else if (action.GetID() == ACTION_NEXT_ITEM && !IsSingleItemNonRepeatPlaylist())
  {
    PlayNext();
    return true;
  }
  else
    return false;
}

bool CPlayListPlayer::OnMessage(CGUIMessage &message)
{
  switch (message.GetMessage())
  {
  case GUI_MSG_NOTIFY_ALL:
    if (message.GetParam1() == GUI_MSG_UPDATE_ITEM && message.GetItem())
    {
      // update the items in our playlist(s) if necessary
      for (Id playlistId : {TYPE_MUSIC, TYPE_VIDEO})
      {
        CPlayList& playlist = GetPlaylist(playlistId);
        CFileItemPtr item = std::static_pointer_cast<CFileItem>(message.GetItem());
        playlist.UpdateItem(item.get());
      }
    }
    break;
  case GUI_MSG_PLAYBACK_STOPPED:
    {
      if (m_iCurrentPlayList != TYPE_NONE && m_bPlaybackStarted)
      {
        CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_STOPPED, 0, 0, m_iCurrentPlayList, m_iCurrentSong);
        CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
        Reset();
        m_iCurrentPlayList = TYPE_NONE;
        return true;
      }
    }
    break;
  case GUI_MSG_PLAYBACK_STARTED:
    {
      m_bPlaybackStarted = true;
    }
    break;
  }

  return false;
}

int CPlayListPlayer::GetNextSong(int offset) const
{
  if (m_iCurrentPlayList == TYPE_NONE)
    return -1;

  const CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
  if (playlist.size() <= 0)
    return -1;

  int song = m_iCurrentSong;

  // party mode
  if (g_partyModeManager.IsEnabled() && GetCurrentPlaylist() == TYPE_MUSIC)
    return song + offset;

  // wrap around in the case of repeating
  if (RepeatedOne(m_iCurrentPlayList))
    return song;

  song += offset;
  if (song >= playlist.size() && Repeated(m_iCurrentPlayList))
    song %= playlist.size();

  return song;
}

int CPlayListPlayer::GetNextSong()
{
  if (m_iCurrentPlayList == TYPE_NONE)
    return -1;
  CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
  if (playlist.size() <= 0)
    return -1;
  int iSong = m_iCurrentSong;

  // party mode
  if (g_partyModeManager.IsEnabled() && GetCurrentPlaylist() == TYPE_MUSIC)
    return iSong + 1;

  // if repeat one, keep playing the current song if its valid
  if (RepeatedOne(m_iCurrentPlayList))
  {
    // otherwise immediately abort playback
    if (m_iCurrentSong >= 0 && m_iCurrentSong < playlist.size() && playlist[m_iCurrentSong]->GetProperty("unplayable").asBoolean())
    {
      CLog::Log(LOGERROR, "Playlist Player: RepeatOne stuck on unplayable item: {}, path [{}]",
                m_iCurrentSong, playlist[m_iCurrentSong]->GetPath());
      CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_STOPPED, 0, 0, m_iCurrentPlayList, m_iCurrentSong);
      CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
      Reset();
      m_iCurrentPlayList = TYPE_NONE;
      return -1;
    }
    return iSong;
  }

  // if we've gone beyond the playlist and repeat all is enabled,
  // then we clear played status and wrap around
  iSong++;
  if (iSong >= playlist.size() && Repeated(m_iCurrentPlayList))
    iSong = 0;

  return iSong;
}

bool CPlayListPlayer::PlayNext(int offset, bool bAutoPlay)
{
  int iSong = GetNextSong(offset);
  const CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);

  if ((iSong < 0) || (iSong >= playlist.size()) || (playlist.GetPlayable() <= 0))
  {
    if(!bAutoPlay)
      CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(559), g_localizeStrings.Get(34201));

    CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_STOPPED, 0, 0, m_iCurrentPlayList, m_iCurrentSong);
    CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
    Reset();
    m_iCurrentPlayList = TYPE_NONE;
    return false;
  }

  return Play(iSong, "", false);
}

bool CPlayListPlayer::PlayPrevious()
{
  if (m_iCurrentPlayList == TYPE_NONE)
    return false;

  const CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
  int iSong = m_iCurrentSong;

  if (!RepeatedOne(m_iCurrentPlayList))
    iSong--;

  if (iSong < 0 && Repeated(m_iCurrentPlayList))
    iSong = playlist.size() - 1;

  if (iSong < 0 || playlist.size() <= 0)
  {
    CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(559), g_localizeStrings.Get(34202));
    return false;
  }

  return Play(iSong, "", false, true);
}

bool CPlayListPlayer::IsSingleItemNonRepeatPlaylist() const
{
  const CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
  return (playlist.size() <= 1 && !RepeatedOne(m_iCurrentPlayList) && !Repeated(m_iCurrentPlayList));
}

bool CPlayListPlayer::Play()
{
  if (m_iCurrentPlayList == TYPE_NONE)
    return false;

  const CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
  if (playlist.size() <= 0)
    return false;

  return Play(0, "");
}

bool CPlayListPlayer::PlaySongId(int songId)
{
  if (m_iCurrentPlayList == TYPE_NONE)
    return false;

  CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
  if (playlist.size() <= 0)
    return Play();

  for (int i = 0; i < playlist.size(); i++)
  {
    if (playlist[i]->HasMusicInfoTag() && playlist[i]->GetMusicInfoTag()->GetDatabaseId() == songId)
      return Play(i, "");
  }
  return Play();
}

bool CPlayListPlayer::Play(const CFileItemPtr& pItem, const std::string& player)
{
  Id playlistId;
  bool isVideo{pItem->IsVideo()};
  bool isAudio{pItem->IsAudio()};

  if (isAudio && !isVideo)
    playlistId = TYPE_MUSIC;
  else if (isVideo && !isAudio)
    playlistId = TYPE_VIDEO;
  else if (pItem->HasProperty("playlist_type_hint"))
  {
    // There are two main cases that can fall here:
    // - If an extension is set on both audio / video extension lists example .strm
    //   see GetFileExtensionProvider() -> GetVideoExtensions() / GetAudioExtensions()
    //   When you play the .strm containing single path, cause that
    //   IsVideo() and IsAudio() methods both return true
    //
    // - When you play a playlist (e.g. .m3u / .strm) containing multiple paths,
    //   and the path played is generic (e.g.without extension) and have no properties
    //   to detect the media type, IsVideo() / IsAudio() both return false
    //
    // for these cases the type is unknown so we rely on the hint
    playlistId = pItem->GetProperty("playlist_type_hint").asInteger32(TYPE_NONE);
  }
  else
  {
    CLog::LogF(LOGWARNING, "ListItem type must be audio or video type. The type can be specified "
                           "by using ListItem::getVideoInfoTag or ListItem::getMusicInfoTag, in "
                           "the case of playlist entries by adding #KODIPROP mimetype value.");
    return false;
  }

  ClearPlaylist(playlistId);
  Reset();
  SetCurrentPlaylist(playlistId);
  Add(playlistId, pItem);

  return Play(0, player);
}

bool CPlayListPlayer::Play(int iSong,
                           const std::string& player,
                           bool bAutoPlay /* = false */,
                           bool bPlayPrevious /* = false */)
{
  if (m_iCurrentPlayList == TYPE_NONE)
    return false;

  CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
  if (playlist.size() <= 0)
    return false;
  if (iSong < 0)
    iSong = 0;
  if (iSong >= playlist.size())
    iSong = playlist.size() - 1;

  // check if the item itself is a playlist, and can be expanded
  // only allow a few levels, this could end up in a loop
  // if they refer to each other in a loop
  for (int i=0; i<5; i++)
  {
    if(!playlist.Expand(iSong))
      break;
  }

  m_iCurrentSong = iSong;
  CFileItemPtr item = playlist[m_iCurrentSong];
  if (item->IsVideoDb() && !item->HasVideoInfoTag())
    *(item->GetVideoInfoTag()) = XFILE::CVideoDatabaseFile::GetVideoTag(CURL(item->GetDynPath()));

  playlist.SetPlayed(true);

  m_bPlaybackStarted = false;

  const auto playAttempt = std::chrono::steady_clock::now();
  bool ret = g_application.PlayFile(*item, player, bAutoPlay);
  if (!ret)
  {
    CLog::Log(LOGERROR, "Playlist Player: skipping unplayable item: {}, path [{}]", m_iCurrentSong,
              CURL::GetRedacted(item->GetDynPath()));
    playlist.SetUnPlayable(m_iCurrentSong);

    // abort on 100 failed CONSECUTIVE songs
    if (!m_iFailedSongs)
      m_failedSongsStart = playAttempt;
    m_iFailedSongs++;
    const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();

    auto now = std::chrono::steady_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_failedSongsStart);

    if ((m_iFailedSongs >= advancedSettings->m_playlistRetries &&
         advancedSettings->m_playlistRetries >= 0) ||
        ((duration.count() >=
          static_cast<unsigned int>(advancedSettings->m_playlistTimeout) * 1000) &&
         advancedSettings->m_playlistTimeout))
    {
      CLog::Log(LOGDEBUG,"Playlist Player: one or more items failed to play... aborting playback");

      // open error dialog
      HELPERS::ShowOKDialogText(CVariant{16026}, CVariant{16027});

      CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_STOPPED, 0, 0, m_iCurrentPlayList, m_iCurrentSong);
      CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
      Reset();
      GetPlaylist(m_iCurrentPlayList).Clear();
      m_iCurrentPlayList = TYPE_NONE;
      m_iFailedSongs = 0;
      m_failedSongsStart = std::chrono::steady_clock::now();
      return false;
    }

    // how many playable items are in the playlist?
    if (playlist.GetPlayable() > 0)
    {
      return bPlayPrevious ? PlayPrevious() : PlayNext();
    }
    // none? then abort playback
    else
    {
      CLog::Log(LOGDEBUG,"Playlist Player: no more playable items... aborting playback");
      CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_STOPPED, 0, 0, m_iCurrentPlayList, m_iCurrentSong);
      CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
      Reset();
      m_iCurrentPlayList = TYPE_NONE;
      return false;
    }
  }

  // reset the start offset of this item
  if (item->GetStartOffset() == STARTOFFSET_RESUME)
    item->SetStartOffset(0);

  //! @todo - move the above failure logic and the below success logic
  //!        to callbacks instead so we don't rely on the return value
  //!        of PlayFile()

  // consecutive error counter so reset if the current item is playing
  m_iFailedSongs = 0;
  m_failedSongsStart = std::chrono::steady_clock::now();
  m_bPlayedFirstFile = true;
  return true;
}

void CPlayListPlayer::SetCurrentSong(int iSong)
{
  if (iSong >= -1 && iSong < GetPlaylist(m_iCurrentPlayList).size())
    m_iCurrentSong = iSong;
}

int CPlayListPlayer::GetCurrentSong() const
{
  return m_iCurrentSong;
}

Id CPlayListPlayer::GetCurrentPlaylist() const
{
  return m_iCurrentPlayList;
}

void CPlayListPlayer::SetCurrentPlaylist(Id playlistId)
{
  if (playlistId == m_iCurrentPlayList)
    return;

  // changing the current playlist while party mode is on
  // disables party mode
  if (g_partyModeManager.IsEnabled())
    g_partyModeManager.Disable();

  m_iCurrentPlayList = playlistId;
  m_bPlayedFirstFile = false;
}

void CPlayListPlayer::ClearPlaylist(Id playlistId)
{
  // clear our applications playlist file
  g_application.m_strPlayListFile.clear();

  CPlayList& playlist = GetPlaylist(playlistId);
  playlist.Clear();

  // its likely that the playlist changed
  CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
  CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
}

CPlayList& CPlayListPlayer::GetPlaylist(Id playlistId)
{
  switch (playlistId)
  {
    case TYPE_MUSIC:
      return *m_PlaylistMusic;
      break;
    case TYPE_VIDEO:
      return *m_PlaylistVideo;
      break;
    default:
      m_PlaylistEmpty->Clear();
      return *m_PlaylistEmpty;
      break;
  }
}

const CPlayList& CPlayListPlayer::GetPlaylist(Id playlistId) const
{
  switch (playlistId)
  {
    case TYPE_MUSIC:
      return *m_PlaylistMusic;
      break;
    case TYPE_VIDEO:
      return *m_PlaylistVideo;
      break;
    default:
      // NOTE: This playlist may not be empty if the caller of the non-const version alters it!
      return *m_PlaylistEmpty;
      break;
  }
}

int CPlayListPlayer::RemoveDVDItems()
{
  int nRemovedM = m_PlaylistMusic->RemoveDVDItems();
  int nRemovedV = m_PlaylistVideo->RemoveDVDItems();

  return nRemovedM + nRemovedV;
}

void CPlayListPlayer::Reset()
{
  m_iCurrentSong = -1;
  m_bPlayedFirstFile = false;
  m_bPlaybackStarted = false;

  // its likely that the playlist changed
  CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
  CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
}

bool CPlayListPlayer::HasPlayedFirstFile() const
{
  return m_bPlayedFirstFile;
}

bool CPlayListPlayer::Repeated(Id playlistId) const
{
  const auto repStatePos = m_repeatState.find(playlistId);
  if (repStatePos != m_repeatState.end())
    return repStatePos->second == RepeatState::ALL;
  return false;
}

bool CPlayListPlayer::RepeatedOne(Id playlistId) const
{
  const auto repStatePos = m_repeatState.find(playlistId);
  if (repStatePos != m_repeatState.end())
    return (repStatePos->second == RepeatState::ONE);
  return false;
}

void CPlayListPlayer::SetShuffle(Id playlistId, bool bYesNo, bool bNotify /* = false */)
{
  if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
    return;

  // disable shuffle in party mode
  if (g_partyModeManager.IsEnabled() && playlistId == TYPE_MUSIC)
    return;

  // do we even need to do anything?
  if (bYesNo != IsShuffled(playlistId))
  {
    // save the order value of the current song so we can use it find its new location later
    int iOrder = -1;
    CPlayList& playlist = GetPlaylist(playlistId);
    if (m_iCurrentSong >= 0 && m_iCurrentSong < playlist.size())
      iOrder = playlist[m_iCurrentSong]->m_iprogramCount;

    // shuffle or unshuffle as necessary
    if (bYesNo)
      playlist.Shuffle();
    else
      playlist.UnShuffle();

    if (bNotify)
    {
      std::string shuffleStr =
          StringUtils::Format("{}: {}", g_localizeStrings.Get(191),
                              g_localizeStrings.Get(bYesNo ? 593 : 591)); // Shuffle: All/Off
      CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(559),  shuffleStr);
    }

    // find the previous order value and fix the current song marker
    if (iOrder >= 0)
    {
      int iIndex = playlist.FindOrder(iOrder);
      if (iIndex >= 0)
        m_iCurrentSong = iIndex;
      // if iIndex < 0, something unexpected happened
      // so dont do anything
    }
  }

  // its likely that the playlist changed
  if (CServiceBroker::GetGUI() != nullptr)
  {
    CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
    CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
  }

  AnnouncePropertyChanged(playlistId, "shuffled", IsShuffled(playlistId));
}

bool CPlayListPlayer::IsShuffled(Id playlistId) const
{
  // even if shuffled, party mode says its not
  if (g_partyModeManager.IsEnabled() && playlistId == TYPE_MUSIC)
    return false;

  if (playlistId == TYPE_MUSIC || playlistId == TYPE_VIDEO)
    return GetPlaylist(playlistId).IsShuffled();

  return false;
}

void CPlayListPlayer::SetRepeat(Id playlistId, RepeatState state, bool bNotify /* = false */)
{
  if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
    return;

  // disable repeat in party mode
  if (g_partyModeManager.IsEnabled() && playlistId == TYPE_MUSIC)
    state = RepeatState::NONE;

  // notify the user if there was a change in the repeat state
  if (m_repeatState[playlistId] != state && bNotify)
  {
    int iLocalizedString;
    if (state == RepeatState::NONE)
      iLocalizedString = 595; // Repeat: Off
    else if (state == RepeatState::ONE)
      iLocalizedString = 596; // Repeat: One
    else
      iLocalizedString = 597; // Repeat: All
    CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(559), g_localizeStrings.Get(iLocalizedString));
  }

  m_repeatState[playlistId] = state;

  CVariant data;
  switch (state)
  {
    case RepeatState::ONE:
      data = "one";
      break;
    case RepeatState::ALL:
      data = "all";
      break;
    default:
      data = "off";
      break;
  }

  // its likely that the playlist changed
  if (CServiceBroker::GetGUI() != nullptr)
  {
    CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
    CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
  }

  AnnouncePropertyChanged(playlistId, "repeat", data);
}

RepeatState CPlayListPlayer::GetRepeat(Id playlistId) const
{
  const auto repStatePos = m_repeatState.find(playlistId);
  if (repStatePos != m_repeatState.end())
    return repStatePos->second;
  return RepeatState::NONE;
}

void CPlayListPlayer::ReShuffle(Id playlistId, int iPosition)
{
  // playlist has not played yet so shuffle the entire list
  // (this only really works for new video playlists)
  if (!GetPlaylist(playlistId).WasPlayed())
  {
    GetPlaylist(playlistId).Shuffle();
  }
  // we're trying to shuffle new items into the currently playing playlist
  // so we shuffle starting at two positions below the current item
  else if (playlistId == m_iCurrentPlayList)
  {
    const auto& components = CServiceBroker::GetAppComponents();
    const auto appPlayer = components.GetComponent<CApplicationPlayer>();
    if ((appPlayer->IsPlayingAudio() && playlistId == TYPE_MUSIC) ||
        (appPlayer->IsPlayingVideo() && playlistId == TYPE_VIDEO))
    {
      GetPlaylist(playlistId).Shuffle(m_iCurrentSong + 2);
    }
  }
  // otherwise, shuffle from the passed position
  // which is the position of the first new item added
  else
  {
    GetPlaylist(playlistId).Shuffle(iPosition);
  }
}

void CPlayListPlayer::Add(Id playlistId, const CPlayList& playlist)
{
  if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
    return;
  CPlayList& list = GetPlaylist(playlistId);
  int iSize = list.size();
  list.Add(playlist);
  if (list.IsShuffled())
    ReShuffle(playlistId, iSize);
}

void CPlayListPlayer::Add(Id playlistId, const CFileItemPtr& pItem)
{
  if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
    return;
  CPlayList& list = GetPlaylist(playlistId);
  int iSize = list.size();
  list.Add(pItem);
  if (list.IsShuffled())
    ReShuffle(playlistId, iSize);
}

void CPlayListPlayer::Add(Id playlistId, const CFileItemList& items)
{
  if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
    return;
  CPlayList& list = GetPlaylist(playlistId);
  int iSize = list.size();
  list.Add(items);
  if (list.IsShuffled())
    ReShuffle(playlistId, iSize);

  // its likely that the playlist changed
  CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
  CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
}

void CPlayListPlayer::Insert(Id playlistId, const CPlayList& playlist, int iIndex)
{
  if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
    return;
  CPlayList& list = GetPlaylist(playlistId);
  int iSize = list.size();
  list.Insert(playlist, iIndex);
  if (list.IsShuffled())
    ReShuffle(playlistId, iSize);
  else if (m_iCurrentPlayList == playlistId && m_iCurrentSong >= iIndex)
    m_iCurrentSong++;
}

void CPlayListPlayer::Insert(Id playlistId, const CFileItemPtr& pItem, int iIndex)
{
  if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
    return;
  CPlayList& list = GetPlaylist(playlistId);
  int iSize = list.size();
  list.Insert(pItem, iIndex);
  if (list.IsShuffled())
    ReShuffle(playlistId, iSize);
  else if (m_iCurrentPlayList == playlistId && m_iCurrentSong >= iIndex)
    m_iCurrentSong++;
}

void CPlayListPlayer::Insert(Id playlistId, const CFileItemList& items, int iIndex)
{
  if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
    return;
  CPlayList& list = GetPlaylist(playlistId);
  int iSize = list.size();
  list.Insert(items, iIndex);
  if (list.IsShuffled())
    ReShuffle(playlistId, iSize);
  else if (m_iCurrentPlayList == playlistId && m_iCurrentSong >= iIndex)
    m_iCurrentSong++;

  // its likely that the playlist changed
  CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
  CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
}

void CPlayListPlayer::Remove(Id playlistId, int iPosition)
{
  if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
    return;
  CPlayList& list = GetPlaylist(playlistId);
  list.Remove(iPosition);
  if (m_iCurrentPlayList == playlistId && m_iCurrentSong >= iPosition)
    m_iCurrentSong--;

  // its likely that the playlist changed
  CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
  CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
}

void CPlayListPlayer::Clear()
{
  if (m_PlaylistMusic)
    m_PlaylistMusic->Clear();
  if (m_PlaylistVideo)
    m_PlaylistVideo->Clear();
  if (m_PlaylistEmpty)
    m_PlaylistEmpty->Clear();
}

void CPlayListPlayer::Swap(Id playlistId, int indexItem1, int indexItem2)
{
  if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
    return;

  CPlayList& list = GetPlaylist(playlistId);
  if (list.Swap(indexItem1, indexItem2) && playlistId == m_iCurrentPlayList)
  {
    if (m_iCurrentSong == indexItem1)
      m_iCurrentSong = indexItem2;
    else if (m_iCurrentSong == indexItem2)
      m_iCurrentSong = indexItem1;
  }

  // its likely that the playlist changed
  CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
  CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
}

void CPlayListPlayer::AnnouncePropertyChanged(Id playlistId,
                                              const std::string& strProperty,
                                              const CVariant& value)
{
  const auto& components = CServiceBroker::GetAppComponents();
  const auto appPlayer = components.GetComponent<CApplicationPlayer>();

  if (strProperty.empty() || value.isNull() ||
      (playlistId == TYPE_VIDEO && !appPlayer->IsPlayingVideo()) ||
      (playlistId == TYPE_MUSIC && !appPlayer->IsPlayingAudio()))
    return;

  CVariant data;
  data["player"]["playerid"] = playlistId;
  data["property"][strProperty] = value;
  CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPropertyChanged",
                                                     data);
}

int PLAYLIST::CPlayListPlayer::GetMessageMask()
{
  return TMSG_MASK_PLAYLISTPLAYER;
}

void PLAYLIST::CPlayListPlayer::OnApplicationMessage(KODI::MESSAGING::ThreadMessage* pMsg)
{
  auto& components = CServiceBroker::GetAppComponents();
  const auto appPlayer = components.GetComponent<CApplicationPlayer>();

  auto wakeScreensaver = []() {
    auto& components = CServiceBroker::GetAppComponents();
    const auto appPower = components.GetComponent<CApplicationPowerHandling>();
    appPower->ResetScreenSaver();
    appPower->WakeUpScreenSaverAndDPMS();
  };

  switch (pMsg->dwMessage)
  {
  case TMSG_PLAYLISTPLAYER_PLAY:
    if (pMsg->param1 != -1)
      Play(pMsg->param1, "");
    else
      Play();
    break;

  case TMSG_PLAYLISTPLAYER_PLAY_SONG_ID:
    if (pMsg->param1 != -1)
    {
      bool *result = (bool*)pMsg->lpVoid;
      *result = PlaySongId(pMsg->param1);
    }
    else
      Play();
    break;

  case TMSG_PLAYLISTPLAYER_NEXT:
    PlayNext();
    break;

  case TMSG_PLAYLISTPLAYER_PREV:
    PlayPrevious();
    break;

  case TMSG_PLAYLISTPLAYER_ADD:
    if (pMsg->lpVoid)
    {
      CFileItemList *list = static_cast<CFileItemList*>(pMsg->lpVoid);

      Add(pMsg->param1, (*list));
      delete list;
    }
    break;

  case TMSG_PLAYLISTPLAYER_INSERT:
    if (pMsg->lpVoid)
    {
      CFileItemList *list = static_cast<CFileItemList*>(pMsg->lpVoid);
      Insert(pMsg->param1, (*list), pMsg->param2);
      delete list;
    }
    break;

  case TMSG_PLAYLISTPLAYER_REMOVE:
    if (pMsg->param1 != -1)
      Remove(pMsg->param1, pMsg->param2);
    break;

  case TMSG_PLAYLISTPLAYER_CLEAR:
    ClearPlaylist(pMsg->param1);
    break;

  case TMSG_PLAYLISTPLAYER_SHUFFLE:
    SetShuffle(pMsg->param1, pMsg->param2 > 0);
    break;

  case TMSG_PLAYLISTPLAYER_REPEAT:
    SetRepeat(pMsg->param1, static_cast<RepeatState>(pMsg->param2));
    break;

  case TMSG_PLAYLISTPLAYER_GET_ITEMS:
    if (pMsg->lpVoid)
    {
      PLAYLIST::CPlayList playlist = GetPlaylist(pMsg->param1);
      CFileItemList *list = static_cast<CFileItemList*>(pMsg->lpVoid);

      for (int i = 0; i < playlist.size(); i++)
        list->Add(std::make_shared<CFileItem>(*playlist[i]));
    }
    break;

  case TMSG_PLAYLISTPLAYER_SWAP:
    if (pMsg->lpVoid)
    {
      auto indexes = static_cast<std::vector<int>*>(pMsg->lpVoid);
      if (indexes->size() == 2)
        Swap(pMsg->param1, indexes->at(0), indexes->at(1));
      delete indexes;
    }
    break;

  case TMSG_MEDIA_PLAY:
  {
    wakeScreensaver();

    // first check if we were called from the PlayFile() function
    if (pMsg->lpVoid && pMsg->param2 == 0)
    {
      // Discard the current playlist, if TMSG_MEDIA_PLAY gets posted with just a single item.
      // Otherwise items may fail to play, when started while a playlist is playing.
      Reset();

      CFileItem *item = static_cast<CFileItem*>(pMsg->lpVoid);
      g_application.PlayFile(*item, "", pMsg->param1 != 0);
      delete item;
      return;
    }

    //g_application.StopPlaying();
    // play file
    if (pMsg->lpVoid)
    {
      CFileItemList *list = static_cast<CFileItemList*>(pMsg->lpVoid);

      if (list->Size() > 0)
      {
        Id playlistId = TYPE_MUSIC;
        for (int i = 0; i < list->Size(); i++)
        {
          if ((*list)[i]->IsVideo())
          {
            playlistId = TYPE_VIDEO;
            break;
          }
        }

        ClearPlaylist(playlistId);
        SetCurrentPlaylist(playlistId);
        if (list->Size() == 1 && !(*list)[0]->IsPlayList())
        {
          CFileItemPtr item = (*list)[0];
          // if the item is a plugin we need to resolve the URL to ensure the infotags are filled.
          if (URIUtils::HasPluginPath(*item) &&
              !XFILE::CPluginDirectory::GetResolvedPluginResult(*item))
          {
            return;
          }
          if (item->IsAudio() || item->IsVideo())
            Play(item, pMsg->strParam);
          else
            g_application.PlayMedia(*item, pMsg->strParam, playlistId);
        }
        else
        {
          // Handle "shuffled" option if present
          if (list->HasProperty("shuffled") && list->GetProperty("shuffled").isBoolean())
            SetShuffle(playlistId, list->GetProperty("shuffled").asBoolean(), false);
          // Handle "repeat" option if present
          if (list->HasProperty("repeat") && list->GetProperty("repeat").isInteger())
            SetRepeat(playlistId, static_cast<RepeatState>(list->GetProperty("repeat").asInteger()),
                      false);

          Add(playlistId, (*list));
          Play(pMsg->param1, pMsg->strParam);
        }
      }

      delete list;
    }
    else if (pMsg->param1 == TYPE_MUSIC || pMsg->param1 == TYPE_VIDEO)
    {
      if (GetCurrentPlaylist() != pMsg->param1)
        SetCurrentPlaylist(pMsg->param1);

      CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_PLAY, pMsg->param2);
    }
  }
  break;

  case TMSG_MEDIA_RESTART:
    g_application.Restart(true);
    break;

  case TMSG_MEDIA_STOP:
  {
    // restore to previous window if needed
    bool stopSlideshow = true;
    bool stopVideo = true;
    bool stopMusic = true;

    Id playlistId = pMsg->param1;
    if (playlistId != TYPE_NONE)
    {
      stopSlideshow = (playlistId == TYPE_PICTURE);
      stopVideo = (playlistId == TYPE_VIDEO);
      stopMusic = (playlistId == TYPE_MUSIC);
    }

    if ((stopSlideshow && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) ||
      (stopVideo && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_VIDEO) ||
      (stopVideo && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_GAME) ||
      (stopMusic && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VISUALISATION))
      CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();

    wakeScreensaver();

    // stop playing file
    if (appPlayer->IsPlaying())
      g_application.StopPlaying();
  }
  break;

  case TMSG_MEDIA_PAUSE:
    if (appPlayer->HasPlayer())
    {
      wakeScreensaver();
      appPlayer->Pause();
    }
    break;

  case TMSG_MEDIA_UNPAUSE:
    if (appPlayer->IsPausedPlayback())
    {
      wakeScreensaver();
      appPlayer->Pause();
    }
    break;

  case TMSG_MEDIA_PAUSE_IF_PLAYING:
    if (appPlayer->IsPlaying() && !appPlayer->IsPaused())
    {
      wakeScreensaver();
      appPlayer->Pause();
    }
    break;

  case TMSG_MEDIA_SEEK_TIME:
  {
    if (appPlayer->IsPlaying() || appPlayer->IsPaused())
      appPlayer->SeekTime(pMsg->param3);

    break;
  }
  default:
    break;
  }
}
