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

#include <ctype.h>
#include <errno.h>

#define PAD(l, n) ((((l) + (n)-1) / (n) * (n)) - (l))

static marslist* targets = NULL;

#define MAXKEY 20

typedef struct remotefile {
    struct remotefile* next;
    char* local;
    char* remote;
    char* host;
    boolean ecfs;
} remotefile;

static remotefile* remote_files = NULL;

typedef struct targetfile {

    struct targetfile* next;
    FILE* file;
    char* name;
    char* key;
    off_t start;

    char* buffer;

    int pipe;

    netcdf_target* nc;

} targetfile;

static timer* target_timer = NULL;

typedef struct targetdata {

    gribfile* file;
    int padding;
    int bufr_padding;
    int count;
    int expect; /* initialised with count_fields() */
    fieldset* fs;
    hypercube* cube; /* Hypercube to speed up field_order(). Call cube_order */
    variable* v;
    boolean order;
    request* r;
    boolean split;
    boolean dispnew;
    boolean netcdf;
    char* target;


    targetfile* current;
    targetfile* files;

    int keycount;
    char* key[MAXKEY];
    int api[MAXKEY];

    boolean has_error;

    int bufsize;
    boolean multitarget_format;

    boolean inited;

} targetdata;

static void target_init(void);
static err target_open(void* data, request*, request*, int);
static err target_close(void* data);
static err target_read(void* data, request* r, void* buffer, long* length);
static err target_write(void* data, request* r, void* buffer, long* length);
static err target_cntl(void* data, int code, void* param, int size);


static option opts[] = {
    {
        "padding",
        "MARS_PADDING",
        "-padding",
        "120",
        t_int,
        sizeof(int),
        OFFSET(targetdata, padding),
    },

    {
        "bufr_padding",
        "MARS_BUFR_PADDING",
        "-bufr_padding",
        "8",
        t_int,
        sizeof(int),
        OFFSET(targetdata, bufr_padding),
    },

    {
        "bufsize",
        "MARS_WRITE_BUFFER",
        "-bufsize",
        "0",
        t_int,
        sizeof(int),
        OFFSET(targetdata, bufsize),
    },

    {
        "multitarget_format",
        "MARS_MULTITARGET_STRICT_FORMAT",
        "-multitarget_format",
        "0",
        t_boolean,
        sizeof(boolean),
        OFFSET(targetdata, multitarget_format),
    },
};

static base_class _targetbase = {

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

    false, /* inited       */

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


    target_init, /* init         */

    target_open,  /* open         */
    target_close, /* close        */

    target_read,  /* read         */
    target_write, /* write        */

    target_cntl, /* cntl        */

};


base_class* targetbase = &_targetbase;


static void target_init(void) {
    target_timer = get_timer("Writing to target file", "writetarget", false);
}


static err close_all(targetdata* g) {
    targetfile* f = g->files;
    err e         = 0;
    f             = g->files;
    while (f) {
        targetfile* n = f->next;
        if (f->pipe) {
            if (pclose(f->file)) {
                marslog(LOG_EROR | LOG_PERR, "pclose(%s)", f->name);
                e = -2;
            }
        }
        else if (f->nc) {
            if (netcdf_target_close(f->nc)) {
                marslog(LOG_EROR | LOG_PERR, "netcdf_target_close(%s)", f->name);
                e = -2;
            }
        }
        else {
            if (timed_fclose(f->file, target_timer)) {
                marslog(LOG_EROR | LOG_PERR, "fclose(%s)", f->name);
                e = -2;
            }
        }

        strfree(f->name);
        if (f->buffer)
            free(f->buffer);
        strfree(f->key);

        FREE(f);
        f = n;
    }

    g->files = g->current = NULL;

    return e;
}

