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

 Copyright 2018 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 <algorithm>
#include <fstream>

#include "fstream_mars_fix.h"

#include "MvPath.hpp"
#include "macro.h"
#include "arith.h"
#include "cgptset.h"


/*
    A GeopointSet is simply a file with
    #GEOPOINTSET
    <Geopoints file>
    <Geopoints file>
    ...
    <Geopoints file>

    i.e., a set of contatenated geopoints files with a #GEOPOINSET header
*/


CGeoptSet::CGeoptSet() :
    Content(tgptset)
{
}

CGeoptSet::CGeoptSet(const CGeoptSet* src) :
    Content(tgptset)
{
    vgpts = src->vgpts;
    format = src->format;
}

CGeoptSet::CGeoptSet(const char* p, int temp) :
    Content(tgptset)
{
    r = empty_request("GEOPOINTSET");
    set_value(r, "PATH", "%s", FullPathName(p).c_str());
    set_value(r, "TEMPORARY", "%d", temp);
    load();
}


CGeoptSet::CGeoptSet(request* s) :
    Content(tgptset)
{
    r = clone_all_requests(s);
    load();
}


CGeoptSet::~CGeoptSet() = default;

void CGeoptSet::Print(void)
{
    std::cout << "geopointset " << '<' << Count() << " geopoints" << '>';
}

int CGeoptSet::Write(FILE* f)
{
    int e = 0;
    fprintf(f, "#GEOPOINTSET\n");
    for (unsigned long i = 1; i < Count() + 1; i++) {
        Content* c = (*this)[i].GetContent();
        e = c->Write(f);
        if (e)
            return e;
    }
    return e;
}

void CGeoptSet::load(void)
{
    if (Count() > 0)  // already loaded
        return;

    if (r) {
        const char* path = get_value(r, "PATH", 0);

        std::ifstream f(path);
        if (!f) {
            marslog(LOG_EROR, "Could not open geopointset file: %s", path);
            return;  // false;
        }

        // read each embedded geopoints file from the 'master file'
        bool readOk = true;
        while (readOk && !f.eof()) {
            auto* gp = new MvGeoPoints();
            readOk = gp->load(f);
            auto* cgp = new CGeopts(*gp);
            Add(*(new Value(cgp)));
        }
    }
}


// -------------------------------------------------------------------------------------------
// Helper functions, used when performing operations between a geopointset and another variable
// - when we want to do geopointset + number, we want to iterate over each geopoints in the
//   set, but only use the same number each time; this function defines the behaviour of each
//   variable type in this regard - each time, do we index or not.
// -------------------------------------------------------------------------------------------

static unsigned long GetElementCount(Value& val)
{
    vtype t = val.GetType();

    switch (t) {
        case tnumber:
            return 1;  // a number has only one element
        case tgrib: {
            fieldset* v;
            val.GetValue(v);
            return v->count;
        }
        case tgptset: {
            CGeoptSet* g;
            val.GetValue(g);
            return g->Count();
        }
        default:
            return 0;
    }
}


static Value GetNthElementForComputation(Value& val, unsigned long i)
{
    vtype t = val.GetType();

    switch (t) {
        case tnumber:
            return val;  // no indexing, always use the same number
        case tgrib: {
            fieldset* v;
            val.GetValue(v);
            if (v->count == 1)  // single fieldset - use for all iterations
                return val;
            else  // multiple fieldsets - use each one sequentially
            {
                fieldset* w = sub_fieldset(v, i, i, 1);
                return Value(w);
            }
        }
        case tgptset: {
            CGeoptSet* g;
            val.GetValue(g);
            if (g->Count() == 1)  // single geopoints - use for all iterations
                return Value((*g)[1]);
            else  // multiple geopoints - use each one sequentially
                return Value((*g)[i]);
        }
        default:
            return Value();  // nil, should result in an error
    }
    /*
    tgeopts  = 32768L,
*/
}


//===========================================================================

