/* $Id$ */
/*
* Copyright (C) 2016 Teluu Inc. (http://www.teluu.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include "MyApp.h"
#include <memory>
#include <winerror.h>
#include <pplawait.h>

using namespace VoipBackEnd;
using namespace Windows::Storage;

const std::string CONFIG_NAME = "pjsua2.json";
const int SIP_PORT  = 5060;
const int LOG_LEVEL = 5;
const std::string THIS_FILE = "MyApp.cpp";

#define GenHresultFromITF(x) (MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, x))

MyAppRT^ MyAppRT::singleton = nullptr;

#define DEFAULT_DESTRUCTOR() \
    delete inPtr;

#define CHECK_EXCEPTION(expr) \
	do { \
	    try { \
		(expr); 	\
	    } catch (pj::Error e) { \
		Platform::Exception^ exp = ref new Platform::COMException( \
		    GenHresultFromITF(e.status)); \
	    } \
	} while (0)

///////////////////////////////////////////////////////////////////////////////
std::string make_string(const std::wstring& wstring)
{
    auto wideData = wstring.c_str();
    int bufferSize = WideCharToMultiByte(CP_UTF8, 0, wideData, 
					 -1, nullptr, 0, NULL, NULL);

    std::unique_ptr<char[]> utf8;
    utf8.reset(new char[bufferSize]);    

    if (WideCharToMultiByte(CP_UTF8, 0, wideData, -1,
	utf8.get(), bufferSize, NULL, NULL) == 0) 
    {	
	return std::string();
    }

    return std::string(utf8.get());    
}

std::wstring make_wstring(const std::string& string)
{
    auto utf8Data = string.c_str();
    int bufferSize = MultiByteToWideChar(CP_UTF8, 0, utf8Data, -1, nullptr, 0);

    std::unique_ptr<wchar_t[]> wide;
    wide.reset(new wchar_t[bufferSize]);

    if (MultiByteToWideChar(CP_UTF8, 0, utf8Data, -1,
	wide.get(), bufferSize) == 0)
    {	
	return std::wstring();
    }	    

    return std::wstring(wide.get());;
}

std::string to_std_str(Platform::String ^in_str)
{
    std::wstring wsstr(in_str->Data());
    return make_string(wsstr);
}

Platform::String^ to_platform_str(const std::string& in_str)
{
    std::wstring wsstr = make_wstring(in_str);
    return ref new Platform::String(wsstr.c_str(), wsstr.length());
}

///////////////////////////////////////////////////////////////////////////////
void ImpLogWriter::write(const pj::LogEntry &entry)
{
    std::wstring msg = make_wstring(entry.msg) + L"\r\n";

    ::OutputDebugString(msg.c_str());    
}
///////////////////////////////////////////////////////////////////////////////
OnRegStateParamRT::OnRegStateParamRT(const pj::OnRegStateParam& param)

{
    inPtr = new pj::OnRegStateParam(param);
}

OnRegStateParamRT::OnRegStateParamRT()
{
    inPtr = new pj::OnRegStateParam();
}

OnRegStateParamRT::~OnRegStateParamRT()
{
    DEFAULT_DESTRUCTOR();
}

pj_status_t OnRegStateParamRT::status::get()
{
    return inPtr->status;
}

unsigned OnRegStateParamRT::code::get()
{
    return static_cast<unsigned>(inPtr->code);
}

Platform::String^ OnRegStateParamRT::reason::get()
{
    return to_platform_str(inPtr->reason);
}

int OnRegStateParamRT::expiration::get()
{
    return inPtr->expiration;
}
///////////////////////////////////////////////////////////////////////////////
void ImpAccount::setCallback(IntAccount^ callback)
{
    cb = callback;
}

void ImpAccount::onIncomingCall(pj::OnIncomingCallParam& prm)
{
    if (cb) {
	if (MyAppRT::Instance->handleIncomingCall(prm.callId)) {
	    CallInfoRT^ info = MyAppRT::Instance->getCallInfo();
	    if (info != nullptr)
		cb->onIncomingCall(info);
	}
    }
}


void ImpAccount::onRegState(pj::OnRegStateParam& prm)
{
    if (cb)
	cb->onRegState(ref new OnRegStateParamRT(prm));
}

///////////////////////////////////////////////////////////////////////////////
CallInfoRT::CallInfoRT(const pj::CallInfo& info)
{
    inPtr = new pj::CallInfo(info);
}

Platform::String^ CallInfoRT::localUri::get()
{
    return to_platform_str(inPtr->localUri);
}

Platform::String^ CallInfoRT::localContact::get()
{
    return to_platform_str(inPtr->localContact);
}

Platform::String^ CallInfoRT::remoteUri::get()
{
    return to_platform_str(inPtr->remoteUri);
}

Platform::String^ CallInfoRT::remoteContact::get()
{
    return to_platform_str(inPtr->remoteContact);
}

INV_STATE CallInfoRT::state::get()
{
    return static_cast<INV_STATE>(inPtr->state);
}

CallInfoRT::~CallInfoRT()
{
    DEFAULT_DESTRUCTOR();
}
///////////////////////////////////////////////////////////////////////////////
CallOpParamRT::CallOpParamRT()
{
    inPtr = new pj::CallOpParam(true);
}

unsigned CallOpParamRT::statusCode::get() 
{
    return static_cast<unsigned>(inPtr->statusCode);
}

void CallOpParamRT::statusCode::set(unsigned val) 
{
    inPtr->statusCode = static_cast<pjsip_status_code>(val);
}

Platform::String^ CallOpParamRT::reason::get() 
{
    return to_platform_str(inPtr->reason);
}

void CallOpParamRT::reason::set(Platform::String^ val) 
{
    inPtr->reason = to_std_str(val);
}

pj::CallOpParam* CallOpParamRT::getPtr()
{
    return inPtr;
}

CallOpParamRT::~CallOpParamRT()
{
    DEFAULT_DESTRUCTOR();
}

///////////////////////////////////////////////////////////////////////////////
ImpCall::ImpCall(pj::Account& account, int call_id) : pj::Call(account,call_id)
{};

void ImpCall::setCallback(IntCall^ callback)
{
    cb = callback;
}

void ImpCall::onCallState(pj::OnCallStateParam& prm)
{
    if (cb) {
	pj::CallInfo info = getInfo();	
	cb->onCallState(ref new CallInfoRT(info));
    }
};

void ImpCall::onCallMediaState(pj::OnCallMediaStateParam& prm)
{
    pj::CallInfo info;    
    try {
	info = getInfo();
    } catch (pj::Error& e) {
	MyAppRT::Instance->writeLog(2, to_platform_str(e.info()));
	return;
    }

    for (unsigned i = 0; i < info.media.size(); i++) {
	pj::CallMediaInfo med_info = info.media[i];
	if ((med_info.type == PJMEDIA_TYPE_AUDIO) &&
	    ((med_info.status == PJSUA_CALL_MEDIA_ACTIVE) ||
	    (med_info.status == PJSUA_CALL_MEDIA_REMOTE_HOLD))
	    )
	{
	    pj::Media* m = getMedia(i);
	    pj::AudioMedia *am = pj::AudioMedia::typecastFromMedia(m);
	    pj::AudDevManager& aud_mgr = 
		pj::Endpoint::instance().audDevManager();

	    try {
		aud_mgr.getCaptureDevMedia().startTransmit(*am);
		am->startTransmit(aud_mgr.getPlaybackDevMedia());	

	    } catch (pj::Error& e) {
		MyAppRT::Instance->writeLog(2, to_platform_str(e.info()));
		continue;
	    }	
	}
    }
};
///////////////////////////////////////////////////////////////////////////////

AccountInfo::AccountInfo(pj::AccountConfig* accConfig) : cfg(accConfig)
{}

Platform::String^ AccountInfo::id::get() {
    return  to_platform_str(cfg->idUri);
}

Platform::String^ AccountInfo::registrar::get() {
    return to_platform_str(cfg->regConfig.registrarUri);
}

Platform::String^ AccountInfo::proxy::get() {
    if (!cfg->sipConfig.proxies.empty()) {
	return to_platform_str(cfg->sipConfig.proxies[0]);
    } else {
	return ref new Platform::String(L"");
    }
}

Platform::String^ AccountInfo::username::get() {
    if (!cfg->sipConfig.authCreds.empty()) {
	pj::AuthCredInfo& info = cfg->sipConfig.authCreds[0];

	return to_platform_str(info.username);
    } else {
	return ref new Platform::String(L"");
    }
}

Platform::String^ AccountInfo::password::get() {
    if (!cfg->sipConfig.authCreds.empty()) {
	pj::AuthCredInfo& info = cfg->sipConfig.authCreds[0];

	return to_platform_str(info.data);
    } else {
	return ref new Platform::String(L"");
    }
}

///////////////////////////////////////////////////////////////////////////////
MyAppRT::MyAppRT()
{        
    logWriter = new ImpLogWriter();

    StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;

    appDir = to_std_str(localFolder->Path);
}

MyAppRT::~MyAppRT()
{        
    delete logWriter;
    if (account)
	delete account;

    if (epConfig)
	delete epConfig;

    if (accConfig)
	delete accConfig;

    if (sipTpConfig)
	delete sipTpConfig;
}

void MyAppRT::loadConfig()
{
    pj::JsonDocument* json = new pj::JsonDocument();

    std::string configPath = appDir + "/" + CONFIG_NAME;

    try {
	/* Load file */
	json->loadFile(configPath);
	pj::ContainerNode root = json->getRootContainer();

	/* Read endpoint config */
	epConfig->readObject(root);

	/* Read transport config */
	pj::ContainerNode tp_node = root.readContainer("SipTransport");
	sipTpConfig->readObject(tp_node);

	/* Read account configs */
	pj::ContainerNode acc_node = root.readContainer("Account");
	accConfig->readObject(acc_node);
    } catch (pj::Error) {
	ep.utilLogWrite(2, "loadConfig", "Failed loading config");

	sipTpConfig->port = SIP_PORT;
    }

    delete json;
}

