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

#include <algorithm>
#include <iomanip>
#include <sstream>
#include <cmath>

#include "MvException.h"
#include "MvLog.h"
#include "StatsCompute.h"
#include <cassert>

#define HOV_SP 1013.25  // stardard pressure

std::map<HovHeightToolkit::VerticalLevelType, std::string> HovHeightToolkit::vertAxisTypeNames_ = {
    {HovHeightToolkit::AsInDataLevelType, "AS_IN_DATA"},
    {HovHeightToolkit::PressureLevelType, "PRESSURE"},
    {HovHeightToolkit::ParamLevelType, "USER"}};


std::map<HovHeightToolkit::InputMode, std::string> HovHeightToolkit::inputModeNames_ = {
    {HovHeightToolkit::AreaInputMode, "AREA"},
    {HovHeightToolkit::NearestGrpInputMode, "NEAREST_GRIDPOINT"},
    {HovHeightToolkit::PointInputMode, "POINT"}};

struct VerticalDataMatrix
{
    std::map<std::string, std::vector<double>> val_;
    std::vector<double> yCoord_;
};

class LevelInterpolator;

class VerticalDataItem
{
    friend class LevelInterpolator;

public:
    VerticalDataItem(int levelNum = 0);
    void addOneLevel();
    double interpolate(int level1, int level2, double targetYCoord) const;
    double extrapolateTop(double targetYCoord, LevelInterpolator* inter) const;
    double extrapolateBottom(double targetYCoord, LevelInterpolator* inter) const;

protected:
    double extrapolate(double targetYCoord, int level1, int level2, LevelInterpolator* inter) const;
    std::vector<double> yCoord_;
    std::vector<double> val_;
    double lnsp_{-1.0};
    bool vCoordComputed_{false};
};

class LevelInterpolator
{
public:
    enum ExtrapolateMode
    {
        ConstantExtrapolation,
        LinearExtrapolation
    };

    int levelNum() const { return static_cast<int>(levels_.size()); }
    int stepNum() const { return static_cast<int>(steps_.size()); }
    bool isEmpty() const { return steps_.size() == 0; }
    void clear();
    bool hasKey(const std::string& key) const { return steps_.find(key) != steps_.end(); }
    double lnsp(const std::string& key) const;
    void setLnsp(const std::string& key, double val);
    void setVCoord(const std::string& key, int level, double val);
    void setValue(const std::string& key, int level, double val);
    void interpolate(VerticalDataMatrix&);
    void setTopLevel(double v) { topLevel_ = v; }
    void setBottomLevel(double v) { bottomLevel_ = v; }
    double bottomLevel() const { return bottomLevel_; }
    double topLevel() const { return topLevel_; }
    bool extrapolate() const { return extrapolate_; }
    void setExtrapolate(bool v) { extrapolate_ = v; }
    ExtrapolateMode extrapolateMode() const { return extrapolateMode_; }
    void setExtrapolateMode(ExtrapolateMode m) { extrapolateMode_ = m; }
    bool extrapolateFixedSign() const { return extrapolateFixedSign_; }
    void setExtrapolateFixedSign(bool b) { extrapolateFixedSign_ = b; }
    void setUseFixedLnsp(bool b) { useFixedLnsp_ = b; }
    bool useFixedLnsp() const { return useFixedLnsp_; }
    void setFixedLnsp(float v) { fixedLnsp_ = v; }
    float fixedLnsp() const { return fixedLnsp_; }

protected:
    int indexOfLevel(int level) const;
    VerticalDataItem* getStep(const std::string& key, bool addIfMissing) const;

    double topLevel_{0.01};
    double bottomLevel_{1100.};
    bool extrapolate_{false};
    ExtrapolateMode extrapolateMode_{ConstantExtrapolation};
    bool extrapolateFixedSign_{true};
    std::vector<long> levels_;
    mutable std::map<std::string, VerticalDataItem> steps_;
    bool useFixedLnsp_{false};
    float fixedLnsp_{1013.25};
};


//===========================================
//
// ModelLevelInfoItem
//
//===========================================

VerticalDataItem::VerticalDataItem(int levelNum)
{
    if (levelNum >= 1) {
        yCoord_ = std::vector<double>(levelNum, mars.grib_missing_value);
        val_ = std::vector<double>(levelNum, mars.grib_missing_value);
    }
}

void VerticalDataItem::addOneLevel()
{
    yCoord_.push_back(mars.grib_missing_value);
    val_.push_back(mars.grib_missing_value);
}

double VerticalDataItem::interpolate(int level1, int level2, double targetYCoord) const
{
    if (!MISSING_VALUE(val_[level1]) && !MISSING_VALUE(val_[level2])) {
        double alpha = (targetYCoord - yCoord_[level1]) / (yCoord_[level2] - yCoord_[level1]);
        return alpha * val_[level2] + (1. - alpha) * val_[level1];
    }
    return HMISSING_VALUE;
}

