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

#ifndef mars_H
#include "mars.h"
#endif

int static eq_pseudo_ok(const char* l, const char* r) {
    if (mars.pseudogrib && mars.dont_check_pseudogrib) {
        if (!eq_string(l, r))
            marslog(LOG_WARN, "PSEUDO GRIB mismatch ignored: %s %s", l, r);
        return 1;
    }
    return eq_string(l, r);
}


static axis_t axis[] = {

    /* From dhsbase.c 'check_grib' */
    {
        "CLASS",
        eq_pseudo_ok,
    },
    {
        "TYPE",
        eq_pseudo_ok,
    },
    {
        "STREAM",
        eq_pseudo_ok,
    },
    {
        "LEVTYPE",
        eq_string,
    },

    {
        "ORIGIN",
        eq_string,
    },
    {
        "PRODUCT",
        eq_string,
    },
    {
        "SECTION",
        eq_string,
    },
    {
        "METHOD",
        eq_integer,
    },
    {
        "SYSTEM",
        eq_integer,
    },

    /* testing */
    /* {"REPRES",      eq_null, }, */

    /* From field order */
    {
        "DATE",
        eq_date,
    },
    {
        "REFDATE",
        eq_date,
    },
    {
        "HDATE",
        eq_date,
    },
    {
        "OFFSETDATE",
        eq_date,
    },
    {
        "TIME",
        eq_time,
    },
    {
        "OFFSETTIME",
        eq_time,
    },
    {
        "ANOFFSET",
        eq_range,
    },

    {
        "REFERENCE",
        eq_range,
    },
    {
        "STEP",
        eq_range,
    },
    {
        "FCMONTH",
        eq_integer,
    },
    {
        "FCPERIOD",
        eq_range,
    },

    {
        "LEADTIME",
        eq_range,
    },
    {
        "OPTTIME",
        eq_range,
    },

    {
        "EXPVER",
        eq_pseudo_ok,
    },
    {
        "DOMAIN",
        eq_string,
    },

    {
        "DIAGNOSTIC",
        eq_integer,
    },
    {
        "ITERATION",
        eq_integer,
    },

    {
        "QUANTILE",
        eq_range,
    },
    {
        "NUMBER",
        eq_integer,
    },

    {
        "LEVELIST",
        eq_coord,
    },
    {
        "LATITUDE",
        eq_coord,
    },
    {
        "LONGITUDE",
        eq_coord,
    },
    {
        "RANGE",
        eq_range,
    },

    {
        "PARAM",
        eq_param,
    },

    {
        "IDENT",
        eq_integer,
    },
    {
        "OBSTYPE",
        eq_integer,
    },
    {
        "INSTRUMENT",
        eq_integer,
    },

    {
        "FREQUENCY",
        eq_integer,
    },
    {
        "DIRECTION",
        eq_integer,
    },

    {
        "CHANNEL",
        eq_integer,
    },
};

static axis_t axisnew[1024]; /* May be enough language attributes for the time being... */
static int init_axis(const request* r) {
    parameter* p     = r->params;
    static int count = 0;

    if (count)
        return count;

    while (p) {
        const request* i  = p->interface;
        boolean take      = true;
        const char* check = NULL;

        if (i && (((check = get_value(i, "check", 0)) != NULL) && EQ(check, "false")))
            take = false;

        if (take) {
            const char* type = get_value(i, "type", 0);
            namecmp cmp      = eq_default;

            axisnew[count].name = p->name;
            if (type) {
                if (EQ(type, "string"))
                    cmp = eq_string;
                else if (EQ(type, "null"))
                    cmp = eq_null;
                else if (EQ(type, "date"))
                    cmp = eq_date;
                else if (EQ(type, "time"))
                    cmp = eq_time;
                else if (EQ(type, "step"))
                    cmp = eq_range;
                else if (EQ(type, "integer"))
                    cmp = eq_integer;
                else if (EQ(type, "coord"))
                    cmp = eq_coord;
                else if (EQ(type, "real"))
                    cmp = eq_real;
                else if (EQ(type, "default"))
                    cmp = eq_default;
            }
            axisnew[count].compare = cmp;
        }
        count++;
        p = p->next;
    }
    return count;
}