static targetfile* open_file(targetdata* g, const char* name, int* error) {
    targetfile* f = NEW_CLEAR(targetfile);
    char* cmode   = target_open_mode(name);
    *error        = 0;

    if (g->netcdf) {
        f->nc = netcdf_target_new(name, NULL, 0);
    }
    else {

        if (*name == '|') {
            f->file = popen(name + 1, cmode);
            f->pipe = 1;
        }
        else {
            if (g->dispnew)
                cmode = "w+";
            f->file = fopen(name, cmode);
            *error  = errno;
        }
        marslog(LOG_DBUG, "target_open, mode: %s", cmode);

        if (f->file == NULL) {
            marslog(LOG_EROR | LOG_PERR, "cannot open %s", name);
            FREE(f);
            return NULL;
        }


        f->name = strcache(name);

        if (g->bufsize) {
            f->buffer = valloc(g->bufsize);
            if (!f->buffer)
                marslog(LOG_WARN, "Could not valloc buffer %d", g->bufsize);
            else if (setvbuf(f->file, f->buffer, _IOFBF, g->bufsize))
                marslog(LOG_WARN | LOG_PERR, "setvbuf failed");
        }


        if (!f->pipe) {
            check_nfs_target(name);

            fseek(f->file, (off_t)0, SEEK_END);
            f->start = ftell(f->file);
        }
    }

    f->name  = strcache(name);
    f->next  = g->files;
    g->files = f;

    return f;
}

#define IS_ECFS(x) ((strcmp(x, "ec") == 0) || (strcmp(x, "ectmp") == 0))

static char* make_target(targetdata* g, request* r, void* message, long length) {
    static char target[1024];
    char word[1024];
    char name[1024];
    char host[1024];
    char val[1024];
    size_t k       = 0;
    int mode       = 0;
    boolean remote = false;
    const char* value;
    const char* p = g->target;
    int api       = 0;

    /* note that message may be null, since for obs we may write a empty target */

    boolean netcdf = message_is_netcdf((unsigned char*)message);

    target[0] = 0;
    host[0]   = 0;

    while (*p) {
        switch (*p) {

            case ':':

                if (!remote && k != 0 && strncmp(g->target, word, k) == 0) {
                    word[k] = 0;

                    /* check if it is a valide host name */

                    if (is_hostname(word)) {
                        k = 0;
                        strcpy(host, word);
                        remote = true;
                    }
                    else if (IS_ECFS(word)) {
                        k = 0;
                        strcpy(host, "ectmp");
                        remote = true;
                    }
                    else
                        word[k++] = *p;
                }
                else
                    word[k++] = *p;

                break;

            case '{':
            case '[':
                if (mode == 0) {
                    word[k] = 0;
                    strcat(target, word);
                    mode = *p;
                    k    = 0;
                    api  = *p == '{';
                }
                else
                    word[k++] = *p;
                break;

            case ']':
            case '}':
                if ((*p == ']' && mode != '[') || (*p == '}' && mode != '{'))
                    word[k++] = *p;
                else {
                    char tmp[1024];
                    size_t size = 0;

                    word[k] = 0;

                    if (mode != 0) {
                        if (api) {
                            size_t size = sizeof(val);
                            strcpy(name, word);
                            if (message != NULL) {
                                grib_handle* h = grib_handle_new_from_message(0, message, length);
                                if (h) {
                                    if (grib_get_string(h, word, val, &size) == 0)
                                        value = val;
                                    grib_handle_delete(h);
                                }
                            }
                            else {
                                value = NULL;
                                strcat(target, "{");
                                strcat(target, word);
                                strcat(target, "}");
                            }
                        }
                        else {
                            value = get_value(r, upcase(word), 0);
                            strcpy(name, upcase(word));
                            if (value == NULL) {
                                strcat(target, "[");
                                strcat(target, word);
                                strcat(target, "]");
                            }
                        }
                        size = 0;
                    }
                    else
                        size = atol(word);
                    if (value) {
                        static boolean already_logged = false;
                        /* New style, more strict to MARS output values,
                           eg, time=0000 instead of 0 */
                        if (g->multitarget_format) {
                            sprintf(tmp, "%0*s", size, value);
                            if (!already_logged)
                                log_statistics("multitarget", "new");
                        }
                        else {
                            if (!already_logged) {
                                marslog(LOG_WARN, "In order to get filenames according to MARS values,");
                                marslog(LOG_WARN, "please, set env. var. MARS_MULTITARGET_STRICT_FORMAT=1");
                                marslog(LOG_WARN, "before executing your MARS request");
                                log_statistics("multitarget", "old");
                            }
                            if (is_number(value))
                                sprintf(tmp, "%0*d", size, atol(value));
                            else
                                sprintf(tmp, "%0*s", size, value);
                        }
                        already_logged = true;

                        if (!g->inited) {
                            if (g->keycount >= MAXKEY) {
                                marslog(LOG_EROR, "Target name: maximum number of variables is %d", MAXKEY);
                                return 0;
                            }
                            g->api[g->keycount]   = api;
                            g->key[g->keycount++] = strcache(name);
                        }

                        strcat(target, tmp);
                    }
                    mode = 0;
                    k    = 0;
                }
                break;

            case ',':
                if (mode == 1) {
                    mode    = 2;
                    word[k] = 0;
                    value   = get_value(r, word, 0);
                    strcpy(name, word);
                    if (value == NULL) {
                        strcat(target, "[");
                        strcat(target, word);
                        strcat(target, ",");
                        mode = 0;
                    }
                    k = 0;
                }
                else
                    word[k++] = *p;
                break;

            default:
                word[k++] = *p;
                break;
        }
        p++;
    }

    word[k] = 0;
    strcat(target, word);


    if (g->inited && (g->netcdf != netcdf)) {
        marslog(LOG_EROR, "Cannot mix NetCDF and other formats");
        return NULL;
    }

    g->netcdf = netcdf;

    if (!g->inited && g->keycount != 0) {
        int i;
        marslog(LOG_INFO, "Enabling automatic target name generation using:");
        for (i = 0; i < g->keycount; i++)
            marslog(LOG_INFO, "%s", g->key[i]);
        g->split  = true;
        g->inited = true;
        g->netcdf = netcdf;
    }


    if (g->keycount && netcdf) {
        marslog(LOG_EROR, "Multi-target mode not implememted for NetCDF data");
        return NULL;
    }

    if (g->keycount)
        marslog(LOG_INFO, "Creating target name: %s", target);

    if (remote) {

        remotefile* f = remote_files;
        while (f) {
            if (strcmp(host, f->host) == 0 && strcmp(target, f->remote) == 0)
                return f->local;
            f = f->next;
        }

        f            = NEW_CLEAR(remotefile);
        f->host      = strcache(host);
        f->remote    = strcache(target);
        f->next      = remote_files;
        f->local     = strcache(marstmp());
        f->ecfs      = IS_ECFS(f->host);
        remote_files = f;

        if (f->ecfs)
            log_statistics("target", "ecfs");
        else
            log_statistics("target", "rcp:%s", f->host);

        strcpy(target, f->local);
    }

    return target;
}