double VerticalDataItem::extrapolateTop(double targetYCoord, LevelInterpolator* inter) const
{
    return extrapolate(targetYCoord, yCoord_.size() - 1, yCoord_.size() - 2, inter);
#if 0
     assert(yCoord_.size() == val_.size());
     int level = yCoord_.size()-1;
     if (yCoord_.size() >= 2) {
         if (mode == LevelInterpolator::ConstantExtrapolation) {
             return MISSING_VALUE(val_[level])?HMISSING_VALUE:val_[level];
         } else if (!MISSING_VALUE(val_[level]) && !MISSING_VALUE(val_[level+1])) {
            double alpha = (targetYCoord - yCoord_[level-1])/(yCoord_[level] - yCoord_[level-1]);
            double rv = alpha * val_[level]  + (1.- alpha) * val_[level-1];
            if (fixedSign) {
                assert(mode == LevelInterpolator::LinearExtrapolation);
                if (val_[level]*rv <= 0) {
                    rv = 0.;
                }
            }
            return rv;
         }
     }
     return MISSING_VALUE(val_[level])?HMISSING_VALUE:val_[level];
#endif
}

double VerticalDataItem::extrapolateBottom(double targetYCoord, LevelInterpolator* inter) const
{
    return extrapolate(targetYCoord, 0, 1, inter);
#if 0
     assert(yCoord_.size() == val_.size());
      int level = 0;
     if (yCoord_.size() >= 2) {
         if (mode == LevelInterpolator::ConstantExtrapolation) {
             return MISSING_VALUE(val_[level])?HMISSING_VALUE:val_[level];
         } else if (!MISSING_VALUE(val_[level]) && !MISSING_VALUE(val_[level-1])) {
             double alpha = (targetYCoord - yCoord_[level+1])/(yCoord_[level] - yCoord_[level+1]);
             double rv = alpha * val_[level]  + (1.- alpha) * val_[level+1];
         }
         if (fixedSign) {
             assert(mode == LevelInterpolator::LinearExtrapolation);
             if (val_[level]*rv <= 0) {
                 rv = 0.;
             }
         }
         return rv;
     }
     return MISSING_VALUE(val_[level])?HMISSING_VALUE:val_[level];
#endif
}

double VerticalDataItem::extrapolate(double targetYCoord, int level1, int level2, LevelInterpolator* inter) const
{
    assert(yCoord_.size() == val_.size());
    if (yCoord_.size() >= 2) {
        if (inter->extrapolateMode() == LevelInterpolator::ConstantExtrapolation) {
            return MISSING_VALUE(val_[level1]) ? HMISSING_VALUE : val_[level1];
        }
        else if (!MISSING_VALUE(val_[level1]) && !MISSING_VALUE(val_[level2])) {
            double alpha = (targetYCoord - yCoord_[level2]) / (yCoord_[level1] - yCoord_[level2]);
            double rv = alpha * val_[level1] + (1. - alpha) * val_[level2];
            if (inter->extrapolateFixedSign()) {
                assert(inter->extrapolateMode() == LevelInterpolator::LinearExtrapolation);
                if (val_[level1] * rv <= 0) {
                    rv = 0.;
                }
            }
            return rv;
        }
    }
    return MISSING_VALUE(val_[level1]) ? HMISSING_VALUE : val_[level1];
}

//===========================================
//
// ModelLevelInfo
//
//===========================================

void LevelInterpolator::clear()
{
    topLevel_ = 0.01;
    bottomLevel_ = 1100.;
    extrapolate_ = false;
    extrapolateMode_ = ConstantExtrapolation;
    extrapolateFixedSign_ = true;
    levels_.clear();
    steps_.clear();
    useFixedLnsp_ = false;
    fixedLnsp_ = 1013.25;
}

int LevelInterpolator::indexOfLevel(int level) const
{
    auto it = std::find(levels_.begin(), levels_.end(), level);
    return (it != levels_.end()) ? (it - levels_.begin()) : -1;
}

VerticalDataItem* LevelInterpolator::getStep(const std::string& key, bool addIfMissing) const
{
    auto it = steps_.find(key);
    if (it == steps_.end()) {
        if (addIfMissing) {
            steps_[key] = VerticalDataItem(levelNum());
            it = steps_.find(key);
            if (useFixedLnsp_) {
                steps_[key].lnsp_ = fixedLnsp_;
            }
        }
        else {
            return nullptr;
        }
    }
    return &(it->second);
}

double LevelInterpolator::lnsp(const std::string& key) const
{
    auto item = getStep(key, false);
    return (item) ? item->lnsp_ : -1.;
}

void LevelInterpolator::setLnsp(const std::string& key, double val)
{
    auto item = getStep(key, true);
    item->lnsp_ = val;
}

