/***************************** LICENSE START ***********************************

 Copyright 2012 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/

#include "MvQOgcRequest.h"

#include <QDebug>
#include <QPair>

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <QtCore5Compat/QRegExp>
#else
#include <QRegExp>
#endif


int MvQGetMapRequest::maxFrame_ = 128;

/*
 Warning!!!

 QDateTime::fromString(QString,Qt::ISODate)

 is not able to handle fractional dates like 1991-01 or
 1991-01-01T02:02. So we need to figure aou the actual date
 format and that has to be used in the second argument of
 this function evereywhere in the code!!
*/


QString MvQOgcRequest::item(QString id) const
{
    QString sid = getStoredId(id);
    if (!sid.isEmpty()) {
        return requestMap_[sid];
    }
    else {
        return {};
    }
}

void MvQOgcRequest::setItem(QString id, QString value)
{
    QString sid = getStoredId(id);
    if (!sid.isEmpty()) {
        QString sFrom = sid + "=" + requestMap_[sid];
        QString sTo = sid + "=" + value;
        request_.replace(sFrom, sTo);
        parse();
    }
}

void MvQOgcRequest::removeItem(QString id)
{
    QString sid = getStoredId(id);
    if (!sid.isEmpty()) {
        QString s = sid + "=" + requestMap_[sid];
        if (!request_.endsWith(s)) {
            s += "&";
        }
        request_.remove(s);

        if (request_.endsWith("&"))
            request_.chop(1);

        parse();
    }
}


QString MvQOgcRequest::getStoredId(QString id) const
{
    QStringList keys = requestMap_.keys();

    if (keys.contains(id)) {
        return id;
    }
    else if (keys.contains(id, Qt::CaseInsensitive)) {
        foreach (QString s, keys) {
            if (s != id && id.compare(s, Qt::CaseInsensitive) == 0) {
                return s;
            }
        }
    }

    return {};
}


void MvQOgcRequest::clear()
{
    request_.clear();
    requestMap_.clear();
}

void MvQOgcRequest::setRequest(QString r)
{
    clear();
    request_ = r;
    parse();
}

void MvQOgcRequest::parse()
{
    requestMap_.clear();

    if (request_.isEmpty())
        return;

    QStringList lstBasic = request_.split("?");
    if (lstBasic.count() < 2) {
        return;
    }

    QStringList lst = lstBasic[1].split("&");
    foreach (QString str, lst) {
        QStringList par = str.split("=");
        if (par.count() >= 2) {
            requestMap_[par[0]] = str.section("=", 1);
        }
    }
}

QString MvQOgcRequest::guessUrl() const
{
    QStringList lst = request_.split("?");
    if (lst.count() > 0)
        return lst[0];
    else
        return {};
}

QString MvQOgcRequest::timeToString(QString t)
{
    if (t.startsWith("P")) {
        int s = periodToSeconds(t);
        // There are minutes or seconds
        if (s % 3600 != 0) {
            t.remove(0, 1);
            return t;
        }
        else {
            return QString::number(s / 3600);
        }
    }
    else {
        // QDateTime d=QDateTime::fromString(correctDateTime(t),Qt::ISODate);
        QDateTime d = QDateTime::fromString(t, timeFormat(t));
        // qDebug() << "timetostring" << t << d << timeFormat(t);

        return d.toString("yyyyMMddhhmmss");
    }
}

// We need this to overcome a Qt bug. If the seconds are missing
// from an ISO 8601 time string qt QDateTime incorrectly interpret the time part of
// the string. So in this case the we add the seconds to the string!

QString MvQOgcRequest::correctDateTime(QString d)
{
    QString res = d;

    bool tzone = d.endsWith("Z");
    d.remove("Z");

    QStringList lst = d.split("T");

    // qDebug() << "corr" << lst;

    if (lst.count() == 2) {
        QStringList t = lst[1].split(":");
        if (t.count() == 2) {
            res = lst[0] + "T" + t[0] + ":" + t[1] + ":00";
            // qDebug() << "corr" << res;

            if (tzone) {
                res += "Z";
            }
        }
    }
    // qDebug() << "corr" << res;
    return res;
}

