/*
 * © Copyright 1996-2012 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.
 */

#include <cstdlib>

#include "mars.h"

#if !defined(NOFDB5)

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

#include "eckit/eckit.h"
#include "eckit/io/Buffer.h"
#include "eckit/io/DataHandle.h"
#include "eckit/io/MemoryHandle.h"
#include "eckit/log/Log.h"
#include "eckit/runtime/Main.h"

#include "metkit/mars/MarsRequest.h"
#include "metkit/mars/TypeAny.h"

#include "fdb5/LibFdb5.h"
#include "fdb5/api/FDB.h"
#include "fdb5/grib/GribArchiver.h"

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

static void     fdb5_init(void);
static err      fdb5_open(void *data,request*,request*,int);
static err      fdb5_close(void *data);
static err      fdb5_read(void *data,request *r,void *buffer,long *length);
static err      fdb5_write(void *data,request *r,void *buffer,long *length);
static boolean  fdb5_check(void *data,request *r);


static void mars_output_callback(void *context, const char* msg) {
    marslog(int(reinterpret_cast<intptr_t>(context)), msg);
}

extern "C" void mars_fdb5_init(int *argc, char **argv) {
	eckit::Main::initialise(*argc, argv);

    // Redirect the output from FDB5 to the relevant channels in marslog.
    // n.b. Don't just use eckit::Log::setCallback, as that sends all the data to the same place.

    if (mars.debug) {
        eckit::Log::setCallback(&mars_output_callback, reinterpret_cast<void*>(LOG_DBUG));
    }

    eckit::Log::info().setCallback(&mars_output_callback, reinterpret_cast<void*>(LOG_INFO));
    eckit::Log::error().setCallback(&mars_output_callback, reinterpret_cast<void*>(LOG_EROR));
    eckit::Log::warning().setCallback(&mars_output_callback, reinterpret_cast<void*>(LOG_WARN));
}


static void fdb5_init(void) {}

#if defined(ECKIT_HAVE_FMEMOPEN) || defined (EC_HAVE_FMEMOPEN)
#define _GNU_SOURCE
#endif



typedef struct fdbdata {
	FILE* f;
	wind* u_v;
    long64 total_bytes;
	timer* fdb_timer;
	double start_time;
	size_t buffer_size;
	char *buffer;
	char *fdb_home;
    hypercube *h;
    int  count;
    int  total_count;
} fdbdata;

static option opts[] = {
    {(char*)"buffer-size", (char*)"FDB5_DIRECT_BUFFER_SIZE", NULL, (char*)"67108864", t_long, sizeof(long),
    int(OFFSET(fdbdata, buffer_size))},

        {(char*)"home", (char*)"MARS_FDB5_HOME",NULL,NULL,t_str,sizeof(char*),
    int(OFFSET(fdbdata, fdb_home))},
};

base_class _fdb5base = {

	NULL,                      /* parent class */
	(char*)"fdb5base",                /* name         */

	false,                     /* inited       */

	sizeof(fdbdata),           /* private size */
	NUMBER(opts),              /* option count */
	opts,                      /* options      */

	fdb5_init,                  /* init         */

	fdb5_open,                  /* open         */
	fdb5_close,                 /* close        */

	fdb5_read,                  /* read         */
	fdb5_write,                 /* write        */

    NULL,                       /* control      */

    fdb5_check,                 /* check        */
};

base_class *fdb5base = &_fdb5base;