void LevelInterpolator::setVCoord(const std::string& key, int level, double val)
{
    auto levIdx = indexOfLevel(level);
    if (levIdx == -1) {
        levels_.push_back(level);
        levIdx = levelNum() - 1;
        for (auto& it : steps_) {
            it.second.addOneLevel();
        }
    }

    auto step = getStep(key, true);
    assert(levelNum() > 0);
    assert(hasKey(key));
    step->yCoord_[levIdx] = val;
}

void LevelInterpolator::setValue(const std::string& key, int level, double val)
{
    auto levIdx = indexOfLevel(level);
    if (levIdx == -1) {
        levels_.push_back(level);
        levIdx = levelNum() - 1;
        for (auto& it : steps_) {
            it.second.addOneLevel();
        }
    }

    auto step = getStep(key, true);
    assert(levelNum() > 0);
    assert(hasKey(key));
    step->val_[levIdx] = val;
}

void LevelInterpolator::interpolate(VerticalDataMatrix& dm)
{
    if (isEmpty())
        return;

    bool targetAscending = (bottomLevel_ < topLevel_);

    // determine target levels by taking the vertical coordinate average on each
    // input level
    std::vector<std::pair<int, double>> sData;
    for (std::size_t idx = 0; idx < levels_.size(); idx++) {
        double yAvg = 0.;
        int validNum = 0;
        for (const auto& it : steps_) {
            auto y = it.second.yCoord_[idx];
            if (!MISSING_VALUE(y)) {
                yAvg += y;
                validNum++;
            }
        }
        if (validNum > 0) {
            yAvg /= validNum;
            sData.emplace_back(idx, yAvg);
        }
    }

    // sort target levels from bottom to top
    std::stable_sort(sData.begin(), sData.end(),
                     [targetAscending](const auto& v1, const auto& v2) { return (targetAscending) ? (v2.second > v1.second) : (v1.second > v2.second); });

    std::vector<int> srcLevIdx;
    for (auto v : sData) {
        srcLevIdx.push_back(v.first);
    }

    // select only target levels in the bottomLevel_/topLevel_ range. The levels right below and above
    // the range are also added
    int startIdx = -1, endIdx = -1;
    double minLevel = std::min(bottomLevel_, topLevel_);
    double maxLevel = std::max(bottomLevel_, topLevel_);
    for (std::size_t i = 0; i < sData.size(); i++) {
        if (sData[i].second >= minLevel && sData[i].second <= maxLevel) {
            if (startIdx == -1) {
                startIdx = i;
            }
            endIdx = i + 1;
        }
    }
    if (startIdx >= endIdx) {
        MvException("No vertical coordinates found within the specified range: [" +
                    std::to_string(minLevel) + "," + std::to_string(maxLevel) + "]");
    }
    else {
        assert(startIdx != -1);
        assert(endIdx != -1);
        if (startIdx > 0) {
            startIdx--;
        }
        if (endIdx >= static_cast<int>(sData.size() - 1)) {
            endIdx = static_cast<int>(sData.size() - 1);
        }
        for (int i = startIdx; i <= endIdx; i++) {
            dm.yCoord_.push_back(sData[i].second);
        }
    }

    // see if extrapolation is needed
    int extraBottom = 0;
    int extraTop = 0;
    if (extrapolate_) {
        if ((targetAscending && bottomLevel_ < dm.yCoord_[0]) ||
            (!targetAscending && bottomLevel_ > dm.yCoord_[0])) {
            dm.yCoord_.insert(dm.yCoord_.begin(), bottomLevel_);
            extraBottom = 1;
        }
        if ((targetAscending && topLevel_ > dm.yCoord_.back()) ||
            (!targetAscending && topLevel_ < dm.yCoord_.back())) {
            dm.yCoord_.push_back(topLevel_);
            extraTop = 1;
        }
    }

    // interpolate + extrapolate values
    int targetLevNum = dm.yCoord_.size();
    for (const auto& it : steps_) {
        auto key = it.first;
        auto* item = &(it.second);
        dm.val_[key] = std::vector<double>(targetLevNum, HMISSING_VALUE);

        // interpolation
        for (int targetIdx = extraBottom; targetIdx < targetLevNum - extraTop; targetIdx++) {
            auto targetYCoord = dm.yCoord_[targetIdx];
            int lev1 = -1;
            int lev2 = -1;
            for (auto srcIdx : srcLevIdx) {
                if (srcIdx >= 0 && !MISSING_VALUE(item->val_[srcIdx])) {
                    if ((targetAscending && item->yCoord_[srcIdx] < targetYCoord) ||
                        (!targetAscending && item->yCoord_[srcIdx] > targetYCoord)) {
                        lev1 = srcIdx;
                    }
                    else if ((targetAscending && item->yCoord_[srcIdx] >= targetYCoord) ||
                             (!targetAscending && item->yCoord_[srcIdx] <= targetYCoord)) {
                        lev2 = srcIdx;
                        break;
                    }
                }
            }

            if (lev1 >= 0 && lev2 >= 0 && abs(lev1 - lev2) < 4) {
                dm.val_[key][targetIdx] = item->interpolate(lev1, lev2, targetYCoord);
            }
            else if (lev1 >= 0 && lev2 == -1) {
                dm.val_[key][targetIdx] = item->val_[lev1];
            }
            else if (lev2 >= 0 && lev1 == -1) {
                dm.val_[key][targetIdx] = item->val_[lev2];
            }
        }

        // extrapolation
        if (extraBottom == 1) {
            int dmIndex = 0;
            dm.val_[key][dmIndex] = item->extrapolateBottom(bottomLevel_, this);
        }
        if (extraTop == 1) {
            int dmIndex = targetLevNum - 1;
            dm.val_[key][dmIndex] = item->extrapolateTop(topLevel_, this);
        }
    }
}

