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

#include "ServiceBroker.h"
#include "guilib/LocalizeStrings.h"
#include "pvr/PVRCachedImages.h"
#include "pvr/PVRManager.h"
#include "pvr/addons/PVRClient.h"
#include "pvr/epg/EpgChannelData.h"
#include "pvr/epg/EpgDatabase.h"
#include "pvr/epg/EpgInfoTag.h"
#include "settings/AdvancedSettings.h"
#include "settings/Settings.h"
#include "settings/SettingsComponent.h"
#include "utils/StringUtils.h"
#include "utils/log.h"

#include <memory>
#include <mutex>
#include <utility>
#include <vector>

using namespace PVR;

CPVREpg::CPVREpg(int iEpgID,
                 const std::string& strName,
                 const std::string& strScraperName,
                 const std::shared_ptr<CPVREpgDatabase>& database)
  : m_iEpgID(iEpgID),
    m_strName(strName),
    m_strScraperName(strScraperName),
    m_channelData(new CPVREpgChannelData),
    m_tags(m_iEpgID, m_channelData, database)
{
}

CPVREpg::CPVREpg(int iEpgID,
                 const std::string& strName,
                 const std::string& strScraperName,
                 const std::shared_ptr<CPVREpgChannelData>& channelData,
                 const std::shared_ptr<CPVREpgDatabase>& database)
  : m_bChanged(true),
    m_iEpgID(iEpgID),
    m_strName(strName),
    m_strScraperName(strScraperName),
    m_channelData(channelData),
    m_tags(m_iEpgID, m_channelData, database)
{
}

CPVREpg::~CPVREpg()
{
  Clear();
}

void CPVREpg::ForceUpdate()
{
  m_bUpdatePending = true;
  m_events.Publish(PVREvent::EpgUpdatePending);
}

void CPVREpg::Clear()
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  m_tags.Clear();
}

void CPVREpg::Cleanup(int iPastDays)
{
  const CDateTime cleanupTime = CDateTime::GetUTCDateTime() - CDateTimeSpan(iPastDays, 0, 0, 0);
  Cleanup(cleanupTime);
}

void CPVREpg::Cleanup(const CDateTime& time)
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  m_tags.Cleanup(time);
}

std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagNow() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_tags.GetActiveTag();
}

std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagNext() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_tags.GetNextStartingTag();
}

std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagPrevious() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_tags.GetLastEndedTag();
}

bool CPVREpg::CheckPlayingEvent()
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  if (m_tags.UpdateActiveTag())
  {
    m_events.Publish(PVREvent::EpgActiveItem);
    return true;
  }
  return false;
}

std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagByBroadcastId(unsigned int iUniqueBroadcastId) const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_tags.GetTag(iUniqueBroadcastId);
}

std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagByDatabaseId(int iDatabaseId) const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_tags.GetTagByDatabaseID(iDatabaseId);
}

std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagBetween(const CDateTime& beginTime, const CDateTime& endTime, bool bUpdateFromClient /* = false */)
{
  std::shared_ptr<CPVREpgInfoTag> tag;

  std::unique_lock<CCriticalSection> lock(m_critSection);
  tag = m_tags.GetTagBetween(beginTime, endTime);

  if (!tag && bUpdateFromClient)
  {
    // not found locally; try to fetch from client
    time_t b;
    beginTime.GetAsTime(b);
    time_t e;
    endTime.GetAsTime(e);

    const std::shared_ptr<CPVREpg> tmpEpg = std::make_shared<CPVREpg>(
        m_iEpgID, m_strName, m_strScraperName, m_channelData, std::shared_ptr<CPVREpgDatabase>());
    if (tmpEpg->UpdateFromScraper(b, e, true))
      tag = tmpEpg->GetTagBetween(beginTime, endTime, false);

    if (tag)
      m_tags.UpdateEntry(tag);
  }

  return tag;
}

