/*
 * Copyright 2010 Funambol, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id$ */

#include "daemon/ProfileComponentsHolder.h"
#include "Logger/LoggerMacroses.h"
#include "serverexchange/Connection.h"
#include "serverexchange/ConnectionInfo.h"
#include "serverexchange/session/Session.h"
#include "serverexchange/wrappers/SAlertCommand.h"
#include "serverexchange/wrappers/SCommandFactory.h"
#include "serverexchange/wrappers/SReplaceCommand.h"
#include "serverexchange/wrappers/SStatusCommand.h"
#include "serverexchange/ICommandsSink.h"
#include "serverexchange/LOSendingStrategy.h"
#include "serverexchange/ResponseProcessor.h"
#include "common/FileSystemUtils.h"

#include <base/errors.h>
#include <ctime>
#if !defined(PLATFORM_ANDROID)
#include <fstream>
#else
#include <fstream>
#endif
#include <wbxml.h>
#include <wbxml_mem.h>
#include "hash.h"
#include "Utils.h"
#include "WBXMLUtils.h"


using namespace NS_DM_Client;
using namespace NS_DM_Client::NS_Communication;
using namespace NS_DM_Client::NS_SyncMLCommand;
using namespace NS_DM_Client::NS_Common;
    
static const char* c_LogName = "Session";
static const char* c_MessageDumpFile = "message.txt";

#define PROPERTY_ACCEPT        "Accept"
#define PROPERTY_CONTENT_TYPE  "Content-Type"
#define ACCEPT_BOTH_MIMES      "application/vnd.syncml.dm+xml, application/vnd.syncml.dm+wbxml"


Session::Session(ConnectionInfo &ci, Funambol::TransportAgent &agent) : 
    m_firstMessage(true),
    m_commandsAreResent(false),
    m_connectionInfo(ci),
    m_pTransportAgent(&agent),
    m_pPCH(NULL),
    m_responseMsg(NULL),
    m_pLOStrategy(NULL),
    m_pResponseProcessor(new(std::nothrow) ResponseProcessor(ci, NULL, NULL))
{
    if(m_pResponseProcessor == NULL) GDLWARN("new ResponseProcessor");
    m_pResponseProcessor->SetSession(*this);
    initializeTransport();
}


Session::~Session()
{
    SAFE_DELETE(m_responseMsg);
    SAFE_DELETE(m_pLOStrategy);
    SAFE_DELETE(m_pResponseProcessor);
    SAFE_DELETE(m_pTransportAgent);
}


bool Session::IsAuthenticationPassed(Funambol::Status &status0)
{
    const char *data = "";
    if (status0.getData())
        data = status0.getData()->getData();
    else
        return false;

    int code = atoi(data);
    GDLDEBUG("status on SyncHdr: %d", code);

    return false;
}


void Session::Connect(NS_SyncMLCommand::CommandStorage & semcommands)
{
    if (0 == semcommands.TryLock())
    {
        GDLDEBUG("%d command%s waiting to be sent",
                 semcommands.Size(), semcommands.Size() == 1 ? " is" : "s are");

        // insert Alert, Replace
        if (semcommands.Size() || m_pLOStrategy)
        {
            for (int i=0; i<semcommands.Size(); ++i)
                m_commands.Add(semcommands[i]);
            semcommands.Clear();
        }
        semcommands.Unlock();

        if (m_pLOStrategy)
        {
            GDLDEBUG("send with LOStrategy [%x]\thas to send next chunk: %s",
                     m_pLOStrategy, m_connectionInfo.state.hasToSendNextChunk ? "yes" : "no");
            if (m_connectionInfo.state.hasToSendNextChunk)
            {
                SendingStrategy::Status status;
                
                GDLDEBUG("Send next LO chunk");
                status = m_pLOStrategy->Send();
                if (SendingStrategy::NeedToSendLO != status)
                    processResponse();
                // TODO - rework to check status from Send()
                if (m_pLOStrategy->Finished())
                {
                    GDLDEBUG("LO sending finished; delete LOStrategy");
                    SAFE_DELETE(m_pLOStrategy);
                    m_connectionInfo.state.hasToSendNextChunk = false;
                    m_commands.Clear();
                    m_commandsToSend.Clear();
                }
            }
            else
            {
                GDLDEBUG("\tdelete LOStrategy (either all LO sent"
                         " or server did not confirmed further chunks transmission)");
                SAFE_DELETE(m_pLOStrategy);
                m_connectionInfo.state.hasToSendNextChunk = false;
            }
        }
        else
        {
            insertSessionInitiationCommands(m_state.InitialAlertCode);
            for (int i=0; i<m_commands.Size(); ++i)
                m_commandsToSend.Add(m_commands[i]);

            SendingStrategy strategy(m_connectionInfo, *this);
            SendingStrategy::Status status;

            status = strategy.Send(m_commandsToSend);

            if (SendingStrategy::NeedToSendLO != status)
                processResponse();

            semcommands.Lock();
            GDLDEBUG("has to resend last commands: %s; need to send LO: %s",
                     m_connectionInfo.state.hasToResendLastCommands ? "yes" : "no",
                     SendingStrategy::NeedToSendLO == status ? "yes" : "no");

            if (m_connectionInfo.state.hasToResendLastCommands)
            {
                for (int i=0; i<m_commands.Size(); ++i)
                {
                    semcommands.Add(m_commands[i]);
                    GDLDEBUG("\t%s", m_commands[i]->Internal()->getName());
                }
                m_commands.Clear();
                m_commandsToSend.Clear();
            }

            m_commandsAreResent = m_connectionInfo.state.hasToResendLastCommands;
            if (SendingStrategy::NeedToSendLO != status)
            {
                m_commands.Clear();
                m_commandsToSend.Clear();
            }

            semcommands.Unlock();

            if (SendingStrategy::NeedToSendLO == status)
            {
                m_pLOStrategy = new(std::nothrow) LOSendingStrategy(m_connectionInfo, m_commands, *this);
                if(m_pLOStrategy == NULL) GDLWARN("new LOSendingStrategy");

                m_commandsAreResent = false;
                m_connectionInfo.state.hasToSendNextChunk = true;
                m_connectionInfo.state.connectionFinished = false;
                GDLDEBUG("");
            }
        }
    }   
}


