/******************************************************************************\
 *  This sample is supplied as is with no implied warranty.  
 *  It is designed to assist you in using the Cisco AnyConnect VPN API. 
 *  It is assumed that you will build a production application and 
 *  refer to this sample as a reference only.
 \*****************************************************************************/

#include "CLIClientImpl.h"

#ifndef _WIN32
#include <unistd.h>
#endif
#include <thread>

#ifdef _WIN32
tstring SEP = L"\n\t";
#include "atlbase.h"
#else
tstring SEP = "\n\t";
#endif

/** @file */

CLIClientImpl :: CLIClientImpl()
               : me_RequestType(REQ_END),
                 ms_user(),
                 ms_pswd(),
                 ms_group(),
                 mb_exit(false),
                 mb_eventAvailable(false),
                 mb_ConnectingStateReceived(false) { }

CLIClientImpl :: ~CLIClientImpl() { }


void
CLIClientImpl :: StateCB(const VPNState state,
                         const VPNSubState subState,
                         const tstring stateString)
{
#ifdef UNICODE
   printf("Current State (%d): %S\n", state, stateString.c_str());
#else
   printf("Current State (%d): %s\n", state, stateString.c_str());
#endif

    switch(me_RequestType)
    {
        case REQ_STATE:
            mb_exit = true;
            break;
        case REQ_DISCONNECT:
            if (VPNState::DISCONNECTED == state)
            {
                mb_exit = true;
            }
            break;
        case REQ_CONNECT:
            if (VPNState::CONNECTED == state)
            {
                mb_exit = true;
            }

            // API may report the current state of VPN (ie., DISCONNECTED) when a connect request is received.
            // States reported after receiving CONNECTING state should be considered for the current connection
            // attempt. Exit the main processing loop for DISCONNECTED or CONNECTED states only after CONNECTING
            // state is received. 
            // 
            if (VPNState::CONNECTING == state)
            {
                mb_ConnectingStateReceived = true;
            }

            if (mb_ConnectingStateReceived && 
                ((VPNState::DISCONNECTED == state) || (VPNState::CONNECTED == state)))
            {
                mb_exit = true;
            }
            break;
        default:
            break;
    }
}


void
CLIClientImpl :: BannerCB(const tstring &banner)
{
	bool bBannerAccepted = true;
	setBannerResponse(bBannerAccepted);
}

void
CLIClientImpl :: PreConnectReminderCB(const tstring &rtstrPreConnectReminder)
{
#ifdef UNICODE
   printf("\n%S", rtstrPreConnectReminder.c_str());
#else
   printf("\n%s", rtstrPreConnectReminder.c_str());
#endif

    printf( "Press enter key to continue... " );
    std::cin.get();
    setPreConnectReminderResponse();
}

void
CLIClientImpl :: NoticeCB(const tstring &notice,
                          const MessageType type,
                          const bool /* bSensitive */)
{
    std::string msgType;
    switch(type)
        {
            case MsgType_Error:
                msgType = "Error";
                break;
            case MsgType_Warn:
                msgType = "Warn";
                break;
            case MsgType_Info:
                msgType = "Info";
                break;
            default:
                msgType = "Unknown";
                break;
        }

#ifdef UNICODE
    printf("Notice (%s): %S\n", msgType.c_str(), notice.c_str());
#else
    printf("Notice (%s): %s\n", msgType.c_str(), notice.c_str());
#endif
}


void
CLIClientImpl :: ExitNoticeCB(const tstring &notice, const int returnCode)
{
#ifdef UNICODE
    printf("ExitNoticeCB (%d): %S\n", returnCode, notice.c_str());
#else
    printf("ExitNoticeCB (%d): %s\n", returnCode, notice.c_str());
#endif
}

void
CLIClientImpl :: showGroups(std::string host)
{
    me_RequestType = REQ_GROUPLIST;
#ifdef UNICODE
    ClientIfc::connect(convertMultiByteToWide(host));
#else
    ClientIfc::connect(host);
#endif
}

void
CLIClientImpl :: connect(std::string host, std::string user,
                         std::string pswd, std::string group)

{
    me_RequestType = REQ_CONNECT;
    // save these user input values until requested by the API.
    //
#ifdef UNICODE
    ms_user = convertMultiByteToWide(user);
    ms_pswd = convertMultiByteToWide(pswd);
    ms_group = convertMultiByteToWide(group);
#else
    ms_user = user;
    ms_pswd = pswd;
    ms_group = group;
#endif

    // This is the main method for initiating a VPN connection.
    // The method can be found in ClientIfc.
    //
#ifdef UNICODE
    ClientIfc::connect(convertMultiByteToWide(host));
#else
    ClientIfc::connect(host);
#endif
}


void
CLIClientImpl :: disconnect()
{
    me_RequestType = REQ_DISCONNECT;
    ClientIfc::disconnect();
	// To ensure VPN service receive the disconnect message, 
    // do a short sleep before CLI terminates.
    //
    // NOTE: This is done only for demonstration purposes and would not be
    // needed in a GUI or other program that runs continuously. A more elegant way
	// to handle this case is to terminate the CLI after checking the disconnect callback message
	// from API.
    //
#ifdef _WIN32
    Sleep(1500);
#else
    sleep(1);
#endif
}

