/***************************** 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 "MvFlextra.h"

#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <vector>

#include "fstream_mars_fix.h"

#include "MvDate.h"

//This is how flextra writes out the trajectory output in trajout.f
//format(i9,2f9.4,f7.4,f7.1,2f8.1,f8.3,f6.1,e9.2)
static const int fieldWidthArr[] = {9, 9, 9, 7, 7, 8, 8, 8, 6, 9};
static std::vector<int> fieldWidthVec(fieldWidthArr, fieldWidthArr + sizeof(fieldWidthArr) / sizeof(fieldWidthArr[0]));

//=========================================================
//
//  MvFlextraItem
//
//=========================================================

MvFlextraItem::MvFlextraItem(int id) :
    id_(id)
{
    pointKeyIndex_["date"]         = 0;
    pointKeyIndex_["elapsedTime"]  = 1;
    pointKeyIndex_["lon"]          = 2;
    pointKeyIndex_["lat"]          = 3;
    pointKeyIndex_["eta"]          = 4;
    pointKeyIndex_["pres"]         = 5;
    pointKeyIndex_["z"]            = 6;
    pointKeyIndex_["zAboveGround"] = 7;
    pointKeyIndex_["pv"]           = 8;
    pointKeyIndex_["theta"]        = 9;

    pointKeyType_["date"]         = DateType;
    pointKeyType_["elapsedTime"]  = IntType;
    pointKeyType_["lon"]          = FloatType;
    pointKeyType_["lat"]          = FloatType;
    pointKeyType_["eta"]          = FloatType;
    pointKeyType_["pres"]         = FloatType;
    pointKeyType_["z"]            = FloatType;
    pointKeyType_["zAboveGround"] = FloatType;
    pointKeyType_["pv"]           = FloatType;
    pointKeyType_["theta"]        = FloatType;
}

std::string MvFlextraItem::metaData(const std::string& key) const
{
    auto it = metaData_.find(key);
    if (it != metaData_.end())
        return it->second;
    else
        return std::string();
}

void MvFlextraItem::pointData(const std::string& key, std::vector<std::string>& data, MvFlextraItem::DataType& type) const
{
    auto it = pointKeyIndex_.find(key);
    if (it != pointKeyIndex_.end()) {
        int id = it->second;
        for (const auto& point : points_) {
            if (id >= 0 && id < static_cast<int>(point.size())) {
                data.push_back(point.at(id));
            }
            else {
                data.push_back(std::string());
            }
        }
    }

    auto itT = pointKeyType_.find(key);
    if (itT != pointKeyType_.end())
        type = itT->second;
}

//=========================================================
//
//  MvFlextra
//
//=========================================================

MvFlextraBlock::MvFlextraBlock()
{
    direction_     = "Forward";
    constantStep_  = true;
    uncertaintyTr_ = false;
}

MvFlextraBlock::~MvFlextraBlock()
{
    for (auto& item : items_) {
        delete item;
    }
}


void MvFlextraBlock::decode(std::string fileName)
{
    std::ifstream in(fileName.c_str());
    std::string line;
    decode(in, line);
    in.close();
}

void MvFlextraBlock::decode(std::ifstream& in, std::string& line)
{
    bool headerPassed      = false;
    bool firstRowPassed    = false;
    bool directionDetected = false;

    std::map<std::string, std::string> trTypeName;
    trTypeName["3-DIMENSIONAL"] = "3D";

    std::map<std::string, std::string> info;
    MvDate baseDate;
    std::string metaDataText;
    std::string keySuffix;
    std::vector<std::string> metaData;
    std::map<std::string, std::vector<std::string> > trInfo;
    std::map<std::string, std::string> constInfo;
    MvFlextraItem* item = 0;

    int cnt = 0;

    //Read meta-data first

    metaData_["Metview::type"] = "FLEXTRA";


    while (getline(in, line)) {
        if (line.find("Number of header lines") != std::string::npos) {
            break;
        }

        else if (line.find("*") != std::string::npos && headerPassed == false) {
            std::string value, valueFrom, valueTo;
            if (parseHeaderLine(line, "TYPE OF TRAJECTORIES:", value)) {
                if (trTypeName.find(value) != trTypeName.end()) {
                    metaData_["type"] = trTypeName[value];
                }
                else {
                    metaData_["type"] = value;
                }
            }
            else if (parseHeaderLine(line, "INTEGRATION SCHEME:", value))
                metaData_["integration"] = value;
            else if (parseHeaderLine(line, "INTERPOLATION METHOD:", value))
                metaData_["interpolation"] = value;
            else if (parseHeaderLine(line, "SPATIAL CFL CRITERION:", value))
                metaData_["cflSpace"] = value;
            else if (parseHeaderLine(line, "TEMPORAL CFL CRITERION:", value))
                metaData_["cflTime"] = value;
            else if (parseHeaderLine(line, "START POINT COMMENT:", value)) {
                metaData_["startComment"] = value;
                metaData_["name"]         = value;
                comment_                  = value;
            }
            else if (parseHeaderLine(line, "MODEL RUN COMMENT:", value))
                metaData_["runComment"] = value;
            else if (parseHeaderLine(line, "NORMAL INTERVAL BETWEEN WIND FIELDS:", value))
                metaData_["normalInterval"] = value;
            else if (parseHeaderLine(line, "MAXIMUM INTERVAL BETWEEN WIND FIELDS:", value))
                metaData_["maxInterval"] = value;
            else if (parseHeaderLine(line, "LONGITUDE RANGE:", valueFrom, valueTo)) {
                metaData_["west"] = valueFrom;
                metaData_["east"] = valueTo;
                if (parseHeaderLine(line, "GRID DISTANCE:", value)) {
                    metaData_["dx"] = value;
                }
            }
            else if (parseHeaderLine(line, "LATITUDE RANGE:", valueFrom, valueTo)) {
                metaData_["south"] = valueFrom;
                metaData_["north"] = valueTo;
                if (parseHeaderLine(line, "GRID DISTANCE:", value)) {
                    metaData_["dy"] = value;
                }
            }
        }
        else if (line.find("DATE:") != std::string::npos) {
            cnt++;

            item = new MvFlextraItem(cnt);
            items_.push_back(item);

            std::ostringstream sst_cnt;
            sst_cnt << cnt;

            //Parse "Date:" line
            std::istringstream sst(line);
            std::string s, sDate, sTime, sIndex;
            sst >> s >> sDate >> s >> sTime >> s >> s >> sIndex;

            s     = sDate;
            sDate = s.substr(0, 4) + "-" + s.substr(4, 2) + "-" + s.substr(6, 2);

            s = sTime;
            if (s.size() < 6) {
                s = std::string(6 - s.size(), '0').append(s);
            }
            sTime = s.substr(0, 2) + ":" + s.substr(3, 2) + ":" + s.substr(4, 2);

            s        = sDate + " " + sTime;
            baseDate = MvDate(s.c_str());

            item->addMetaData("startDate", sDate);
            item->addMetaData("startTime", sTime);
            item->addMetaData("id", sst_cnt.str());
            item->addMetaData("stopIndex", sIndex);

            //Initialise the rest of the meta-data
            item->addMetaData("startLat", "-");
            item->addMetaData("startLon", "-");
            item->addMetaData("startEta", "-");
            item->addMetaData("startPres", "-");
            item->addMetaData("startZ", "-");
            item->addMetaData("startZAboveGround", "-");
            item->addMetaData("startPv", "-");
            item->addMetaData("startTheta", "-");

            //Get header line
            getline(in, line);

            headerPassed   = true;
            firstRowPassed = false;
        }
        else if (headerPassed) {
            if (!firstRowPassed) {
                std::istringstream issMeta(line);
                std::string s, lon, lat, eta, pres, z, z_oro, pv, theta;
                issMeta >> s >> lon >> lat >> eta >> pres >> z >> z_oro >> pv >> theta;
                item->addMetaData("startLat", lat);
                item->addMetaData("startLon", lon);
                item->addMetaData("startEta", eta);
                item->addMetaData("startPres", pres);
                item->addMetaData("startZ", z);
                item->addMetaData("startZAboveGround", z_oro);
                item->addMetaData("startPv", pv);
                item->addMetaData("startTheta", theta);

                firstRowPassed = true;
            }

            if (!directionDetected) {
                std::istringstream iss(line);
                std::string s;
                iss >> s;

                if (s != "0") {
                    if (s.find("-") != std::string::npos) {
                        direction_ = "Backward";
                    }
                    metaData_["direction"] = direction_;
                    directionDetected      = true;
                }
            }

            //Parse the line with the data taking the widths form the
            //original flextra ouput fortran FORMAT statement
            std::vector<std::string> pointData;
            int pos = 0;
            for (size_t i = 0; i < fieldWidthVec.size(); i++) {
                pointData.push_back(line.substr(pos, fieldWidthVec[i]));
                pos += fieldWidthVec[i];
            }

            std::istringstream iss(pointData[0]);
            double d;
            iss >> d;

            MvDate md = baseDate;
            md += d / 86400.;

            char buf[100];
            md.Format("yyyy-mm-dd HH:MM:SS", buf);

            pointData.insert(pointData.begin(), std::string(buf));

            item->addPoint(pointData);
        }
    }


    std::ostringstream ssNum;
    ssNum << items_.size();
    metaData_["trNum"] = ssNum.str();

    //Find meta-data that is the same for all the trajectories
    //Check lat-lon
    if (isMetaDataConst("startLat") && isMetaDataConst("startLon")) {
        metaData_["startLat"] = items_.at(0)->metaData("startLat");
        metaData_["startLon"] = items_.at(0)->metaData("startLon");
    }

    if (isMetaDataConst("startDate") && isMetaDataConst("startTime")) {
        metaData_["startDate"] = items_.at(0)->metaData("startDate");
        metaData_["startTime"] = items_.at(0)->metaData("startTime");
    }

    std::string keys[6] = {"startEta", "startPres", "startZ", "startZAboveGround", "startPv", "startTheta"};
    for (auto s : keys) {
        if (isMetaDataConst(s)) {
            metaData_[s] = items_.at(0)->metaData(s);
        }
    }

    //Check the timestep type (constant ot non-constant)
    checkStepType();


    //in.close();
}


void MvFlextraBlock::writeAll(const std::string& outFile, int& metaDataCnt)
{
    std::ofstream out;
    out.open(outFile.c_str());

    metaDataCnt = 0;
    writeMetaData(out, metaDataCnt);

    //Write out trajectory data
    for (auto item : items_) {
        for (const auto& P : item->points()) {
            out << item->id();
            for (const auto& S : P) {
                out << "," << S;
            }
            out << std::endl;
        }
    }

    out.close();
}

void MvFlextraBlock::writeHighlightPoints(const std::string& outFile, int& metaDataCnt, std::string periodHour)
{
    std::ofstream out;
    out.open(outFile.c_str());

    metaDataCnt = 0;
    writeMetaData(out, metaDataCnt);

    //Write out trajectory data
    for (auto item : items_) {
        for (auto itP = item->points().begin(); itP != item->points().end(); itP++) {
            const std::vector<std::string>& data = (*itP);

            if (data.size() > 0) {
                MvDate md(data.at(0).c_str());
                if (md.Minute() == 0 && md.Second() == 0) {
                    int h = md.Hour();

                    bool found = false;
                    if (periodHour == "6h")
                        found = (h % 6 == 0);
                    else if (periodHour == "12h")
                        found = (h % 12 == 0);
                    else if (periodHour == "24h")
                        found = (h == 0);
                    else if (periodHour == "48h" && data.size() > 1) {
                        std::istringstream iss(data[1]);
                        int isec;
                        iss >> isec;
                        found = (h == 0 && (isec / 86400) % 2 == 0);
                    }

                    if (found) {
                        out << item->id() << "," << data[0] << "," << data[2] << "," << data[3] << std::endl;
                    }
                }
            }
        }
    }

    out.close();
}


void MvFlextraBlock::writeLabels(const std::string& outFile, int& metaDataCnt, std::string periodHour, std::vector<std::string>& labels)
{
    std::ofstream out;
    out.open(outFile.c_str());

    metaDataCnt = 0;
    writeMetaData(out, metaDataCnt);

    //Write out trajectory data
    for (auto item : items_) {
        for (const auto& data : item->points()) {
            if (data.size() > 0) {
                MvDate md(data.at(0).c_str());
                if (md.Minute() == 0 && md.Second() == 0) {
                    int h = md.Hour();

                    bool found = false;
                    if (periodHour == "6h")
                        found = (h % 6 == 0);
                    else if (periodHour == "12h")
                        found = (h % 12 == 0);
                    else if (periodHour == "24h")
                        found = (h == 0);
                    else if (periodHour == "48h" && data.size() > 1) {
                        std::istringstream iss(data[1]);
                        int isec;
                        iss >> isec;
                        found = (h == 0 && (isec / 86400) % 2 == 0);
                    }

                    if (found) {
                        char buf1[100];
                        md.Format("dd:HH", buf1);
                        out << "1"
                            << "," << data[0] << "," << data[2] << "," << data[3] << std::endl;
                        labels.push_back(buf1);
                    }
                }
            }
        }
    }

    out.close();
}

void MvFlextraBlock::writeMetaData(std::ofstream& out, int& metaDataCnt)
{
    metaDataCnt = 0;

    //Write out global meta-data
    for (auto it = metaData_.begin(); it != metaData_.end(); it++) {
        out << it->first + "=" + it->second << " ";
    }
    out << "direction=" << direction_ << std::endl;
    metaDataCnt++;

    //Write out trajectory meta-data
    std::string keys[12] = {"id", "startDate", "startTime", "startLat", "startLon", "startEta",
                            "startPres", "startZ", "startZAboveGround", "startPv", "startTheta", "stopIndex"};

    for (auto& key : keys) {
        std::string s;
        for (auto& item : items_) {
            std::string val = item->metaData(key);
            if (s.empty()) {
                s.append(val);
            }
            else {
                s.append("/" + val);
            }
        }

        if (!s.empty()) {
            out << key + "_TR=" + s << std::endl;
            metaDataCnt++;
        }
    }
}


bool MvFlextraBlock::parseHeaderLine(const std::string& line, const std::string& key, std::string& value)
{
    bool found = false;
    std::string::size_type pos;
    if ((pos = line.find(key)) != std::string::npos) {
        std::istringstream sst(std::string(line, pos + key.size()));
        std::string s;
        while (sst >> s) {
            if (s.find("*") == std::string::npos) {
                if (value.empty()) {
                    value = s;
                }
                else {
                    value.append(s);
                }
                found = true;
            }
        }
    }

    return found;
}

bool MvFlextraBlock::parseHeaderLine(const std::string& line, const std::string& key, std::string& fromValue, std::string& toValue)
{
    bool found = false;
    std::string::size_type pos;
    if ((pos = line.find(key)) != std::string::npos) {
        std::istringstream sst(std::string(line, pos + key.size()));
        std::string s;
        sst >> fromValue;
        sst >> s;
        sst >> toValue;
        found = true;
    }

    return found;
}

bool MvFlextraBlock::isMetaDataConst(const std::string& key)
{
    if (items_.size() == 0)
        return false;

    std::string val = items_.at(0)->metaData(key);
    if (val.empty())
        return false;

    for (unsigned int i = 1; i < items_.size(); i++) {
        if (items_.at(i)->metaData(key) != val) {
            return false;
        }
    }

    return true;
}

void MvFlextraBlock::checkStepType()
{
    for (auto item : items_) {
        const std::vector<std::vector<std::string> >& pts = item->points();

        if (pts.size() < 3)
            continue;

        std::istringstream iss0(pts.at(0).at(1));
        double first;
        iss0 >> first;

        std::istringstream iss1(pts.at(1).at(1));
        double prev;
        iss1 >> prev;

        double diff   = prev - first;
        bool sameDiff = true;
        for (unsigned int i = 2; i < item->points().size() && i < 6; i++) {
            std::istringstream iss(item->points().at(i).at(1));
            double d;
            iss >> d;

            if (d - prev != diff) {
                sameDiff = false;
                break;
            }
            else
                prev = d;
        }

        if (sameDiff)
            constantStep_ = true;
        else
            constantStep_ = false;

        return;
    }

    constantStep_ = true;
}


//=========================================================
//
//  MvFlextra
//
//=========================================================

MvFlextra::MvFlextra(const std::string& fileName) :
    fileName_(fileName)
{
    decode();
}

MvFlextra::~MvFlextra()
{
    for (auto& block : blocks_) {
        delete block;
    }
}


void MvFlextra::decode()
{
    std::ifstream in(fileName_.c_str());

    std::string line;

    while (line.find("Number of header lines") != std::string::npos || getline(in, line)) {
        auto* data = new MvFlextraBlock;
        blocks_.push_back(data);
        data->decode(in, line);
    }

    in.close();


    //Guess if a trajectory block contains uncertainty trajecories or not!
    //It is working only if the FLEXTRA file containing several blocks was
    //generated by metview.
    std::vector<std::pair<std::string, bool> > ref;
    for (auto& block : blocks_) {
        std::string comment = block->comment();
        bool cstep          = block->constantStep();

        std::pair<std::string, bool> p = make_pair(comment, cstep);

        bool newRef = true;
        for (auto& j : ref) {
            if (j == p) {
                block->setUncertantyTr(true);
                newRef = false;
                break;
            }
        }

        if (newRef)
            ref.push_back(p);
    }
}

void MvFlextra::write(FILE* f)
{
    std::ifstream in(fileName_.c_str());
    std::string line;

    while (getline(in, line)) {
        fputs(line.c_str(), f);
        fputs("\n", f);
    }
    in.close();
}


void MvFlextra::write(const std::string& outFile, int block)
{
    if (block < 0 || block >= static_cast<int>(blocks_.size()))
        return;

    std::ofstream out;
    out.open(outFile.c_str());

    std::ifstream in(fileName_.c_str());
    std::string line;

    int cnt = -1;
    while (getline(in, line)) {
        if (line.find("Number of header lines") != std::string::npos) {
            cnt++;
        }

        if (cnt > block)
            break;

        if (cnt == block) {
            out << line << std::endl;
        }
    }

    out.close();
}