static err select_file(targetdata* g, request* r, int retry, void* message, long length) {

    int error = 0;
    int i;
    char buffer[1024];
    char val[1024];
    char* k       = NULL;
    char* t       = NULL;
    targetfile* f = 0;

    if (!g->inited) {
        /* First time in */
        t         = make_target(g, r, message, length);
        g->inited = true;
        if (!t)
            return -1;
    }
    else if (!g->split)
        return 0;

    buffer[0] = 0;

    for (i = 0; i < g->keycount; i++) {
        const char* value = NULL;

        if (g->api[i]) {
            size_t size = sizeof(val);
            if (message != NULL) {
                grib_handle* h = grib_handle_new_from_message(0, message, length);
                if (h) {
                    if (grib_get_string(h, g->key[i], val, &size) == 0)
                        value = val;
                    grib_handle_delete(h);
                }
            }
        }
        else
            value = get_value(r, g->key[i], 0);

        if (value == NULL) {
            marslog(LOG_EROR, "Target split: no value for %s", g->key[i]);
            return -1;
        }

        strcat(buffer, value);
        strcat(buffer, "+");
    }

    k = strcache(buffer);

    if (g->current && g->current->key == k)
        return 0;

    f = g->files;
    while (f) {
        if (f->key == k) {
            g->current = f;
            return 0;
        }
        f = f->next;
    }


    t = t ? t : make_target(g, r, message, length);
    if (!t)
        return -1;

    g->current = open_file(g, t, &error);
    if (!g->current) {

        if (error == EMFILE && retry) {
            err e;
            marslog(LOG_WARN, "Closing all target files");

            e = close_all(g);
            if (e)
                return e;
            return select_file(g, r, 0, message, length);
        }

        return -1;
    }
    else {
        if (!in_list(targets, t, NULL))
            add_to_list(&targets, t, NULL);
    }

    g->current->key = k;

    return 0;
}