void
CLIClientImpl :: getStats()
{
    me_RequestType = REQ_STATISTICS;
    if (isConnected())
    {
        // To ensure we receive the first set of stats before we try to print,
        // do a short sleep.
        //
        // NOTE: This is done only for demonstration purposes and would not be
        // needed in a GUI or other program that runs continuously.
        //
        ClientIfc::getStats();
    }
    else
    {
        printf("Tunnel not up, no stats to print.\n");
        mb_exit = true;
    }
}

// In an application, this method would be used to signal to the program that
// the VPN service is available to this application.  Once this method is
// received, the application will be ready to call method slike connect.
//
// For this example and to keep the code simple, the method is not utilized
// as described.
//

void
CLIClientImpl :: ServiceReadyCB() { }

void
CLIClientImpl :: UserPromptCB(ConnectPromptInfo &ConnectPrompt)
{
    if (me_RequestType == REQ_CONNECT)
    {
        // For example purposes, only allow one try at setting user data.
        //
        // Look for requested user input fields and fill in previously
        // stored values.
        //
        setUserData(ConnectPrompt);
        // Now that the user data has been entered, submit the response.
        //
        UserSubmit();
    }
    else if (me_RequestType == REQ_GROUPLIST)
    {
        printGroupList(ConnectPrompt);
        mb_exit = true;
    }
}

/*! \fn
 *
 */

void
CLIClientImpl :: setUserData(ConnectPromptInfo &ConnectPrompt)
{
#ifdef UNICODE
    printf("User Message: %S\n", ConnectPrompt.getMessage().c_str());
#else
    printf("User Message: %s\n", ConnectPrompt.getMessage().c_str());
#endif

    // Create a list to hold the names of the individual PromptEntry objects.
    //
    std::list<tstring> promptNames;
    // Get the list of names associated with the PromptEntry objects.
    //
    ConnectPrompt.getListPromptNames(promptNames);
    // This set of code cycles through the list of names that reference the
    // individual PromptEntry objects.  As each name is accessed it can be used
    // to retrieve a specific instance of a PromptEntry object.  There is also a
    // method (ConnectPromptInfo::getListPromptEntry()) to retrieve the list of
    // PromptEntry objects directly, allowing the individual PromtpEntry object
    // to be accessed in a different manner if desired.
    //
    std::list<tstring> :: iterator name_iter;
    for (name_iter = promptNames.begin();
                     name_iter != promptNames.end(); ++name_iter)
    {
        // name_iter represents a single name from the promptNames list.
        //
        tstring promptName = *name_iter;
        PromptEntry *entry = ConnectPrompt.getPromptEntry(promptName);

        // For this demo program, we'll assume any combo box is for
        // group selection.
        //
        if (entry->getPromptType() == Prompt_Combo)
        {
            entry->setValue(ms_group);
        }
        tstring entryName = entry->getPromptName();

        // PromptEntry::Username and PromptEntry::Password are string currently
        // resolving to the wide char values "username" and "password".
        // 
        if (entryName == PromptEntry::Username)
        {
            entry->setValue(ms_user);
        }
        else if (entryName == PromptEntry::Password)
        {
            entry->setValue(ms_pswd);
        }
    }
}


void
CLIClientImpl :: printHostList()
{
    // getHostNames can be found in ClientIfc
    //
    std::list<tstring> hostList = getHostNames();
    std::list<tstring> :: iterator host_iter;
    tstring hosts;
    for (host_iter = hostList.begin();
         host_iter != hostList.end(); ++host_iter)
    {
        hosts += *host_iter + SEP;
    }

#ifdef UNICODE
    printf("Host List:\n\t%S\n", hosts.c_str());
#else
    printf("Host List:\n\t%s\n", hosts.c_str());
#endif
}


void
CLIClientImpl :: printDefaultHost()
{
#ifdef UNICODE
    printf("Default host: %S\n", getDefaultHostName().c_str());
#else
    printf("Default host: %s\n", getDefaultHostName().c_str());
#endif
}


void
CLIClientImpl :: printGroupList(ConnectPromptInfo &ConnectPrompt)
{
    std::list<tstring> promptNames;
    // get the list of PromptEntry received.
    //
    ConnectPrompt.getListPromptNames(promptNames);

    std::list<tstring> :: iterator name_iter;
    for (name_iter = promptNames.begin();
                     name_iter != promptNames.end(); ++name_iter)
    {
        PromptEntry *entry = ConnectPrompt.getPromptEntry(*name_iter);
        // For this demo program, we'll assume any combo box is for
        // group selection.
        //
        if (entry->getPromptType() == Prompt_Combo)
        {
            tstring groupList;
            std::list<tstring>::const_iterator lo_Citer;

            for (lo_Citer = entry->getValueOptions().begin();
                 lo_Citer != entry->getValueOptions().end(); ++lo_Citer)
            {
                tstring value = *lo_Citer;
                groupList += SEP + (*lo_Citer);
            }


#ifdef UNICODE
            printf("Group List: %S\n", groupList.c_str());
#else
            printf("Group List: %s\n", groupList.c_str());
#endif
        }

    }
}