int axisindex(const char* name) {
    int i = 0;
    for (i = 0; i < NUMBER(axis); i++) {
        if (EQ(name, axis[i].name))
            return i;
    }
    return -1;
}

namecmp comparator(const char* name) {
    static char* dontcompare = NULL;
    static boolean first     = true;
    int i                    = 0;

    if (first) {
        dontcompare = getenv("MARS_DONT_CHECK");
        first       = false;
    }

    if (dontcompare != NULL) {
        if (EQ(dontcompare, name))
            return eq_null;
    }

    if ((i = axisindex(name)) != -1)
        return axis[i].compare;
    marslog(LOG_WARN, "No comparator for %s", name);
    return eq_string;
}

/********************/
/* index accessors  */
/********************/

static int count_index(const hypercube* h) {
    int i = 0, n = 0;

    for (i = 0; i < h->size; ++i)
        n += h->set[i];
    return n;
}

static int count_holes(const hypercube* h, int cnt) {
    int i = 0, n = 0;

    for (i = 0; i < cnt; ++i)
        n += h->set[i];

    return (n == cnt) ? 0 : (cnt - n);
}

void print_hypercube_index(const hypercube* h) {
    int i = 0;

    for (i = 0; i < h->size; ++i)
        printf(" %d", h->set[i]);
    printf("\n");
}

static void reset_index(hypercube* h, int v) {
    memset(h->set, v, h->size);
}


static int pos_to_index(const hypercube* h, int pos, int* from, int* last) {
    for (; *from < h->size; ++(*from)) {
        *last += h->set[*from];
        if (*last > pos)
            return (*from)++;
    }
    return -1;
}

static void set_index(hypercube* h, int index, int value) {
    if (index < 0 || index >= h->count) {
        marslog(LOG_EXIT, "Internal error, bad hypercube index %d", index);
    }

    if (index >= h->max) {
        int old = h->max;
        while (index >= h->max)
            h->max += 4096;

        h->set = h->set ? REALLOC(h->set, h->max) : MALLOC(h->max);
        memset(h->set + old, 0, h->max - old);
    }

    if (index >= h->size)
        h->size = index + 1;

    h->set[index] = value;
}

static int get_index(hypercube* h, int index) {
    if (index < 0 || index >= h->count) {
        marslog(LOG_EXIT, "Internal error, bad hypercube index %d", index);
    }

    if (index >= h->size)
        return 0;

    return h->set[index];
}

static int cmp_index(const hypercube* a, const hypercube* b) {
    if (a->size == b->size)
        return memcmp(a->set, b->set, a->size);
    return -1;
}

/**************************/
/* End of index accessors */
/**************************/


/*******************/
/* axis accessors  */
/*******************/

static int count_axis(const hypercube* h) {
    if (h && h->cube)
        return count_values(h->cube, "AXIS");

    return -1;
}


static const char* get_axis(const hypercube* h, int pos) {
    const char* axis = NULL;
    if (pos < count_axis(h)) {
        axis = get_value(h->cube, "AXIS", pos);
    }
    return axis;
}

static void add_axis(hypercube* h, const char* axis) {
    add_value(h->cube, "AXIS", "%s", axis);
}

static void reset_axis(hypercube* h) {
    unset_value(h->cube, "AXIS");
}

static void remove_axis(hypercube* h, const char* axis) {
    unset_param_value(h->cube, "AXIS", axis);
}

static void cube_values(hypercube* h, const char* p) {
    valcpy(h->cube, h->r, (char*)p, (char*)p);
}

static int count_dimensions(const hypercube*, const char*);