bool Session::IsResponseValid(const char *, uint resplength)
{
    return false;
}


bool Session::IsSendingLO()
{
    return m_pLOStrategy != NULL;
}


void Session::SendMessage(FStringBuffer &message)
{
    Funambol::URL url(m_connectionInfo.GetSessionURL());
    m_pTransportAgent->setURL(url);

    const char *xml = message.c_str();
    removeEndLineChars(message);
    PrintMessage(c_LogName, "send message", xml);
    
    if (m_connectionInfo.settings.UseWBXML)
    {
        const char *wbxml = NULL;
        unsigned int wbxmllength = 0;
        int result;
        if (WBXML_OK == (result = WBXMLUtils::FormatToWBXML(xml, &wbxml, &wbxmllength)))
        {
            prepareTransport(wbxml, wbxmllength);
            // TODO - merge dump and printmsg
            dumpMessage(wbxml, wbxmllength, "send message - wbxml");
            //PrintMessage(c_LogName, "send message", xml);
            m_responseMsg = m_pTransportAgent->sendMessage(wbxml, wbxmllength);
            wbxml_free((void*)wbxml);
        }
        else
        {
            GDLERROR("FAILED to convert syncml to wbxml, err code %d", result);
            return;
        }
    }
    else
    {
        int length = strlen(xml);
        prepareTransport(xml, length);
        // TODO - merge dump and printmsg
        dumpMessage(xml, length, "send message - plain syncml");
        //PrintMessage(c_LogName, "send message", xml);
        m_responseMsg = m_pTransportAgent->sendMessage(xml, length);
    }
}


void Session::SetCommandsSink(ICommandsSink &cs)
{
    m_pResponseProcessor->SetCommandsSink(cs);
}


void Session::SetPCH(ProfileComponentsHolder &pch)
{
    m_pPCH = &pch;
    m_pResponseProcessor->SetPCH(pch);
}


void Session::checkAcceptTypes()
{
    const char * accept = m_pTransportAgent->getResponseProperty("Accept");

    if (accept)
    {
        if (strstr(accept, MIMETYPE_SYNCMLDM_XML))
            m_connectionInfo.settings.UseWBXML = false;
        else if (strstr(accept, MIMETYPE_SYNCMLDM_WBXML))
            m_connectionInfo.settings.UseWBXML = true;
    }
}


void Session::initializeTransport()
{
    if (m_pTransportAgent != NULL)
    {
        m_pTransportAgent->setCompression(false);
        m_pTransportAgent->setReadBufferSize(5000);
        if (!m_connectionInfo.settings.CertificatesLocation.empty())
        {
            m_pTransportAgent->setSSLServerCertificates(m_connectionInfo.settings.CertificatesLocation.c_str());
            m_pTransportAgent->setSSLVerifyServer(m_connectionInfo.settings.SSLVerifyServer);
            m_pTransportAgent->setSSLVerifyHost(m_connectionInfo.settings.SSLVerifyHost);
        }
        
        m_pTransportAgent->setUserAgent(m_connectionInfo.acconfig.getUserAgent());
    }
}


void Session::dumpMessage(const char *msg, size_t size, const char *prefix)
{
GDLDEBUG("ENTER");

    if (!m_connectionInfo.settings.MessagesDumpPath.empty())
    {

#if defined(PLATFORM_ANDROID)
        if (!CreatePath(m_connectionInfo.settings.MessagesDumpPath.c_str(), &NS_Logging::GetLogger(c_LogName)))
        {
            GDLERROR("can't create path for message dump");
            GDLDEBUG("LEAVE");
            return;
        }
#endif

        time_t rawtime;
        struct tm * timeinfo;
        memset(&rawtime, 0, sizeof(rawtime));
        time ( &rawtime );
        timeinfo = localtime ( &rawtime );

#if !defined(PLATFORM_ANDROID)
        std::ofstream out(m_connectionInfo.settings.MessagesDumpPath.c_str(), std::ios_base::app);
        out << asctime (timeinfo) << " " << prefix << ", size " << size << " bytes" << std::endl;
        out.write(msg, size);
        out << std::endl << std::endl;
        out.close();
#else // PLATFORM_ANDROID
        std:: string filePath = m_connectionInfo.settings.MessagesDumpPath + "/" + std::string(c_MessageDumpFile);
        std::ofstream out((const char*)filePath.c_str(), std::ios::app);
        out << asctime (timeinfo) << " " << prefix << ", size " << size << " bytes" << std::endl;
        out.write(msg, size);
        out << std::endl << std::endl;
        out.close();
#endif
    }
    else
    {
        GDLDEBUG("message dump path is empty");
    }
GDLDEBUG("LEAVE");
}


