/* Blinker
   Copyright 2011-2019 Stefan Schuermans <stefan@blinkenarea.org>
   Copyleft GNU public license - http://www.gnu.org/copyleft/gpl.html
   a blinkenarea.org project */

#include <list>
#include <string>

#include <BlinkenLib/BlinkenFrame.h>
#include <BlinkenLib/BlinkenMovie.h>

#include "Directory.h"
#include "File.h"
#include "InStreamFile.h"
#include "ListTracker.h"
#include "ListTracker_impl.h"
#include "Mgrs.h"
#include "Module.h"
#include "OutStreamFile.h"
#include "Player.h"
#include "PlayerMovie.h"
#include "StreamRecv.h"
#include "Time.h"
#include "TimeCallee.h"

namespace Blinker {

/**
 * @brief constructor
 * @param[in] name module name
 * @param[in] mgrs managers
 * @param[in] dirBase base directory
 */
Player::Player(const std::string &name, Mgrs &mgrs,
               const Directory &dirBase):
  Module(name, mgrs, dirBase),
  m_fileOutStream(dirBase.getFile("outstream"), mgrs.m_streamMgr),
  m_fileHaltStream(dirBase.getFile("haltstream"), mgrs.m_streamMgr),
  m_fileInSync(dirBase.getFile("insync"), mgrs.m_syncMgr),
  m_playlistTracker(*this, dirBase.getSubdir("playlist")),
  m_curValid(false),
  m_unknownMovie(false),
  m_curEntry(m_playlistTracker.m_list.begin()),
  m_curFrame(0),
  m_curFrameStart(0),
  m_curFrameEnd(0),
  m_curChange(false),
  m_halted(false)
{
  m_maxDeviation.fromMs(30);

  // init and load playlist
  m_fileHaltStream.setStreamRecv(this);
  m_fileInSync.setSyncRecv(this);
  m_playlistTracker.init();
  checkCurChanged();
}

/// virtual destructor
Player::~Player()
{
  // cancel time callback request
  m_mgrs.m_callMgr.cancelTimeCall(this);

  // free all movies, detach from stream sources
  m_playlistTracker.clear();
  m_fileHaltStream.setStreamRecv(NULL);
  m_fileInSync.setSyncRecv(NULL);
}

/// check for update of configuration
void Player::updateConfig()
{
  // output stream name file was modified -> re-get output stream
  if (m_fileOutStream.checkModified()) {
    m_fileOutStream.update();
    sendFrame();
  }

  // halt stream name file was modified -> re-get halt stream
  if (m_fileHaltStream.checkModified())
    m_fileHaltStream.update();

  // input sync stream name file was modified -> re-get input sync stream
  if (m_fileInSync.checkModified())
    m_fileInSync.update();

  // playlist update
  m_playlistTracker.updateConfig();
  checkCurChanged();
}

/**
 * @brief set current frame
 * @param[in] stream stream name
 * @param[in] pFrame current frame (NULL for none)
 */
void Player::setFrame(const std::string &stream, stBlinkenFrame *pFrame)
{
  // this is coming from the halt stream, which will halt the player
  // whenever a frame is available on this halt stream

  // halt stream active -> halt player
  if (pFrame)
    halt();

  // halt stream inactive -> continue playing
  else
    cont();

  (void)stream; // unused
}

/**
 * @brief send synchronization information
 * @param[in] sync sync stream name
 * @param[in] pInfo synchronization information
 */
void Player::sendInfo(const std::string &sync, Info &info)
{
  bool change = false;

  // halt player if pause flag is set
  if (info.pause)
    halt();

  // seek to other movie if not correct one
  if (m_curEntry == m_playlistTracker.m_list.end() ||
      !checkName(m_curEntry->m_name, info.name)) {
    // movie found -> seek to it
    seekToMovie(info.name);
    change = true;
  }

  // seek to requested position in movie
  if (m_curValid) {
    if (seekToPos(info.pos))
      change = true;
  }

  // send frame and update timed callback if something changed
  if (change) {
    sendFrame();
    if (m_curValid && !m_halted) {
      m_nextTime = Time::now() + m_remainTime; // remainTime changed -> update
      m_mgrs.m_callMgr.requestTimeCall(this, m_nextTime);
    }
  }

  // continue playing if pause flag is not set
  if (!info.pause)
    cont();

  (void)sync; // unused
}

/// callback when requested time reached
void Player::timeCall()
{
  // leave if halted
  if (m_halted)
    return;

  // leave if time is not yet ready for next frame
  if (Time::now() < m_nextTime) {
    // request call at time for next frame
    m_mgrs.m_callMgr.requestTimeCall(this, m_nextTime);
    return;
  }

  // go to next frame
  ++m_curFrame;
  m_curFrameStart = m_curFrameEnd;

  // process new current frame
  procFrame();
}

/// halt player
void Player::halt()
{
  // player already halted -> leave
  if (m_halted)
    return;

  m_halted = true;

  if (m_curValid) {
    // store remaining frame time
    m_remainTime = m_nextTime - Time::now();
    if (m_remainTime < Time::zero)
      m_remainTime = Time::zero;
  }

  // cancel time call
  m_mgrs.m_callMgr.cancelTimeCall(this);
}

/// continue playing
void Player::cont()
{
  // player not halted -> leave
  if (!m_halted)
    return;

  m_halted = false;

  if (m_curValid) {
    // determine time for next frame
    m_nextTime = Time::now() + m_remainTime;
    // schedule time call
    m_mgrs.m_callMgr.requestTimeCall(this, m_nextTime);
  }
}

/// check if current movie changed and react
void Player::checkCurChanged()
{
  // current movie changed
  if (m_curChange) {
    m_curChange = false;

    // go to begin of new current movie and start playing now
    m_curFrame = 0;
    m_curFrameStart = 0;
    m_curFrameEnd = 0;
    m_remainTime = Time::zero;
    m_nextTime = Time::now();
    procFrame();

  } // if (m_curChange)
}

/// process current frame
void Player::procFrame()
{
  // movie finished -> next movie
  //   use while loops to handle empty movies / empty playlist
  m_curValid = true;
  bool wrapped = false;
  while (true) {
    // playlist finished -> re-start from beginning
    while (m_curEntry == m_playlistTracker.m_list.end()) {
      if (!m_unknownMovie) // do not go to first movie if "playing" unkn. movie
        m_curEntry = m_playlistTracker.m_list.begin();
      m_curFrame = 0;
      m_curFrameStart = 0;
      m_curFrameEnd = 0;
      // detect empty playlist or playlist with only empty movies
      //   also leave here in case of "playing" unknown movie
      if (wrapped) {
        m_curValid = false;
        break;
      }
      wrapped = true;
    }
    if (!m_curValid)
      break;
    // movie not yet finished -> done
    if (m_curEntry->m_pObj->m_pMovie &&
        m_curFrame < BlinkenMovieGetFrameCnt(m_curEntry->m_pObj->m_pMovie))
      break;
    // movie finished -> next movie
    ++m_curEntry;
    m_curFrame = 0;
    m_curFrame = 0;
    m_curFrameStart = 0;
  } // while (true)

  // send new frame to stream
  sendFrame();

  // if a frame is there
  if (m_curValid) {
    // get frame time and calculate absolute time for next frame
    stBlinkenFrame *pFrame =
      BlinkenMovieGetFrame(m_curEntry->m_pObj->m_pMovie, m_curFrame);
    m_remainTime.fromMs(BlinkenFrameGetDuration(pFrame));
    m_nextTime += m_remainTime;
    m_curFrameEnd = m_curFrameStart + m_remainTime;
    // request call at time for next frame
    if (!m_halted)
      m_mgrs.m_callMgr.requestTimeCall(this, m_nextTime);
  }
}

/// send current frame to output stream
void Player::sendFrame()
{
  // frame available -> send it
  if (m_curValid) {
    stBlinkenFrame *pFrame =
      BlinkenMovieGetFrame(m_curEntry->m_pObj->m_pMovie, m_curFrame);
    m_fileOutStream.setFrame(pFrame);
  }
  // no frame available -> send this information
  else
    m_fileOutStream.setFrame(NULL);
}

/**
 * @brief seek to movie by name
 * @param[in] name name of movie to seek to (name cmp according to SyncRecv)
 */
void Player::seekToMovie(const std::string &name)
{
  // find movie by name
  PlaylistIt it = m_playlistTracker.m_list.begin();
  while (it != m_playlistTracker.m_list.end()) {
    if (checkName(it->m_name, name))
      break;
    ++it;
  }

  // movie not found -> invalidate current movie, "play unkown movie"
  if (it == m_playlistTracker.m_list.end()) {
    m_curValid = false;
    m_unknownMovie = true;
    m_curEntry = m_playlistTracker.m_list.end();
    return;
  }

  // seek to movie
  m_curValid = false;
  m_unknownMovie = false;
  m_curEntry = it;
  m_curFrame = 0;
  m_remainTime = Time::zero;
  if (m_curEntry->m_pObj->m_pMovie) {
    stBlinkenFrame *pFrame =
      BlinkenMovieGetFrame(m_curEntry->m_pObj->m_pMovie, m_curFrame);
    if (pFrame) {
      m_remainTime.fromMs(BlinkenFrameGetDuration(pFrame));
      m_curValid = true;
    }
  }
  m_curFrameStart = Time::zero;
  m_curFrameEnd = m_remainTime;
  m_nextTime = Time::now() + m_remainTime;
}

/**
 * @brief seek to position in current movie
 * @param[in] tgtPos target position in current movie to seek to
 * @return true if movie position has been changed, false otherwise
 */
bool Player::seekToPos(const Time &tgtPos)
{
  bool change = false;

  // get remaining frame time and current position in movie
  if (!m_halted) { // remaining frame time always up-to-date in halted mode
    m_remainTime = m_nextTime - Time::now();
    if (m_remainTime < Time::zero)
      m_remainTime = Time::zero;
  }
  Time pos = m_curFrameEnd - m_remainTime;

  // seek to earlier position in movie if needed
  if (pos > tgtPos + m_maxDeviation) {
    Time goback = pos - tgtPos;
    seekBackwards(goback);
    change = true;
  } // if (pos > tgtPos + m_maxDeviation)

  // seek to later position in movie if needed
  if (pos + m_maxDeviation < tgtPos) {
    Time go = tgtPos - pos;
    seekForwards(go);
    change = true;
  } // if (pos + m_maxDeviation < tgtPos)

  return change;
}

/**
 * @brief seek backwards in current movie
 * @param[in] goback time to seek backwards in current movie
 */
void Player::seekBackwards(Time goback)
{
  // time of current frame
  Time frame = m_curFrameEnd - m_curFrameStart;

  // time the current frame has already been shown
  Time shown = frame - m_remainTime;

  while (goback > Time::zero) {

    // correct by just showing current frame some time longer
    if (goback <= shown) {
      m_remainTime += goback;
      break;
    }

    // go back to begin of current frame
    goback -= shown;
    m_remainTime = frame;

    // go back to end of previous frame
    if (m_curFrame <= 0)
      break;
    m_curFrame--;
    m_curFrameEnd = m_curFrameStart;
    frame = Time::zero;
    stBlinkenFrame *pFrame =
      BlinkenMovieGetFrame(m_curEntry->m_pObj->m_pMovie, m_curFrame);
    if (pFrame)
      frame.fromMs(BlinkenFrameGetDuration(pFrame));
    m_curFrameStart = m_curFrameEnd - frame;
    m_remainTime = Time::zero;
    shown = frame;

  } // while (goback > Time::zero)
}

/**
 * @brief seek forwards in current movie
 * @param[in] go time to seek forwards in current movie
 */
void Player::seekForwards(Time go)
{
  // frame count of current movie
  int frameCnt = BlinkenMovieGetFrameCnt(m_curEntry->m_pObj->m_pMovie);

  // time of current frame
  Time frame = m_curFrameEnd - m_curFrameStart;

  while (go > Time::zero) {

    // correct by just showing current frame some time shorter
    if (go <= m_remainTime) {
      m_remainTime -= go;
      break;
    }

    // go to end of current frame
    go -= m_remainTime;
    m_remainTime = Time::zero;

    // go to begin of next frame
    //   end of movie -> // stay at last frame (go to begin of last frame)
    if (m_curFrame + 1 < frameCnt) {
      m_curFrame++;
      m_curFrameStart = m_curFrameEnd;
    }
    frame = Time::zero;
    stBlinkenFrame *pFrame =
      BlinkenMovieGetFrame(m_curEntry->m_pObj->m_pMovie, m_curFrame);
    if (pFrame)
      frame.fromMs(BlinkenFrameGetDuration(pFrame));
    m_curFrameEnd = m_curFrameStart + frame;
    m_remainTime = frame;

  } // while (go > Time::zero)
}

} // namespace Blinker

