<%pre>

#include <livefeatures.h>
#include <setup.h>
#include <tools.h>
#include <timers.h>
#include <epg_events.h>

using namespace vdrlive;

bool checkEpg(const cChannel *channel) {
// true if this channel must be displayed.
// this is true if:
// - the channel is not a group seperator
// - we also display channels without EPG or if this channel has EPG
  if (channel->GroupSep() ) return false;
  if (LiveSetup().GetShowChannelsWithoutEPG() ) return true;
  LOCK_SCHEDULES_READ;
  const cSchedule *channelSchedule = Schedules->GetSchedule(channel);
  return channelSchedule && (channelSchedule->Events()->First() );
}

enum class tState {
  None,
  Inactive,
  Active,
  Recording
};

struct SchedEntry {
  std::string title;
  std::string short_description;
  std::string description;
  std::string description_trunc;
  std::string start;
  std::string end;
  std::string day;
  std::string epgid;
  std::string label;
  bool truncated;
  tState timer;
  int start_row;
  int row_count;

  static SchedEntry DummyEntry(int start_row, int row_count) {
    SchedEntry en;
    en.start_row = start_row;
    en.row_count = row_count;
    // no title and no start time = dummy event
    en.title = "";
    en.start = "";
    return en;
  }
};

std::vector<std::string> times_names;
std::vector<std::string> channel_groups_names;
</%pre>
<%args>
  int channel = -1;
  unsigned int time_para = 0;
</%args>
<%session scope="global">
  bool logged_in(false);
</%session>
<%request scope="global">
  unsigned int channel_group=0;
  unsigned int time_selected=0;
  cString prev;
  cString next;
</%request>
<%include>page_init.eh</%include>
<%cpp>
  reply.setHeader (tnt::httpheader::cacheControl, "no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate", true);
  if (!logged_in && LiveSetup().UseAuth()) {
    cToSvConcat<0> targetUrl = "/login.html?redirect=";
    targetUrl.appendUrlEscaped(request.getQuery());
    return reply.redirect(targetUrl.data());
  }
  pageTitle = tr("MultiSchedule");