void MvQOgcRequest::decodePeriod(const QString& period, QMap<QString, int>& res)
{
    QString pDate, pTime;

    // Test
    if (period.startsWith("P") == false)
        return;

    QStringList lst = period.split("T");
    pDate = lst[0];
    if (lst.count() > 1) {
        pTime = lst[1];
    }

    // qDebug() << period << pDate << pTime;

    if (pDate.isEmpty() == false) {
        // Year
        QRegExp rx("(\\d+Y)");
        if (rx.indexIn(pDate) > -1) {
            res["year"] = rx.cap(1).remove("Y").toInt();
            // qDebug() << "year: " << res["year"];
        }

        // Month
        rx = QRegExp("(\\d+M)");
        if (rx.indexIn(pDate) > -1) {
            res["month"] = rx.cap(1).remove("M").toInt();
            // qDebug() << "Month: " << res["month"];
        }

        // Day
        rx = QRegExp("(\\d+D)");
        if (rx.indexIn(pDate) > -1) {
            res["day"] = rx.cap(1).remove("D").toInt();
            // qDebug() << "Day: " << res["day"];
        }
    }

    if (pTime.isEmpty() == false) {
        // Hour
        QRegExp rx("(\\d+H)");
        if (rx.indexIn(pTime) > -1) {
            res["hour"] = rx.cap(1).remove("H").toInt();
            // qDebug() << "hour: " << res["hour"];
        }

        // Minute
        rx = QRegExp("(\\d+M)");
        if (rx.indexIn(pTime) > -1) {
            res["minute"] = rx.cap(1).remove("M").toInt();
            // qDebug() << "Month: " << res["minute"];
        }

        // Minute
        rx = QRegExp("(\\d+S)");
        if (rx.indexIn(pTime) > -1) {
            res["sec"] = rx.cap(1).remove("S").toInt();
            // qDebug() << "Second: " << res["sec"];
        }
    }
}

void MvQOgcRequest::addPeriod(QDateTime& dt, const QMap<QString, int>& period)
{
    if (period.contains("year")) {
        dt = dt.addYears(period["year"]);
        // qDebug() << d;
    }

    if (period.contains("month")) {
        dt = dt.addMonths(period["month"]);
        // qDebug() << d;
    }

    if (period.contains("day")) {
        dt = dt.addDays(period["day"]);
        // qDebug() << d;
    }

    if (period.contains("hour")) {
        dt = dt.addSecs(period["hour"] * 3600);
        // qDebug() << d;
    }
    if (period.contains("minute")) {
        dt = dt.addSecs(period["minute"] * 60);
        // qDebug() << d;
    }
    if (period.contains("sec")) {
        dt = dt.addSecs(period["sec"]);
        // qDebug() << d;
    }
}

int MvQOgcRequest::periodToSeconds(QString t)
{
    QMap<QString, int> period;
    decodePeriod(t, period);

    QDateTime dtBase = QDateTime::currentDateTime();
    QDateTime dtEnd = dtBase;
    addPeriod(dtEnd, period);
    return dtBase.secsTo(dtEnd);
}

QString MvQOgcRequest::timeFormat(QString d)
{
    QString tf;

    bool tzone = d.endsWith("Z");
    d.remove("Z");

    QStringList lst = d.split("T");


    if (lst.count() > 0) {
        QStringList dlst = lst[0].split("-");

        if (dlst.count() > 0)
            tf += "yyyy";

        if (dlst.count() > 1)
            tf += "-MM";

        if (dlst.count() == 3)
            tf += "-dd";
    }


    if (lst.count() > 1) {
        QStringList tlst = lst[1].split(":");

        if (tlst.count() > 0)
            tf += "Thh";

        if (tlst.count() > 1)
            tf += ":mm";

        if (tlst.count() == 3) {
            if (!tlst[2].contains(".")) {
                tf += ":ss";
            }
            else {
                tf += ":ss.zzz";
            }
        }
    }

    if (tzone)
        tf += "Z";

    return tf;
}

//====================================================
MvQGetMapRequest::MvQGetMapRequest()
{
    stdParams_ << "REQUEST"
               << "SERVICE"
               << "VERSION"
               << "LAYERS"
               << "STYLES"
               << "WIDTH"
               << "HEIGHT"
               << "FORMAT"
               << "TRANSPARENT"
               << "TIME"
               << "ELEVATION"
               << "CRS"
               << "SRS"
               << "BBOX";
}


