/*
 * © 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/message/MessageArchiver.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) {}


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::MessageArchiver 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 */
