//============================================================================
// Copyright 2009- ECMWF.
// This software is licensed under the terms of the Apache Licence version 2.0
// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
// In applying this licence, ECMWF does not waive the privileges and immunities
// granted to it by virtue of its status as an intergovernmental organisation
// nor does it submit to any jurisdiction.
//
//============================================================================

#include "OutputFileClient.hpp"

#include <cstring>

#include "UIDebug.hpp"
#include "UiLog.hpp"

//#define UI_OUTPUTFILECLIENT_DEBUG__
//#define UI_OUTPUTFILECLIENT_DETAILED_DEBUG__

OutputFileClient::OutputFileClient(const std::string& host,const std::string& portStr,QObject* parent) :
    OutputClient(host,portStr,parent)
{
    versionClient_ = new OutputVersionClient(host, portStr, parent);
    connect(versionClient_,SIGNAL(finished()), this, SLOT(slotVersionFinished()));
    connect(versionClient_,SIGNAL(error(QString)), this, SLOT(slotVersionError(QString)));
}

void OutputFileClient::clearResult()
{
    if(out_) {
        out_->close();
        out_.reset();
    }
}

void OutputFileClient::slotConnected()
{ 
    readStarted_ = false;
    reqType_ = NoRequest;
    std::string s;
    if (deltaPos_ > 0) {
        reqType_ = DeltaRequest;
        soc_->write("delta ",6);
        s = std::to_string(deltaPos_) + " " + std::to_string(remoteModTime_) + " " +
               ((remoteCheckSum_.empty())?"x":remoteCheckSum_);
    } else  {
        if (versionClient_->version() == 2) {
            reqType_ = GetFRequest;
            soc_->write("getf ",5);
        } else {
            reqType_ = GetRequest;
            soc_->write("get ",4);
        }
    }

    assert(reqType_ != NoRequest);
    s += " " + remoteFile_ + "\n";
    soc_->write(s.c_str(), strlen(s.c_str())+1);
}

void OutputFileClient::slotError(QAbstractSocket::SocketError err)
{
#ifdef UI_OUTPUTFILECLIENT_DEBUG__
    UiLog().dbg() <<  UI_FN_INFO  << "err=" << soc_->errorString();
#endif

    if (err == QAbstractSocket::RemoteHostClosedError) {
        //Probably there was no error and the connection was closed because all
        //the data was transferred. Unfortunately the logserver does not send anything
        //at the end of the data transfer.
        if(total_ > 0 || reqType_ == DeltaRequest) {
            soc_->abort();
            if(out_)
			{
				out_->setTransferDuration(stopper_.elapsed());
				out_->setFetchDate(QDateTime::currentDateTime());               
                //out_->print();
                out_->close();
			}

			Q_EMIT finished();
            return;
		}
    }

    // an error happened
    soc_->abort();
    if(out_)
    {
        out_->close();
        out_.reset();
    }
    Q_EMIT error(soc_->errorString());;
}

void OutputFileClient::getFile(const std::string& name)
{
    getFile(name, 0, 0, "");
}

void OutputFileClient::getFile(const std::string& name, size_t deltaPos, unsigned int modTime, const std::string& checkSum)
{
    // stop running the current transfer
    soc_->abort();
    clearResult();

#ifdef UI_OUTPUTFILECLIENT_DEBUG__
    UiLog().dbg() << UI_FN_INFO << "name=" << name << " deltaPos=" << deltaPos << " modTime=" << modTime << " checksum=" << checkSum;
#endif

    remoteFile_=name;
    deltaPos_ = deltaPos;
    remoteModTime_ = modTime;
    remoteCheckSum_ = checkSum;

    // we need to know the version. When it finishes will call getFile again
    if (versionClient_->versionStatus() == OutputVersionClient::VersionNotFetched) {
#ifdef UI_OUTPUTFILECLIENT_DEBUG__
        UiLog().dbg() << "OutputFileClient::getFile --> Try to get logserver version";
#endif
        versionClient_->getVersion();
        return;
    }

    // only verson 2 can handle delta increments
    if (versionClient_->version() != 2) {
#ifdef UI_OUTPUTFILECLIENT_DETAILED_DEBUG__
        UiLog().dbg() << "OutputFileClient::getFile --> deltaPos set to 0";
#endif
        deltaPos_ = 0;
    }

	remoteFile_=name;
	out_.reset();
    out_=VFile_ptr(VFile::create(true)); //we will delete the file from disk
    out_->setDeltaContents(deltaPos_ >0);
    out_->setSourcePath(name);
    out_->setFetchMode(VFile::LogServerFetchMode);
    out_->setFetchModeStr("served by " + longName());

    total_=0;
    lastProgress_=0;
    readStarted_=false;
    reqType_ = NoRequest;
    estimateExpectedSize();

    connectToHost(host_,port_);
}