class SubGeoSetFunction : public Function
{
public:
    SubGeoSetFunction(const char* n) :
        Function(n, 2, tgptset, tnumber) {}
    virtual Value Execute(int arity, Value* arg);
};

Value SubGeoSetFunction::Execute(int, Value* arg)
{
    CGeoptSet* p;
    long n;

    arg[0].GetValue(p);
    arg[1].GetValue(n);

    if (n < 1 || n > (long)p->Count())
        return Error("GeopointSet index is %ld, but should be from 1 to %ld", n, p->Count());

    Value g = (*p)[n];  // CGeoptSet[n] takes 1-based indexes

    return g;
}


//===========================================================================

class CreateGeopointsSetFunction : public Function
{
public:
    CreateGeopointsSetFunction(const char* n) :
        Function(n)
    {
        info = "Creates a new geopoints_set";
    }
    virtual int ValidArguments(int arity, Value* arg);
    virtual Value Execute(int arity, Value* arg);
};

int CreateGeopointsSetFunction::ValidArguments(int arity, Value* arg)
{
    // args, if given, must all be geopoints
    for (int i = 0; i < arity; i++)
        if (arg[i].GetType() != tgeopts)
            return false;

    return true;
}

Value CreateGeopointsSetFunction::Execute(int arity, Value* arg)
{
    auto* gs = new CGeoptSet();

    for (int i = 0; i < arity; i++) {
        CGeopts* gp;
        arg[i].GetValue(gp);
        gs->Add(*(new Value(gp)));
    }
    return Value(gs);
}

//===========================================================================
class CountGeoSetFunction : public Function
{
public:
    CountGeoSetFunction(const char* n) :
        Function(n, 1, tgptset) {}
    virtual Value Execute(int arity, Value* arg);
};

Value CountGeoSetFunction::Execute(int, Value* arg)
{
    CGeoptSet* g;
    arg[0].GetValue(g);
    // g->load();
    return Value(g->Count());
}


//===========================================================================

class MergeGeopointSetFunction : public Function
{
public:
    MergeGeopointSetFunction(const char* n) :
        Function(n) {}
    virtual Value Execute(int arity, Value* arg);
    virtual int ValidArguments(int arity, Value* arg);
};

int MergeGeopointSetFunction::ValidArguments(int arity, Value* arg)
{
    // args are: (tgptset, [tgptset|tgeopts])
    if (arity != 2)
        return false;

    if (arg[0].GetType() != tgptset)
        return false;

    if (arg[1].GetType() != tgptset && arg[1].GetType() != tgeopts)
        return false;

    return true;
}

Value MergeGeopointSetFunction::Execute(int, Value* arg)
{
    CGeoptSet* geoset;
    arg[0].GetValue(geoset);

    if (arg[1].GetType() == tgeopts) {
        CGeopts* gp;
        arg[1].GetValue(gp);
        auto* newgpset = new CGeoptSet(geoset);
        newgpset->Add(*(new Value(gp)));
        return Value(newgpset);
    }
    else if (arg[1].GetType() == tgptset) {
        CGeoptSet* gset2;
        arg[1].GetValue(gset2);
        auto* newgpset = new CGeoptSet(geoset);
        unsigned long n = gset2->Count();
        for (unsigned long i = 1; i < n + 1; i++)
            newgpset->Add((*gset2)[i]);
        return Value(newgpset);
    }

    return Value(new CGeoptSet());
}

//===========================================================================

class FilterGeoSetFunction : public Function
{
public:
    FilterGeoSetFunction(const char* n) :
        Function(n, 2, tgptset, trequest) {}
    virtual Value Execute(int arity, Value* arg);
};

Value FilterGeoSetFunction::Execute(int, Value* arg)
{
    CGeoptSet* gps;
    request* f;

    arg[0].GetValue(gps);
    arg[1].GetValue(f);

    // if the filter request is empty, then we return the input gset as output
    if (!f->params) {
        return Value(new CGeoptSet(gps));
    }

    // otherwise, do the filtering
    auto* result = new CGeoptSet();
    unsigned long c = gps->Count();
    for (unsigned long i = 1; i <= c; i++) {
        Value& val = (*gps)[i];
        CGeopts* gpi;
        val.GetValue(gpi);
        if (gpi->doesMetadataMatch(f))
            result->Add(val);
    }

    if (result->Count() > 0)
        return Value(result);
    else
        return Value();  // no results - return nil
}