void MvQGetMapRequest::setRequest(QString r, QStringList timeDimName)
{
    timeDimName_ = timeDimName;

    clear();
    request_ = r;
    parse();
}

void MvQGetMapRequest::editRequest(QString r)
{
    // timeDimName_=timeDimName;

    clear();
    request_ = r;
    parse();
}

void MvQGetMapRequest::clear()
{
    MvQOgcRequest::clear();

    requestFrame_.clear();
    outFile_.clear();
}

void MvQGetMapRequest::parse()
{
    requestFrame_.clear();
    outFile_.clear();

    MvQOgcRequest::parse();

    // Parse for time dimension

    QList<QPair<QString, QStringList> > frameLst;
    QList<int> indexRange, indexState;

    // qDebug() << "timeDimName_" << timeDimName_;

    foreach (QString dimName, dims()) {
        if (requestMap_.contains(dimName)) {
            QStringList values;

            bool temporal = timeDimName_.contains(dimName, Qt::CaseInsensitive);
            foreach (QString item, requestMap_[dimName].split(",")) {
                if (item.isEmpty() == false) {
                    if (temporal) {
                        decodeTimeDimItem(item, values);
                    }
                    else {
                        decodeDimItem(item, values);
                    }
                }
            }

            if (values.count() > 0) {
                frameLst << qMakePair(dimName, values);
                indexRange << values.count();
                indexState << 0;
            }
        }
    }

    // qDebug() << "frames" << frameLst;

    if (frameLst.count() > 0) {
        bool moreIndex = true;
        int currentPos = indexState.count() - 1;

        while (moreIndex) {
            // qDebug() << "index" << indexState;
            QString req = request_;

            // qDebug() << req;
            for (int i = 0; i < indexState.count(); i++) {
                QString dimName = frameLst[i].first;
                QString value = frameLst[i].second.at(indexState[i]);

                QString frameReq = dimName + "=" + requestMap_[dimName];
                // qDebug() << "frameReq" << frameReq;
                req.replace(frameReq, dimName + "=" + value);
            }

            requestFrame_ << req;
            outFile_ << " ";
            moreIndex = getNextVariation(currentPos, indexRange, indexState, true);

            if (requestFrame_.size() >= maxFrame_) {
                break;
            }
        }
    }
    else {
        requestFrame_ << request_;
        outFile_ << " ";
    }

    /*if(requestMap_.contains("TIME"))
    {
        QString timeReq="TIME=" + requestMap_["TIME"];
        foreach(QString time, requestMap_["TIME"].split("/"))
        {
            QString req=request_;
            req.replace(timeReq,"TIME=" + time);
            requestStep_ << req;
            outFile_ <<  " ";

        }
        time_=requestMap_["TIME"].split("/");
    }*/
    // else
    //{
    //	requestStep_ << request_;
    //	outFile_ <<  " ";
    // }
}

bool MvQGetMapRequest::getNextVariation(int& currentPos, QList<int> ranges, QList<int>& currentState, bool forward)
{
    // qDebug() << currentPos << currentState << ranges;

    if (currentState[currentPos] == ranges[currentPos] - 1) {
        if (currentPos == 0) {
            return false;
        }
        if (forward && currentPos != ranges.count() - 1) {
            currentPos++;
            return getNextVariation(currentPos, ranges, currentState, true);
        }
        else {
            currentPos--;
            for (int i = currentPos + 1; i < currentState.count(); i++) {
                currentState[i] = 0;
            }
            return getNextVariation(currentPos, ranges, currentState, false);
        }
    }
    else {
        if (currentPos == 0 || currentPos == ranges.count() - 1 || forward == false) {
            currentState[currentPos]++;
            if (currentPos != ranges.count() - 1) {
                currentPos++;
            }
        }
        else {
            if (currentPos != ranges.count() - 1) {
                currentPos++;
            }

            return getNextVariation(currentPos, ranges, currentState, true);
        }

        return true;
    }

    return false;
}

void MvQGetMapRequest::setOutFile(int index, QString path)
{
    if (index >= 0 && index < outFile_.count()) {
        outFile_[index] = path;
    }
}

