/* 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 <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <sstream>
#include <stdarg.h>
#include <stdio.h>
#include <string>
#include <string.h>
#include <thread>

#ifdef BLINKER_CFG_LINPHONE
#include <linphone/linphonecore.h>
#endif

#include "Directory.h"
#include "File.h"
#include "Module.h"
#include "NameFile.h"
#include "OpConn.h"
#include "OpConnIf.h"
#include "SipPhone.h"
#include "Time.h"
#include "TimeCallee.h"

namespace Blinker {

#ifdef BLINKER_CFG_LINPHONE

/**
 * everything using the linphone library happens in a separate thread,
 * because some call to linphone take long (up to two seconds)
 */
namespace linphone {

static char const *loglevel2str(OrtpLogLevel lev)
{
  switch(lev) {
  case ORTP_DEBUG: return "DEBUG";
  case ORTP_MESSAGE: return "MESSAGE";
  case ORTP_WARNING: return "WARNING";
  case ORTP_ERROR: return "ERROR";
  case ORTP_FATAL: return "FATAL";
  case ORTP_TRACE: return "TRACE";
  case ORTP_LOGLEV_END: return "LOGLEV_END";
  default: return "?";
  }
}

static char const *gstate2str(LinphoneGlobalState gstate)
{
  switch (gstate) {
  case LinphoneGlobalOff: return "Off";
  case LinphoneGlobalStartup: return "Startup";
  case LinphoneGlobalOn: return "On";
  case LinphoneGlobalShutdown: return "Shutdown";
  default: return "?";
  }
}

static char const *rstate2str(LinphoneRegistrationState rstate)
{
  switch (rstate) {
  case LinphoneRegistrationNone: return "None";
  case LinphoneRegistrationProgress: return "Progress";
  case LinphoneRegistrationOk: return "Ok";
  case LinphoneRegistrationCleared: return "Cleared";
  case LinphoneRegistrationFailed: return "Failed";
  default: return "?";
  }
}

static char const *cstate2str(LinphoneCallState cstate)
{
  switch (cstate) {
  case LinphoneCallIdle: return "Idle";
  case LinphoneCallIncomingReceived: return "IncomingReceived";
  case LinphoneCallOutgoingInit: return "OutgoingInit";
  case LinphoneCallOutgoingProgress: return "OutgoingProgress";
  case LinphoneCallOutgoingRinging: return "OutgoingRinging";
  case LinphoneCallOutgoingEarlyMedia: return "OutgoingEarlyMedia";
  case LinphoneCallConnected: return "Connected";
  case LinphoneCallStreamsRunning: return "StreamsRunning";
  case LinphoneCallPausing: return "Pausing";
  case LinphoneCallPaused: return "Paused";
  case LinphoneCallResuming: return "Resuming";
  case LinphoneCallRefered: return "Refered";
  case LinphoneCallError: return "Error";
  case LinphoneCallEnd: return "End";
  case LinphoneCallPausedByRemote: return "PausedByRemote";
  case LinphoneCallUpdatedByRemote: return "UpdatedByRemote";
  case LinphoneCallIncomingEarlyMedia: return "IncomingEarlyMedia";
  case LinphoneCallUpdating: return "Updating";
  case LinphoneCallReleased: return "Released";
  default: return "?";
  }
}

std::string glLogFileName; ///< name of global log file
std::ofstream glLogStream; ///< stream to open global log file
unsigned int glLogLineCnt; ///< number of lines already logged

/**
 * @brief log information from linphone library
 * @param[in] lev log level
 * @param[in] fmt format string
 * @param[in] args arguments to insert into format string
 */
void log_handler(OrtpLogLevel lev, const char *fmt, va_list args)
{
  // (re-)open logfile on first line
  if (glLogLineCnt == 0) {
    // close old file and clean up (just to be on the safe side)
    glLogStream.close();
    glLogStream.clear();
    // keep previous log file
    rename(glLogFileName.c_str(), (glLogFileName + ".prev").c_str());
    // open new file
    glLogStream.open(glLogFileName.c_str(), std::ios::out);
  }
  // write log line
  char buffer[4096];
  vsnprintf(buffer, sizeof(buffer) - 1, fmt, args);
  buffer[sizeof(buffer) - 1] = 0;
  glLogStream << Time::now().toStr() << " "
              << loglevel2str(lev) << " " << buffer << std::endl;
  // count lines
  ++glLogLineCnt;
  // close file if maximum line count reached
  if (glLogLineCnt >= 10000) {
    // close old file and clean up
    glLogStream.close();
    glLogStream.clear();
    // reset line count, so new file is opened on next line
    glLogLineCnt = 0;
  }
}

/// global init
void init(std::string const &logFileName)
{
  glLogFileName = logFileName;
  glLogLineCnt = 0;
  linphone_core_set_log_handler(log_handler);
}

/// data of SIP client worker thread
struct Data {
  SipPhone::ConfigData const *configData; ///< config data for worker thread
  SipPhone::SharedData *sharedData; ///< data shared with main Blinker thread
  LinphoneCoreVTable callbacks; ///< callback table
  LinphoneCore *lc; ///< linphone core object, if active
  LinphoneCall *call; ///< current call, if available
  std::string playback; ///< name of playback file
  std::ofstream logStream; ///< stream to open log file
  unsigned int logLineCnt; ///< number of lines already logged to file
};

/**
 * @breif set playback file
 * @param[in,out] data data of SIP client
 * @param[in] name name of sound file (without extension)
 */
void set_playback(Data &data, std::string const &name)
{
  Directory dir(data.configData->soundDir);
  data.playback = dir.getFile(name + ".wav").getPath();
}

void start_playback(Data &data)
{
  if (data.lc && ! data.playback.empty()) {
    linphone_core_set_play_file(data.lc, data.playback.c_str());
  }
}

/**
 * @brief write log message
 * @param[in,out] data data of SIP client
 * @param[in] line lie to log
 */
void log(Data &data, std::string const &line)
{
  // (re-)open logfile on first line
  if (data.logLineCnt == 0) {
    // close old file and clean up (just to be on the safe side)
    data.logStream.close();
    data.logStream.clear();
    // keep previous log file
    rename(data.configData->logFileName.c_str(),
           (data.configData->logFileName + ".prev").c_str());
    // open new file
    data.logStream.open(data.configData->logFileName.c_str(), std::ios::out);
  }
  // write log line
  data.logStream << Time::now().toStr() << " " << line << std::endl;
  // count lines
  ++data.logLineCnt;
  // close file if maximum line count reached
  if (data.logLineCnt >= 10000) {
    // close old file and clean up
    data.logStream.close();
    data.logStream.clear();
    // reset line count, so new file is opened on next line
    data.logLineCnt = 0;
  }
}

/**
 * @brief global SIP state changed
 * @param[in] lc linphone core object
 * @param[in] gstate new global SIP state
 * @param[in] message informational message
 */
void global_state_changed(struct _LinphoneCore *lc,
                          LinphoneGlobalState gstate,
                          const char *message)
{
  Data *data = (Data*)linphone_core_get_user_data(lc);

  std::stringstream line;
  line << "global state changed to " << gstate2str(gstate) << ": " << message;
  log(*data, line.str());
}

/**
 * @brief registration state changed
 * @param[in] lc linphone core object
 * @param[in] cfg proxy configuration
 * @param[in] rstate new registration state
 * @param[in] message informational message
 */
void registration_state_changed(struct _LinphoneCore *lc,
                                LinphoneProxyConfig *cfg,
                                LinphoneRegistrationState rstate,
                                const char *message)
{
  Data *data = (Data*)linphone_core_get_user_data(lc);
  (void)cfg;

  std::stringstream line;
  line << "registration state changed to " << rstate2str(rstate) << ": "
       << message;
  log(*data, line.str());
}

/**
 * @brief call state changed
 * @param[in] lc linphone core object
 * @param[in] call call object
 * @param[in] cstate new call state
 * @param[in] message informational message
 */
void call_state_changed(struct _LinphoneCore *lc, LinphoneCall *call,
                        LinphoneCallState cstate, const char *message)
{
  Data *data = (Data*)linphone_core_get_user_data(lc);

  std::stringstream line;
  line << "call state changed to " << cstate2str(cstate) << ": "
       << message;
  log(*data, line.str());

  // current call -> handle state changes
  if (data->call == call) {
    switch (cstate) {
    case LinphoneCallStreamsRunning:
      start_playback(*data);
      break;
    case LinphoneCallRefered:
    case LinphoneCallError:
    case LinphoneCallEnd:
    case LinphoneCallReleased:
      data->call = nullptr;
      {
        std::lock_guard<std::mutex> lock(data->sharedData->mtx);
        data->sharedData->terminated = true;
      }
      break;
    default:
      break;
    }
  }
  // other call, but current call active -> reject call
  else if (data->call) {
    switch (cstate) {
    case LinphoneCallIncomingReceived:
      log(*data, "rejecting call (busy)");
      linphone_core_decline_call(lc, call, LinphoneReasonBusy);
      break;
    default:
      break;
    }
  }
  // no call active -> accept call
  else {
    switch (cstate) {
    case LinphoneCallIncomingReceived:
      data->call = call;
      {
        std::lock_guard<std::mutex> lock(data->sharedData->mtx);
        data->sharedData->accepted = true;
      }
      data->playback.clear();
      log(*data, "accepting call");
      linphone_core_accept_call(lc, call);
      break;
    default:
      break;
    }
  }
}

/**
 * @brief DTMF received
 * @param[in] lc linphone core object
 * @param[in] call call object
 * @param[in] dtmf DTMF as ASCII character
 */
void dtmf_received(struct _LinphoneCore* lc, LinphoneCall *call, int dtmf)
{
  Data *data = (Data*)linphone_core_get_user_data(lc);

  // check if current call
  bool current = data->call == call;

  // check DTMF
  char c;
  if (dtmf >= '0' && dtmf <= '9') {
    c = dtmf;
  } else if (dtmf == '*') {
    c = '*';
  } else if (dtmf == '#') {
    c = '#';
  } else {
    c = '?';
  }

  std::stringstream line;
  line << "dtmf received " << (current ? "(current call)" : "(other call)")
       << ": " << c;
  log(*data, line.str());

  // ignore DTMF from other calls
  if (! current) {
    return;
  }

  // report DTMF keys
  {
    std::lock_guard<std::mutex> lock(data->sharedData->mtx);
    data->sharedData->dtmf += c;
  }
}

/**
 * @brief register if not active and login data available
 * @param[in,out] data data of SIP client
 * @param[in] server SIP server
 * @param[in] username SIP username
 * @param[in] password SIP password
 */
void do_register(Data &data, std::string const &server,
                 std::string const &username, std::string &password)
{
  if (data.lc) {
    return;
  }
  if (server.empty()) {
    return;
  }

  // create linphone core object
  data.lc = linphone_core_new(&data.callbacks, NULL, NULL, &data);
  if (! data.lc) {
    log(data, "failed to create linphone core");
    return;
  }
  log(data, "linphone core created");

  // set auth data
  if (! username.empty() && ! password.empty()) {
    LinphoneAuthInfo *auth_info =
        linphone_auth_info_new(username.c_str(), username.c_str(),
                               password.c_str(), "", "");
    if (! auth_info) {
      log(data, "failed to create auth info");
      linphone_core_destroy(data.lc);
      data.lc = nullptr;
      return;
    }
    linphone_core_add_auth_info(data.lc, auth_info);
    log(data, "auth info set");
  } else {
    log(data, "no auth data available");
  }

  // configure proxy
  std::string sipserver = "sip:" + server;
  std::string identity = "sip:" + username + "@" + server;
  LinphoneProxyConfig *proxy = linphone_core_create_proxy_config(data.lc);
  if (! proxy) {
    log(data, "failed to create proxy config");
    linphone_core_destroy(data.lc);
    data.lc = nullptr;
    return;
  }
  linphone_proxy_config_set_server_addr(proxy, sipserver.c_str());
  linphone_proxy_config_set_identity(proxy, identity.c_str());
  linphone_proxy_config_enable_register(proxy, TRUE);
  linphone_core_add_proxy_config(data.lc, proxy);
  log(data, "proxy config set");

  // tell linphone not to rind and to use files instead of sound card
  linphone_core_set_ring(data.lc, nullptr);
  linphone_core_use_files(data.lc, TRUE);
  linphone_core_set_play_file(data.lc, nullptr);
}

/**
 * @brief deregister if active
 * @param[in,out] data data of SIP client
 */
void do_deregister(Data &data)
{
  if (! data.lc) {
    return;
  }

  log(data, "destroying...");
  linphone_core_destroy(data.lc);
  data.lc = nullptr;
  data.call = nullptr;
  log(data, "destroyed");
}

/**
 * @brief play sound on call if active
 * @param[in,out] data data of SIP client
 * @param[in] name name of sound
 */
void do_play(Data &data, std::string const &name)
{
  if (! data.lc) {
    return;
  }
  if (! data.call) {
    return;
  }

  std::stringstream line;
  line << "playing sound \"" << name << "\"";
  log(data, line.str());
  set_playback(data, name);
  start_playback(data);
}

/**
 * @brief hangup call if active
 * @param[in,out] data data of SIP client
 */
void do_hangup(Data &data)
{
  if (! data.lc) {
    return;
  }
  if (! data.call) {
    return;
  }

  log(data, "terminating call...");
  linphone_core_terminate_call(data.lc, data.call);
  data.call = nullptr;
  log(data, "call terminated");
}

/**
 * @brief linphone worker thread
 * @param[in] soundDir directory of sound files
 * @param[in,out] data shared data for communication with main Blinker thread
 */
void worker(SipPhone::ConfigData const &configData,
            SipPhone::SharedData &sharedData)
{
  // set up data of SIP client
  Data data;
  data.configData = &configData;
  data.sharedData = &sharedData;
  memset(&data.callbacks, 0, sizeof(data.callbacks));
  data.callbacks.global_state_changed = global_state_changed;
  data.callbacks.registration_state_changed = registration_state_changed;
  data.callbacks.call_state_changed = call_state_changed;
  data.callbacks.dtmf_received = dtmf_received;
  data.lc = nullptr;
  data.call = nullptr;
  data.logLineCnt = 0;

  // main loop of worker - with shared data locked
  std::chrono::milliseconds timeout(10);
  std::unique_lock<std::mutex> lock(sharedData.mtx);
  while (sharedData.run) {

    // hangup
    if (sharedData.hangup) {
      sharedData.hangup = false;
      sharedData.reqPlay = false;
      sharedData.dtmf.clear();
      sharedData.terminated = true;
      // execute hangup with unlocked shared data (parallelism!)
      lock.unlock();
      do_hangup(data);
      lock.lock();
      continue; // re-check everything after re-locking mutex
    }

    // re-register (including initial registration and derigstration)
    if (sharedData.reregister) {
      sharedData.reregister = false;
      sharedData.hangup = false;
      sharedData.reqPlay = false;
      sharedData.dtmf.clear();
      sharedData.terminated = true;
      std::string server = sharedData.server;
      std::string username = sharedData.username;
      std::string password = sharedData.password;
      // execute re-registration with unlocked shared data (parallelism!)
      lock.unlock();
      do_deregister(data);
      do_register(data, server, username, password);
      lock.lock();
      continue; // re-check everything after re-locking mutex
    }

    // play sound
    if (sharedData.reqPlay) {
      sharedData.reqPlay = false;
      std::string name = sharedData.reqPlayName;
      // execute hangup with unlocked shared data (parallelism!)
      lock.unlock();
      do_play(data, name);
      lock.lock();
      continue; // re-check everything after re-locking mutex
    }

    // background processing by linphone if active
    if (data.lc) {
      lock.unlock();
      linphone_core_iterate(data.lc);
      lock.lock();
    }

    // nothing to do, wait for signal
    sharedData.cond.wait_for(lock, timeout);

  } // while (sharedData.run)
}

} // namespace linphone