static int set_axis(hypercube* h) {
    int i     = 0;
    int count = (h && h->r) ? 1 : -1;

    reset_axis(h);
    for (i = (NUMBER(axis) - 1); i >= 0; --i) {
        int n = count_dimensions(h, axis[i].name);
        if (n > 1) {
            add_axis(h, axis[i].name);
            cube_values(h, axis[i].name);
            count *= n;
        }
    }

    return count;
}

/*************************/
/* End of axis accessors */
/*************************/

/*******************/
/* Cube dimensions */
/*******************/


static int count_dimensions(const hypercube* h, const char* axis) {
    int dims = -1;
    if (h && h->r)
        dims = count_values(h->r, axis);
    return dims;
}

/**************************/
/* End of cube dimensions */
/**************************/

/**************************/
/* Auxiliary functions    */
/**************************/


static int count_hypercube(const request* r) {
    int i = 0, count = 1;
    for (i = 0; i < NUMBER(axis); ++i) {
        int c = count_values(r, axis[i].name);
        count *= c ? c : 1;
    }

    return count;
}

int cube_order(const hypercube* h, const request* r) {
    return _cube_position(h, r, true);
}

static int cube_position(const hypercube* h, const request* r) {
    return _cube_position(h, r, false);
}

static void reserve_index_cache(hypercube* h, int size) {
    if (size == 0)
        return;

    if (h->index_cache != 0)
        FREE(h->index_cache);
    marslog(LOG_DBUG, "Allocating hypercube index_cache: %d entries", size);
    h->index_cache      = NEW_ARRAY_CLEAR(int, size);
    h->index_cache_size = size;
}

int _cube_position(const hypercube* h, const request* r, boolean remove_holes) {
    request* cube = h->cube;
    int c         = count_axis(h);
    int index     = 0;
    int i         = 0;
    int n         = 1;
    int ok        = 0;

    if (h->index_cache == 0 || h->index_cache_size != c)
        reserve_index_cache((hypercube*)h, c);

    for (i = 0; i < c; ++i) {
        const char* axis = get_axis(h, i);
        const char* v    = get_value(r, axis, 0);
        const char* w    = NULL;
        int dims         = count_dimensions(h, axis);
        int k            = 0;
        int count        = count_values(cube, axis);
        int last         = h->index_cache[i];

        for (k = 0; k < count; k++) {
            int j = (k + last) % count;
            w     = get_value(cube, axis, j);
            if (h->compare ? h->compare[i](w, v) : (w == v)) {
                index += j * n;
                n *= dims;
                ok++;
                ((hypercube*)h)->index_cache[i] = j;
                break;
            }
            else
                marslog(LOG_DBUG, "_cube_position, %s, %s != %s [%scompare function available]", axis, w, v, h->compare ? "" : "no ");
        }
    }

    if (remove_holes) {
        int holes = 0;
        if (count_index(h) != h->size)
            holes = count_holes(h, index);
        index -= holes;
    }

    return (ok == c) ? index : -1;
}

static void explain_cube_position(const hypercube* h, const request* r) {
    request* cube = h->cube;
    int c         = count_axis(h);
    int i         = 0;

    if (h->index_cache == 0 || h->index_cache_size != c)
        reserve_index_cache((hypercube*)h, c);

    for (i = 0; i < c; ++i) {
        const char* axis = get_axis(h, i);
        const char* v    = get_value(r, axis, 0);
        const char* w    = NULL;
        int k            = 0;
        int count        = count_values(cube, axis);
        int last         = h->index_cache[i];
        int ok           = 0;


        for (k = 0; k < count; k++) {
            int j = (k + last) % count;
            w     = get_value(cube, axis, j);
            if (h->compare ? h->compare[i](w, v) : (w == v)) {
                ok++;
                ((hypercube*)h)->index_cache[i] = j;
                break;
            }
        }

        if (!ok)
            marslog(LOG_EROR, "Could not match %s (%s)", axis, v ? v : "(null)");
    }
}

