/* 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 <string>

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

#include "Device.h"
#include "Directory.h"
#include "File.h"
#include "InStreamFile.h"
#include "Mgrs.h"
#include "Module.h"
#include "NameFile.h"
#include "Output.h"
#include "Protocol.h"
#include "ProtocolFile.h"
#include "SerCfgFile.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
 */
Output::Output(const std::string &name, Mgrs &mgrs,
               const Directory &dirBase):
  Module(name, mgrs, dirBase),
  m_fileInStream(dirBase.getFile("instream"), mgrs.m_streamMgr),
  m_fileProtocol(dirBase.getFile("protocol")),
  m_fileDevice(dirBase.getFile("device")),
  m_fileSerCfg(dirBase.getFile("ser_cfg")),
  m_pDevice(NULL)
{
  // set up
  m_fileInStream.setStreamRecv(this);
  readProto();
  openDevice();
}

/// virtual destructor
Output::~Output()
{
  // clean up
  closeDevice();
  m_fileInStream.setStreamRecv(NULL);
  m_mgrs.m_callMgr.cancelTimeCall(this);
}

/// check for update of configuration
void Output::updateConfig()
{
  // input stream name file was modified -> re-get input stream
  if (m_fileInStream.checkModified())
    m_fileInStream.update();

  // protocol file was modified -> re-read protocol
  if (m_fileProtocol.checkModified())
    readProto();

  // device file or serial settings file was modified -> re-open device
  if (m_fileDevice.checkModified() || m_fileSerCfg.checkModified())
    openDevice();
}

/**
 * @brief set current frame
 * @param[in] stream stream name
 * @param[in] pFrame current frame (NULL for none)
 */
void Output::setFrame(const std::string &stream, stBlinkenFrame *pFrame)
{
  outputFrame();
  (void)stream; // unused
  (void)pFrame; // unused
}

/// callback when requested time reached
void Output::timeCall()
{
  // device is not open -> try to (re-)open it
  if (!m_pDevice) {
    openDevice();
    return;
  }

  // buffer not empty -> try to output rest of data
  if (!m_buffer.empty()) {
    outputBufferedData();
    return;
  }

  // (re-)output current frame
  outputFrame();
}

/// (re-)read protocol
void Output::readProto()
{
  // update protocol from file
  m_fileProtocol.update();

  // re-output current frame using new protocol
  outputFrame();
}

/// open device
void Output::openDevice()
{
  closeDevice();

  // get new device name and new serial config
  m_fileDevice.update();
  m_fileSerCfg.update();

  // open device
  if (m_fileDevice.m_valid)
    m_pDevice = new Device(m_fileDevice.m_obj.m_str);

  // serial config available -> configure serial port
  if (m_pDevice && m_fileSerCfg.m_valid) {
    if (!m_pDevice->setSerCfg(m_fileSerCfg.m_obj))
      closeDevice(); // error -> close device
  }

  // output current frame to device
  outputFrame();
}

/// close device
void Output::closeDevice()
{
  // close device
  if (m_pDevice) {
    delete m_pDevice;
    m_pDevice = NULL;
  }

  // reset internal status
  m_buffer.clear();
  m_dropped = false;

  updateTimeCallback();
}

/// output current frame to device
void Output::outputFrame()
{
  stBlinkenFrame *pFrame;
  char buf[65536];
  int len;
  std::string data;

  // no protocol or no device -> leave
  if (!m_fileProtocol.m_valid || !m_pDevice)
    return;

  // get current frame from input stream
  pFrame = m_fileInStream.getCurFrame();

  // convert frame to protocol data
  if (pFrame)
    len = BlinkenFrameToNetwork(pFrame, m_fileProtocol.m_obj.m_proto,
                                buf, sizeof(buf));
  else
    len = BlinkenProtoMakePacket(m_fileProtocol.m_obj.m_proto,
                                 BlinkenPacketStreamEnd, buf, sizeof(buf));
  if (len < 0)
    len = 0;
  data.assign(buf, len);

  outputFrameData(data);

  updateTimeCallback();
}

/**
 * @brief output frame data to device
 * @param[in] data data of one frame to output to device
 */
void Output::outputFrameData(const std::string &data)
{
  // add data to buffer (if current buffer contains less than 10 frames)
  if (m_buffer.size() < data.size() * 10) {
    m_buffer += data;
    m_dropped = false; // last frame was not dropped
  } else {
    m_dropped = true; // remember that last frame was dropped
  }

  outputBufferedData();
}

/// write data in output buffer to device
void Output::outputBufferedData()
{
  // no device -> forget buffered data, leave
  if (!m_pDevice) {
    m_buffer.clear();
    return;
  }

  // write (at least some) data to device
  std::string::size_type len;
  if (!m_pDevice->write(m_buffer, len)) {
    closeDevice(); // error -> close device
    return;
  }

  // remove written data from buffer
  m_buffer = m_buffer.substr(len);

  updateTimeCallback();
}

/// update time callback request
void Output::updateTimeCallback()
{
  /* no device -> call in 1s for re-opening device
     buffer empty, last frame not dropped -> call in 1s for re-outputting
     buffer not empty -> call in 50ms for outputting more data
     last frame dropped -> call in 50ms for frame update */
  Time delay(1);
  if (m_pDevice && (!m_buffer.empty() || m_dropped))
    delay.fromMs(50);
  m_mgrs.m_callMgr.requestTimeCall(this, Time::now() + delay);
}

} // namespace Blinker