//===========================================
//
// HovHeightToolkit
//
//===========================================

HovHeightToolkit::HovHeightToolkit()
{
    levHandler_ = new LevelInterpolator();
}

HovHeightToolkit::~HovHeightToolkit()
{
    delete levHandler_;
}

void HovHeightToolkit::GenerateKey(std::string& str, MvRequest& rq)
{
    // The level value is ignored to build the key
    int par = rq("PARAM");
    int level = 0;
    const char* expver = rq("EXPVER");

    std::ostringstream oss;
    oss << std::setfill('0')
        << std::setw(1) << 'p'
        << std::setw(3) << par
        << std::setw(4) << level
        << std::setw(4) << (expver ? expver : "_")
        << std::ends;

    str = oss.str();

    return;
}

void HovHeightToolkit::collectVerticalData(MvField& field, const std::string& timeKey, double ilevel)
{
    if (inputLevelType_.empty()) {
        inputLevelType_ = field.levelTypeString();
        if (outputLevelType_.empty()) {
            outputLevelType_ = inputLevelType_;
        }

        if (inputLevelType_ == "ml" && outputLevelType_ == "pl") {
            if (!levHandler_->useFixedLnsp() && levHandler_->isEmpty()) {
                throw MvException(moduleLabel_ +
                                  "No lnsp data was found! Cannot interpolate input model level data for pressure levels!");
            }
        }
        else if (vertAxisType_ != ParamLevelType && inputLevelType_ != outputLevelType_) {
            throw MvException(moduleLabel_ + "Input (=" + inputLevelType_ +
                              ") and output (=" + outputLevelType_ +
                              ") level types must be the same with the current settings!");
        }
    }

    // Conversion from model level to pressure level is needed
    if (inputLevelType_ == "ml" && outputLevelType_ == "pl") {
        int ml = static_cast<int>(ilevel);
        if (levHandler_->useFixedLnsp()) {
            auto p = field.meanML_to_Pressure_byLNSP(levHandler_->fixedLnsp(), ml) / 100.;
            levHandler_->setVCoord(timeKey, ml - 1, p);
            levHandler_->setValue(timeKey, ml - 1, computeValue(field));
        }
        else {
            assert(!levHandler_->isEmpty());
            if (levHandler_->hasKey(timeKey)) {
                auto lnsp = levHandler_->lnsp(timeKey);
                if (!MISSING_VALUE(lnsp)) {
                    // compute pressue in hPa
                    auto p = field.meanML_to_Pressure_byLNSP(lnsp, ml) / 100.;
                    levHandler_->setVCoord(timeKey, ml - 1, p);
                    levHandler_->setValue(timeKey, ml - 1, computeValue(field));
                }
            }
            else {
                std::string msg("Trying to convert from model levels to pressure levels "
                                "but cannot find lnsp field for time step ");
                msg += TimeKeyToUserString(timeKey);
                throw MvException(moduleLabel_ + msg);
            }
        }
    }
    else if (vertAxisType_ == ParamLevelType) {
        //        assert(!mlevInter_->isEmpty());
        int lev = static_cast<int>(ilevel);
        //        if (mlevInter_->hasKey(timeKey)) {
        if (true) {
            auto param = static_cast<int>(field.parameter());
            if (param == vertCoordParamId_) {
                levHandler_->setVCoord(timeKey, lev, computeValue(field));
            }
            else {
                levHandler_->setValue(timeKey, lev, computeValue(field));
            }
        }
        else {
            throw MvException(moduleLabel_ + "Invalid date/time in input data!");
        }
        // Standard vertical coordinates
    }
    else {
        double level = ilevel;
        auto iter = std::find(coord1_.begin(), coord1_.end(), level);
        if (iter == coord1_.end())
            coord1_.push_back(level);
    }
}