static err  fdb5_open(void *data, request *r, request *ev, int mode)
{
    try {
        fdbdata *fdb = (fdbdata *) data;
        fdb->h = nullptr;
        fdb->count = 0;
        fdb->total_count = 0;

        if(fdb->fdb_home) {
            marslog(LOG_INFO, "FDB home %s", fdb->fdb_home);
            fdb5::LibFdb5::instance().libraryHome(fdb->fdb_home);
        }
        else {
            fdb5::LibFdb5::instance().libraryHome(std::string()); // reset to empty in case we visit multiple FDBs
        }

        fdb->fdb_timer = get_timer("FDB time", NULL, true);

        if(EQ(r->name,"RETRIEVE")) {

            fdb->u_v   = wind_new(r,&fdb->total_bytes, 1);

            parameter *p = r->params;

            metkit::mars::MarsRequest req("retrieve");

            while(p)
            {
                if(p->name[0] == '_') {
                    p = p->next;
                    continue;
                }

                std::string name(lowcase(p->name));
                std::string s = lowcase(p->values->name);

                std::vector<std::string> x;
                value* v = p->values;
                while(v) {
                    x.push_back(lowcase(v->name));
                    v = v->next;
                }

                req.setValuesTyped(new metkit::mars::TypeAny(name), x);

                marslog(LOG_DBUG, "key %s = %s", p->name, p->values->name);

                p = p->next;
            }

            fdb->buffer = (char*)malloc(fdb->buffer_size);

            // Initialise to null, to ensure cleanup is aborted if error occurs in initialisation
            fdb->f = nullptr;

            timer *index = get_timer("FDB index time",NULL,true);
            {
                timer_start(index);

                fdb5::FDB fdbApi;
                fdb->f = fdbApi.retrieve(req)->openf("r", true);
                setvbuf(fdb->f,fdb->buffer,_IOFBF,fdb->buffer_size);

                timer_stop(index, 0);
            }
        }
        else if(EQ(r->name,"ARCHIVE")) {
            fdb->h = new_hypercube_from_mars_request(r);
            fdb->count = 0;
            fdb->total_count = count_fields(r);
//            print_all_requests(r);
            marslog(LOG_DBUG, "FDB base total number fields to archive %d", fdb->total_count);
        }
        else {
            marslog(LOG_EROR,"Unrecognised FDB database verb=%s", r->name);
            return -2;
        }
    }
    catch(std::exception& e) {
        marslog(LOG_EROR,"FDB open error %s", e.what());
        return -2;
    }
    catch(...) {
        marslog(LOG_EROR,"FDB open error - unknown exception");
        return -2;
    }

    return 0;
}

static err fdb5_close(void *data)
{
    try {
        fdbdata *fdb = (fdbdata*)data;

        if (fdb->f != nullptr) {
            fclose(fdb->f);
        }
        wind_free(fdb->u_v);

        timer_partial_rate(fdb->fdb_timer, fdb->start_time, fdb->total_bytes);

        if(fdb->h)
            free_hypercube(fdb->h);

        free(fdb->buffer);

        if(fdb->total_count) { /* check all fields were archived */
            if(fdb->count != fdb->total_count) {
                marslog(LOG_EROR,"FDB number of archived fields mismatch expected %d archived %d", fdb->total_count, fdb->count);
                return -2;
            }
        }
    }
    catch(std::exception& e) {
        marslog(LOG_EROR,"FDB close error %s", e.what());
        return -2;
    }
    catch(...) {
        marslog(LOG_EROR,"FDB close error - unknown exception");
        return -2;
    }

    return 0;
}


static err  fdb5_read(void *data, request *r, void *buffer, long *length)
{
    try {
        fdbdata *fdb = (fdbdata*)data;
        return timed_wind_next(fdb->u_v, fdb->f, (char *) buffer, length, fdb->fdb_timer);
    }
    catch(std::exception& e) {
        marslog(LOG_EROR,"FDB read error %s", e.what());
        return -2;
    }
    catch(...) {
        marslog(LOG_EROR,"FDB read error - unknown exception");
        return -2;
    }
}

static err  fdb5_write(void *data, request *r, void *buffer, long *length)
{
    try {
        fdbdata *fdb = (fdbdata *) data;

        // print_all_requests(r);
        int e = remove_field_from_hypercube(fdb->h, r, fdb->count + 1);
        if (e != NOERR) {
            return e;
        }

        fdb5::Key check;

        fdb5::GribArchiver archiver(check);

        eckit::MemoryHandle mh(buffer, size_t(*length));

        *length = archiver.archive(mh);

        fdb->total_bytes += *length;
        fdb->count++;

        return 0;
    } catch (std::exception &e) {
        marslog(LOG_EROR, "FDB5 write error %s", e.what());
        return -2;
    } catch (...) {
        marslog(LOG_EROR, "FDB5 write error - unknown exception");
        return -2;
    }
}

static boolean  fdb5_check(void *data,request *r)
{
	if(track(r))
		return true;

	if(is_bufr(r) || image(r))
		return false;

	return true;
}


#else /* NOFDB5 */

extern "C" {

    base_class *fdb5base = &_nullbase;

	void mars_fdb5_init(int *argc, char **argv) {
	}

} // extern "C"

#endif /* NOFDB5 */