boolean cube_contains(const hypercube* h, const request* r) {
    request* cube = h->cube;
    int c         = count_axis(h);
    int i         = 0;
    int j         = 0;

    if (h->index_cache == 0 || h->index_cache_size != c)
        reserve_index_cache((hypercube*)h, c);

    for (i = 0; i < c; ++i) {
        const char* axis = get_axis(h, i);

        const char* w = NULL;
        int k         = 0;
        int count     = count_values(cube, axis);
        int last      = h->index_cache[i];

        int j         = 0;
        const char* v = get_value(r, axis, j++);
        int ok        = (v == NULL);

        while (v && !ok) {

            for (k = 0; k < count && !ok; k++) {
                int j = (k + last) % count;
                w     = get_value(cube, axis, j);
                /* printf("w: %s, axis: %s, j: %d\n",w,axis,j); */
                if (h->compare ? h->compare[i](w, v) : (w == v)) {
                    ok++;
                    ((hypercube*)h)->index_cache[i] = j;
                    break;
                }
            }
            v = get_value(r, axis, j++);
        }

        if (!ok) {
            marslog(LOG_DBUG, "cube_contains: Could not match %s (%s)", axis, v ? v : "(null)");
            return false;
        }
    }
    return true;
}
void cube_indexes(const hypercube* h, request* r, int* indexes, int size) {
    request* cube = h->cube;
    int c         = count_axis(h);
    int i         = 0;
    int index     = 0;
    int n         = 1;
    int ok        = 0;

    if (size < c) {
        marslog(LOG_WARN, "MARS internal error in cube_indexes. size=%d < axis=%d", size, c);
    }

    if (h->index_cache == 0 || h->index_cache_size != c)
        reserve_index_cache((hypercube*)h, c);

    for (i = 0; i < c; ++i) {
        const char* axis = get_axis(h, i);
        const char* v    = get_value(r, axis, 0);
        const char* w    = NULL;
        int dims         = count_dimensions(h, axis);
        int j            = 0;
        int k            = 0;
        int count        = count_values(cube, axis);
        int last         = h->index_cache[i];


        for (k = 0; k < count; k++) {
            j = (k + last) % count;
            w = get_value(cube, axis, j);
            if (h->compare ? h->compare[i](w, v) : (w == v)) {
                index += j * n;
                n *= dims;
                ok++;
                ((hypercube*)h)->index_cache[i] = j;
                break;
            }
        }
        indexes[i] = j;
    }
}

/*********************************/
/* End of Auxiliary functions    */
/*********************************/


hypercube* new_hypercube(const request* r) {
    hypercube* h = NEW_CLEAR(hypercube);
    int total = 0, count = 0;
    int n           = 0;
    const char* val = 0;

    h->r    = clone_one_request(r);
    h->cube = empty_request("CUBE");

    h->count = total = count_hypercube(r);
    count            = set_axis(h);

    h->compare = 0;

    if ((total != count) || (count == 0)) {
        marslog(LOG_EROR, "Internal error while computing hypercube fields");
        marslog(LOG_EROR, "Number of fields in request %d", total);
        marslog(LOG_EROR, "Number of fields in hypercube %d", count);
    }
    set_index(h, count - 1, 1);
    memset(h->set, 1, count);

    /* This is expensive, but makes the iterator with only
       those parameters found as axis */
    h->iterator = empty_request(0);
    for (n = 0; n < NUMBER(axis); ++n)
        if ((val = get_value(h->r, axis[n].name, 0)))
            set_value(h->iterator, axis[n].name, val);

    return h;
}

int add_field_to_hypercube(hypercube* h, request* r) {
    return -1;
}

