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

#ifndef BLINKER_PHONE_IMPL_H
#define BLINKER_PHONE_IMPL_H

#include <map>
#include <sstream>
#include <string>
#include <vector>

#include "Directory.h"
#include "File.h"
#include "IoCallee.h"
#include "ListTracker.h"
#include "ListTracker_impl.h"
#include "Mgrs.h"
#include "Module.h"
#include "OpConn.h"
#include "OpConnIf.h"
#include "OpMgr.h"
#include "Phone.h"
#include "PhoneExtension.h"
#include "PhoneExtension_impl.h"
#include "SettingFile.h"
#include "StringParser.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
 */
template<typename ADDR, typename SOCK>
Phone<ADDR, SOCK>::Phone(const std::string &name, Mgrs &mgrs,
                         const Directory &dirBase):
  Module(name, mgrs, dirBase),
  m_fileBind(dirBase.getFile("bind")),
  m_fileServer(dirBase.getFile("server")),
  m_extListTracker(*this, dirBase.getSubdir("extensions")),
  m_pSock(NULL)
{
  // read server address
  readServer();

  // create and bind socket
  createSock();

  // load extensions
  m_extListTracker.init();
}

/// virtual destructor
template<typename ADDR, typename SOCK>
Phone<ADDR, SOCK>::~Phone()
{
  // unload extensions
  m_extListTracker.clear();

  // close all connections
  while (!m_connLineMap.empty()) {
    ConnLineMap::iterator it = m_connLineMap.begin();
    it->first->close();
    sendHangup(it->second);
    m_connLineMap.erase(it);
  }

  // destroy socket
  destroySock();

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

/// check for update of configuration
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::updateConfig()
{
  // bind address file was modified -> re-create socket
  if (m_fileBind.checkModified()) {
    destroySock();
    createSock();
  }

  // server address file was modified -> re-read server address
  if (m_fileServer.checkModified())
    readServer();

  // extensions update
  m_extListTracker.updateConfig();
}

/// callback when requested time reached
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::timeCall()
{
  Time now = Time::now();

  // server timeout -> re-register
  if (m_timeRegister <= now)
    sendRegister();

  // send heartbeat
  if (m_timeHeartbeat <= now)
    sendHeartbeat();
}

/**
 * @brief callback when I/O object is readable
 * @param[in] io I/O object that is readable
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::ioReadCall(Io *io)
{
  // reception on socket
  if (io == m_pSock)
    receiveFromSock();
}

/**
 * @brief callback when I/O object is writable
 * @param[in] io I/O object that is writable
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::ioWriteCall(Io *io)
{
  (void)io; // unused
}

/**
 * @brief key command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] key key that was pressed
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::opConnRecvKey(OpConn *pConn, char key)
{
  // the key command does not make sense in this direction, ignore it
  (void)pConn; // unused
  (void)key; // unused
}

/**
 * @brief play command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] sound name of sound to play
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::opConnRecvPlay(OpConn *pConn, const std::string &sound)
{
  // get phone line of connection
  unsigned int line;
  if (!conn2line(pConn, line))
    return;

  // send play message
  sendPlay(line, sound);
}

/**
 * @brief operator connection is closed
 * @param[in] pConn operator connection object
 *
 * The connection may not be used for sending any more in this callback.
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::opConnClose(OpConn *pConn)
{
  // get phone line of connection
  unsigned int line;
  if (!conn2line(pConn, line))
    return;

  // send hangup message
  sendHangup(line);

  // remove connection from maps
  m_connLineMap.erase(pConn);
  m_lineConnMap.erase(line);

  // remove command buffer for this line
  m_cmdBufMap.erase(line);
}

/// (re-)read server address
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::readServer()
{
  m_fileServer.update();

  // register with new server
  sendRegister();
}

/// create socket and bind it
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::createSock()
{
  // create socket
  if (!m_pSock) {
    m_pSock = new SOCK();
    if (!m_pSock)
      return;
  }

  // get bind address from bind address setting file
  m_fileBind.update();
  if (!m_fileBind.m_valid) {
    delete m_pSock;
    m_pSock = NULL;
    return;
  }

  // bind socket
  if (!m_pSock->bind(m_fileBind.m_obj)) {
    delete m_pSock;
    m_pSock = NULL;
    return;
  }

  // request callback on recepetion
  m_mgrs.m_callMgr.requestIoReadCall(this, m_pSock);

  // register with server
  sendRegister();
}

/// destroy socket
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::destroySock()
{
  // cancel callback request
  m_mgrs.m_callMgr.cancelIoReadCall(this, m_pSock);

  // destroy socket
  if (m_pSock) {
    delete m_pSock;
    m_pSock = NULL;
  }
}

/// register with server
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::sendRegister()
{
  ADDR addr;
  if (m_pSock)
    m_pSock->getAddr(addr);
  std::stringstream strm;
  strm << "register:" << addr.getPort();
  sendNow(0, strm.str());

  // set time for next register message and next heartbeat
  Time now = Time::now();
  m_timeRegister = now + m_serverTimeout;
  m_timeHeartbeat = now + m_heartbeatInterval;
  updateTimeCallback();
}

/// send heartbeat to server
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::sendHeartbeat()
{
  sendNow(0, "heartbeat");

  // set time for next heartbeat
  m_timeHeartbeat = Time::now() + m_heartbeatInterval;
  updateTimeCallback();
}

/**
 * @brief send play message
 * @param[in] line number of line to request play on
 * @param[in] sound name of sound to request
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::sendPlay(unsigned int line, const std::string &sound)
{
  std::stringstream strm;
  strm << "playbackground:" << sound;
  sendBuf(line, strm.str());
}

/**
 * @brief send hangup message
 * @param[in] line number of line to hangup
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::sendHangup(unsigned int line)
{
  sendNow(line, "hangup:18"); // nobody knows what the 18 meanas
}

/**
 * @brief send accept message
 * @param[in] line number of line accept on
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::sendAccept(unsigned int line)
{
  sendNow(line, "accept");
}

/**
 * @brief send message buffered to server
 * @param[in] line number of line to hangup
 * @param[in] msg message to send
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::sendBuf(unsigned int line, const std::string &msg)
{
  CmdBufMap::iterator itCmdBuf = m_cmdBufMap.find(line);
  // line not yet connected -> buffer command
  if (itCmdBuf != m_cmdBufMap.end())
    // buffer command
    itCmdBuf->second.push_back(msg);
  // line is connected -> send now
  else
    sendNow(line, msg);
}

/**
 * @brief send message to server now
 * @param[in] line number of line to hangup
 * @param[in] msg message to send
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::sendNow(unsigned int line, const std::string &msg)
{
  std::stringstream strm;
  strm << line << ":" << msg;
  sendSock(strm.str());
}

/**
 * @brief send message to socket
 * @param[in] msg message to send
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::sendSock(const std::string &msg)
{
  // check that there is a socket and a server address
  if (!m_pSock || !m_fileServer.m_valid)
    return;

  // send message
  m_pSock->send(msg, m_fileServer.m_obj);
}

/// receive data from socket
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::receiveFromSock()
{
  std::string msg;
  ADDR addr;

  // make sure socket exists
  if (!m_pSock)
    return;

  // receive (leave if no reception)
  if (!m_pSock->recv(msg, addr))
    return;

  // check that packet came from server address (but ignore port)
  if (!m_fileServer.m_valid)
    return; // no server configured
  addr.setPort(m_fileServer.m_obj.getPort());
  if (addr != m_fileServer.m_obj)
    return; // mismatch

  // reset server timeout
  m_timeRegister = Time::now() + m_serverTimeout;
  updateTimeCallback();

  // process message
  serverMsg(msg);
}

/**
 * @brief process message from server
 * @param[in] msg message from server
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::serverMsg(const std::string &msg)
{
  // get line number and command
  StringParser parser(msg);
  unsigned int line;
  std::string cmd;
  if (!parser.uintNo(line) || !parser.fixChr(':') ||
      !parser.untilDelim(":", false, cmd))
    return;
  if (line < 1) // we are only interested in real lines here
    return;

  // incoming call
  if (cmd == "setup") {
    std::string caller, extension;
    if (!parser.fixChr(':') || !parser.untilDelim(":", true, caller) ||
        !parser.fixChr(':') || !parser.untilDelim(":", false, extension))
      return;
    incomingCall(line, extension);
  }

  // call connected
  else if (cmd == "connected") {
    connected(line);
  }

  // hangup
  else if (cmd == "onhook") {
    hangup(line);
  }

  // DTMF symbol (i.e. key pressed)
  else if (cmd == "dtmf") {
    char key;
    if (!parser.fixChr(':') || !parser.oneChrOf("0123456789*#", key))
      return;
    keyPressed(line, key);
  }
}

/**
 * @brief process incoming call
 * @param[in] line number of phone line
 * @param[in] extension extenstion called (i.e. phone number)
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::incomingCall(unsigned int line,
                                     const std::string &extension)
{
  // hangup old call on this line (to be on the safe side)
  hangup(line);

  // buffer further commands to be sent to this line until it is connected
  // -> create command buffer for this line
  m_cmdBufMap[line];

  // lookup extension
  ExtMap::iterator itExt = m_extMap.find(extension);
  if (itExt == m_extMap.end())
    return; // unknown extension -> ignore

  // try to open operator connection to module
  OpConn *pConn = m_mgrs.m_opMgr.connect(itExt->second, this);
  if (!pConn) // module is not ready for connection
    return;

  // add connection to maps
  m_lineConnMap[line] = pConn;
  m_connLineMap[pConn] = line;

  // send accept message
  sendAccept(line);
}

/**
 * @brief phone line connected
 * @param[in] line number of phone line
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::connected(unsigned int line)
{
  // get buffered commands for this line
  CmdBuf &cmdBuf = m_cmdBufMap[line];

  // send buffered commands now
  for (CmdBuf::const_iterator cmd = cmdBuf.begin();
       cmd != cmdBuf.end(); ++cmd) {
    std::stringstream strm;
    strm << line << ":" << *cmd;
    sendSock(strm.str());
  }

  // delete command buffer for this line
  m_cmdBufMap.erase(line);
}

/**
 * @brief hangup on phone line
 * @param[in] line number of phone line
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::hangup(unsigned int line)
{
  // get connection
  OpConn *pConn;
  if (!line2conn(line, pConn))
    return; // no connection on this line

  // close connection
  pConn->close();

  // remove connection from maps
  m_lineConnMap.erase(line);
  m_connLineMap.erase(pConn);

  // remove command buffer for this line
  m_cmdBufMap.erase(line);
}

/**
 * @brief key pressed on phone line
 * @param[in] line number of phone line
 * @param[in] key key that has been pressed
 */
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::keyPressed(unsigned int line, char key)
{
  // get connection
  OpConn *pConn;
  if (!line2conn(line, pConn))
    return; // no connection on this line

  // send key press event over connection
  pConn->sendKey(key);
}

/**
 * @brief get phone line number from operator connection
 * @param[in] pConn operator connection object
 * @param[out] line phone line number
 * @return if phone line was found
 */
template<typename ADDR, typename SOCK>
bool Phone<ADDR, SOCK>::conn2line(OpConn *pConn, unsigned int &line) const
{
  ConnLineMap::const_iterator itConn = m_connLineMap.find(pConn);
  if (itConn == m_connLineMap.end())
    return false;
  line = itConn->second;
  return true;
}

/**
 * @brief get operator connection from phone line number
 * @param[in] line phone line number
 * @param[out] pConn operator connection object
 * @return if phone line was found
 */
template<typename ADDR, typename SOCK>
bool Phone<ADDR, SOCK>::line2conn(unsigned int line, OpConn *&pConn) const
{
  LineConnMap::const_iterator itLine = m_lineConnMap.find(line);
  if (itLine == m_lineConnMap.end())
    return false;
  pConn = itLine->second;
  return true;
}

/// update time callback
template<typename ADDR, typename SOCK>
void Phone<ADDR, SOCK>::updateTimeCallback()
{
  m_mgrs.m_callMgr.requestTimeCall(this, m_timeRegister < m_timeHeartbeat
                                         ? m_timeRegister : m_timeHeartbeat);
}

template<typename ADDR, typename SOCK>
const Time Phone<ADDR, SOCK>::m_serverTimeout(60);

template<typename ADDR, typename SOCK>
const Time Phone<ADDR, SOCK>::m_heartbeatInterval(20);

} // namespace Blinker

#endif // #ifndef BLINKER_PHONE_IMPL_H

