/*
 * (C) Copyright 1996-2016 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.
 */

/*! \file NetcdfMatrixInterpretor.h
    \brief Implementation of the Template class NetcdfMatrixInterpretor.

    Magics Team - ECMWF 2004

    Started: Tue 17-Feb-2004

    Changes:

*/

#include <limits>

#include "Coordinate.h"
#include "Factory.h"
#include "Layer.h"
#include "NetcdfData.h"
#include "NetcdfMatrixInterpretor.h"
#include "TextVisitor.h"
#include "Tokenizer.h"
using namespace magics;

#include "magics_windef.h"
#ifdef MAGICS_ON_WINDOWS
#include <iterator>
#endif


NetcdfMatrixInterpretor::NetcdfMatrixInterpretor() {}


NetcdfMatrixInterpretor::~NetcdfMatrixInterpretor() {}


bool NetcdfMatrixInterpretor::interpretAsMatrix(Matrix** matrix) {
    MagLog::debug() << "NetcdfMatrixInterpretor::interpret()--->" << *this << "\n";
    if (*matrix)
        return false;

    try {
        matrix_ = new Matrix();

        *matrix = matrix_;
        if (!matrix_->empty())
            return false;

        matrix_->missing(std::numeric_limits<double>::max());

        Netcdf netcdf(path_, dimension_method_);
        double missing = netcdf.getMissing(field_, missing_attribute_);
        matrix_->missing(missing);

        string title = netcdf.getAttribute("title", string("NO TITLE"));


        x();
        y();
        // get the data ...


        map<string, string> first, last;
        setDimensions(dimension_, first, last);
        vector<double> rows    = dateRows_.empty() ? rows_ : dateRows_;
        vector<double> columns = dateColumns_.empty() ? columns_ : dateColumns_;
        int index              = 0;
        for (vector<double>::iterator r = rows.begin(); r != rows.end(); r++) {
            vector<string> dims;
            ostringstream x, y;
            if (magCompare(dimension_method_, "index")) {
                y << y_ << "/" << index << "/" << index;
                x << x_ << "/" << 0 << "/" << columns.size() - 1;
            }
            else {
                x.precision(20);
                y.precision(20);

                y << y_ << "/" << *r << "/" << *r;
                x << x_ << "/" << columns.front() << "/" << columns.back();
            }
            std::copy(dimension_.begin(), dimension_.end(), std::back_inserter(dims));
            dims.push_back(y.str());
            dims.push_back(x.str());
            index++;

            setDimensions(dims, first, last);
            vector<double> data;
            netcdf.get(field_, data, first, last);
            for (vector<double>::iterator d = data.begin(); d != data.end(); d++) {
                matrix_->push_back(*d);
            }
        }


        MagLog::debug() << "matrix_[" << matrix_->size() << ", " << scaling_ << ", " << offset_ << "]"
                        << "\n";

        matrix_->multiply(scaling_);
        matrix_->plus(offset_);


        MagLog::debug() << "matrix_[" << matrix_->size() << ", " << scaling_ << ", " << offset_ << "\n";


        vector<double> col;


        matrix_->setColumnsAxis(columns_);
        matrix_->setRowsAxis(rows_);


        MagLog::dev() << *matrix_ << "\n";
    }
    catch (MagicsException& e) {
        MagLog::error() << e << "\n";
        delete matrix_;
        matrix_ = NULL;
        return false;
    }
    return true;
}


/*!
 Class information are given to the output-stream.
*/
void NetcdfMatrixInterpretor::print(ostream& out) const {
    out << "NetcdfMatrixInterpretor[";
    NetcdfInterpretor::print(out);

    out << "]";
}