// Called during the very first scan.
bool HovHeightToolkit::collectLnsp(MvRequest& data)
{
    // model level input with pressure vertical axis
    if (vertAxisType_ == PressureLevelType) {
        if (levHandler_->useFixedLnsp()) {
            return true;
        }

        // Initialize fieldset
        MvFieldSet fs(data);
        MvFieldSetIterator iter(fs);
        MvField field;
        std::string keystr, timekey;
        iter.rewind();
        iter.sort("LEVELIST", '>');
        iter.sort("EXPVER");
        iter.sort("PARAM");

        bool hasMlData = false;
        while ((field = iter())) {
            MvRequest rq = field.getRequest();
            if (!hasMlData) {
                hasMlData = field.isModelLevel();
            }
            if (isParamLnsp(static_cast<int>(field.parameter()))) {
                rq = field.getRequest();
                GenerateKey(keystr, rq);
                GenerateTimeKey(timekey, rq);
                levHandler_->setLnsp(timekey, computeValue(field));
            }
        }

        if (!hasMlData || levHandler_->stepNum() == 0) {
            levHandler_->clear();
            return false;
        }
        else {
            return true;
        }
    }

    return false;
}

bool HovHeightToolkit::ComputeSecondCoord()
{
    // The variable coord1_ was already initialized during
    // the initialisation of the parameters

    // Initialise number of points
    this->NPoints(coord1_.size());

    return true;
}

double HovHeightToolkit::computeValue(MvField& field) const
{
    MvFieldExpander x(field);
    if (inputMode_ == PointInputMode) {
        return field.interpolateAt(x1_, y1_);
    }
    else if (inputMode_ == NearestGrpInputMode) {
        return field.nearestGridpoint(x1_, y1_, false);
    }
    else if (inputMode_ == AreaInputMode) {
        StatsComputePtr comp = makeStatsCompute();
        assert(comp);
        return field.computeInArea(y1_, x1_, y2_, x2_, true, comp);
    }
    return mars.grib_missing_value;
}

bool HovHeightToolkit::ComputeValues(MvField& field, int index)
{
    xint_[index] = computeValue(field);
    return true;
}

bool HovHeightToolkit::GenerateData(MvRequest& data)
{
    // MvLog().dbg() << MV_FN_INFO;
    // MvLog().dbg() << "levelNum=" << levHandler_->levelNum() << " stepNum=" << levHandler_->stepNum();
    //    for (auto v: lnsp_) {
    //        MvLog().dbg() << "key=" << v.first << " lnsp=" << v.second;
    //        MvLog().dbg() << "lnsp=" << v.second;
    //    }

    // Compute the vertical data matrix for model level data
    // interpoltated to a time-pressure grid (i.e. matrix)
    VerticalDataMatrix vData;
    if (!levHandler_->isEmpty()) {
        levHandler_->interpolate(vData);
        coord1_ = vData.yCoord_;
    }

    // Write initial info and variables to the netCDF. It uses the number of
    // levels, so it must be called after the ML level interpolation
    if (!this->Initialiser())
        return false;

    // Allocate internal variables
    if (xint_ != nullptr) {
        delete[] xint_;
        xint_ = nullptr;
    }
    xint_ = new double[nrPoints_];

    // Process the ML data already interpolated to pressure levels
    if (!levHandler_->isEmpty()) {
        // we assume there is only one param!
        ParamInfo* par = nullptr;
        std::string paramKey;
        for (const auto& it : params_) {
            paramKey = it.first;
            par = it.second;
            break;
        }
        if (par) {
            for (auto it : vData.val_) {
                auto key = it.first;
                for (std::size_t i = 0; i < it.second.size(); i++) {
                    xint_[i] = (it.second)[i];
                }

                par->FillIndex(xint_, key, nrPoints_);
            }
            if (!NcWriteData(paramKey)) {
                return false;
            }
        }
        else {
            return false;
        }
        // native levels
    }
    else {
        // Sort data by level, expver and param
        MvFieldSet fs(data);
        MvFieldSetIterator iter(fs);
        iter.rewind();
        iter.sort("LEVELIST", '>');
        iter.sort("STEP");
        iter.sort("TIME");
        iter.sort("DATE");
        iter.sort("EXPVER");
        iter.sort("PARAM");

        // Generate data
        int currentGenerated = 0, lastNrGenerated = -1;
        int ind = 0;
        std::string sfirst = "FIRSTFIELD";
        std::string timeKey, keystr;
        std::string lastTimeKey = sfirst;
        std::string lastKey = sfirst;
        MvField field, lastField;
        ParamIterator paramIter;
        while ((field = iter())) {
            // skip lnsp
            if (!levHandler_->isEmpty() && isParamLnsp(static_cast<int>(field.parameter()))) {
                continue;
            }

            currentGenerated++;

            // Create keys
            MvRequest rq = field.getRequest();
            GenerateKey(keystr, rq);
            GenerateTimeKey(timeKey, rq);

            // Compute data
            if (!ComputeValues(field, ind++))
                return false;

            // Get all the computed level values for each time
            if (ind < nrPoints_) {
                // Extra check
                if (lastTimeKey != timeKey && lastTimeKey != sfirst)
                    throw MvException(moduleLabel_ + "Input data not consistent. Probably number of levels is not the same for all time stamps");

                lastTimeKey = timeKey;
                continue;
            }

            // Make sure the time stamps are the same
            if (lastTimeKey != timeKey)
                throw MvException(moduleLabel_ + "Input data not consistent. Probably number of levels is not the same for all time stamps");

            // Save data in the ParamInfo structure
            ParamInfo* par = this->GetParamInfo(keystr);
            if (!par)
                return false;

            par->FillIndex(xint_, timeKey, nrPoints_);

            // Write out data, if it can be grouped together
            if (lastKey != keystr && lastKey != sfirst) {
                if (!NcWriteData(lastKey))
                    return false;
                lastNrGenerated = currentGenerated;
            }

            lastKey = keystr;
            lastField = field;
            lastTimeKey = sfirst;
            ind = 0;
        }

        // Write out the data in the netcdf file
        if (lastNrGenerated <= currentGenerated) {
            if (!NcWriteData(lastKey))
                return false;
        }
    }

    netcdf_->close();

    return true;
}