QString MvQGetMapRequest::outFile(int index)
{
    if (index >= 0 && index < outFile_.count()) {
        return outFile_[index];
    }
    return {};
}

QString MvQGetMapRequest::requestFrame(int index)
{
    if (index >= 0 && index < requestFrame_.count()) {
        return requestFrame_[index];
    }
    return {};
}

void MvQGetMapRequest::dimValuesForFrame(QMap<QString, QString>& val, int frame)
{
    if (frame < 0 || frame >= requestFrame_.count()) {
        return;
    }

    MvQOgcRequest r;
    r.setRequest(requestFrame_[frame]);

    foreach (QString s, dims()) {
        val[s] = r.item(s);
    }
}

bool MvQGetMapRequest::isTimeDim(QString name)
{
    return (timeDimName_.contains(name, Qt::CaseInsensitive) ||
            name.compare("TIME", Qt::CaseInsensitive) == 0);
}


/*void MvQGetMapRequest::timeDimValuesForFrame(QMap<QString,QString>& val, int step)
{
    if(step  < 0 || step >= requestStep_.count())
    {
        return;

    }

    MvQOgcRequest r;
    foreach(QString s,timeDimName_)
    {
        r.setRequest(requestStep_[step]);
        val[s]=r.item(s);
    }
}

void MvQGetMapRequest::nonTimeDimValues(QMap<QString,QString>& val)
{
    if(requestMap_.contains("ELEVATION"))
    {
        val["ELEVATION"]=requestMap_["ELEVATION"];
    }
    foreach(QString s,requestMap_.keys())
    {
        if(s.startsWith("DIM_") && timeDimName_.indexOf(s) == -1 )
        {
            val[s]=requestMap_[s];
        }
    }
}*/

QString MvQGetMapRequest::crs() const
{
    if (requestMap_.contains("CRS")) {
        return requestMap_["CRS"];
    }
    else if (requestMap_.contains("SRS")) {
        return requestMap_["SRS"];
    }

    return {};
}

QString MvQGetMapRequest::minX()
{
    if (requestMap_.contains("BBOX")) {
        QString bb = requestMap_["BBOX"];
        QStringList lst = bb.split(",");
        if (lst.count() == 4) {
            return lst[0];
        }
    }
    return {};
}

QString MvQGetMapRequest::maxX()
{
    if (requestMap_.contains("BBOX")) {
        QString bb = requestMap_["BBOX"];
        QStringList lst = bb.split(",");
        if (lst.count() == 4) {
            return lst[2];
        }
    }
    return {};
}

QString MvQGetMapRequest::minY()
{
    if (requestMap_.contains("BBOX")) {
        QString bb = requestMap_["BBOX"];
        QStringList lst = bb.split(",");
        if (lst.count() == 4) {
            return lst[1];
        }
    }
    return {};
}

QString MvQGetMapRequest::maxY()
{
    if (requestMap_.contains("BBOX")) {
        QString bb = requestMap_["BBOX"];
        QStringList lst = bb.split(",");
        if (lst.count() == 4) {
            return lst[3];
        }
    }
    return {};
}


QString MvQGetMapRequest::layers()
{
    if (requestMap_.contains("LAYERS")) {
        return requestMap_["LAYERS"];
    }

    return {};
}