int remove_field_from_hypercube(hypercube* h, request* r, int n) {
    parameter* p;
    int o = cube_position(h, r);

    if (o < 0) {
        marslog(LOG_EROR, "Field %d is unknown", n);
        explain_cube_position(h, r);
        print_one_request(r);
        return HYPERCUBE_ERROR;
    }

    if (get_index(h, o) == 0) {
        marslog(LOG_EROR, "Field %d is duplicated or not described in request", n);
        print_one_request(r);
        return HYPERCUBE_ERROR;
    }

    p = r->params;
    while (p) {
        if (*p->name != '_' && !count_values(h->cube, p->name)) {
            if (axisindex(p->name) != -1) {
                marslog(LOG_EROR, "Field %d has %s = %s, but %s is not in request", n, p->name, get_value(r, p->name, 0), p->name);
                print_one_request(r);
                return HYPERCUBE_ERROR;
            }
        }
        p = p->next;
    }

    set_index(h, o, 0);

    return NOERR;
}

void remove_name_from_hypercube(hypercube* h, const char* ignore) {
    unset_value(h->r, ignore);
    unset_value(h->cube, ignore);
    unset_param_value(h->cube, "AXIS", ignore);
}

void print_hypercube(const hypercube* h) {
    print_all_requests(h->r);
    print_all_requests(h->cube);
    marslog(LOG_INFO, "%d active out of %d fields described\n", count_index(h), h->size);
}

void free_hypercube(hypercube* h) {
    if (h) {
        free_all_requests(h->r);
        free_all_requests(h->cube);
        free_all_requests(h->iterator);
        FREE(h->index_cache);
        FREE(h->compare);
        FREE(h->set);
        FREE(h);
    }
}

request* _get_cubelet(hypercube* h, int index) {
    int i = 0;
    int c = count_axis(h);

    for (i = 0; i < c; ++i) {
        const char* axis = get_axis(h, i);
        int dims         = count_dimensions(h, axis);
        int coord        = index % dims;
        const char* val  = get_value(h->r, axis, coord);

        if (!val) {
            marslog(LOG_EROR, "MARS internal error handling field %d", index);
            marslog(LOG_EROR, "No value for '%s'[%d] from request", axis, coord);
            marslog(LOG_EROR, "Contact the MARS group");
            marsexit(1);
        }
        set_value(h->iterator, axis, "%s", val);
        index /= dims;
    }
    return h->iterator;
}

request* get_cubelet(hypercube* h, int index) {
    int from = 0, last = 0;
    int pos = pos_to_index(h, index, &from, &last);

    return (pos >= 0) ? _get_cubelet(h, pos) : NULL;
}

request* next_cubelet(hypercube* h, int* from, int* last) {
    int pos = pos_to_index(h, *last, from, last);
    return (pos >= 0) ? _get_cubelet(h, pos) : NULL;
}

static void copy_cube(hypercube* a, const hypercube* b, int v) {
    request* r = NULL;
    int from = 0, last = 0;

    while ((r = next_cubelet((hypercube*)b, &from, &last))) {
        int new_index = cube_position(a, r);
        set_index(a, new_index, v);
    }
}


hypercube* merge_cube(const hypercube* a, const hypercube* b, int init, int va, int vb) {
    hypercube* n = NULL;
    request* r   = clone_one_request(a->r);

    reqmerge(r, b->r);

    n = new_hypercube(r);
    free_all_requests(r);

    reset_index(n, init);

    copy_cube(n, a, va);
    copy_cube(n, b, vb);

    return n;
}

struct stuff_1 {
    hypercube* c;
    request* r;
};

static void reqcb_1(const request* r, int count, char* names[],
                    char* vals[], void* data) {
    struct stuff_1* s = (struct stuff_1*)data;
    int i;

    for (i = 0; i < count; i++)
        if (vals[i])
            set_value(s->r, names[i], vals[i]);

    set_index(s->c, cube_position(s->c, s->r), 1);
}