std::string HovHeightToolkit::GetTitle(ParamInfo* par)
{
    char title[256];
    sprintf(title, "Hovmoeller of %s %s (%.1f%s/%.1f%s/%.1f%s/%.1f%s)",
            par->ParamLongName().c_str(), par->ExpVerTitle().c_str(),
            (y1_ < 0) ? -y1_ : y1_, (y1_ < 0) ? "S" : "N",
            (x1_ < 0) ? -x1_ : x1_, (x1_ < 0) ? "W" : "E",
            (y2_ < 0) ? -y2_ : y2_, (y2_ < 0) ? "S" : "N",
            (x2_ < 0) ? -x2_ : x2_, (x2_ < 0) ? "W" : "E");

    return {title};
}

bool HovHeightToolkit::GetInputInfo(MvRequest& in)
{
    x1_ = 0.;
    x2_ = 0.;
    y1_ = 0.;
    y2_ = 0.;
    lnspParamId_ = 152;
    vertAxisType_ = NoLevelType;
    inputLevelType_.clear();
    outputLevelType_.clear();
    levHandler_->clear();

    // Input mode
    inputMode_ = NoInputMode;
    if (const char* ch = in("INPUT_MODE")) {
        auto imName = std::string(ch);
        for (const auto& it : inputModeNames_) {
            if (it.second == imName) {
                inputMode_ = it.first;
                break;
            }
        }

        if (inputMode_ == AreaInputMode) {
            y1_ = in("AREA", 0);
            x1_ = in("AREA", 1);
            y2_ = in("AREA", 2);
            x2_ = in("AREA", 3);

            // Make sure coordinates follow Mars rules (n/w/s/e)
            if (x1_ > x2_) {
                std::swap(x1_, x2_);
            }
            if (y2_ > y1_) {
                std::swap(y1_, y2_);
            }
            stats_ = statsType(in);
        }
        else if (inputMode_ == PointInputMode || inputMode_ == NearestGrpInputMode) {
            y1_ = in("POINT", 0);
            x1_ = in("POINT", 1);
        }
        else {
            throw MvException(moduleLabel_ + "Invalid INPUT_MODE=" + imName);
        }
    }
    else {
        throw MvException(moduleLabel_ + "No INPUT_MODE specified");
    }

    if ((const char*)in("LNSP_PARAM")) {
        lnspParamId_ = (int)in("LNSP_PARAM");
    }

    // Get vertical axis type
    if (const char* ch = (const char*)in("VERTICAL_LEVEL_TYPE")) {
        std::string sch(ch);
        for (const auto& it : vertAxisTypeNames_) {
            if (it.second == sch) {
                vertAxisType_ = it.first;
                break;
            }
        }
    }

    if (vertAxisType_ == PressureLevelType) {
        outputLevelType_ = "pl";
    }

    if ((const char*)in("VERTICAL_COORDINATE_PARAM")) {
        vertCoordParamId_ = (int)in("VERTICAL_COORDINATE_PARAM");
    }

    if (vertAxisType_ == ParamLevelType && vertCoordParamId_ <= 0) {
        throw MvException(moduleLabel_ + "invalid VERTICAL_COORDINATE_PARAM specified!");
    }

    lnspParamId_ = (int)in("LNSP_PARAM");

    if (const char* ch = in("USE_FIXED_SURFACE_PRESSURE")) {
        levHandler_->setUseFixedLnsp((strcmp(ch, "ON") == 0));
        if (levHandler_->useFixedLnsp()) {
            if ((const char*)in("FIXED_SURFACE_PRESSURE")) {
                auto sp = (double)in("FIXED_SURFACE_PRESSURE");
                if (sp <= 1E-5) {
                    throw MvException(moduleLabel_ + "invalid FIXED_SURFACE_PRESSURE specified!");
                }
                levHandler_->setFixedLnsp(std::log(sp * 100.));
            }
        }
    }

    // Set axis orientation
    verticalAxis_ = HOV_GEO;

    // Initialize x/y resolution  ??? Do we need this ???
    gridEW_ = gridNS_ = 1;  // in("RESOLUTION");

    if ((const char*)in("BOTTOM_LEVEL")) {
        levHandler_->setBottomLevel((double)in("BOTTOM_LEVEL"));
    }

    if ((const char*)in("TOP_LEVEL")) {
        levHandler_->setTopLevel((double)in("TOP_LEVEL"));
    }

    if (const char* ch = in("VERTICAL_COORDINATE_EXTRAPOLATE")) {
        levHandler_->setExtrapolate(strcmp(ch, "ON") == 0);
    }

    if (levHandler_->extrapolate()) {
        if (const char* ch = in("VERTICAL_COORDINATE_EXTRAPOLATE_MODE")) {
            levHandler_->setExtrapolateMode((strcmp(ch, "CONSTANT") == 0) ? LevelInterpolator::ConstantExtrapolation : LevelInterpolator::LinearExtrapolation);
        }
        if (const char* ch = in("VERTICAL_COORDINATE_EXTRAPOLATE_FIXED_SIGN")) {
            levHandler_->setExtrapolateFixedSign((strcmp(ch, "ON") == 0) ? true : false);
        }
    }

    return true;
}

