/*
 * © 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 <ctype.h>
#include "mars.h"

#define DB_COUNT 2

static void multi_init(void);
static err multi_open(void* data, request*, request*, int);
static err multi_close(void* data);
static err multi_read(void* data, request* r, void* buffer, long* length);
static err multi_write(void* data, request* r, void* buffer, long* length);
static boolean multi_check(void* data, request* r);
static err multi_validate(void* data, request*, request*, int);
static err multi_archive(void* data, request* r);
static err multi_cntl(void* data, int code, void* param, int size);


typedef struct multidata {
    char* base[2];
    boolean* retrieve[2];
    boolean* archive[2];
    database* db[2];
    request* r;
    hypercube* cube;
    request* grib;
    int current;
    int count;
    int got;
    int obs;
    int ai;
    int odb;
    int expect;
    char* found;
    char* ignore;
    request* seen;
    boolean drain;
    boolean remove_bufr_duplicates;
} multidata;

static option opts[] = {
    {"base1", NULL, NULL, NULL,
     t_str, sizeof(char*), OFFSET(multidata, base[0])},

    {"base2", NULL, NULL, NULL,
     t_str, sizeof(char*), OFFSET(multidata, base[1])},

    {"retrieve1", NULL, NULL, "1",
     t_boolean, sizeof(boolean), OFFSET(multidata, retrieve[0])},

    {"retrieve2", NULL, NULL, "1",
     t_boolean, sizeof(boolean), OFFSET(multidata, retrieve[1])},

    {"archive1", NULL, NULL, "1",
     t_boolean, sizeof(boolean), OFFSET(multidata, archive[0])},

    {"archive2", NULL, NULL, "1",
     t_boolean, sizeof(boolean), OFFSET(multidata, archive[1])},

    {"ignore", NULL, NULL, NULL,
     t_str, sizeof(char*), OFFSET(multidata, ignore)},

    {"drain", NULL, NULL, "1",
     t_boolean, sizeof(boolean), OFFSET(multidata, drain)},
};


base_class _multibase = {

    NULL,        /* parent class */
    "multibase", /* name         */

    false, /* inited       */

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

    multi_init, /* init         */

    multi_open,  /* open         */
    multi_close, /* close        */

    multi_read,  /* read         */
    multi_write, /* write        */

    multi_cntl, /* control      */

    multi_check, /* check        */
    NULL,        /* query        */

    multi_archive, /* archive      */
    NULL,          /* admin        */

    multi_validate, /* validate */


};


base_class* multibase = &_multibase;

static void multi_init(void) {
}


static int eof(multidata* multi) {
    if (multi->odb && multi->got > 0) {
        return ODB_FOUND_EOF;
    }
    return EOF;
}

static err open_next(multidata* multi) {
    const char* name;
    request* cachesetup = 0;
    const request* setup;

    if (multi->current >= 0 && multi->db[multi->current]) {
        err e                     = database_close(multi->db[multi->current]);
        multi->db[multi->current] = NULL;
        if (e)
            return e;
    }

    do {
        multi->current++;

        if (multi->current >= DB_COUNT)
            return eof(multi);
    } while (!multi->retrieve[multi->current]);

    setup = findbase(multi->base[multi->current], multi->r);
    if (!setup)
        return -2;


    multi->db[multi->current] = openbase(setup, multi->r, &name, &cachesetup, READ_MODE);
    if (!multi->db[multi->current])
        return -2;

    marslog(LOG_INFO, "Multibase visit %s", name);
    return NOERR;
}

static err multi_open(void* data, request* r, request* e, int mode) {
    multidata* multi = (multidata*)data;

    multi->current = -1;
    multi->r       = clone_all_requests(r);


    if (mode == WRITE_MODE) {
        return 0;
    }


    multi->expect = count_fields(r);

    multi->cube = new_hypercube_from_mars_request(multi->r);

    unset_value(multi->r, "EXPECT");
    multi->count = count_fields(multi->r);

    if (multi->count)
        multi->found = NEW_ARRAY_CLEAR(char, multi->count);

    multi->grib = empty_request("GRIB");

    multi->obs = observation(r);
    multi->ai  = track(r) || feedback(r);
    multi->odb = is_odb(r);

    multi->remove_bufr_duplicates = mars.remove_bufr_duplicates;
    mars.remove_bufr_duplicates   = multi->obs;

    if (multi->ai) {
        int dates = count_values(r, "DATE");
        int times = count_values(r, "TIME");
        if (dates > 1 || times > 1) {
            marslog(LOG_EROR, "Multibase: for type AI and TF, only one DATE and one TIME are allowed");
            return -2;
        }
    }

    return open_next(multi);
}