std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpg::GetTimeline(
    const CDateTime& timelineStart,
    const CDateTime& timelineEnd,
    const CDateTime& minEventEnd,
    const CDateTime& maxEventStart) const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_tags.GetTimeline(timelineStart, timelineEnd, minEventEnd, maxEventStart);
}

bool CPVREpg::UpdateEntries(const CPVREpg& epg)
{
  std::unique_lock<CCriticalSection> lock(m_critSection);

  /* copy over tags */
  m_tags.UpdateEntries(epg.m_tags);

  /* update the last scan time of this table */
  m_lastScanTime = CDateTime::GetUTCDateTime();
  m_bUpdateLastScanTime = true;

  m_events.Publish(PVREvent::Epg);
  return true;
}

namespace
{

bool IsTagExpired(const std::shared_ptr<CPVREpgInfoTag>& tag)
{
  // Respect epg linger time.
  const int iPastDays = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
      CSettings::SETTING_EPG_PAST_DAYSTODISPLAY);
  const CDateTime cleanupTime(CDateTime::GetUTCDateTime() - CDateTimeSpan(iPastDays, 0, 0, 0));

  return tag->EndAsUTC() < cleanupTime;
}

} // unnamed namespace

bool CPVREpg::UpdateEntry(const EPG_TAG* data, int iClientId)
{
  if (!data)
    return false;

  const std::shared_ptr<CPVREpgInfoTag> tag =
      std::make_shared<CPVREpgInfoTag>(*data, iClientId, m_channelData, m_iEpgID);

  return !IsTagExpired(tag) && m_tags.UpdateEntry(tag);
}

bool CPVREpg::UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE newState)
{
  bool bRet = true;
  bool bNotify = true;

  if (newState == EPG_EVENT_CREATED || newState == EPG_EVENT_UPDATED)
  {
    std::unique_lock<CCriticalSection> lock(m_critSection);
    bRet = !IsTagExpired(tag) && m_tags.UpdateEntry(tag);
  }
  else if (newState == EPG_EVENT_DELETED)
  {
    std::unique_lock<CCriticalSection> lock(m_critSection);
    const std::shared_ptr<CPVREpgInfoTag> existingTag = m_tags.GetTag(tag->UniqueBroadcastID());
    if (!existingTag)
    {
      bRet = false;
    }
    else
    {
      if (IsTagExpired(existingTag))
      {
        m_tags.DeleteEntry(existingTag);
      }
      else
      {
        bNotify = false;
      }
    }
  }
  else
  {
    CLog::LogF(LOGERROR, "Unknown epg event state value: {}", newState);
    bRet = false;
  }

  if (bRet && bNotify)
    m_events.Publish(PVREvent::EpgItemUpdate);

  return bRet;
}

bool CPVREpg::Update(time_t start,
                     time_t end,
                     int iUpdateTime,
                     int iPastDays,
                     const std::shared_ptr<CPVREpgDatabase>& database,
                     bool bForceUpdate /* = false */)
{
  bool bUpdate = false;
  std::shared_ptr<CPVREpg> tmpEpg;

  {
    std::unique_lock<CCriticalSection> lock(m_critSection);

    if (!m_lastScanTime.IsValid())
    {
      database->GetLastEpgScanTime(m_iEpgID, &m_lastScanTime);

      if (!m_lastScanTime.IsValid())
      {
        m_lastScanTime.SetFromUTCDateTime(time_t(0));
        m_bUpdateLastScanTime = true;
      }
    }

    // enforce advanced settings update interval override for channels with no EPG data
    if (m_tags.IsEmpty() && m_channelData->ChannelId() > 0) //! @todo why the channelid check?
      iUpdateTime = CServiceBroker::GetSettingsComponent()
                        ->GetAdvancedSettings()
                        ->m_iEpgUpdateEmptyTagsInterval;

    if (bForceUpdate)
    {
      bUpdate = true;
    }
    else
    {
      // check if we have to update
      time_t iNow = 0;
      CDateTime::GetUTCDateTime().GetAsTime(iNow);

      time_t iLastUpdate = 0;
      m_lastScanTime.GetAsTime(iLastUpdate);

      bUpdate = (iNow > iLastUpdate + iUpdateTime);
    }

    if (bUpdate)
    {
      tmpEpg = std::make_shared<CPVREpg>(m_iEpgID, m_strName, m_strScraperName, m_channelData,
                                         std::shared_ptr<CPVREpgDatabase>());
    }
  }

  // remove obsolete tags
  Cleanup(iPastDays);

  bool bGrabSuccess = true;

  if (bUpdate)
  {
    bGrabSuccess = tmpEpg->UpdateFromScraper(start, end, bForceUpdate) && UpdateEntries(*tmpEpg);

    if (!bGrabSuccess)
      CLog::LogF(LOGERROR, "Failed to update table '{}'", Name());
  }

  m_bUpdatePending = false;
  return bGrabSuccess;
}