bool NetcdfMatrixInterpretor::x() {
    if (!columns_.empty())
        return false;

    Netcdf netcdf(path_, dimension_method_);
    map<string, string> first, last;
    setDimensions(dimension_, first, last);

    try {
        netcdf.get(x_, columns_, first, last);
        baseDateX_ = "";
        if (!reference_date(netcdf, x_, refDateX_, baseDateX_, columns_, dateColumns_))
            cf_date(netcdf, x_, refDateX_, baseDateX_, columns_, dateColumns_);
    }
    catch (...) {
        try {
            int x = netcdf.getDimension(x_);
            if (!x) {
                MagLog::warning() << "No valid X dimension.." << endl;
                return false;
            }
            for (int i = 0; i < x; i++)
                columns_.push_back(i);
        }
        catch (...) {
            MagLog::warning() << "No valid X dimension.." << endl;
            throw MagicsException("Could not find any X axis");
        }
    }

    if (aux_x_.empty())
        return true;
    try {
        vector<double> aux;

        netcdf.get(aux_x_, aux, first, last);
        if (!aux.empty()) {
            // small check to make sure that we do not have problem :
            ostringstream geominx, geomaxx;
            if (magCompare(geo_x_convention_, "latlon")) {
                geominx << aux.front() << "/" << columns_.front();
                geomaxx << aux.back() << "/" << columns_.back();
            }
            else {
                geominx << columns_.front() << "/" << aux.front();
                geomaxx << columns_.back() << "/" << aux.back();
            }
            geoMinX_ = geominx.str();
            geoMaxX_ = geomaxx.str();
        }
    }
    catch (...) {
        throw MagicsException("Could not find any X axis");
    }

    return true;
}

bool NetcdfMatrixInterpretor::y() {
    if (!rows_.empty())
        return false;

    Netcdf netcdf(path_, dimension_method_);
    map<string, string> first, last;
    setDimensions(dimension_, first, last);
    try {
        netcdf.get(y_, rows_, first, last);

        baseDateY_ = "";
        if (!reference_date(netcdf, y_, refDateY_, baseDateY_, rows_, dateRows_))
            cf_date(netcdf, y_, refDateY_, baseDateY_, rows_, dateRows_);
    }
    catch (...) {
        try {
            int y = netcdf.getDimension(y_);
            if (!y) {
                MagLog::warning() << "No valid Y dimension.." << endl;
                return false;
            }

            for (int i = 0; i < y; i++)
                rows_.push_back(i);
        }
        catch (...) {
            MagLog::warning() << "No valid Y dimension.." << endl;
            throw MagicsException("Could not find any Y axis");
        }
    }

    if (aux_y_.empty())
        return true;
    try {
        vector<double> aux;
        netcdf.get(aux_y_, aux, first, last);
        if (!aux.empty()) {
            ostringstream geominy, geomaxy;
            if (magCompare(geo_y_convention_, "latlon")) {
                geominy << aux.front() << "/" << rows_.front();
                geomaxy << aux.back() << "/" << rows_.back();
            }
            else {
                geominy << rows_.front() << "/" << aux.front();
                geomaxy << rows_.back() << "/" << aux.back();
            }

            geoMinY_ = geominy.str();
            geoMaxY_ = geomaxy.str();
        }
    }
    catch (...) {
        throw MagicsException("Could not find any X axis");
    }
    return true;
}
void NetcdfMatrixInterpretor::getReady(const Transformation& transformation) {
    // adjust the data to the transformation..


    refDateX_ = transformation.getReferenceX();
    columns_.clear();
    x();


    refDateY_ = transformation.getReferenceY();
    rows_.clear();
    y();
}