#define MAX_DAYS 3
#define MAX_HOURS 8
#define MINUTES_PER_ROW 5
#define CHARACTERS_PER_ROW 30

  std::vector<std::vector<int> > channel_groups_numbers;
  {
// build the groups of channels to display
    std::string channelGroupsGen = LiveSetup().GetChannelGroupsGen();
    channel_groups_names.clear();
    channel_groups_numbers.clear();
    int lastChannel = LiveSetup().GetLastChannel();
    LOCK_CHANNELS_READ;
    if (lastChannel == 0) lastChannel = Channels->MaxNumber();
    if (channelGroupsGen == "individual") {
      std::string channelGroups = LiveSetup().GetChannelGroups();
      for (cSv channelGroup: cSplit(channelGroups, ';', eSplitDelimBeginEnd::optional)) {
        int cur_group_count = 0;
        channel_groups_names.push_back(std::string() );
        channel_groups_numbers.push_back(std::vector<int>() );
        for (cSv thisChannel: cSplit(channelGroup, ',', eSplitDelimBeginEnd::optional)) {
          if (cur_group_count++ != 0)
            channel_groups_names.back() += " | ";
          else
            channel_groups_names.back() += cToSvConcat(thisChannel, ": ");
          int channel_no = parse_int<int> (thisChannel);
          if (channel_no == 0) {
            esyslog("live: check settings->channel groups: could not convert '%.*s' into a channel number", (int)thisChannel.length(), thisChannel.data() );
            continue;
          }
          const cChannel* Channel = Channels->GetByNumber(channel_no);
          if (!Channel) {
            esyslog("live: check settings->channel groups: could not find channel no %d, '%.*s'.", channel_no, (int)thisChannel.length(), thisChannel.data() );
            continue;
          }
          channel_groups_numbers.back().push_back(Channel->Number() );
          channel_groups_names.back() += std::string(Channel->Name() );
        }
      }
    } else if (channelGroupsGen == "flat") {
      channel_groups_names.push_back(tr("All channels"));
      channel_groups_numbers.push_back(std::vector<int>() );
      for (const cChannel *channel = Channels->First(); channel && (channel->Number() <= lastChannel); channel = Channels->Next(channel)) {
        if (checkEpg(channel)) channel_groups_numbers.back().push_back(channel->Number() );
      }
    } else if (channelGroupsGen == "vdr") {
      // channel groups from vdr
      for (const cChannel *channel = Channels->First(); channel && (channel->Number() <= lastChannel); channel = Channels->Next(channel))
      {
        if (channel == Channels->First() ) {
          channel_groups_numbers.push_back(std::vector<int>() );
          if (channel->GroupSep()) channel_groups_names.push_back(channel->Name() );
          else {
            channel_groups_names.push_back(tr("First (unnamed) group"));
            if (checkEpg(channel)) channel_groups_numbers.back().push_back( channel->Number() );
          }
        } else {
          if (channel->GroupSep()) {
            channel_groups_numbers.push_back( std::vector<int>() );
            channel_groups_names.push_back(channel->Name() );
          }
          else if (checkEpg(channel)) channel_groups_numbers.back().push_back(channel->Number() );
        }
      }
    } else {
// 5 channels in each group. for channelGroupsGen == "default" or channelGroupsGen.empty()
      int i = -1;
      for (const cChannel *channel = Channels->First(); channel && (channel->Number() <= lastChannel); channel = Channels->Next(channel))
      {
        if (!checkEpg(channel)) continue;
        if ( (++i % 5) == 0 ) {
          channel_groups_names.push_back(std::string(cToSvConcat(channel->Number(), ": ")));
          channel_groups_numbers.push_back(std::vector<int>() );
        } else
          channel_groups_names.back() += " | ";
        channel_groups_numbers.back().push_back(channel->Number() );
        channel_groups_names.back() += std::string(channel->Name() );
      }
    }
  }

  if (channel_groups_numbers.size() == 0) channel_groups_numbers.push_back( std::vector<int>() );
  if (channel < 0)
  {
    if (cDevice::CurrentChannel())
    {
      // find group corresponding to current channel
      int curGroup = 0;
      int curChannel = cDevice::CurrentChannel();
      for ( std::vector< std::vector<int> >::iterator grIt = channel_groups_numbers.begin();
            grIt != channel_groups_numbers.end() && channel < 0; ++grIt, ++curGroup )
      {
        for ( std::vector<int>::iterator chIt = (*grIt).begin();
               chIt != (*grIt).end() && channel < 0; ++ chIt )
        {
          if ( *chIt == curChannel ) channel_group = channel = curGroup;
        }
      }
      // if nothing is found, fall back to group 0
      if (channel < 0) channel = 0;
    }
    else
    {
      channel_group = channel;
    }
  }

  if ( channel >= (int)channel_groups_numbers.size() ) channel = 0;
  channel_group = channel;

  std::vector<time_t> times_start;
  {
    // build time list
    times_names.clear();
    times_start.clear();

    // calculate time of midnight (localtime) and convert back to GMT
    time_t now = (time(NULL)/3600)*3600;
    time_t now_local = time(NULL);
    struct tm tm_r;
    if ( localtime_r( &now_local, &tm_r ) == 0 ) {
      std::stringstream builder;
      builder << "cannot represent timestamp " << now_local << " as local time";
      throw std::runtime_error( builder.str() );
    }
    tm_r.tm_hour=0;
    tm_r.tm_min=0;
    tm_r.tm_sec=0;
    time_t midnight = mktime( &tm_r );

    // add four 8h steps per day to the time list
    for (int i=0; i<4*MAX_DAYS ; i++ )
    {
      times_start.push_back( midnight + MAX_HOURS*3600*i );
    }
    std::vector< time_t > offsets;
    std::string string_times = LiveSetup().GetTimes();
    for (cSv part: cSplit(string_times, ';', eSplitDelimBeginEnd::optional))
    {
      time_t hour, min;
      if (cSplit(part, ':', "Additional times, multischedule").getValues(hour, min) != 2)
      {
        esyslog("live: Additional times, multischedule, ERROR parsing time '%.*s'", (int)part.length(), part.data() );
        continue;
      }
      offsets.push_back(hour*60*60 + min*60);
    };
    // add the time of the favourites to the time list
    for (int i=0; i< MAX_DAYS ; i++ )
    {
      std::vector< time_t >::const_iterator offset = offsets.begin();
      for ( ; offset != offsets.end(); ++offset )
      {
        times_start.push_back( midnight + 24*3600*i + *offset );
      }
    }
    // add now
    times_start.push_back( now );
    // sort the times
    std::sort( times_start.begin(), times_start.end() );
    // delete every time which has already passed
    while ( *times_start.begin()< now )
      times_start.erase(times_start.begin() );

    // build the corresponding names
    for ( std::vector< time_t >::const_iterator start = times_start.begin();
        start != times_start.end(); ++start )
    {
      cToSvConcat times_name;
      times_name.appendDateTime(tr("%A, %x"), *start);
      times_name.concat(' ');
      times_name.appendDateTime(tr("%I:%M %p"), *start);
      times_names.push_back(std::string(times_name));
    }
    // the first time is now
    times_names[0]=tr("Now");

    if ( time_para >= times_names.size() )
      time_para = times_names.size()-1;
    time_selected=time_para;
  }

  if (channel_group > 0)
    prev = cString::sprintf("href=\"multischedule.html?time_para=%i&channel=%i\"", time_selected, channel_group - 1);
  if (channel_group < channel_groups_numbers.size() - 1)
    next = cString::sprintf("href=\"multischedule.html?time_para=%i&channel=%i\"", time_selected, channel_group + 1);
  </%cpp>
  <& pageelems.doc_type &>
  <html>
    <head>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>VDR Live - <$ pageTitle $></title>
      <& pageelems.stylesheets &>
      <& pageelems.ajax_js &>
      <& pageelems.create_html_js &>
      <script type="text/javascript"><!--
      function KeyboardInput(ev) {
        if (ev.ctrlKey)
          switch (ev.which) {
          case 37: // left
            <?? *prev ? "window.location." << *prev ?>
            return false;
          case 39: // right
            <?? *next ? "window.location." << *next ?>
            return false;
          }
          switch (ev.key) {
          case '<':
            <?? *prev ? "window.location." << *prev ?>
            return false;
          case '>':
            <?? *next ? "window.location." << *next ?>
            return false;
          }
        return true;
      }
      //--></script>
    </head>
    <body onpagehide="saveScrollPosition('content')" onpageshow="restoreScrollPosition()" onkeydown="return KeyboardInput(event);">
      <& pageelems.logo &>
      <& menu active=("multischedule") component=("multischedule.channel_selection") &>
      <div id="content">
        <div class="spacebar"><# spacer with fade-out effect #></div>
  <%cpp>
    time_t now = time(NULL);
    if (time_para >= times_start.size() ) time_para = times_start.size()-1;
    time_t sched_start = (times_start[ time_para ]/300)*300;
    time_t max_hours = parse_int<time_t>( LiveSetup().GetScheduleDuration() );
    if (max_hours == 0) {
      esyslog("live: could not convert '%s' into a schedule duration", LiveSetup().GetScheduleDuration().c_str());
      max_hours = 8;
    };
    if (max_hours > 48) max_hours = 48;

    time_t sched_end = sched_start + 60 * 60 * max_hours;
    int sched_end_row = ( sched_end - sched_start ) / 60 / MINUTES_PER_ROW;
    std::vector<std::vector<SchedEntry> > table;
    table.reserve(channel_groups_numbers[ channel ].size() );
    std::vector<std::string> channel_names(channel_groups_numbers[ channel ].size() );
    std::vector<tChannelID> channel_IDs(channel_groups_numbers[ channel ].size() );
    if ( channel >= (int)channel_groups_numbers.size() )
      channel = channel_groups_numbers.size()-1;
    for ( unsigned int j = 0; j<channel_groups_numbers[ channel ].size(); j++)
    {
      table.emplace_back();
      int prev_row = -1;
      int chan = channel_groups_numbers[ channel ][ j ];

      LOCK_TIMERS_READ;  // required here for correct lock order
      LOCK_CHANNELS_READ;
      const cChannel* Channel = Channels->GetByNumber(chan);
      if ( ! Channel )
        continue;
      if ( Channel->GroupSep() || !Channel->Name() || !*Channel->Name() )
        continue;
      channel_names[ j ] = Channel->Name();
      channel_IDs[ j ] = Channel->GetChannelID();

      LOCK_SCHEDULES_READ;
      cSchedule const* Schedule = Schedules->GetSchedule( Channel );
      if ( ! Schedule )
      {
        // insert dummy event for the entire channel if no EPG data
        table[ j ].push_back( SchedEntry::DummyEntry(0 , sched_end_row));
        prev_row = sched_end_row;
        continue;
      }
      for (const cEvent *Event = Schedule->Events()->First(); Event; Event = Schedule->Events()->Next(Event) )
      {
        if (Event->EndTime() <= sched_start) continue;
        if (Event->StartTime() >= sched_end) continue;
        if (Event->StartTime() == 0) continue;

        if ( prev_row < 0 && Event->StartTime() > sched_start + MINUTES_PER_ROW )
        {
          // insert dummy event at start
          table[ j ].push_back( SchedEntry() );
          SchedEntry &en=table[ j ].back();
          int event_start_row = (Event->StartTime() - sched_start) / 60 / MINUTES_PER_ROW;
          en.start_row = 0;
          en.row_count = event_start_row;
          if ( en.row_count < 1 )
            en.row_count = 1;
          // no title and no start time = dummy event
          en.title = "";
          en.start = "";
          prev_row = en.start_row + en.row_count;
        }
        table[ j ].push_back( SchedEntry() );
        SchedEntry &en=table[j].back();

        en.title = cSv(Event->Title());
        utf8_sanitize_string(en.title);
        en.short_description = cSv(Event->ShortText());
        utf8_sanitize_string(en.short_description);
        en.description = cSv(Event->Description());
        utf8_sanitize_string(en.description);
        en.start = cToSvDateTime(tr("%I:%M %p"), Event->StartTime());
        en.end   = cToSvDateTime(tr("%I:%M %p"), Event->EndTime());
        en.day = cToSvDateTime(tr("%A, %b %d %Y"), Event->StartTime());
        en.epgid = EpgEvents::EncodeDomId(Channel->GetChannelID(), Event->EventID());
        en.label = cToSvConcat(en.start).append(tr(" - ")).append(en.title);
        const cTimer* timer = TimerManager::GetTimer(Event, Channel, Timers);
        if (!timer)
          en.timer = tState::None;
        else if (timer->Recording())
          en.timer = tState::Recording;
        else if (timer->Flags() & tfActive)
          en.timer = tState::Active;
        else
          en.timer = tState::Inactive;

        en.start_row = prev_row > 0 ? prev_row : 0;
        int end_time = Schedule->Events()->Next(Event) ?
          Schedule->Events()->Next(Event)->StartTime() :
          Event->EndTime();
        if (end_time > sched_end)
          end_time = sched_end;
        int next_event_start_row = (end_time - sched_start) / 60 / MINUTES_PER_ROW;
        en.row_count = next_event_start_row - en.start_row;
        if ( en.row_count < 1 )
          en.row_count = 1;
        prev_row = en.start_row + en.row_count;

        // truncate description if too long
        en.truncated=false;
        en.description_trunc=StringWordTruncate( en.description,
                             CHARACTERS_PER_ROW*(en.row_count-2),
                             en.truncated );
    }
    if ( table[ j ].begin() == table[ j ].end() )
    {
      // no entries... create a single dummy entry
      table[ j ].push_back( SchedEntry() );
      SchedEntry &en=table[ j ].back();
      en.start_row = 0;
      en.row_count = sched_end_row;
      // no title and no start time = dummy event
      en.title = "";
      en.start = "";
    }
  }