std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpg::GetTags() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_tags.GetAllTags();
}

bool CPVREpg::QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database)
{
  // Note: It is guaranteed that both this EPG instance and database instance are already
  //       locked when this method gets called! No additional locking is needed here!

  if (!database)
  {
    CLog::LogF(LOGERROR, "No EPG database");
    return false;
  }

  if (m_iEpgID <= 0 || m_bChanged)
  {
    const int iId = database->Persist(*this, m_iEpgID > 0);
    if (iId > 0 && m_iEpgID != iId)
    {
      m_iEpgID = iId;
      m_tags.SetEpgID(iId);
    }
  }

  if (m_tags.NeedsSave())
    m_tags.QueuePersistQuery();

  if (m_bUpdateLastScanTime)
    database->QueuePersistLastEpgScanTimeQuery(m_iEpgID, m_lastScanTime);

  m_bChanged = false;
  m_bUpdateLastScanTime = false;

  return true;
}

bool CPVREpg::QueueDeleteQueries(const std::shared_ptr<CPVREpgDatabase>& database)
{
  if (!database)
  {
    CLog::LogF(LOGERROR, "No EPG database");
    return false;
  }

  std::unique_lock<CCriticalSection> lock(m_critSection);

  // delete own epg db entry
  database->QueueDeleteEpgQuery(*this);

  // delete last scan time db entry for this epg
  database->QueueDeleteLastEpgScanTimeQuery(*this);

  // delete all tags for this epg from db
  m_tags.QueueDelete();

  Clear();

  return true;
}

std::pair<CDateTime, CDateTime> CPVREpg::GetFirstAndLastUncommitedEPGDate() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_tags.GetFirstAndLastUncommitedEPGDate();
}

bool CPVREpg::UpdateFromScraper(time_t start, time_t end, bool bForceUpdate)
{
  if (m_strScraperName.empty())
  {
    CLog::LogF(LOGERROR, "No EPG scraper defined for table '{}'", m_strName);
  }
  else if (m_strScraperName == "client")
  {
    if (!CServiceBroker::GetPVRManager().EpgsCreated())
      return false;

    if (!m_channelData->IsEPGEnabled() || m_channelData->IsHidden())
    {
      // ignore. not interested in any updates.
      return true;
    }

    const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
    if (client)
    {
      if (!client->GetClientCapabilities().SupportsEPG())
      {
        CLog::LogF(LOGERROR, "The backend for channel '{}' on client '{}' does not support EPGs",
                   m_channelData->ChannelName(), m_channelData->ClientId());
      }
      else if (!bForceUpdate && client->GetClientCapabilities().SupportsAsyncEPGTransfer())
      {
        // nothing to do. client will provide epg updates asynchronously
        return true;
      }
      else
      {
        CLog::LogFC(LOGDEBUG, LOGEPG, "Updating EPG for channel '{}' from client '{}'",
                    m_channelData->ChannelName(), m_channelData->ClientId());
        return (client->GetEPGForChannel(m_channelData->UniqueClientChannelId(), this, start, end) == PVR_ERROR_NO_ERROR);
      }
    }
    else
    {
      CLog::LogF(LOGERROR, "Client '{}' not found, can't update", m_channelData->ClientId());
    }
  }
  else // other non-empty scraper name...
  {
    CLog::LogF(LOGERROR, "Loading the EPG via scraper is not yet implemented!");
    //! @todo Add Support for Web EPG Scrapers here
  }

  return false;
}