void NetcdfMatrixInterpretor::visit(Transformation& transformation) {
    // get the data ...
    // by default, respect the internal organisation of the data..
    try {
        refDateX_ = (transformation.getAutomaticX()) ? "" : transformation.getReferenceX();
        x();
        refDateY_ = (transformation.getAutomaticY()) ? "" : transformation.getReferenceY();
        y();

        if (transformation.getAutomaticX()) {
            if (!baseDateX_.empty()) {
                transformation.setDataMinMaxX(columns_.front(), columns_.back(), baseDateX_);
            }
            else if (!geoMinX_.empty()) {
                string coords = geoMinX_ + "/" + geoMaxX_;
                transformation.setDataMinMaxX(columns_.front(), columns_.back(), coords);
            }
            else {
                transformation.setMinMaxX(columns_.front(), columns_.back());
            }
        }
        if (transformation.getAutomaticY()) {
            if (!baseDateY_.empty()) {
                transformation.setDataMinMaxY(rows_.front(), rows_.back(), baseDateY_);
            }
            else if (!geoMinY_.empty()) {
                string coords = geoMinY_ + "/" + geoMaxY_;
                transformation.setDataMinMaxY(rows_.front(), rows_.back(), coords);
            }
            else {
                transformation.setMinMaxY(rows_.front(), rows_.back());
            }
        }
    }
    catch (...) {
        throw MagicsException("Could not find any X axis");
    }
}
void NetcdfMatrixInterpretor::customisedPoints(const Transformation& transformation, const std::set<string>&,
                                               CustomisedPointsList& points, int thinning) {
    refDateX_ = transformation.getReferenceX();
    refDateY_ = transformation.getReferenceY();

    Matrix* uc = 0;
    Matrix* vc = 0;
    field_     = x_component_;
    if (!interpretAsMatrix(&uc))
        return;
    field_ = y_component_;
    if (!interpretAsMatrix(&vc))
        return;

    vector<double>::iterator u = uc->begin();
    vector<double>::iterator v = vc->begin();


    for (int row = 0; row < rows_.size(); row += thinning)
        for (int column = 0; column < columns_.size(); column += thinning) {
            CustomisedPoint* point = new CustomisedPoint();
            point->longitude(columns_[column]);
            point->latitude(rows_[row]);
            (*point)["x_component"] = (*uc)(row, column);
            (*point)["y_component"] = (*vc)(row, column);

            ++u;
            ++v;
            points.push_back(point);
        }
}
void NetcdfMatrixInterpretor::statsData(map<string, vector<double> >& stats) {
    if (matrix_) {
        for (unsigned int i = 0; i < matrix_->size(); i++) {
            if (matrix_->at(i) != matrix_->missing()) {
                stats["value"].push_back(matrix_->at(i));
            }
        }
    }
}

void NetcdfMatrixInterpretor::visit(MetaDataCollector& mdc) {
    mdc["_datatype"] = "NetCDF_matrix";
    mdc["path"]      = path_;
    mdc["x"]         = x_;
    mdc["y"]         = y_;
    mdc["value"]     = field_;
    mdc["statsType"] = "scalar";

    Netcdf nc(path_, dimension_method_);

    string attrKey;
    string attrVal;

    // Value attributes
    getAttributes(nc, field_, attrKey, attrVal);
    if (!attrKey.empty()) {
        mdc["valueAttrKey"]   = attrKey;
        mdc["valueAttrValue"] = attrVal;
    }
}

void NetcdfMatrixInterpretor::visit(ValuesCollector& vcp, PointsList&) {
    vcp.setCollected(true);

    ASSERT(matrix_);

    const Transformation& transformation = vcp.transformation();
    MatrixHandler* box                   = transformation.prepareData(*matrix_);
    for (ValuesCollector::iterator point = vcp.begin(); point != vcp.end(); ++point) {
        point->push_back(new ValuesCollectorData(point->x(), point->y(), box->nearest(point->y(), point->x()), -1.));
    }

    // for (ValuesCollector::iterator point =  points.begin(); point != points.end(); ++point) {
    //	point->push_back(new ValuesCollectorData(point->x(), point->y(),matrix_->nearest(point->y(), point->x()),-1.));
    //}
}


bool NetcdfMatrixInterpretor::interpretAsPoints(PointsList& points, const Transformation& transformation) {
    refDateX_ = transformation.getReferenceX();
    refDateY_ = transformation.getReferenceY();

    Matrix* data = 0;

    if (!interpretAsMatrix(&data))
        return false;

    vector<double>::iterator d = data->begin();
    for (vector<double>::iterator row = rows_.begin(); row != rows_.end(); ++row)
        for (vector<double>::iterator column = columns_.begin(); column != columns_.end(); ++column) {
            UserPoint* point = new UserPoint(*column, *row, *d);
            ++d;
            points.push_back(point);
        }
    return true;
}
