/* 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 <map>
#include <string>

#include "BoolFile.h"
#include "Directory.h"
#include "File.h"
#include "ListTracker.h"
#include "ListTracker_impl.h"
#include "Mgrs.h"
#include "Module.h"
#include "NameFile.h"
#include "OpConn.h"
#include "OpConnIf.h"
#include "OpMgr.h"
#include "OpSplitter.h"
#include "OpSplitterExtension.h"
#include "SettingFile.h"
#include "Time.h"
#include "TimeCallee.h"
#include "UIntFile.h"

namespace Blinker {

/**
 * @brief constructor
 * @param[in] name module name
 * @param[in] mgrs managers
 * @param[in] dirBase base directory
 */
OpSplitter::OpSplitter(const std::string &name, Mgrs &mgrs,
                         const Directory &dirBase):
  Module(name, mgrs, dirBase),
  m_fileSound(dirBase.getFile("sound")),
  m_fileMaxConn(dirBase.getFile("maxconn")),
  m_fileSingle(dirBase.getFile("single")),
  m_extListTracker(*this, dirBase.getSubdir("extensions"))
{
  // load extensions
  m_extListTracker.init();

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

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

  // close all connections
  while (!m_mapLocal.empty()) {
    MapLocal::iterator it = m_mapLocal.begin();
    it->first->close();
    m_mapLocal.erase(it);
  }
  while (!m_mapInOut.empty()) {
    MapInOut::iterator it = m_mapInOut.begin();
    it->first->close();
    m_mapInOut.erase(it);
  }
  while (!m_mapOutIn.empty()) {
    MapOutIn::iterator it = m_mapOutIn.begin();
    it->first->close();
    m_mapOutIn.erase(it);
  }

  // unload extensions
  m_extListTracker.clear();

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

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

  // max. number of conn. file was modified -> re-read max. number of conn.
  if (m_fileMaxConn.checkModified())
    m_fileMaxConn.update();

  // single mode config file was modified -> re-read single mode config
  if (m_fileSingle.checkModified())
    m_fileSingle.update();

  // extensions update
  m_extListTracker.updateConfig();
}

/// callback when requested time reached
void OpSplitter::timeCall()
{
  // process pending actions for local connections
  MapLocal::iterator it = m_mapLocal.begin();
  while (it != m_mapLocal.end()) {
    // close connection
    if (it->second.m_close) {
      MapLocal::iterator itDel = it;
      ++it; // we have to increment ourself to make erasing in loop work
      itDel->first->close();
      m_mapLocal.erase(itDel);
      continue; // it already incremented -> skip rest of loop
    }
    // send sound play request on marked local operator connection
    if (it->second.m_sendPlay) {
      it->second.m_sendPlay = false;
      if (m_fileSound.m_valid)
        it->first->sendPlay(m_fileSound.m_obj.m_str);
    }
    ++it;
  }
}

/**
 * @brief check if accepting new operator connction is possible
 * @param[in] name operator interface name
 * @return if accepting new connection is possible
 */