const std::string& CPVREpg::ConvertGenreIdToString(int iID, int iSubID)
{
  unsigned int iLabelId = 19499;
  switch (iID)
  {
    case EPG_EVENT_CONTENTMASK_MOVIEDRAMA:
      iLabelId = (iSubID <= 8) ? 19500 + iSubID : 19500;
      break;
    case EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS:
      iLabelId = (iSubID <= 4) ? 19516 + iSubID : 19516;
      break;
    case EPG_EVENT_CONTENTMASK_SHOW:
      iLabelId = (iSubID <= 3) ? 19532 + iSubID : 19532;
      break;
    case EPG_EVENT_CONTENTMASK_SPORTS:
      iLabelId = (iSubID <= 11) ? 19548 + iSubID : 19548;
      break;
    case EPG_EVENT_CONTENTMASK_CHILDRENYOUTH:
      iLabelId = (iSubID <= 5) ? 19564 + iSubID : 19564;
      break;
    case EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE:
      iLabelId = (iSubID <= 6) ? 19580 + iSubID : 19580;
      break;
    case EPG_EVENT_CONTENTMASK_ARTSCULTURE:
      iLabelId = (iSubID <= 11) ? 19596 + iSubID : 19596;
      break;
    case EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS:
      iLabelId = (iSubID <= 3) ? 19612 + iSubID : 19612;
      break;
    case EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE:
      iLabelId = (iSubID <= 7) ? 19628 + iSubID : 19628;
      break;
    case EPG_EVENT_CONTENTMASK_LEISUREHOBBIES:
      iLabelId = (iSubID <= 7) ? 19644 + iSubID : 19644;
      break;
    case EPG_EVENT_CONTENTMASK_SPECIAL:
      iLabelId = (iSubID <= 3) ? 19660 + iSubID : 19660;
      break;
    case EPG_EVENT_CONTENTMASK_USERDEFINED:
      iLabelId = (iSubID <= 8) ? 19676 + iSubID : 19676;
      break;
    default:
      break;
  }

  return g_localizeStrings.Get(iLabelId);
}

std::shared_ptr<CPVREpgChannelData> CPVREpg::GetChannelData() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_channelData;
}

void CPVREpg::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  m_channelData = data;
  m_tags.SetChannelData(data);
}

int CPVREpg::ChannelID() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_channelData->ChannelId();
}

const std::string& CPVREpg::ScraperName() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_strScraperName;
}

const std::string& CPVREpg::Name() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_strName;
}

int CPVREpg::EpgID() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_iEpgID;
}

bool CPVREpg::UpdatePending() const
{
  return m_bUpdatePending;
}

bool CPVREpg::NeedsSave() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  return m_bChanged || m_bUpdateLastScanTime || m_tags.NeedsSave();
}

bool CPVREpg::IsValid() const
{
  std::unique_lock<CCriticalSection> lock(m_critSection);
  if (ScraperName() == "client")
    return m_channelData->ClientId() != -1 && m_channelData->UniqueClientChannelId() != PVR_CHANNEL_INVALID_UID;

  return true;
}

void CPVREpg::RemovedFromContainer()
{
  m_events.Publish(PVREvent::EpgDeleted);
}

int CPVREpg::CleanupCachedImages(const std::shared_ptr<CPVREpgDatabase>& database)
{
  const std::vector<std::string> urlsToCheck = database->GetAllIconPaths(EpgID());
  const std::string owner = StringUtils::Format(CPVREpgInfoTag::IMAGE_OWNER_PATTERN, EpgID());

  return CPVRCachedImages::Cleanup({{owner, ""}}, urlsToCheck);
}