void MyAppRT::saveConfig()
{
    pj::JsonDocument* json = new pj::JsonDocument();

    std::string configPath = appDir + "/" + CONFIG_NAME;

    try {
	/* Write endpoint config */
	json->writeObject(*epConfig);	

	/* Write transport config */
	pj::ContainerNode tp_node = json->writeNewContainer("SipTransport");
	sipTpConfig->writeObject(tp_node);

	/* Write account configs */	
	pj::ContainerNode acc_node = json->writeNewContainer("Account");
	accConfig->writeObject(acc_node);

	/* Save file */
	json->saveFile(configPath);
    } catch (pj::Error) {
	ep.utilLogWrite(2, "saveConfig", "Failed saving config");

	sipTpConfig->port = SIP_PORT;
    }

    delete json;
}

MyAppRT^ MyAppRT::Instance::get()
{
    if (MyAppRT::singleton == nullptr)
    {
	if (MyAppRT::singleton == nullptr)
	{
	    MyAppRT::singleton = ref new MyAppRT();
	}
    }

    return MyAppRT::singleton;
}

void MyAppRT::init(IntAccount^ iAcc, IntCall^ iCall)
{    
    /* Create endpoint */
    try {
	ep.libCreate();
    } catch (pj::Error e) {
	return;
    }

    intAcc = iAcc;
    intCall = iCall;

    epConfig = new pj::EpConfig;
    accConfig = new pj::AccountConfig;
    sipTpConfig = new pj::TransportConfig;

    /* Load config */    
    loadConfig();

    /* Override log level setting */
    epConfig->logConfig.level = LOG_LEVEL;
    epConfig->logConfig.consoleLevel = LOG_LEVEL;

    /* Set log config. */
    pj::LogConfig *log_cfg = &epConfig->logConfig;
    log_cfg->writer = logWriter;
    log_cfg->decor = log_cfg->decor &
	~(::pj_log_decoration::PJ_LOG_HAS_CR |
	  ::pj_log_decoration::PJ_LOG_HAS_NEWLINE);

    /* Set ua config. */
    pj::UaConfig* ua_cfg = &epConfig->uaConfig;
    ua_cfg->userAgent = "Pjsua2 WinRT " + ep.libVersion().full;
    ua_cfg->mainThreadOnly = false;

    /* Init endpoint */
    try
    {
	ep.libInit(*epConfig);
    } catch (pj::Error& e)
    {
	ep.utilLogWrite(2,THIS_FILE,e.info());
	return;
    }

    /* Create transports. */

    try {
	sipTpConfig->port = 5060;
	ep.transportCreate(::pjsip_transport_type_e::PJSIP_TRANSPORT_TCP,
			   *sipTpConfig);
    } catch (pj::Error& e) {
	ep.utilLogWrite(2,THIS_FILE,e.info());	
    }

    try {
	ep.transportCreate(::pjsip_transport_type_e::PJSIP_TRANSPORT_UDP,
			   *sipTpConfig);
    } catch (pj::Error& e) {
	ep.utilLogWrite(2,THIS_FILE,e.info());
    }    

    /* Create accounts. */    
    account = new ImpAccount();
    if (accConfig->idUri.length() == 0) {
	accConfig->idUri = "sip:localhost";	
    } else {
	ua_cfg->stunServer.push_back("stun.pjsip.org");	
    }
    //accConfig->natConfig.iceEnabled = true;
    account->create(*accConfig);
    account->setCallback(iAcc);

    /* Start. */
    try {
	ep.libStart();
    } catch (pj::Error& e) {
	ep.utilLogWrite(2,THIS_FILE,e.info());
	return;
    }
}