void OutputFileClient::slotRead()
{
	const qint64 size = 64*1024;
	char buf[size];
	quint64 len = 0;

	while((len = soc_->read(buf,size)) > 0)
	{
        // parse+remove "header" in "getf" and "delta" modes from the beginnig of the resulting data
        if (!readStarted_ && (reqType_ == GetFRequest || reqType_ == DeltaRequest)) {
            readStarted_ = true;
            // an error code was sent back
            if (!parseResultHeader(buf, len)) {
                soc_->abort();
                clearResult();
#ifdef UI_OUTPUTFILECLIENT_DEBUG__
                UiLog().dbg() << UI_FN_INFO << "error in parsing reult";
#endif
                Q_EMIT error("transfer failed");
                return;
            }
        }

		std::string err;
		if(!out_->write(buf,len,err))
		{
			soc_->abort();
			Q_EMIT error("write failed");
            return;
		}
		total_ += len;

		if(total_/progressChunk_ > lastProgress_)
		{
			lastProgress_=total_/progressChunk_;

            int prog=0;
            if(expected_ > 0)
            {
                prog=static_cast<int>(100.*static_cast<float>(total_)/static_cast<float>(expected_));
                if(prog>100) prog=100;
                Q_EMIT progress(QString::number(lastProgress_) + "/" + QString::number(expected_/progressChunk_) +
                               " " + progressUnits_  + " transferred",prog);
            }
            else
                Q_EMIT progress(QString::number(lastProgress_) + " " + progressUnits_ + " transferred",prog);
		}
	}
}

bool OutputFileClient::parseResultHeader(char* buf, quint64& len)
{
    //  format:
    //      retCode:modTime:checkSum:result_text
    //
    //      where:
    //          - retCode is a 1-digit number
    //          - modeTime/checkSum can be empty
    //          - result_text can be empty
    //      note:
    //          - in getf mode the retCode can only be "'0"
    //          - in delta mode:
    //                retCode = "0" : delta transferred
    //                retCode = "1" : the whole file transferred
    //          - in delta mode a single "1" message is allowed
    //
    //  e.g.:
    //      0:1662369142:8fssaf:abcd
    //      0:1662369142::abcd
    //      1:::
    //      1

    if ((reqType_ == GetFRequest || reqType_ == DeltaRequest)) {
        if (len <=0) {
            return false;
        }

        // is the whole message is "0" there is no delta to add!!
        if (reqType_ == DeltaRequest && len == 1 && buf[0] == '1') {
            len = 0;
            out_->setSourceModTime(remoteModTime_);
            out_->setSourceCheckSum(remoteCheckSum_);
            return true;
        }
#ifdef UI_OUTPUTFILECLIENT_DETAILD_DEBUG__
        UiLog().dbg() << UI_FN_INFO << "len=" << len << " buf=" << buf ;
#endif
        int pos1=0;
        int pos2=0;

        // get return code
        std::string v;
        if (!getHeaderValue(buf, len, pos1, pos2, v)) {
            return false;
        }
        if (reqType_ == GetFRequest) {
            if (v != "0") {
                return false;
            }
        }
        if (reqType_ == DeltaRequest) {
            // the whole file was sent back
            if (v == "0") {
                deltaPos_ = 0;
                out_->setDeltaContents(false);
            } else if (v != "1") {
                return false;
            }
        }

        // get modtime
        if (pos1 >= pos2 || buf[pos2] != ':') {
            return false;
        }

        // get modtime
        pos1=pos2;
        if (!getHeaderValue(buf, len, pos1, pos2, v)) {
            return false;
        }
        if (!v.empty()) {
            unsigned int uv = 0;
            try {
                uv = std::stoul(v);
            } catch (...) {
                uv = 0;
            }
            out_->setSourceModTime(uv);
        }

        // get checksum
        pos1=pos2;
        if (!getHeaderValue(buf, len, pos1, pos2, v)) {
            return false;
        }
        if (!v.empty()) {
            out_->setSourceCheckSum(v);
        }

        // remove "header" from the bufr
        Q_ASSERT(pos2>=3);
        auto startPos = pos2 + 1;
        Q_ASSERT(startPos>=4);
        for (size_t i=startPos; i < len; i++) {
            buf[i-startPos]=buf[i];
        }
        Q_ASSERT(static_cast<unsigned int>(startPos) <= len);
        len -= (startPos);
    }
    return true;
}

bool OutputFileClient::getHeaderValue(char* buf, quint64 len, int pos1, int& pos2, std::string& val)
{
#ifdef UI_OUTPUTFILECLIENT_DETAILD_DEBUG__
    UiLog().dbg() << UI_FN_INFO << "pos1=" << pos1 << " pos2=" << pos2;
#endif
    val = std::string();
    if (pos1==0 || buf[pos1] == ':') {
        const int dLenMax=200;
        char d[dLenMax];
        int dLen=0;
        if (pos1 > 0) {
            pos1++;
        }
        pos2=pos1;
        for (quint64 i=pos1; i < len && dLen < dLenMax-1; i++) {
            if (buf[i] == ':') {
                pos2 = i;
#ifdef UI_OUTPUTFILECLIENT_DETAILD_DEBUG__
                UiLog().dbg() << " pos2=" << pos2;
#endif
                int dSize = pos2-pos1;
                if (dSize <=0) {
                    return true;
                }
                d[dLen] = '\0';
                val = std::string(d);
#ifdef UI_OUTPUTFILECLIENT_DETAILD_DEBUG__
                UiLog().dbg() << " -1=" << d[dLen-1] << " d=" << d << " dLen=" << dLen;
                UiLog().dbg() << " val=" << val << " dLen=" << dLen;
#endif
                return true;
            }
            d[dLen] = buf[i];
            dLen++;
        }
    }
    return false;;
}