void MvQGetMapRequest::decodeTimeDimItem(QString item, QStringList& values)
{
    QStringList lst = item.split("/");

    // qDebug() << "Time:" << lst;


    // QDateTime dm=QDateTime::fromString(lst[0],Qt::ISODate);
    // qDebug() << dm.toString("-yyyy") << dm.toString("MM") << dm.toString("dd")  <<
    // dm.toString("hh") << dm.toString("mm") << dm.toString("ss");

    // Period
    if (lst.count() == 3) {
        QString tf = timeFormat(lst[0]);

        // qDebug() << "tf" << tf;

        QDateTime startDate = QDateTime::fromString(lst[0], tf);
        QDateTime endDate = QDateTime::fromString(lst[1], tf);

        // QDateTime startDate=QDateTime::fromString(MvQOgcRequest::correctDateTime(lst[0]),Qt::ISODate);
        // QDateTime endDate=QDateTime::fromString(MvQOgcRequest::correctDateTime(lst[1]),Qt::ISODate);

        // qDebug() <<  startDate << endDate;

        QMap<QString, int> period;
        MvQOgcRequest::decodePeriod(lst[2], period);

        // qDebug() << period;

        int cnt = 0;
        QDateTime dt = startDate;
        while (dt <= endDate && cnt < maxFrame_) {
            // QString s=dt.toString(Qt::ISODate);
            // s+="Z";

            QString s = dt.toString(tf);

            values.push_back(s);
            MvQOgcRequest::addPeriod(dt, period);
            cnt++;
            if (dt <= startDate)
                break;
        }

        // qDebug() << "time: " << startDate << endDate << period;
        // qDebug() << "value_" << values;
    }
    else if (lst.count() == 1 && lst[0].isEmpty() == false) {
        values.push_back(item);
    }
}

void MvQGetMapRequest::decodeDimItem(QString item, QStringList& values)
{
    QStringList lst = item.split("/");

    // qDebug() << "Decode other dim:" << lst;

    if (lst.count() == 3) {
        float minVal = lst[0].toFloat();
        float maxVal = lst[1].toFloat();
        float stepVal = lst[2].toFloat();

        float v = minVal;
        int num = 0;
        while (v <= maxVal && num < 100) {
            QString s = QString::number(v);
            values << s;
            v += stepVal;
            if (v <= minVal)
                break;
        }
    }
    else if (lst.count() == 1 && lst[0].isEmpty() == false) {
        values.push_back(item);
    }
}


QStringList MvQGetMapRequest::dims() const
{
    QStringList dims;
    foreach (QString s, requestMap_.keys()) {
        if (s.startsWith("DIM_", Qt::CaseInsensitive) || timeDimName_.indexOf(s) != -1 ||
            s.compare("ELEVATION", Qt::CaseInsensitive) == 0 || s.compare("TIME", Qt::CaseInsensitive) == 0) {
            dims << s;
        }
    }

    return dims;
}


QString MvQGetMapRequest::item(QString id) const
{
    // qDebug() << "id" << id;

    if (id == "mv_key_url") {
        return guessUrl();
    }
    else if (id == "mv_key_crs") {
        return crs();
    }
    else if (id == "mv_key_tdim") {
        QStringList lst;
        foreach (QString s, timeDimName_) {
            if (s.compare("TIME", Qt::CaseInsensitive) != 0) {
                lst << s + "=" + requestMap_[s];
            }
        }
        return lst.join("&");
    }
    else if (id == "mv_key_otherdim") {
        QStringList lst;
        foreach (QString s, requestMap_.keys()) {
            if (s.startsWith("DIM_", Qt::CaseInsensitive) && timeDimName_.indexOf(s) == -1) {
                lst << s + "=" + requestMap_[s];
            }
        }

        // qDebug() << lst.join("&");
        return lst.join("&");
    }
    else if (id == "mv_key_extrapar") {
        QStringList lst;
        QStringList dimLst = dims();
        foreach (QString s, requestMap_.keys()) {
            if (!stdParams_.contains(s, Qt::CaseInsensitive) &&
                !dimLst.contains(s, Qt::CaseInsensitive)) {
                lst << s + "=" + requestMap_[s];
            }
        }
        return lst.join("&");
    }
    else {
        return MvQOgcRequest::item(id);
    }
}