void Session::prepareTransport(const char *, uint)
{
    if (m_connectionInfo.settings.UseWBXML)
    {
        m_pTransportAgent->setProperty(PROPERTY_ACCEPT, ACCEPT_BOTH_MIMES);
        m_pTransportAgent->setProperty(PROPERTY_CONTENT_TYPE, MIMETYPE_SYNCMLDM_WBXML);
    }
    else
    {
        m_pTransportAgent->setProperty(PROPERTY_ACCEPT, MIMETYPE_SYNCMLDM_XML);
        m_pTransportAgent->setProperty(PROPERTY_CONTENT_TYPE, MIMETYPE_SYNCMLDM_XML);
    }
}


void Session::insertAlertReplace(int code)
{
    SCommandPtr ptrReplaceDevInf = SCommandFactory::CreateReplaceDevInf(*m_connectionInfo.devinf);
    if(ptrReplaceDevInf.get() == NULL)
    {
        GDLWARN("ptrReplaceDevInf is NULL");
    }

    if (m_state.AddInitialAlert)
    {
        SCommandPtr ptrAlert(new(std::nothrow) SAlertCommand(code));
        if(ptrAlert.get() == NULL)
        {
            GDLWARN("ptrAlert is NULL");
        }

        m_commandsToSend.InsertFirst(ptrReplaceDevInf);
        m_commandsToSend.InsertFirst(ptrAlert);
    }
    else
    {
        if (m_state.InitialAlert.get())
            m_commandsToSend.InsertFirst(m_state.InitialAlert);
        m_commandsToSend.Add(ptrReplaceDevInf);
    }
}


void Session::notifyTransportError()
{
    if (m_pPCH)
        m_pPCH->GetNotificationCenter()->NotifyDMSessionStatus(e_sessionEnd, e_network, e_networkFailure);
    else
        GDLDEBUG("PCH is not set");
}


void Session::processResponse()
{
    const char * mime   = m_pTransportAgent->getResponseProperty(PROPERTY_CONTENT_TYPE);
    int resplength      = m_pTransportAgent->getResponseSize();
    
    if (resplength == 0 || m_responseMsg == NULL || m_responseMsg[0] == '\0')
    {
        GDLDEBUG("no response from server {len, respmsg} = {%d, %x}", resplength, m_responseMsg);
        GDLDEBUG("transport error: %d, '%s'", getLastErrorCode(), getLastErrorMsg());

        notifyTransportError();
        m_connectionInfo.state.connectionFinished = true;
        return;
    }

    bool checkForCred = !IsResponseValid(m_responseMsg, resplength);
    SetServerAuthenticated(!checkForCred);

    m_pResponseProcessor->SetCheckForCred(checkForCred);

    if (mime && (strstr(mime, MIMETYPE_SYNCMLDM_WBXML)))
    {
        m_connectionInfo.settings.UseWBXML = true; // switch session to wbxml
        GDLDEBUG(" Server response - WBXML [%d bytes]", resplength);
        dumpMessage(m_responseMsg, resplength, "Server response - wbxml");
    
        char *xml = NULL;
        int result;
        if (WBXML_OK == (result = WBXMLUtils::Parse(m_responseMsg, resplength, &xml)))
        {
            PrintMessage(c_LogName, "Server response - decoded wbxml", xml);
            m_pResponseProcessor->Process(xml);
            wbxml_free(xml);
        }
        else
        {
            GDLERROR("FAILED to decode wbxml response, err code %d", result);
            // todo - error status - same as no answer ?
        }
    }
    else if ((mime && (strstr(mime, MIMETYPE_SYNCMLDM_XML))) || !mime)
    {
        dumpMessage(m_responseMsg, strlen(m_responseMsg), "Server response - plain syncml");
        PrintMessage(c_LogName, "Server response - plain syncml", m_responseMsg);
        m_pResponseProcessor->Process(m_responseMsg);
    }
    else
    {
        GDLERROR("Server replied with type %s; skip reply", mime);
        GDLERROR("\tmessage:\n\n%s", m_responseMsg);
        m_connectionInfo.state.connectionFinished = true;
        // todo - error status - same as no answer ?
    }
    
    checkAcceptTypes();
    
    SAFE_DELETE_ARR(m_responseMsg);
}


void Session::removeEndLineChars(FStringBuffer &message)
{
    if (!m_connectionInfo.settings.KeepNewLines)
        message.replaceAll("\n", "");
}