</%cpp>
<table class="multischedule" cellspacing="0" cellpadding="0">
  <tr class="header">
    <td class="time"><div class="boxheader"><div class="caption"><$ tr("Time") $></div></div></td>
    <td class="spacer"> &nbsp; </td>
<# // first line of table:channel names #>
% for ( unsigned int channel = 0; channel< channel_names.size() ; channel++) {
    <td class="channel"><div class="boxheader"><div class="caption">
%   if (channel_IDs[channel].Valid()) {
      <a href="schedule.html?channelid=<$ channel_IDs[channel] $>">
%   }
      <$ channel_names[channel] $>
%   if (channel_IDs[channel].Valid()) {
      </a>
%   }
    </div><div class="caption tools">
%   if (cDevice::PrimaryDevice() && cDevice::PrimaryDevice()->HasDecoder() ) {
<& pageelems.ajax_action_href action="switch_channel" tip=(tr("Switch to this channel")) param=(channel_IDs[channel]) image="zap.png" className="icon" alt="" &>
%   }
<& pageelems.hls_channel channelId=(channel_IDs[channel]) className="icon" &>
<& pageelems.m3u_playlist_channel channelId=(channel_IDs[channel]) className="icon" &>
    </div></div></td>
    <td class="spacer"> &nbsp; </td>
% }
  </tr>