#endif // #ifdef BLINKER_CFG_LINPHONE

/**
 * @brief global initialization (call once before using this class)
 * @param[in] globalLogDir directory for global SIP log files
 */
void SipPhone::init(const Directory &globalLogDir)
{
#ifdef BLINKER_CFG_LINPHONE
  linphone::init(globalLogDir.getFile("sip.log").getPath());
#endif
}

/**
 * @brief constructor
 * @param[in] name module name
 * @param[in] mgrs managers
 * @param[in] dirBase base directory
 */
SipPhone::SipPhone(const std::string &name, Mgrs &mgrs,
                   const Directory &dirBase):
  Module(name, mgrs, dirBase),
  m_fileServer(dirBase.getFile("server")),
  m_fileUsername(dirBase.getFile("username")),
  m_filePassword(dirBase.getFile("password")),
  m_fileTarget(dirBase.getFile("target")),
  m_configData(),
  m_worker(),
  m_sharedData(),
  m_curConn(nullptr)
{
  m_configData.soundDir = dirBase.getSubdir("sounds").getPath();
  m_configData.logFileName = dirBase.getFile("sip.log").getPath();
  m_sharedData.run = true;
  m_sharedData.reregister = false;
  m_sharedData.reqPlay = false;
  m_sharedData.hangup = false;
  m_sharedData.accepted = false;
  m_sharedData.terminated = false;

  m_mgrs.m_callMgr.requestTimeCall(this, Time::now());

#ifdef BLINKER_CFG_LINPHONE
  m_worker = std::thread(linphone::worker, std::ref(m_configData),
                         std::ref(m_sharedData));
#endif

  sipRegister();
}