void MvQGetMapRequest::setItem(QString id, QString value)
{
    if (id.compare("VERSION", Qt::CaseInsensitive) == 0) {
        QString s = item("VERSION");
        if (!s.isEmpty()) {
            if (value < "1.3.0") {
                QString crsId = MvQOgcRequest::getStoredId("CRS");
                if (!crsId.isEmpty()) {
                    request_.replace(crsId + "=", "SRS=");
                    parse();
                }
            }
            else {
                QString srsId = MvQOgcRequest::getStoredId("SRS");
                if (!srsId.isEmpty()) {
                    request_.replace(srsId + "=", "CRS=");
                    parse();
                }
            }
        }

        MvQOgcRequest::setItem(id, value);
    }
    if (id == "mv_key_url") {
        QString s = guessUrl();
        request_.replace(s, value);
    }
    else if (id == "mv_key_crs") {
        QString s = item("VERSION");
        if (s.isEmpty()) {
            if (requestMap_.contains("CRS")) {
                MvQOgcRequest::setItem("CRS", value);
            }
            else if (requestMap_.contains("SRS")) {
                MvQOgcRequest::setItem("SRS", value);
            }
        }
        else if (s < "1.3.0")
            MvQOgcRequest::setItem("SRS", value);
        else
            MvQOgcRequest::setItem("CRS", value);
    }
    else if (id == "mv_key_extrapar") {
        setExtraPar(value);
    }
    else if (id == "mv_key_tdim") {
        setDims(value, true);
    }
    else if (id == "mv_key_otherdim") {
        setDims(value, false);
    }
    else {
        MvQOgcRequest::setItem(id, value);
    }
}

void MvQGetMapRequest::setDims(QString value, bool temporal)
{
    // qDebug() << "setDims" << value << temporal;

    QMap<QString, bool> dimLst;
    if (temporal) {
        foreach (QString s, dims()) {
            if (s.startsWith("DIM_", Qt::CaseInsensitive) && timeDimName_.indexOf(s) != -1) {
                dimLst[s] = false;
            }
        }
    }
    else {
        foreach (QString s, dims()) {
            if (s.startsWith("DIM_", Qt::CaseInsensitive) && timeDimName_.indexOf(s) == -1) {
                dimLst[s] = false;
            }
        }
    }

    QStringList dimNames = dimLst.keys();

    // qDebug() << dimNames;

    QStringList lst = value.split("&");
    for (int i = 0; i < lst.count(); i++) {
        QStringList items = lst[i].split("=");
        if (items.count() > 1) {
            if (items[0].startsWith("DIM_", Qt::CaseInsensitive)) {
                QString dim = items[0];
                QString dimValue = lst[i].right(lst[i].count() - dim.count() - 1);
                QString sid = getStoredId(dim);

                // qDebug() << "setDim" << dim << dimValue;

                if (!sid.isEmpty() && dimNames.contains(sid, Qt::CaseInsensitive)) {
                    MvQOgcRequest::setItem(dim, dimValue);
                    dimLst[sid] = true;
                }
                else {
                    if (temporal)
                        timeDimName_ << dim;

                    request_ += "&" + lst[i];
                    parse();
                }
            }
        }
    }

    // qDebug() << dimLst;

    // Remove unsepcified dims
    foreach (QString dim, dimLst.keys()) {
        if (dimLst[dim] == false) {
            removeItem(dim);
        }
    }
}

void MvQGetMapRequest::setExtraPar(QString value)
{
    QMap<QString, bool> pars;

    foreach (QString s, requestMap_.keys()) {
        if (!stdParams_.contains(s, Qt::CaseInsensitive) &&
            !s.startsWith("DIM_", Qt::CaseInsensitive)) {
            pars[s] = false;
        }
    }

    QStringList lst = value.split("&");
    for (int i = 0; i < lst.count(); i++) {
        QStringList items = lst[i].split("=");
        if (items.count() > 1) {
            if (!stdParams_.contains(items[0], Qt::CaseInsensitive) &&
                !items[0].startsWith("DIM_", Qt::CaseInsensitive)) {
                QString par = items[0];
                QString parValue = lst[i].right(lst[i].count() - par.count() - 1);
                QString sid = getStoredId(par);

                // qDebug() << "setPar" << par << parValue;

                if (!sid.isEmpty() && pars.contains(sid)) {
                    MvQOgcRequest::setItem(par, parValue);
                    pars[sid] = true;
                }
                else {
                    request_ += "&" + lst[i];
                    parse();
                }

                pars[par] = true;
            }
        }
    }

    // Remove unsepcified pars
    foreach (QString par, pars.keys()) {
        if (pars[par] == false) {
            removeItem(par);
        }
    }
}

void MvQGetMapRequest::removeItem(QString id)
{
    MvQOgcRequest::removeItem(id);

    foreach (QString s, timeDimName_) {
        if (s.compare(id, Qt::CaseInsensitive) == 0) {
            timeDimName_.removeAll(s);
        }
    }
}