<%cpp>
  bool odd=true;
  std::vector<std::vector<SchedEntry>::iterator> cur_event;
  cur_event.reserve(table.size() );
  for (size_t i = 0; i < table.size(); i++) cur_event.push_back(table[i].begin());
  for (int row = 0 ; row < sched_end_row; row++ ) {
    int minutes= ( (sched_start +  row * 60 * MINUTES_PER_ROW ) % 3600 ) / 60;
    std::string row_class;
    if (minutes < MINUTES_PER_ROW) odd = !odd; // full hour, swap odd/even
    if ( (sched_start +  row * 60 * MINUTES_PER_ROW) <= now &&
         (sched_start +  (row+1) * 60 * MINUTES_PER_ROW) > now ) row_class +=" current_row ";
    row_class += odd ? " odd " : " even ";
</%cpp>
    <tr>
<# second + line, first colomn: time #>
      <td class="<? row == sched_end_row - 1 ? "bottomrow" ?> time leftcol rightcol <$ row_class $>">
%   if (minutes < MINUTES_PER_ROW) {
        <$ cToSvDateTime(tr("%I:%M %p"), sched_start +  row * 60 * MINUTES_PER_ROW)  $>
%   } else {
        &nbsp;
%   }
      </td>
<# second + line, second + colomns: epg entries #>
%   for (unsigned int channel = 0; channel < channel_names.size() ; channel++) {
      <td class = "spacer"> &nbsp; </td>
<%cpp>
      if (cur_event[channel] == table[channel].end()
       || cur_event[channel]->start_row != row )
      {
        // no new event in this channel, skip it
        SchedEntry &en=*(--table[channel].end());
        if (row == 0 || en.start_row + en.row_count == row) {
</%cpp>
      <td class="bottomrow event topaligned leftcol rightcol" rowspan="<$ sched_end_row - row $>">
            <div class=" content1 " >
            <div class= "start withmargin"><? row == 0 ? "No EPG available" ?><? row != 0 ? "EPG end" ?></div>
          </div>
      </td>
<%cpp>
        }
        continue;
      }

      SchedEntry &en=*cur_event[channel];
      if (en.title.empty() && en.start.empty() )
      {
      // empty dummy event
</%cpp>
      <td class="<? row + en.row_count == sched_end_row ? "bottomrow" ?> event topaligned leftcol rightcol" rowspan="<$ en.row_count $>">
          <div class=" content1 " >
          <div class= "start withmargin"><? en.row_count == sched_end_row ? "EPG empty" ?></div>
          </div>
        </td>
<%cpp>
        ++cur_event[channel];
        continue;
      }
      // output an event cell
</%cpp>
      <td class="<? row + en.row_count == sched_end_row ? "bottomrow" ?> event topaligned leftcol rightcol <$ en.timer == tState::None ? "" : (en.timer == tState::Inactive ? "has_inactive_timer" : "has_timer") $>" rowspan="<$ en.row_count $>">
      <div class=" content1 " >
      <div class=" tools1 " >
        <& pageelems.event_timer epgid=(en.epgid) className="icon" &>
%     if (LiveFeatures<features::epgsearch>().Recent() ) {
        <a href="searchresults.html?searchplain=<$$ cToSvUrlEscaped(en.title) $>"><img class="icon" src="<$ LiveSetup().GetThemedLink("img", "search.png") $>" alt="" <& tooltip.hint text=(tr("Search for repeats")) label=(en.label) className="click-action" &> /></a>
%     } else {
        <img class="icon <? en.timer == tState::Recording ? "fading" ?>" src="<$ LiveSetup().GetThemedLink("img", en.timer == tState::Recording || en.timer == tState::Active ? "record_timer_active.png" : "record_timer_inactive.png") $>">
%     } // display of IMDb icon to be determined in subroutine, otherwise inconsistent
        <& pageelems.imdb_info_href title=(en.title) className="icon" label=(en.label) &>
        </div><div class= "start withmargin"><$ en.start $></div>
        <div class="title withmargin"><$ en.title $></div>
%     if (en.row_count > 2 && !en.short_description.empty() ) {
          <div class="short withmargin"><$ en.short_description.empty() ? "&nbsp;" : en.short_description $></div>
%     } if (en.row_count > 3 && ! en.description_trunc.empty() ) {
        <div class="description withmargin"><$en.description_trunc$>
%       if (en.truncated) {
          ...</div><div class="more withmargin"><a <& tooltip.hint text=(tr("Click to view details")) &><& tooltip.display domId=en.epgid &>><$ tr("more") $></a>
%       }
        </div>
%     }
        </div></div>
      </td>
<%cpp>
      // move to next event for this channel
      ++cur_event[channel];
    }
</%cpp>
    </tr>
% }
      </table>
      <div class="spacebar"><# spacer with fade-out effect #></div>
    </div>
  </body>