/// virtual destructor
SipPhone::~SipPhone()
{
  if (m_curConn) {
    m_curConn->close();
    m_curConn = nullptr;
  }

  sipDeregister();

  {
    std::lock_guard<std::mutex> lock(m_sharedData.mtx);
    m_sharedData.run = false;
  }
  m_sharedData.cond.notify_one();
#ifdef BLINKER_CFG_LINPHONE
  m_worker.join();
#endif

  m_mgrs.m_callMgr.cancelTimeCall(this);
}

/// check for update of configuration
void SipPhone::updateConfig()
{
  // server, username or password file modified -> re-connect
  if (m_fileServer.checkModified() || m_fileUsername.checkModified() ||
                                      m_filePassword.checkModified()) {
    // re-register
    sipDeregister();
    sipRegister();
  }

  // target file was modified -> re-get target operator interface to connect to
  if (m_fileTarget.checkModified()) {
    m_fileTarget.update();
  }
}

/// callback when requested time reached
void SipPhone::timeCall()
{
  Time now = Time::now();

  // get information from SIP phone worker
  bool accepted;
  std::string dtmf;
  bool terminated;
  {
    std::lock_guard<std::mutex> lock(m_sharedData.mtx);
    accepted = m_sharedData.accepted;
    m_sharedData.accepted = false;
    dtmf = m_sharedData.dtmf;
    m_sharedData.dtmf.clear();
    terminated = m_sharedData.terminated;
    m_sharedData.terminated = false;
  }

  // phone call has terminated or new one has been accepted
  if (terminated || accepted) {
    // close connection if any
    if (m_curConn) {
      // request hang up of phone
      {
        std::lock_guard<std::mutex> lock(m_sharedData.mtx);
        m_sharedData.hangup = true;
      }
      // close and forget connection
      m_curConn->close();
      m_curConn = nullptr;
    }
  }

  // phone call has been accepted
  if (accepted) {
    // try to open new connection to target operator interface
    if (m_fileTarget.m_valid) {
      m_curConn = m_mgrs.m_opMgr.connect(m_fileTarget.m_obj.m_str, this);
    }
  }

  // DTMF received -> send to operator connection
  if (! dtmf.empty()) {
    if (m_curConn) {
      for (char key: dtmf) {
        m_curConn->sendKey(key);
      }
    }
  }

  // request next call
  Time interval;
  interval.fromMs(20);
  m_mgrs.m_callMgr.requestTimeCall(this, now + interval);
}