void MyAppRT::deInit()
{
    saveConfig();

    try {
	ep.libDestroy();
    } catch (pj::Error) {}

}

void MyAppRT::writeLog(int level, Platform::String^ message)
{
    ep.utilLogWrite(level, "MyAppRT", to_std_str(message));
}

void MyAppRT::hangupCall()
{
    if (activeCall) {
	if (!isThreadRegistered())
	{
	    registerThread("hangupCall");
	}
	
	delete activeCall;
	activeCall = NULL;
    }
};

void MyAppRT::answerCall(CallOpParamRT^ prm)
{
    if (!isThreadRegistered())
    {
	registerThread("answerCall");
    }
    CHECK_EXCEPTION(activeCall->answer(*prm->getPtr()));
};

void MyAppRT::makeCall(Platform::String^ dst_uri)
{
    if (!isThreadRegistered())
    {
	registerThread("makeCall");
    }
    CallOpParamRT^ param = ref new CallOpParamRT;
    activeCall = new ImpCall(*account, -1);
    activeCall->setCallback(intCall);

    CHECK_EXCEPTION(activeCall->makeCall(to_std_str(dst_uri),*param->getPtr()));
};

CallInfoRT^ MyAppRT::getCallInfo()
{
    if (activeCall) {
	pj::CallInfo info = activeCall->getInfo();
	return ref new CallInfoRT(info);
    }

    return nullptr;
}