</html>
<%include>page_exit.eh</%include>

<%def channel_selection>
<form action="multischedule.html" method="get" id="channels">
  <span>
  <label for="channel"><$ tr("Channel") $>:&nbsp;<span class="bold"></span></label>
  <select name="channel" id="channel" onchange="document.forms.channels.submit()" >
% for (unsigned int i = 0; i < channel_groups_names.size(); ++i) {
    <option value="<$ i $>"<?? i == channel_group ? " selected=\"selected\"" ?>>
    <$ channel_groups_names[i] $></option>
% }
  </select>
  <label for="time_para"><$ tr("Time") $>:&nbsp;<span class="bold"></span></label>
  <select name="time_para" id="time_para" onchange="document.forms.channels.submit()" >
% for (unsigned int i = 0; i < times_names.size(); ++i) {
    <option value="<$ i $>"<?? i == time_selected ? " selected=\"selected\"" ?>>
    <$ times_names[i] $></option>
% }
  </select>
  </span>
</form>
<span class="sep">|</span>
<?? *prev ? "<a " << *prev << ">"?>«&nbsp;<$ tr("Prev. entry") $><?? *prev ? "</a>" ?>
<span class="sep">|</span>
<?? *next ? "<a " << *next << ">"?><$ tr("Next entry") $>&nbsp;»<?? *next ? "</a>" ?>
</%def>