static err target_open(void* data, request* r, request* e, int mode) {
    targetdata* g       = (targetdata*)data;
    const char* fs      = get_value(r, "FIELDSET", 0);
    const char* target  = no_quotes(get_value(r, "TARGET", 0));
    boolean isfield     = (!observation(r) && !image(r) && !simulated_image(r) && !fetch(r) && !feedback(r) && !bias(r) && !track(r));
    const char* disp    = get_value(r, "DISP", 0);
    const char* padding = get_value(r, "PADDING", 0);

    const char* type = get_value(r, "TYPE", 0);
    boolean isfg     = type && EQ(type, "FG");

    if ((target || (!target && !fs)) && mars.webmars_target) {
        target = mars.webmars_target;
    }


    if (isfg)
        g->r = un_first_guess(r);
    else
        g->r = clone_all_requests(r);

    g->dispnew = (disp && EQ(disp, "NEW"));

    if (mode == WRITE_MODE) {
        if (observation(r))
            g->padding = g->bufr_padding;

        if (fetch(r))
            g->padding = 0;

        if (is_odb(r))
            g->padding = 0;

        if (target == NULL && fs == NULL) {
            marslog(LOG_EROR, "TARGET or FIELDSET value is missing");
            return -1;
        }

        /* Use provided padding if any */
        if (padding)
            if (isdigit(*padding))
                g->padding = atoi(padding);

        /* No fieldset allowed for image or */

        if (target == NULL && !isfield) {
            marslog(LOG_EROR, "FIELDSET not valid for non-field requests");
            return -2;
        }

        if ((target == NULL || fs == NULL) && (EQ(r->name, "WRITE") || EQ(r->name, "COMPUTE"))) {
            marslog(LOG_EROR, "TARGET and FIELDSET value are missing");
            return -2;
        }

        if (fs && (!(EQ(r->name, "WRITE") || EQ(r->name, "COMPUTE"))) && isfield) {
            int count = count_fields(r);

            g->expect = count;

            g->fs = new_fieldset(count);
            if (g->fs == NULL)
                return -1;
            g->v = new_variable(fs, g->fs, 0);

            if ((count == 0) && EQ(r->name, "RETRIEVE"))
                g->order = false;
            else
                g->order = true;

            g->file = new_gribfile(NULL);
            target  = g->file->fname;

            g->cube = new_hypercube_from_mars_request(g->r);
        }

        g->target = strcache(target);
    }
    else {
        marslog(LOG_EXIT, "target_open read not imp");
    }

    return 0;
}

char* target_open_mode(const char* target) {
    if (*target == '|')
        return "w";

    if (in_list(targets, target, NULL))
        return "a+";
    else {
        add_to_list(&targets, target, NULL);
        return "w+";
    }
}

static err target_close(void* data) {
    targetdata* g = (targetdata*)data;
    targetfile* f = NULL;
    err e         = 0;
    int i;

    marslog(LOG_DBUG, "target_close");

    free_all_requests(g->r);

    e = close_all(g);

    strfree(g->target);

    for (i = 0; i < MAXKEY; i++)
        strfree(g->key[i]);

    if (g->fs && !g->has_error) {
        free_hypercube(g->cube);
        /* if EXPECT=ANY, g->expect is 0, so don't check */
        if (g->expect && check_fieldset(g->fs, g->expect)) {
            free_variable(g->v);
            e = -2;
        }
    }

    return e;
}

static err target_read(void* data, request* r, void* buffer, long* length) {
    return -1;
}