/**
 * @brief key command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] key key that was pressed
 */
void SipPhone::opConnRecvKey(OpConn *pConn, char key)
{
  // ignore keys received
  (void)pConn;
  (void)key;
}

/**
 * @brief play command received on operator connection
 * @param[in] pConn operator connection object
 * @param[in] sound name of sound to play
 */
void SipPhone::opConnRecvPlay(OpConn *pConn, const std::string &sound)
{
  // only process current connection
  if (pConn != m_curConn) {
    return;
  }

  // request playing sound
  {
    std::lock_guard<std::mutex> lock(m_sharedData.mtx);
    m_sharedData.reqPlay = true;
    m_sharedData.reqPlayName = 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.
 */
void SipPhone::opConnClose(OpConn *pConn)
{
  // only process current connection
  if (pConn != m_curConn) {
    return;
  }

  // request hang up of phone
  {
    std::lock_guard<std::mutex> lock(m_sharedData.mtx);
    m_sharedData.hangup = true;
  }

  // forget connection
  m_curConn = nullptr;
}

/// (re-)register with SIP server
void SipPhone::sipRegister()
{
  // read settings
  m_fileServer.update();
  m_fileUsername.update();
  m_filePassword.update();

  // put server, username and password into shared data, trigger registration
  {
    std::lock_guard<std::mutex> lock(m_sharedData.mtx);
    m_sharedData.server = m_fileServer.m_valid
                          ? m_fileServer.m_obj.toStr() : std::string();
    m_sharedData.username = m_fileUsername.m_valid
                            ? m_fileUsername.m_obj.toStr() : std::string();
    m_sharedData.password = m_filePassword.m_valid
                            ? m_filePassword.m_obj.toStr() : std::string();
    m_sharedData.reregister = true;
  }
  m_sharedData.cond.notify_one();
}

/// deregister with SIP server
void SipPhone::sipDeregister()
{
  // remove login infor from shared data, trigger re-registration
  {
    std::lock_guard<std::mutex> lock(m_sharedData.mtx);
    m_sharedData.server.clear();
    m_sharedData.username.clear();
    m_sharedData.password.clear();
    m_sharedData.reregister = true;
  }
  m_sharedData.cond.notify_one();
}

} // namespace Blinker

