/* 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 "Loveletter.h"
#include "LoveletterMovie.h"
#include "Mgrs.h"
#include "Module.h"
#include "NameFile.h"
#include "OutStreamFile.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
 */
Loveletter::Loveletter(const std::string &name, Mgrs &mgrs,
               const Directory &dirBase):
  Module(name, mgrs, dirBase),
  m_fileSound(dirBase.getFile("sound")),
  m_fileOutStream(dirBase.getFile("outstream"), mgrs.m_streamMgr),
  m_fileHaltStream(dirBase.getFile("haltstream"), mgrs.m_streamMgr),
  m_movieTracker(*this, dirBase.getSubdir("movies")),
  m_curValid(false),
  m_pCurMovie(NULL),
  m_curFrame(0),
  m_curChange(false),
  m_halted(false),
  m_pOpConn(NULL),
  m_sendPlay(false)
{
  // load movies
  m_fileHaltStream.setStreamRecv(this);
  m_movieTracker.init();
  checkCurChanged();

  // open operator connection interface
  m_mgrs.m_opMgr.open(m_name, this);
}

/// virtual destructor
Loveletter::~Loveletter()
{
  // close operator connection interface
  m_mgrs.m_opMgr.close(m_name);
  closeOpConn();

  // cancel time callback request
  m_mgrs.m_callMgr.cancelTimeCall(this);

  // free all movies
  m_movieTracker.clear();
  m_fileHaltStream.setStreamRecv(NULL);
}

/// check for update of configuration
void Loveletter::updateConfig()
{
  // sound name file was modified -> re-read sound name
  if (m_fileSound.checkModified())
    m_fileSound.update();

  // 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();

  // movie update
  m_movieTracker.updateConfig();
  checkCurChanged();
}

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

  // halt stream came to life -> halt playing
  if (pFrame && !m_halted) {
    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);
  }

  // halt stream ended -> continue playing
  else if (!pFrame && m_halted) {
    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);
    }
  }

  (void)stream; // unused
}

/// callback when requested time reached
void Loveletter::timeCall()
{
  // send sound play request on operator connection
  if (m_sendPlay) {
    m_sendPlay = false;
    if (m_pOpConn && m_fileSound.m_valid)
      m_pOpConn->sendPlay(m_fileSound.m_obj.m_str);
  }

  // leave if halted
  if (m_halted)
    return;

  // leave if time is not yet ready to 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;

  // process new current frame
  procFrame();
}

/**
 * @brief check if accepting new operator connection is possible
 * @param[in] name operator interface name
 * @return if accepting new connection is possible
 */
bool Loveletter::acceptNewOpConn(const std::string &name)
{
  // accept new connection if none active
  return !m_pOpConn;
  (void)name; // unused
}

/**
 * @brief new operator connection
 * @param[in] name operator interface name
 * @param[in] pConn operator connection object
 *
 * The new connection may not yet be used for sending inside this callback.
 */
void Loveletter::newOpConn(const std::string &name, OpConn *pConn)
{
  closeOpConn(); // close old connection (to be on the safe side)
  m_pOpConn = pConn; // remember new connection
  m_sendPlay = true; // schedule sending play request
  m_mgrs.m_callMgr.requestTimeCall(this, Time::now());
  (void)name; // unused
}

/**
 * @brief key command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] key key that was pressed
 */
void Loveletter::opConnRecvKey(OpConn *pConn, char key)
{
  switch (key) {
    // begin new movie number
    case '*':
      m_movieNumber.clear();
      break;
    // add digit to movie number
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      m_movieNumber += key;
      break;
    // exit / play movie
    case '#':
      if (m_movieNumber.empty())
        closeOpConn();
      else
        startPlaying();
      break;
  }
  (void)pConn; // unused
}

/**
 * @brief play command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] sound name of sound to play
 */
void Loveletter::opConnRecvPlay(OpConn *pConn, const std::string &sound)
{
  // this events does not make sense in this direction, ignore it
  (void)pConn; // unused
  (void)sound; // unused
}

/**
 * @brief operator connection is closed
 * @param[in] pConn operator connection object
 *
 * The connection may not be used for sending any more in this callback.
 */
void Loveletter::opConnClose(OpConn *pConn)
{
  m_pOpConn = NULL;
  stopPlaying();
  (void)pConn; // unused
}

/// check if current movie changed and react
void Loveletter::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_remainTime = Time::zero;
    m_nextTime = Time::now();
    procFrame();

  } // if (m_curChange)
}

/// process current frame
void Loveletter::procFrame()
{
  // a movie is selected and this is loaded
  if (m_pCurMovie && m_pCurMovie->m_pMovie) {
    int frameCnt = BlinkenMovieGetFrameCnt(m_pCurMovie->m_pMovie);
    // movie not empty
    if (frameCnt > 0) {
      // movie finsihed -> restart
      if (m_curFrame >= frameCnt)
        m_curFrame = 0;
      // valid
      m_curValid = true;
    }
    // movie empty -> not valid
    else
      m_curValid = false;
  }
  // no movie selected or selected one not loaded -> not valid
  else
    m_curValid = false;

  // 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_pCurMovie->m_pMovie, m_curFrame);
    m_remainTime.fromMs(BlinkenFrameGetDuration(pFrame));
    m_nextTime += 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 Loveletter::sendFrame()
{
  // frame available -> send it
  if (m_curValid) {
    stBlinkenFrame *pFrame =
      BlinkenMovieGetFrame(m_pCurMovie->m_pMovie, m_curFrame);
    m_fileOutStream.setFrame(pFrame);
  }
  // no frame available -> send this information
  else
    m_fileOutStream.setFrame(NULL);
}

/// close current operator connection
void Loveletter::closeOpConn()
{
  if (m_pOpConn) {
    m_pOpConn->close();
    m_pOpConn = NULL;
  }

  stopPlaying();
};

/// start playing movie (indictaed by m_movieNumber)
void Loveletter::startPlaying()
{
  // find movie and make it current
  MovieMap::const_iterator itMovie = m_movieMap.find(m_movieNumber);
  if (itMovie == m_movieMap.end())
    m_pCurMovie = NULL; // not found -> no movie
  else
    m_pCurMovie = itMovie->second;

  // start playing movie
  m_curChange = true;
  checkCurChanged();

  // begin new movie number
  m_movieNumber.clear();
}

/// stop playing movie
void Loveletter::stopPlaying()
{
  // forget movie number
  m_movieNumber.clear();

  // stop playing movie
  m_pCurMovie = NULL;
  m_curChange = true;
  checkCurChanged();
}

} // namespace Blinker