bool HovHeightToolkit::GetInputInfo(MvNetCDF* netcdf)
{
    // Get common information from the netCDF file
    if (!GetInputCommonInfo(netcdf))
        return false;

    // Retrieve attributes values
    MvRequest req = netcdf->getRequest();

    // input mode
    if (const char* ch = (const char*)req("input_mode")) {
        std::string sch(ch);
        inputMode_ = NoInputMode;
        for (const auto& it : inputModeNames_) {
            if (it.second == sch) {
                inputMode_ = it.first;
                break;
            }
        }

        if (inputMode_ == AreaInputMode) {
            // Retrieve stats value
            const char* statCh = (const char*)req("area_statistics");
            // older files do not contain this attribute
            if (statCh == nullptr) {
                stats_ = MeanStats;
            }
            else {
                stats_ = statsType(statCh);
            }
        }
        else if (inputMode_ == NoInputMode) {
            throw MvException(moduleLabel_ +
                              "Netcdf input file error: invalid input_mode=" +
                              std::string(ch) + " found!");
        }
    }
    else {
        // we do not fail because older files do not have this attribute, but
        // we use the default values
        inputMode_ = AreaInputMode;
        stats_ = MeanStats;
    }

    // Retrieve vertical type axes flag
    if (const char* ch = (const char*)req("vertical_type")) {
        std::string sch(ch);
        vertAxisType_ = NoLevelType;
        for (const auto& it : vertAxisTypeNames_) {
            if (it.second == sch) {
                vertAxisType_ = it.first;
                break;
            }
        }
        if (vertAxisType_ == NoLevelType) {
            throw MvException(moduleLabel_ +
                              "Netcdf input file error: invalid vertical_type=" +
                              std::string(ch) + " found!");
        }
    }
    else {
        throw MvException(moduleLabel_ + "Netcdf input file error: vertical_type attribute not found");
    }

    // Set axis orientation
    verticalAxis_ = HOV_GEO;

    return true;
}

void HovHeightToolkit::NcWriteGlobalAttributesApp()
{
    auto itIm = inputModeNames_.find(inputMode_);
    if (itIm != inputModeNames_.end()) {
        netcdf_->addAttribute("input_mode", itIm->second.c_str());
    }
    else {
        throw MvException(moduleLabel_ +
                          "Could not determine the value for input_mode NetCDF attribute!");
    }

    if (inputMode_ == AreaInputMode) {
        netcdf_->addAttribute("area_statistics", statsName(stats_).c_str());
    }

    auto it = vertAxisTypeNames_.find(vertAxisType_);
    if (it != vertAxisTypeNames_.end()) {
        netcdf_->addAttribute("vertical_type", it->second.c_str());
    }
    else {
        throw MvException(moduleLabel_ +
                          "Could not determine the value for vertical_type NetCDF attribute!");
    }
}