VFile_ptr OutputFileClient::result() const
{
	return out_;
}

void OutputFileClient::setExpectedSize(qint64 v)
{
    expected_=v;
}

int OutputFileClient::maxProgress() const
{
    return expected_/progressChunk_;
}

void OutputFileClient::setDir(VDir_ptr dir)
{
    if(dir_ && dir && dir_.get() == dir.get())
        return;

    dir_=dir;
    estimateExpectedSize();
}

void OutputFileClient::estimateExpectedSize()
{
#ifdef UI_OUTPUTFILECLIENT_DETAILED_DEBUG__
    UI_FN_DBG
#endif
    if(!dir_ || deltaPos_ > 0)
    {
        expected_=0;
        return;
    }

    for(int i=0; i < dir_->count(); i++)
    {
//#ifdef _UI_OUTPUTFILECLIENT_DETAILED_DEBUG
//        UiLog().dbg() << " file: " << dir_->fullName(i);
//#endif
        if(dir_->fullName(i) == remoteFile_)
        {
            expected_=dir_->items().at(i)->size_;
#ifdef UI_OUTPUTFILECLIENT_DETAILED_DEBUG__
            UiLog().dbg() << " expected size=" << expected_;
#endif
            return;
        }
    }

    expected_=0;
#ifdef UI_OUTPUTFILECLIENT_DETAILED_DEBUG__
    UiLog().dbg() << " expected size=" << QString::number(expected_);
#endif
}

void OutputFileClient::slotVersionFinished()
{
#ifdef UI_OUTPUTFILECLIENT_DETAILED_DEBUG__
    UI_FN_DBG
#endif
    Q_ASSERT(versionClient_->versionStatus() != OutputVersionClient::VersionNotFetched);
    getFile(remoteFile_, deltaPos_, remoteModTime_, remoteCheckSum_);
}

void OutputFileClient::slotVersionError(QString errorText)
{
#ifdef UI_OUTPUTFILECLIENT_DETAILED_DEBUG__
    UiLog().dbg() << UI_FN_INFO << "errorText=" << errorText;
#endif
    Q_ASSERT(versionClient_->versionStatus() != OutputVersionClient::VersionNotFetched);
    getFile(remoteFile_, deltaPos_,remoteModTime_, remoteCheckSum_);
}

//===================================
//
// OutputVersionClient
//
//===================================

void OutputVersionClient::timeoutError()
{
    versionStatus_ = VersionFailedToFetch;
}

void OutputVersionClient::slotConnected()
{
    soc_->write("version",7);
    soc_->write("\n",1);
}

void OutputVersionClient::slotError(QAbstractSocket::SocketError err)
{
    if (err == QAbstractSocket::RemoteHostClosedError) {
        //Probably there was no error and the connection was closed because all
        //the data was transferred. Unfortunately the logserver does not send anything
        //at the end of the data transfer.
        if (dataSize_ > 0) {
            versionStatus_ = VersionFetched;
            buildVersion();
            soc_->abort();
            Q_EMIT finished();
            return;
        }
    }

    versionStatus_ = VersionFailedToFetch;
    soc_->abort();
    UiLog().dbg() << UI_FN_INFO << "logserver[" + longName() +
                     " ] could not get version! version is set to " << version_;
    Q_EMIT error(soc_->errorString());
}

void OutputVersionClient::slotRead()
{
    const qint64 size = 64*1024;
    char buf[size];
    qint64 len = 0;

    while((len = soc_->read(buf,size)) > 0)
    {
#ifdef UI_OUTPUTFILECLIENT_DETAILED_DEBUG__
        UiLog().dbg() << UI_FN_INFO << "buf=" << buf;
#endif

        std::string err;
        if(dataSize_ + len  < maxDataSize_)
        {
            memcpy(data_+dataSize_,buf,len);
            dataSize_+=len;
            data_[dataSize_] = '\0';
        } else {
            soc_->abort();
            Q_EMIT error("write failed");
        }
    }
}

void OutputVersionClient::getVersion()
{
    UiLog().dbg() << UI_FN_INFO << "logserver=" << longName();
    versionStatus_ = VersionBeingFetched;
    version_ = 0;
    dataSize_ = 0;
    connectToHost(host_,port_);
}

void OutputVersionClient::buildVersion()
{
    try {
        version_ = std::stoi(data_);
    } catch (...) {
        version_ = 0;
    }

    if (version_ != 2) {
        version_ = 0;
    }
    UiLog().dbg() << UI_FN_INFO << "logserver[" + longName() + "] version=" <<
                          version_;
}
