#include "nctypes.h"
#include "mars.h"

/*
TODO:

    - check that each variable have a standard_name and a unit
    - match variables with:
        - same standard_name
        - same unit
        - same dimensions
            same dimemnsions means that dimention variable match

*/


#define Y(a)                          \
    do {                              \
        int i;                        \
        for (i = 0; i < depth; i++)   \
            printf("   ");            \
        printf("%s: %s\n", #a, c->a); \
    } while (0)
#define X(a)                                 \
    do {                                     \
        int i;                               \
        for (i = 0; i < depth; i++)          \
            printf("   ");                   \
        printf("%s: %ld\n", #a, (long)c->a); \
    } while (0)
#define Z(a)                                  \
    do {                                      \
        int i;                                \
        for (i = 0; i < depth; i++)           \
            printf("   ");                    \
        printf("%s: %g\n", #a, (double)c->a); \
    } while (0)
#define T(a)                                            \
    do {                                                \
        int i;                                          \
        for (i = 0; i < depth; i++)                     \
            printf("   ");                              \
        printf("%s: %s\n", #a, netcdf_type_name(c->a)); \
    } while (0)


const char* netcdf_type_name(int type) {
    switch (type) {
        case NC_BYTE:
            return "NC_BYTE";
            break;

        case NC_SHORT:
            return "NC_SHORT";
            break;

        case NC_LONG:
            return "NC_LONG";
            break;

        case NC_CHAR:
            return "NC_CHAR";
            break;

        case NC_FLOAT:
            return "NC_FLOAT";
            break;

        case NC_DOUBLE:
            return "NC_DOUBLE";
            break;

        default:
            return "<UNKNOWN>";
    }
}

size_t netcdf_type_size(int type) {
    switch (type) {
        case NC_BYTE:
            return sizeof(unsigned char);
            break;

        case NC_SHORT:
            return sizeof(short);
            break;

        case NC_LONG:
            return sizeof(long);
            break;

        case NC_CHAR:
            return sizeof(char);
            break;

        case NC_FLOAT:
            return sizeof(float);
            break;

        case NC_DOUBLE:
            return sizeof(double);
            break;

        default:
            marslog(LOG_EROR, "Attempt to get the size of an unknow netcdf type %d", type);
            return 0;
    }
}


netcdf_attribute* netcdf_attribute_new(netcdf_attribute_list* list, const char* owner, const char* name, int id, int type, int len) {
    netcdf_attribute* f = NEW_CLEAR(netcdf_attribute);
    f->name             = strcache(name);
    f->owner            = strcache(owner);
    f->id               = id;
    f->type             = type;
    f->len              = len;

    if (list->last) {
        list->last->next = f;
        list->last       = f;
    }
    else {
        list->first = list->last = f;
    }

    list->cnt++;

    return f;
}

netcdf_attribute* netcdf_attribute_clone(netcdf_attribute_list* list, netcdf_attribute* c) {
    netcdf_attribute* f = netcdf_attribute_new(list, c->owner, c->name, c->id, c->type, c->len);
    f->long_value       = c->long_value;
    f->char_value       = strcache(c->char_value);
    f->float_value      = c->float_value;
    f->double_value     = c->double_value;
    return f;
}


netcdf_dimension* netcdf_dimension_new(netcdf_dimension_list* list, netcdf_field* owner, const char* name, int id, int len) {
    netcdf_dimension* f = NEW_CLEAR(netcdf_dimension);
    f->name             = strcache(name);
    f->owner            = owner;
    f->id               = id;
    f->len              = len;

    if (list->last) {
        list->last->next = f;
        list->last       = f;
    }
    else {
        list->first = list->last = f;
    }

    list->cnt++;

    return f;
}

netcdf_dimension* netcdf_dimension_clone(netcdf_field* target, netcdf_dimension_list* list, netcdf_dimension* c) {
    netcdf_dimension* f = netcdf_dimension_new(list, target, c->name, c->id, c->len);
    return f;
}


static netcdf_dimension* find_dim_by_id(netcdf_dimension_list* list, int id) {
    netcdf_dimension* c = list->first;
    while (c) {
        if (c->id == id) {
            return c;
        }
        c = c->next;
    }
    marslog(LOG_EROR, "Cannot find dimension %d", id);
    return NULL;
}

static netcdf_dimension* find_dim_by_name(netcdf_dimension_list* list, const char* name) {
    netcdf_dimension* c = list->first;
    while (c) {
        if (c->name == name) {
            return c;
        }
        c = c->next;
    }
    marslog(LOG_EROR, "Cannot find dimension %s", name);
    abort();
    return NULL;
}

static void netcdf_hypercube_init(netcdf_hypercube* cube, int ndims, netcdf_dimension** dims) {
    int i;

    cube->ndims = ndims;

    for (i = 0; i < ndims; i++) {
        cube->dims[i] = dims[i];
    }
}

size_t netcdf_hypercube_size(netcdf_hypercube* cube) {
    size_t result = 1;
    int i;

    for (i = 0; i < cube->ndims; i++) {
        result *= cube->dims[i]->len;
    }
    return result;
}

size_t netcdf_hypercube_index(netcdf_hypercube* cube, size_t* coord) {
    size_t n = 1;
    size_t a = 0;
    int i;

    // The fact that this is in reverse is important for addToDimension

    for (i = cube->ndims - 1; i >= 0; i--) {
        a += coord[i] * n;
        n *= cube->dims[i]->len;
    }

    return a;
}

void netcdf_hypercube_coordinates(netcdf_hypercube* cube, size_t index, size_t* result) {
    int i;
    for (i = cube->ndims - 1; i >= 0; i--) {
        size_t d  = cube->dims[i]->len;
        result[i] = (index % d);
        index /= d;
    }
}

/*
static void addLoop(
    Ordinal      d,
    Ordinal      which,
    Ordinal      where,
    Ordinal      count,
    Ordinal      depth,
    HyperCube&   target,
    const HyperCube::Dimensions&  dims,
    HyperCube::Coordinates& coord,
    HyperCube::Remapping&   remap)
{
    if(d == depth)
        remap.push_back(target.index(coord));
    else {

        int k = 0;
        for( size_t i = 0; i < dims[d]; i++,k++)
        {
            if(which == d && i == where) k += count;
            coord[d]   = k;
            addLoop(d+1,which,where,count,depth,target,dims,coord,remap);
        }
    }
}

HyperCube HyperCube::addToDimension(Ordinal which,
    Ordinal where,Ordinal howMuch,Remapping& remap) const
{

    remap.clear();
    remap.reserve(count());

    Dimensions  newdims = dimensions_;
    Coordinates coord(dimensions_.size());


    newdims[which] += howMuch;

    HyperCube target(newdims);

    addLoop(0,which,where,howMuch,dimensions_.size(),target,dimensions_,coord,remap);

    return target;
}*/

netcdf_variable* netcdf_variable_new(netcdf_variable_list* list, netcdf_field* owner,
                                     netcdf_dimension_list* dim_list, const char* name, int id, int type, int ndims, int* dims, int nattr) {
    netcdf_variable* f = NEW_CLEAR(netcdf_variable);
    int i;

    f->name  = strcache(name);
    f->owner = owner;

    f->id    = id;
    f->type  = type;
    f->nattr = nattr;

    if (dims && dim_list) {
        netcdf_dimension* d[NC_MAX_VAR_DIMS];
        for (i = 0; i < ndims; i++) {
            d[i] = find_dim_by_id(dim_list, dims[i]);
        }
        netcdf_hypercube_init(&f->cube, ndims, d);
    }

    if (list->last) {
        list->last->next = f;
        list->last       = f;
    }
    else {
        list->first = list->last = f;
    }

    list->cnt++;

    return f;
}

netcdf_variable* netcdf_variable_clone(netcdf_field* target, netcdf_variable_list* list, netcdf_variable* c) {
    netcdf_variable* f = netcdf_variable_new(list, c->owner, NULL, c->name, c->id, c->type, c->cube.ndims, NULL, c->nattr);
    int i;
    netcdf_dimension* d[NC_MAX_VAR_DIMS];

    for (i = 0; i < c->cube.ndims; i++) {
        d[i] = find_dim_by_name(&target->dimensions, c->cube.dims[i]->name);
    }
    netcdf_hypercube_init(&f->cube, c->cube.ndims, d);


    netcdf_attribute* a = c->attributes.first;
    while (a) {
        netcdf_attribute_clone(&f->attributes, a);
        a = a->next;
    }

    return f;
}

netcdf_field* netcdf_field_new(netcdf_field_list* list, const char* path, int tmp) {
    netcdf_field* f = NEW_CLEAR(netcdf_field);
    f->path         = strcache(path);
    f->tmp          = tmp;

    if (list->last) {
        list->last->next = f;
        list->last       = f;
    }
    else {
        list->first = list->last = f;
    }

    list->cnt++;

    return f;
}

static void netcdf_dimension_delete(netcdf_dimension_list* list) {
    netcdf_dimension* c = list->first;
    while (c) {
        netcdf_dimension* n = c->next;

        strfree(c->name);

        FREE(c);
        c = n;
    }
}

static void netcdf_attribute_delete(netcdf_attribute_list* list) {
    netcdf_attribute* c = list->first;
    while (c) {
        netcdf_attribute* n = c->next;

        strfree(c->name);
        strfree(c->char_value);
        strfree(c->owner);

        FREE(c);
        c = n;
    }
}

static void netcdf_variable_delete(netcdf_variable_list* list) {
    netcdf_variable* c = list->first;
    while (c) {
        netcdf_variable* n = c->next;

        strfree(c->name);

        netcdf_attribute_delete(&c->attributes);

        FREE(c);
        c = n;
    }
}

void netcdf_field_delete(netcdf_field_list* list) {
    netcdf_field* c = list->first;
    while (c) {
        netcdf_field* n = c->next;
        if (c->tmp) {
            unlink(c->path);
        }
        strfree(c->path);

        netcdf_attribute_delete(&c->global_attributes);
        netcdf_variable_delete(&c->variables);
        netcdf_dimension_delete(&c->dimensions);

        FREE(c->variables_by_dimensions);
        FREE(c->variables_by_natural_order);

        // free_all_requests(c->r);
        FREE(c);
        c = n;
    }
}

static void netcdf_dimension_print(netcdf_dimension_list* list, int depth) {
    netcdf_dimension* c = list->first;
    int i;
    for (i = 0; i < depth; i++)
        printf("   ");
    printf("=> dimensions\n");
    while (c) {
        Y(name);
        X(id);
        X(len);
        c = c->next;
    }
    for (i = 0; i < depth; i++)
        printf("   ");
    printf("<= dimensions\n");
}

static void netcdf_attribute_print(netcdf_attribute_list* list, int depth) {
    netcdf_attribute* c = list->first;
    int i;
    for (i = 0; i < depth - 1; i++)
        printf("   ");
    printf("=> attributes\n");
    while (c) {
        Y(name);
        X(id);
        T(type);
        X(len);

        X(short_value);
        X(long_value);
        Y(char_value);
        Z(float_value);
        Z(double_value);
        c = c->next;
    }
    for (i = 0; i < depth - 1; i++)
        printf("   ");
    printf("<= attributes\n");
}

static void netcdf_variable_print(netcdf_variable_list* list, int depth) {
    netcdf_variable* c = list->first;
    int i;
    for (i = 0; i < depth - 1; i++)
        printf("   ");
    printf("=> variables\n");
    while (c) {
        Y(name);
        X(id);
        T(type);
        X(cube.ndims);
        X(nattr);
        netcdf_attribute_print(&c->attributes, depth + 1);
        c = c->next;
    }
    for (i = 0; i < depth - 1; i++)
        printf("   ");
    printf("<= variables\n");
}

void netcdf_field_print(netcdf_field_list* list, int depth) {
    netcdf_field* c = list->first;
    int i;
    for (i = 0; i < depth - 1; i++)
        printf("   ");
    printf("=> fields\n");
    while (c) {
        Y(path);
        X(tmp);
        X(number_of_dimensions);
        X(number_of_variables);
        X(number_of_global_attributes);
        X(id_of_unlimited_dimension);
        X(format);
        netcdf_attribute_print(&c->global_attributes, depth + 1);
        netcdf_variable_print(&c->variables, depth + 1);
        netcdf_dimension_print(&c->dimensions, depth + 1);
        c = c->next;
    }
    for (i = 0; i < depth - 1; i++)
        printf("   ");
    printf("<= fields\n");
}


netcdf_variable* netcdf_variable_by_name(netcdf_variable_list* list, const char* name) {
    netcdf_variable* c = list->first;
    while (c) {
        netcdf_variable* n = c->next;
        if (name == c->name)
            return c;

        c = c->next;
    }
    marslog(LOG_EROR, "netcdf_variable_by_name: cannot find %s", name);
    return NULL;
}

netcdf_attribute* netcdf_attribute_by_name(netcdf_attribute_list* list, const char* name) {
    netcdf_attribute* c = list->first;
    while (c) {
        netcdf_attribute* n = c->next;
        if (name == c->name)
            return c;

        c = c->next;
    }
    marslog(LOG_EROR, "netcdf_attribute_by_name: cannot find %s", name);
    return NULL;
}

netcdf_dimension* netcdf_dimension_by_name(netcdf_dimension_list* list, const char* name) {
    netcdf_dimension* c = list->first;
    while (c) {
        netcdf_dimension* n = c->next;
        if (name == c->name)
            return c;

        c = c->next;
    }
    marslog(LOG_EROR, "netcdf_dimension_by_name: cannot find %s", name);
    return NULL;
}

size_t netcdf_variable_number_of_values(netcdf_variable* c) {
    return netcdf_hypercube_size(&c->cube);
}

void* netcdf_variable_get_values(netcdf_variable* c, size_t* count) {
    void* buffer = 0;
    int len      = netcdf_variable_number_of_values(c);
    int in;
    int e;

    if ((e = nc_open(c->owner->path, NC_NOWRITE, &in)) != NC_NOERR) {
        marslog(LOG_EROR, "nc_open(%s): %s", c->owner->path, nc_strerror(e));
        return NULL;
    }


    switch (c->type) {

            // case NC_BYTE:
            //     break;

        case NC_SHORT:
            buffer = NEW_ARRAY(short, len);

            if ((e = nc_get_var_short(in, c->id, buffer)) != NC_NOERR) {
                marslog(LOG_EROR, "nc_get_var_short(%s): %s", c->owner->path, nc_strerror(e));
                FREE(buffer);
                return NULL;
            }


            break;

        case NC_LONG:
            buffer = NEW_ARRAY(long, len);

            if ((e = nc_get_var_long(in, c->id, buffer)) != NC_NOERR) {
                marslog(LOG_EROR, "nc_get_var_long(%s): %s", c->owner->path, nc_strerror(e));
                FREE(buffer);
                return NULL;
            }


            break;

            // case NC_CHAR:
            //     break;

        case NC_FLOAT:
            buffer = NEW_ARRAY(float, len);

            if ((e = nc_get_var_float(in, c->id, buffer)) != NC_NOERR) {
                marslog(LOG_EROR, "nc_get_var_float(%s): %s", c->owner->path, nc_strerror(e));
                FREE(buffer);
                return NULL;
            }

            break;

        case NC_DOUBLE:

            buffer = NEW_ARRAY(double, len);

            if ((e = nc_get_var_double(in, c->id, buffer)) != NC_NOERR) {
                marslog(LOG_EROR, "nc_get_var_double(%s): %s", c->owner->path, nc_strerror(e));
                FREE(buffer);
                return NULL;
            }

            break;

        default:
            marslog(LOG_EROR, "Unknow netcdf type(%s) %d %s", c->owner->path, c->type, netcdf_type_name(c->type));
            return NULL;
    }

    if ((e = nc_close(in)) != NC_NOERR) {
        marslog(LOG_EROR, "nc_close(%s): %s", c->owner->path, nc_strerror(e));
        return NULL;
    }

    *count = len;
    return buffer;
}

netcdf_variable* netcdf_dimension_get_variable(netcdf_dimension* d) {
    netcdf_variable* v = netcdf_variable_by_name(&d->owner->variables, d->name);
    return v;
}

err netcdf_variable_by_dimension(netcdf_variable_list* list, netcdf_dimension* d, netcdf_variable** result, size_t* count) {
    netcdf_variable* c = list->first;
    size_t n           = 0;
    int i;

    while (c) {

        for (i = 0; i < c->cube.ndims; i++) {
            if (c->cube.dims[i] == d) {
                if (n < *count) {
                    result[n++] = c;
                }
                else {
                    marslog(LOG_EXIT, "List of netcdf variables too small (%ld)", (long)*count);
                    return -2;
                }
            }
        }

        c = c->next;
    }

    *count = n;
    return 0;
}
