/*
 *  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 "PVRTimerRuleMatcher.h"

#include "XBDateTime.h"
#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" // PVR_CHANNEL_INVALID_UID
#include "pvr/epg/EpgInfoTag.h"
#include "pvr/timers/PVRTimerInfoTag.h"
#include "utils/RegExp.h"

using namespace PVR;

CPVRTimerRuleMatcher::CPVRTimerRuleMatcher(const std::shared_ptr<CPVRTimerInfoTag>& timerRule, const CDateTime& start)
: m_timerRule(timerRule),
  m_start(CPVRTimerInfoTag::ConvertUTCToLocalTime(start))
{
}

CPVRTimerRuleMatcher::~CPVRTimerRuleMatcher() = default;

std::shared_ptr<CPVRChannel> CPVRTimerRuleMatcher::GetChannel() const
{
  if (m_timerRule->GetTimerType()->SupportsChannels())
    return m_timerRule->Channel();

  return {};
}

CDateTime CPVRTimerRuleMatcher::GetNextTimerStart() const
{
  if (!m_timerRule->GetTimerType()->SupportsStartTime())
    return CDateTime(); // invalid datetime

  const CDateTime startDateLocal = m_timerRule->GetTimerType()->SupportsFirstDay()
                                   ? m_timerRule->FirstDayAsLocalTime()
                                   : m_start;
  const CDateTime startTimeLocal = m_timerRule->StartAsLocalTime();
  CDateTime nextStart(startDateLocal.GetYear(), startDateLocal.GetMonth(), startDateLocal.GetDay(),
                      startTimeLocal.GetHour(), startTimeLocal.GetMinute(), 0);

  const CDateTimeSpan oneDay(1, 0, 0, 0);
  while (nextStart < m_start)
  {
    nextStart += oneDay;
  }

  if (m_timerRule->GetTimerType()->SupportsWeekdays() &&
      m_timerRule->m_iWeekdays != PVR_WEEKDAY_ALLDAYS)
  {
    bool bMatch = false;
    while (!bMatch)
    {
      int startWeekday = nextStart.GetDayOfWeek();
      if (startWeekday == 0)
        startWeekday = 7;

      bMatch = ((1 << (startWeekday - 1)) & m_timerRule->m_iWeekdays);
      if (!bMatch)
        nextStart += oneDay;
    }
  }

  return nextStart.GetAsUTCDateTime();
}

bool CPVRTimerRuleMatcher::Matches(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
{
  return epgTag &&
         CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->EndAsUTC()) > m_start &&
         MatchSeriesLink(epgTag) &&
         MatchChannel(epgTag) &&
         MatchStart(epgTag) &&
         MatchEnd(epgTag) &&
         MatchDayOfWeek(epgTag) &&
         MatchSearchText(epgTag);
}

bool CPVRTimerRuleMatcher::MatchSeriesLink(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
{
  if (m_timerRule->GetTimerType()->RequiresEpgSeriesLinkOnCreate())
    return epgTag->SeriesLink() == m_timerRule->SeriesLink();
  else
    return true;
}

bool CPVRTimerRuleMatcher::MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
{
  if (m_timerRule->GetTimerType()->SupportsAnyChannel() &&
      m_timerRule->m_iClientChannelUid == PVR_CHANNEL_INVALID_UID)
    return true; // matches any channel

  if (m_timerRule->GetTimerType()->SupportsChannels())
    return m_timerRule->m_iClientChannelUid != PVR_CHANNEL_INVALID_UID &&
           epgTag->ClientID() == m_timerRule->m_iClientId &&
           epgTag->UniqueChannelID() == m_timerRule->m_iClientChannelUid;
  else
    return true;
}

bool CPVRTimerRuleMatcher::MatchStart(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
{
  if (m_timerRule->GetTimerType()->SupportsFirstDay())
  {
    // only year, month and day do matter here...
    const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC());
    const CDateTime startEpg(startEpgLocal.GetYear(),
                             startEpgLocal.GetMonth(),
                             startEpgLocal.GetDay(), 0, 0, 0);
    const CDateTime firstDayLocal = m_timerRule->FirstDayAsLocalTime();
    const CDateTime startTimer(firstDayLocal.GetYear(),
                               firstDayLocal.GetMonth(),
                               firstDayLocal.GetDay(), 0, 0, 0);
    if (startEpg < startTimer)
      return false;
  }

  if (m_timerRule->GetTimerType()->SupportsStartAnyTime() &&
      m_timerRule->m_bStartAnyTime)
    return true; // matches any start time

  if (m_timerRule->GetTimerType()->SupportsStartTime())
  {
    // only hours and minutes do matter here...
    const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC());
    const CDateTime startEpg(2000, 1, 1, startEpgLocal.GetHour(), startEpgLocal.GetMinute(), 0);
    const CDateTime startTimerLocal = m_timerRule->StartAsLocalTime();
    const CDateTime startTimer(2000, 1, 1, startTimerLocal.GetHour(), startTimerLocal.GetMinute(), 0);
    return startEpg >= startTimer;
  }
  else
    return true;
}

bool CPVRTimerRuleMatcher::MatchEnd(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
{
  if (m_timerRule->GetTimerType()->SupportsEndAnyTime() &&
      m_timerRule->m_bEndAnyTime)
    return true; // matches any end time

  if (m_timerRule->GetTimerType()->SupportsEndTime())
  {
    // only hours and minutes do matter here...
    const CDateTime endEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->EndAsUTC());
    const CDateTime endEpg(2000, 1, 1, endEpgLocal.GetHour(), endEpgLocal.GetMinute(), 0);
    const CDateTime endTimerLocal = m_timerRule->EndAsLocalTime();
    const CDateTime endTimer(2000, 1, 1, endTimerLocal.GetHour(), endTimerLocal.GetMinute(), 0);
    return endEpg <= endTimer;
  }
  else
    return true;
}

bool CPVRTimerRuleMatcher::MatchDayOfWeek(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
{
  if (m_timerRule->GetTimerType()->SupportsWeekdays())
  {
    if (m_timerRule->m_iWeekdays != PVR_WEEKDAY_ALLDAYS)
    {
      const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC());
      int startWeekday = startEpgLocal.GetDayOfWeek();
      if (startWeekday == 0)
        startWeekday = 7;

      return ((1 << (startWeekday - 1)) & m_timerRule->m_iWeekdays);
    }
  }
  return true;
}

bool CPVRTimerRuleMatcher::MatchSearchText(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
{
  if (m_timerRule->GetTimerType()->SupportsEpgFulltextMatch() &&
      m_timerRule->m_bFullTextEpgSearch)
  {
    if (!m_textSearch)
    {
      m_textSearch.reset(new CRegExp(true /* case insensitive */));
      m_textSearch->RegComp(m_timerRule->m_strEpgSearchString);
    }
    return m_textSearch->RegFind(epgTag->Title()) >= 0 ||
           m_textSearch->RegFind(epgTag->EpisodeName()) >= 0 ||
           m_textSearch->RegFind(epgTag->PlotOutline()) >= 0 ||
           m_textSearch->RegFind(epgTag->Plot()) >= 0;
  }
  else if (m_timerRule->GetTimerType()->SupportsEpgTitleMatch())
  {
    if (!m_textSearch)
    {
      m_textSearch.reset(new CRegExp(true /* case insensitive */));
      m_textSearch->RegComp(m_timerRule->m_strEpgSearchString);
    }
    return m_textSearch->RegFind(epgTag->Title()) >= 0;
  }
  else
    return true;
}