static err multi_close(void* data) {
    multidata* multi = (multidata*)data;
    int ret          = 0;
    int i;
    int e;


    for (i = 0; i < DB_COUNT; i++)
        if (multi->db[i] != 0) {
            e = database_close(multi->db[i]);
            if (e)
                ret = e;
        }

    free_all_requests(multi->seen);
    free_all_requests(multi->grib);
    free_all_requests(multi->r);
    free_hypercube(multi->cube);
    FREE(multi->found);

    mars.remove_bufr_duplicates = multi->remove_bufr_duplicates;
    return ret;
}

static boolean duplicate(multidata* multi, void* buffer, long length) {
    int i = 0;

    if (multi->obs) {
        /* bufr postproc will filter out duplicates */
        return false;
    }

    if (multi->odb) {
        /* For now, we accept duplicates */
        return false;
    }

    if (multi->ai) {
        /* We visit only one  database */
        multi->got += 1;
        return false;
    }

    if (grib_to_request(multi->grib, buffer, length) != NOERR) {
        marslog(LOG_WARN, "Multi-base: error in grib_to_request");
        return false;
    }

    if (multi->ignore) {
        set_value(multi->grib, multi->ignore, get_value(multi->r, multi->ignore, 0));
    }

    if (multi->count == 0) {  // The user has provided 'ALL'

        const request* r = multi->seen;

        while (r) {
            if (reqcmp(r, multi->grib, 0) == 0) {
                return true;
            }
            r = r->next;
        }

        multi->grib->next = multi->seen;
        multi->seen       = multi->grib;

        multi->grib = empty_request("GRIB");
    }
    else {

        i = cube_order(multi->cube, multi->grib);

        if (i < 0 || i >= multi->count) {
            marslog(LOG_WARN, "Multi-base: unexpected grib");
            print_all_requests(multi->grib);
            return false;
        }

        if (multi->found[i])
            return true;

        multi->got++;
        multi->found[i] = true;
    }

    return false;
}


static err multi_read(void* data, request* r, void* buffer, long* length) {
    multidata* multi = (multidata*)data;
    int ret          = 0;
    long save        = *length;


    /* To see.... */
    if (multi->expect > 0 && multi->got == multi->expect) {
        if (multi->drain) {
            while (database_read(multi->db[multi->current], r, buffer, length) == NOERR) {
                /* consume left over */
            }
        }
        return eof(multi);
    }


    while (multi->current < DB_COUNT) {
        *length = save;
        ret     = database_read(multi->db[multi->current], r, buffer, length);

        switch (ret) {
            case NOERR:
                if (!duplicate(multi, buffer, *length))
                    return NOERR;
                break;


            case ODB_FOUND_EOF:
                multi->got += 1;
            case EOF:

                if (multi->ai) {
                    if (multi->got) {
                        return EOF;
                    }
                }

                ret = open_next(multi);
                if (ret)
                    return ret;
                break;

            default:
                marslog(LOG_EROR, "Multibase error %d", ret);
                return ret;
                break;
        }
    }

    return eof(multi);
}

static err multi_write(void* data, request* r, void* buffer, long* length) {
    marslog(LOG_EROR, "Cannot write on Multi-database");
    return -2;
}

static boolean multi_check(void* data, request* r) {
    return is_grib(r) || is_bufr(r);
}

static err multi_validate(void* data, request* r, request* e, int mode) {
    multidata* multi = (multidata*)data;
    const char* name;
    const request* setup;
    int i;

    for (i = 0; i < DB_COUNT; i++) {
        setup = findbase(multi->base[i], multi->r);
        if (!setup)
            return -1;
        name = get_value(setup, "class", 0);
        if (database_validate(base_class_by_name(name), multi->base[i], r, e, mode) != 0)
            return -1;
    }

    return 0;
}

static err multi_archive(void* data, request* r) {
    multidata* multi = (multidata*)data;
    unset_value(multi->r, "DATABASE");
    if (multi->archive[0]) {
        add_value(multi->r, "DATABASE", "%s", multi->base[0]);
    }

    if (multi->archive[1]) {
        add_value(multi->r, "DATABASE", "%s", multi->base[1]);
    }
    return handle_archive(multi->r, NULL);
}


static err multi_cntl(void* data, int code, void* param, int size) {
    multidata* multi = (multidata*)data;
    int e, err = 0;
    int ok = 0;
    while ((e = open_next(multi)) != EOF) {
        if (e) {
            err = e;
        }
        else {
            ok += 1;
        }
    }

    if (code == CNTL_FLUSH || code == CNTL_STAGE) {
        /* If we are successful once, it's OK */
        if (ok) {
            err = 0;
        }
    }

    return err;
}