static err target_write(void* data, request* r, void* buffer, long* length) {
    long len      = *length;
    targetdata* g = (targetdata*)data;
    off_t pos;

    if (select_file(g, r, 1, buffer, *length) != 0)
        return -2;


    if (g->netcdf) {
        return netcdf_target_add_buffer(g->current->nc, buffer, *length);
    }

    if (g->fs)
        pos = ftell(g->current->file);

    /* marslog(LOG_DBUG,"target_write"); */

    if ((len = timed_fwrite(buffer, 1, *length, g->current->file, target_timer)) != *length) {
        marslog(LOG_EROR | LOG_PERR, "Error while writing to disk");
        *length = len;
        return EOF;
    }

    if (*length == 0)
        return 0;

    if (g->padding) {
        static int first = true;
        size_t pad       = PAD(len, g->padding);
        static char c[10240]; /* 10Kb of padding should be enough */

        if (first) {
            memset(c, 0, sizeof(c));
            first = false;
        }

        timer_start(target_timer);
        if (fwrite(c, 1, pad, g->current->file) != pad) {
            marslog(LOG_EROR | LOG_PERR, "Error while writing to disk");
            timer_stop(target_timer, 0);
            *length = len;
            return EOF;
        }
        timer_stop(target_timer, (long64)pad);
    }

    if (g->fs) {
        int n = g->order ? cube_order(g->cube, r) : g->count++;
        if (n < 0) {
            if (mars.debug) {
                request* w = empty_request("GRIB");
                marslog(LOG_DBUG, "Got order %d for following field", n);
                grib_to_request(w, buffer, *length);
                print_one_request(w);
                free_all_requests(w);
            }
        }

        return set_field(g->fs, read_field(g->file, pos, *length), n);
    }

    return 0;
}

static err target_cntl(void* data, int code, void* param, int size) {
    targetdata* g = (targetdata*)data;
    targetfile* f = 0;
    switch (code) {
        case CNTL_REWIND:
            g->count = 0;
            f        = g->files;
            while (f) {
                if (f->pipe) {
                    marslog(LOG_EROR, "Cannot rewind a pipe");
                    return -2;
                }
                fseek(f->file, f->start, SEEK_SET);
                ftruncate(fileno(f->file), f->start);
                f = f->next;
            }
            marslog(LOG_DBUG, "target_cntl");
            return NOERR;
            /*NOTREACHED*/
            break;

        case CNTL_ERROR:
            g->has_error = true;
            marslog(LOG_DBUG, "target_cntl");
            return NOERR;
            /*NOTREACHED*/
            break;

        default:
            return -1;
    }
}

err send_remote_targets() {

    remotefile* f = remote_files;
    char buf[80];
    char buffer[2048];
    err e     = 0;
    char* cmd = 0;

    if (remote_files == NULL)
        return 0;

    start_timer();

    while (f != NULL && e == 0) {
        marslog(LOG_INFO, "Sending %s:%s", f->host, f->remote);

        if (f->ecfs)
            cmd = "$ECFS_SYS_PATH/ecp.p -o";
        else
            cmd = "scp";

        sprintf(buffer, "%s %s %s:%s 2>&1", cmd, f->local, f->host, f->remote);

        if (system(buffer) != 0) {
            marslog(LOG_EROR | LOG_PERR, "Command \"%s\" failed", buffer);
            e = -2;
        }

        f = f->next;
    }


    f = remote_files;
    while (f) {
        remotefile* n = f->next;
        strfree(f->host);
        strfree(f->local);
        strfree(f->remote);
        FREE(f);
        f = n;
    }

    stop_timer(buf);
    if (*buf)
        marslog(LOG_INFO, "Sending time %s", buf);

    return e;
}

#if !mars_client_HAVE_NETCDF

netcdf_target* netcdf_target_new(const char* path, const char* format, int more_flags) {
    marslog(LOG_EROR, "Error calling netcdf_target_new, NETCDF not supported");
    return 0;
}

err netcdf_target_add_buffer(netcdf_target* target, const void* message, size_t length) {
    marslog(LOG_EROR, "Error calling netcdf_target_add_buffer, NETCDF not supported");
    return -2;
}

err netcdf_target_close(netcdf_target* target) {
    marslog(LOG_EROR, "Error calling netcdf_target_close, NETCDF not supported");
    return -2;
}

#endif