pj_bool_t MyAppRT::handleIncomingCall(int callId)
{    
    if (activeCall) {
	return PJ_FALSE;
    }

    activeCall = new ImpCall(*account, callId);
    activeCall->setCallback(intCall);

    return PJ_TRUE;
}

void MyAppRT::registerThread(Platform::String^ name)
{
    CHECK_EXCEPTION(ep.libRegisterThread(to_std_str(name)));
}

bool MyAppRT::isThreadRegistered()
{
    // Some threads are registered using PJLIB API, ep.libIsThreadRegistered() will return false on those threads.
    //return ep.libIsThreadRegistered();
    return pj_thread_is_registered() != PJ_FALSE;
}

AccountInfo^ MyAppRT::getAccountInfo()

{    
    return ref new AccountInfo(accConfig);
}

void MyAppRT::modifyAccount(Platform::String^ id, 
			    Platform::String^ registrar,
			    Platform::String^ proxy, 			    
			    Platform::String^ username,
			    Platform::String^ password)
{    
    if (id->IsEmpty()) {
	accConfig->idUri = "sip:localhost";
	accConfig->regConfig.registrarUri = "";
	accConfig->sipConfig.authCreds.clear();
	accConfig->sipConfig.proxies.clear();
    } else {
	pj::AuthCredInfo info = pj::AuthCredInfo("Digest", "*", 
						 to_std_str(username), 0, 
						 to_std_str(password));

	accConfig->idUri = to_std_str(id);
	accConfig->regConfig.registrarUri = to_std_str(registrar);

	accConfig->sipConfig.authCreds.clear();    
	accConfig->sipConfig.authCreds.push_back(info);

	accConfig->sipConfig.proxies.clear();
	accConfig->sipConfig.proxies.push_back(to_std_str(proxy));
    }

    if (!isThreadRegistered())
    {
	registerThread("registerAccount");
    }

    account->modify(*accConfig);

    /* Save config. */
    saveConfig();
}