hypercube* new_hypercube_from_mars_request(const request* r) {
    int i;
    int n;

    struct stuff_1 s;

#if 0
	const request *lang = mars_language_from_request(r);
	int count = 0;

	count = init_axis(lang);
	marslog(LOG_DBUG,"cube %s",r->kind);
	/* print_all_requests(mars_language_from_request(r)); */
	marslog(LOG_INFO,"NUMBER(axis): %d, number axisnew: %d",NUMBER(axis),count);
#endif

    s.c = new_hypercube(r);
    s.r = clone_one_request(r);

    reset_index(s.c, 0);
    names_loop(r, reqcb_1, &s);

    free_one_request(s.r);

    /* add single paramters */

    for (i = 0; i < NUMBER(axis); i++) {
        int m = count_values(r, axis[i].name);
        if (m == 1) {
            add_value(s.c->cube, "AXIS", axis[i].name);
            set_value(s.c->cube, axis[i].name, get_value(r, axis[i].name, 0));
        }
    }

    n = count_values(s.c->cube, "AXIS");
    if (n)
        s.c->compare = NEW_ARRAY(namecmp, n);

    for (i = 0; i < n; i++)
        s.c->compare[i] = comparator(get_value(s.c->cube, "AXIS", i));

    return s.c;
}

/* This one doesn't have single parameters in CUBE */
hypercube* new_simple_hypercube_from_mars_request(const request* r) {
    int i;
    int n;

    struct stuff_1 s;
    s.c = new_hypercube(r);
    s.r = clone_one_request(r);

    reset_index(s.c, 0);
    names_loop(r, reqcb_1, &s);

    free_one_request(s.r);
    n = count_values(s.c->cube, "AXIS");
    if (n)
        s.c->compare = NEW_ARRAY(namecmp, n);

    for (i = 0; i < n; i++)
        s.c->compare[i] = comparator(get_value(s.c->cube, "AXIS", i));

    return s.c;
}

hypercube* new_hypercube_from_fieldset_cb(fieldset* fs, void (*callback)(request*, void*), void* data) {
    int i;
    request* r = empty_request(0);
    hypercube* c;

    if (!fs)
        return 0;

    for (i = 0; i < fs->count; i++) {
        request* s = field_to_request(fs->fields[i]);
        reqmerge(r, s);
    }

    callback(r, data);

    c = new_hypercube(r);
    reset_index(c, 0);
    for (i = 0; i < fs->count; i++) {
        request* s = field_to_request(fs->fields[i]);
        set_index(c, cube_position(c, s), 1);
    }

    if (fs->count != count_index(c)) {
        marslog(LOG_EROR, "Duplicate fields found in fieldset");
        marslog(LOG_EROR, "Number of fields in fieldset %d", fs->count);
        marslog(LOG_EROR, "Number of fields in hypercube %d", count_index(c));
        free_hypercube(c);
        return 0;
    }

    return c;
}

static void empty_callback(request* r, void* data) {
    /* Empty */
}

hypercube* new_hypercube_from_fieldset(fieldset* fs) {
    return new_hypercube_from_fieldset_cb(fs, empty_callback, NULL);
}


hypercube* new_hypercube_from_file(const char* path) {
    fieldset* fs = read_fieldset(path, 0);
    hypercube* c = new_hypercube_from_fieldset(fs);
    free_fieldset(fs);

    return c;
}

hypercube* add_cube(const hypercube* a, const hypercube* b) {
    return merge_cube(a, b, 0, 1, 1);
}

hypercube* remove_cube(const hypercube* a, const hypercube* b) {
    return merge_cube(a, b, 0, 1, 0);
}

int hypercube_field_count(const hypercube* a) {
    return count_index(a);
}

int hypercube_cube_size(const hypercube* a) {
    return count_hypercube(a->r);
}

int hypercube_compare(const hypercube* a, const hypercube* b) {
    if (hypercube_field_count(a) != hypercube_field_count(b)) {
        return 1;
    }

    if (cmp_index(a, b) != 0) {
        return 1;
    }

    return 0;
}
