#include "mars.h"
#include "nctypes.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;
}