bool HovHeightToolkit::NcWriteSecondCoord()
{
    // Add second coordinate to the netCDF file
    std::vector<std::string> v_sdim(1, GetSecondCoordName());
    std::vector<long> v_ndim(1, nrPoints_);
    MvNcVar* ncx = netcdf_->addVariable(v_sdim[0], ncDouble, v_ndim, v_sdim);
    ncx->addAttribute("long_name", "Atmospheric Levels");
    ncx->addAttribute("units", "hPa");
    ncx->put(coord1_, (long)nrPoints_);

    return true;
}

bool HovHeightToolkit::consistencyCheck(MvRequest& viewReq, MvRequest& dataReq)
{
    std::string msg;
    std::string label1 = "view";
    std::string label2 = "data";

    if (!MvRequest::paramEqualToVerb(viewReq, dataReq, "TYPE", label1, label2, msg)) {
        throw MvException(moduleLabel_ + msg);
    }

    std::vector<std::string> params = {"INPUT_MODE", "VERTICAL_LEVEL_TYPE"};
    for (const auto& name : params) {
        if (!MvRequest::paramsEqual(viewReq, dataReq, name, label1, label2, msg)) {
            throw MvException(moduleLabel_ + msg);
        }
    }

    params.clear();
    const char* ch = (const char*)viewReq("INPUT_MODE");
    assert(ch);
    auto imStr = std::string(ch);
    if (imStr == "AREA") {
        params.emplace_back("AREA");
        params.emplace_back("AREA_STATISTICS");
    }
    else {
        params.emplace_back("POINT");
    }

    for (const auto& name : params) {
        if (!MvRequest::paramsEqual(viewReq, dataReq, name, label1, label2, msg)) {
            throw MvException(moduleLabel_ + msg);
        }
    }

    return true;
}

MvRequest HovHeightToolkit::CreateViewRequest()
{
    MvRequest viewReq("MHOVMOELLERVIEW");

    viewReq("TYPE") = this->ApplicationType();
    viewReq.addValue("AREA", y1_);
    viewReq.addValue("AREA", x1_);
    viewReq.addValue("AREA", y2_);
    viewReq.addValue("AREA", x2_);

    auto it = vertAxisTypeNames_.find(vertAxisType_);
    if (it != vertAxisTypeNames_.end()) {
        viewReq("VERTICAL_LEVEL_TYPE") = it->second.c_str();
    }
    else {
        throw MvException(moduleLabel_ +
                          "Cannot build view request! Invalid VERTICAL_LEVEL_TYPE was found!");
    }


    viewReq("TOP_LEVEL") = levHandler_->topLevel();
    viewReq("BOOTOM_LEVEL") = levHandler_->bottomLevel();

    viewReq("_CLASS") = "MHOVMOELLERVIEW";
    viewReq("_DEFAULT") = 1;
    viewReq("_DATAATTACHED") = "YES";

    return viewReq;
}

//--------------------------------------------------------------

void HeightHovmoeller::serve(MvRequest& in, MvRequest& out)
{
    std::cout << "Request IN:" << std::endl;
    in.print();

    // Compute Hovmoeller diagram
    origReq_ = in;
    if (!Compute(in, out))
        setError(1, "HeightHovmoeller-> Failed to generate Hovmoeller Vertical");

    std::cout << "Request OUT:" << std::endl;
    out.print();
}

//-------------------------------------------------------------------------

// Translate Metview 3 HeightHovmoeller Data to Metview 4 definition. Call Metview 4
// server to process the job.
void HeightHovmoellerM3::serve(MvRequest& in, MvRequest& out)
{
    // Send a general warning message
    setError(0, "The Metview 3 Vertical-Hovmoeller DATA icon is deprecated. An automatic translation to the correspondent Metview 4 icon will be performed internally, but may not work for all cases. It is recommended to manually replace the old icons with their new equivalents.");

    // There are input parameters that are no longer available in Metview 4.
    // Remove them and send a warning message.
    setError(0, "The Metview 3 Height-Hovmoeller DATA icon is deprecated. Parameters TIME_AXIS_DIRECTION, HEIGHT_AXIS_DIRECTION, TIME_AXIS and HEIGHT_AXIS will not be translated internally.");
    MvRequest req = in;
    req.unsetParam("TIME_AXIS_DIRECTION");
    req.unsetParam("HEIGHT_AXIS_DIRECTION");
    req.unsetParam("TIME_AXIS");
    req.unsetParam("HEIGHT_AXIS");

    // Parameter HEIGHT_AXIS_TYPE is replaced by VERTICAL_LEVEL_TYPE in Metview 4
    if ((const char*)req("HEIGHT_AXIS_TYPE")) {
        req("VERTICAL_LEVEL_TYPE") = req("HEIGHT_AXIS_TYPE");
        req.unsetParam("HEIGHT_AXIS_TYPE");
    }

    // Keep the remaining parameters and update the VERB
    req.setVerb("VERTICAL_HOVM");

    // Call the server to process the job
    HeightHovmoeller::serve(req, out);
}