//===========================================================================
// These are single-argument functions such as sin, cos, abs...

class GeoSetUnOp : public Function
{
    const char* fname_;

public:
    GeoSetUnOp(const char* n, uniproc) :
        Function(n, 1, tgptset)
    {
        fname_ = strcache(n);
    }
    virtual Value Execute(int arity, Value* arg);
};

Value GeoSetUnOp::Execute(int, Value* arg)
{
    CGeoptSet* g;  // original  geopointset
    CGeoptSet* p;  // resultant geopointset

    arg[0].GetValue(g);
    p = new CGeoptSet();

    // for each geopoints var, call this function on that gpt
    for (unsigned long i = 1; i < g->Count() + 1; i++) {
        Owner()->Push((*g)[i]);
        Owner()->CallFunction(fname_, 1);
        Value y = Owner()->Pop();
        if (y.GetType() == tgeopts) {
            p->Add(y);  // add the result to p
        }
        else {
            return Error("Result of unary operator on geopoints returned non-geopoints");
        }
    }

    return Value(p);
}

//===========================================================================
// Value + GeopointSet

class ValueGeoSetBinOp : public Function
{
    const char* fname_;

public:
    ValueGeoSetBinOp(const char* n, binproc) :
        Function(n) { fname_ = strcache(n); }
    virtual Value Execute(int arity, Value* arg);
    virtual int ValidArguments(int arity, Value* arg);
};

int ValueGeoSetBinOp::ValidArguments(int arity, Value* arg)
{
    if (arity != 2)
        return false;
    if (arg[0].GetType() == tgptset || arg[1].GetType() == tgptset)
        return true;
    return false;
}

Value ValueGeoSetBinOp::Execute(int, Value* arg)
{
    CGeoptSet* g;

    if (arg[0].GetType() == tgptset)
        arg[0].GetValue(g);
    else
        arg[1].GetValue(g);

    unsigned long i1 = GetElementCount(arg[0]);
    unsigned long i2 = GetElementCount(arg[1]);

    if ((i1 != i2) && (i1 != 1 && i2 != 1))  // not the same, and not both 1
    {
        return Error("Functions on geopointsets require the same number of elements or 1; we have %d and %d", i1, i2);
    }

    unsigned long c = std::max(i1, i2);

    auto* p = new CGeoptSet();

    // for each geopoints var, call this function on that gpt
    for (unsigned long i = 1; i < c + 1; i++)  // 1-based indexing
    {
        Owner()->Push(GetNthElementForComputation(arg[0], i));
        Owner()->Push(GetNthElementForComputation(arg[1], i));
        Owner()->CallFunction(fname_, 2);
        Value y = Owner()->Pop();
        if (y.GetType() == tgeopts) {
            p->Add(y);  // add the result to p
        }
        else {
            return Error("Result of operation on number and geopoints returned non-geopoints");
        }
    }

    return Value(p);
}


//-------------------------------------------------------------------
//===========================================================================

static void install(Context* c)
{
    int i;

    // Unary op

    for (i = 0; UniOps[i].symb; i++)
        c->AddFunction(new GeoSetUnOp(UniOps[i].symb, UniOps[i].proc));
    for (i = 0; BinOps[i].symb; i++)
        c->AddFunction(new ValueGeoSetBinOp(BinOps[i].symb, BinOps[i].proc));

    c->AddFunction(new CountGeoSetFunction("count"));
    c->AddFunction(new CreateGeopointsSetFunction("create_geo_set"));
    c->AddFunction(new MergeGeopointSetFunction("&"));
    c->AddFunction(new SubGeoSetFunction("[]"));
    c->AddFunction(new FilterGeoSetFunction("filter"));
}

static Linkage linkage(install);