void
CLIClientImpl :: StatsCB(VPNStats &rStats)
{
    if (REQ_STATISTICS == me_RequestType)
    {
        tstring stats = SEP + VPNStats::getTranslatedLabel(VPNStats::State)
                            + rStats.getStatValue(VPNStats::State);
            stats += SEP + VPNStats::getTranslatedLabel(VPNStats::TunnelingMode)
                         + rStats.getStatValue(VPNStats::TunnelingMode);
            stats += SEP + VPNStats::getTranslatedLabel(VPNStats::TimeConnected)
                         + rStats.getStatValue(VPNStats::TimeConnected);
            stats += SEP + VPNStats::getTranslatedLabel(VPNStats::BytesSent)
                         + rStats.getStatValue(VPNStats::BytesSent);
            stats += SEP + VPNStats::getTranslatedLabel(VPNStats::BytesReceived)
                         + rStats.getStatValue(VPNStats::BytesReceived);

            // Now get the data for the active protocol.
            //
            // NOTE: There is also Secure and non-secure route information.  This
            //       data can be retrieved in a manner similar to protocolInfo
            //       using the methods getSecureRoutes() and getNonsecureRoutes()
            //
            std::list<ProtocolInfo *> protInfo = rStats.getProtocolInfo();
            for (std::list<ProtocolInfo *>::iterator iter = protInfo.begin();
                                                    iter != protInfo.end();
                                                    iter++)
            {
                if ((*iter)->isActive())
                {
                    stats += SEP + VPNStats::getTranslatedLabel(ProtocolInfo::Cipher)
                                +(*iter)->getProtocolValue(ProtocolInfo::Cipher);
                    stats += SEP + VPNStats::getTranslatedLabel(ProtocolInfo::Protocol)
                                +(*iter)->getProtocolValue(ProtocolInfo::Protocol);
                    stats += SEP + VPNStats::getTranslatedLabel(ProtocolInfo::Compression)
                                +(*iter)->getProtocolValue(ProtocolInfo::Compression);
                }
            }

#ifdef UNICODE
            printf("VPN Stats:%S\n",stats.c_str());
#else
            printf("VPN Stats:%s\n",stats.c_str());
#endif
        mb_exit = true;
    }
}

void
CLIClientImpl :: CertBlockedCB(const tstring &rtstrUntrustedServer)
{
    setCertBlockedResponse(false);
}

void
CLIClientImpl :: CertWarningCB(const tstring &rtstrUntrustedServer,
                               const std::list<tstring> &rltstrCertErrors,
                               bool bAllowImport)
{
    setCertWarningResponse(false, false);
}

// These methods provide a means to convert strings from wide to multibyte
// and vice-versa.
//

std::wstring
CLIClientImpl :: convertMultiByteToWide(const std::string &sInputData)
{
    std::wstring wsDataBuffer;

    // determine number of bytes that will be written
    //
#ifdef _WIN32
    int bufSize = MultiByteToWideChar(CP_UTF8, 0, sInputData.c_str(),
                                      -1, NULL, 0);
#else
    int bufSize = mbstowcs(NULL, sInputData.c_str(), 0);
#endif

    if (bufSize > 0)
    {
        wchar_t *wzTmpBuf = new wchar_t[bufSize];
#ifdef _WIN32
        int converted = MultiByteToWideChar(CP_UTF8, 0, sInputData.c_str(),
                                            -1, wzTmpBuf, bufSize);
        converted--;  // drop the terminating NULL.
#else
        int converted = mbstowcs(wzTmpBuf, sInputData.c_str(), bufSize);
#endif
        wsDataBuffer.assign(wzTmpBuf, converted);
        delete[] wzTmpBuf;
    }
    else
    {
        printf("convertMultiByteToWide : Conversion failed\n");
    }

    return wsDataBuffer;
}


// NOTE: Ideal implemenation of CLIClientImpl::EventAvailable() should signal CLIClientImpl::ProcessEvents()
// to handle the event. CLIClientImpl::ProcessEvents() running on a different thread handles the event to
// exercise some control for the request (i.e., State, Connect etc .,).
// 
void
CLIClientImpl::EventAvailable()
{
    mtx.lock();
    mb_eventAvailable = true;
    mtx.unlock();
}


void
CLIClientImpl::ProcessEvents()
{
    while (!mb_exit)
    {
        mtx.lock();
        if (mb_eventAvailable)
        {
            mb_eventAvailable = false;
            ClientIfc::ProcessEvents();
        }
        mtx.unlock();
        std::this_thread::sleep_for(std::chrono::milliseconds(20));
    }
}

void
CLIClientImpl::SetRequestType(RequestType e_RequestType)
{
    me_RequestType = e_RequestType;
}