bool OpSplitter::acceptNewOpConn(const std::string &name)
{
  // if maximum number of connections if limited, check it
  if (m_fileMaxConn.m_valid &&
      m_mapLocal.size() + m_mapInOut.size() >= m_fileMaxConn.m_obj.m_uint)
    return false; // too many connection

  // single mode
  if (m_fileSingle.m_valid && m_fileSingle.m_obj.m_bool) {

    // accept first connection
    if (m_mapLocal.empty() && m_mapInOut.empty())
      return true;

    // no operator interface selected yet
    if (m_singleOp.empty())
      return false;

    // additional connections: ask selected operator interface
    return m_mgrs.m_opMgr.canConnect(m_singleOp);

  } // if m_fileSingle

  return true;

  (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 OpSplitter::newOpConn(const std::string &name, OpConn *pConn)
{
  // this is a new incoming connection

  // single mode and not first connection
  if (m_fileSingle.m_valid && m_fileSingle.m_obj.m_bool) {
    if (!m_mapLocal.empty() || !m_mapInOut.empty()) {

      // try to open outgoiong operator connection to selected operator intf.
      OpConn *pOutConn = m_mgrs.m_opMgr.connect(m_singleOp, this);
      if (!pOutConn) {
        // module is not ready for connection
        newLocal(pConn, true); // make conn. a local conn. to be closed soon
        return;
      }

      // establish connection forwarding
      m_mapInOut[pConn] = pOutConn;
      m_mapOutIn[pOutConn] = pConn;

      return;
    } // if !m_mapLocal.empty() ...
  } // if m_fileSingle ...

  // make connection a local connection
  newLocal(pConn);

  (void)name; // unused
}

/**
 * @brief key command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] key key that was pressed
 */
void OpSplitter::opConnRecvKey(OpConn *pConn, char key)
{
  // local connection -> process
  MapLocal::iterator itLocal = m_mapLocal.find(pConn);
  if (itLocal != m_mapLocal.end()) {
    localKey(itLocal, key);
    return;
  }

  // incoming to outgoing connection -> forward
  MapInOut::iterator itInOut = m_mapInOut.find(pConn);
  if (itInOut != m_mapInOut.end()) {
    itInOut->second->sendKey(key);
    return;
  }

  // outgoing to incoming connection -> forward
  MapOutIn::iterator itOutIn = m_mapOutIn.find(pConn);
  if (itOutIn != m_mapOutIn.end()) {
    itOutIn->second->sendKey(key);
    return;
  }
}

/**
 * @brief play command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] sound name of sound to play
 */
void OpSplitter::opConnRecvPlay(OpConn *pConn, const std::string &sound)
{
  // play command does not make sense for local connections -> ignore

  // incoming to outgoing connection -> forward
  MapInOut::iterator itInOut = m_mapInOut.find(pConn);
  if (itInOut != m_mapInOut.end()) {
    itInOut->second->sendPlay(sound);
    return;
  }

  // outgoing to incoming connection -> forward
  MapOutIn::iterator itOutIn = m_mapOutIn.find(pConn);
  if (itOutIn != m_mapOutIn.end()) {
    itOutIn->second->sendPlay(sound);
    return;
  }
}

/**
 * @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 OpSplitter::opConnClose(OpConn *pConn)
{
  // local connection -> forget this connection
  MapLocal::iterator itLocal = m_mapLocal.find(pConn);
  if (itLocal != m_mapLocal.end()) {
    m_mapLocal.erase(itLocal);
    return;
  }

  // incoming to outgoing connection -> close outgoing connection as well
  MapInOut::iterator itInOut = m_mapInOut.find(pConn);
  if (itInOut != m_mapInOut.end()) {
    itInOut->second->close();
    m_mapOutIn.erase(itInOut->second);
    m_mapInOut.erase(itInOut);
    if (m_mapInOut.empty()) // last connection closed -> no operator intf. sel.
      m_singleOp.clear();
    return;
  }

  // outgoing to incoming connection
  MapOutIn::iterator itOutIn = m_mapOutIn.find(pConn);
  if (itOutIn != m_mapOutIn.end()) {
    OpConn * pInConn = itOutIn->second;
    m_mapInOut.erase(pInConn);
    m_mapOutIn.erase(itOutIn);
    if (m_mapInOut.empty()) // last connection closed -> no operator intf. sel.
      m_singleOp.clear();
    // single mode and not last connection
    if (m_fileSingle.m_valid && m_fileSingle.m_obj.m_bool) {
      if (!m_mapLocal.empty() || !m_mapInOut.empty()) {
        // close incoming connection as well
        pInConn->close();
        return;
      }
    }
    // make incoming connection local
    newLocal(pInConn);
    return;
  }
}

/**
 * @brief create new local connection
 * @param[in] pConn connection to make local
 * @param[in] close if to close connection as soon as possible
 */
void OpSplitter::newLocal(OpConn *pConn, bool close /* = false */)
{
  // add connection to local connection map
  Local &local = m_mapLocal[pConn];
  local.m_number.clear(); // no extension dialed so far
  local.m_sendPlay = !close; // send play command if close not requested
  local.m_close = close;
  m_mgrs.m_callMgr.requestTimeCall(this, Time::now());
}

/**
 * @brief a key has been pressed for a local connection
 * @param[in] itLocal local connection
 * @param[in] key the key pressed
 */
void OpSplitter::localKey(MapLocal::iterator itLocal, char key)
{
  switch (key) {
    // begin new extension number
    case '*':
      itLocal->second.m_number.clear();
      break;
    // add digit to extension number
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      itLocal->second.m_number += key;
      break;
    // close local connection / call extension
    case '#':
      if (itLocal->second.m_number.empty()) {
        itLocal->first->close();
        m_mapLocal.erase(itLocal);
      } else {
        callExtension(itLocal);
      }
      break;
  }
}

/**
 * @brief call extension dialed over local connection
 * @param[in] itLocal local connection
 */
void OpSplitter::callExtension(MapLocal::iterator itLocal)
{
  // lookup extension
  ExtMap::iterator itExt = m_extMap.find(itLocal->second.m_number);
  if (itExt == m_extMap.end()) {
    // unknown extension -> ignore
    itLocal->second.m_number.clear(); // begin new extension number
    return;
  }

  // save selected operator interface for single mode
  m_singleOp = itExt->second;

  // try to open outgoiong operator connection to module
  OpConn *pOutConn = m_mgrs.m_opMgr.connect(itExt->second, this);
  if (!pOutConn) {
    // module is not ready for connection
    itLocal->second.m_number.clear(); // begin new extension number
    return;
  }

  // establish connection forwarding
  m_mapInOut[itLocal->first] = pOutConn;
  m_mapOutIn[pOutConn] = itLocal->first;

  // connection is no more a local one
  m_mapLocal.erase(itLocal);
}

} // namespace Blinker

