// pstampparse ---> imagetool but this program is going to be
// deleted so we'll just leave the external name alone

#include <pslib.h>
#include <psmodules.h>
#include <string.h>
#include <strings.h>
#include <ohana.h>

typedef enum {
    UNKNOWN_IMAGE = -1,
    RAW_IMAGE,
    CHIP_IMAGE,
    WARP_IMAGE,
    STACK_IMAGE,
    DIFF_IMAGE
} itImageType;

typedef enum {
    BY_ID,
    BY_EXP,
    BY_COORD,
    UNKNOWN_LOOKUP_TYPE
} itLookupType;

typedef struct {
    pmConfig        *config;
    itLookupType    lookupType;
    itImageType     imageType;
    psString        id;
    psString        class_id;
    bool            verbose;
    bool            simple;
    double          mjd_min;
    double          mjd_max;
    double          ra;
    double          dec;
    psString        filter;
} itOptions;


static void usage()
{
    // XXX: fill this out
    psErrorStackPrint(stderr, "argument error");
    exit(1);
}

static itImageType parseImageType(psString type)
{
    itImageType itype;

    if (type == NULL) {
        psError(PS_ERR_UNKNOWN, true, "NULL image type");
        itype = UNKNOWN_IMAGE;
        return itype;
    }

    if (strcmp(type, "raw") == 0) {
        itype = RAW_IMAGE;
    } else if (strcmp(type, "chip") == 0) {
        itype = CHIP_IMAGE;
    } else if (strcmp(type, "warp") == 0) {
        itype = WARP_IMAGE;
    } else if (strcmp(type, "diff") == 0) {
        itype = DIFF_IMAGE;
    } else if (strcmp(type, "stack") == 0) {
        itype = STACK_IMAGE;
    } else {
        psError(PS_ERR_UNKNOWN, true, "unknown image type %s", type);
        itype = UNKNOWN_LOOKUP_TYPE;
    }

    return itype;
}
static void getParams(itOptions *options, int argnum, int *pArgc, char *argv[])
{
    if (*pArgc < 3) {
        fprintf(stderr, "values required for type and id\n");
        usage();
    }

    options->imageType = parseImageType(argv[argnum]);
    psArgumentRemove(argnum, pArgc, argv);
    options->id = argv[argnum];
    psArgumentRemove(argnum, pArgc, argv);

    if ((options->imageType == RAW_IMAGE) || (options->imageType == CHIP_IMAGE)) {
        if (*pArgc < 2) {
            fprintf(stderr, "value required for class_id\n");
            usage();
        }
        options->class_id = argv[argnum];
    
        psArgumentRemove(argnum, pArgc, argv);
    }
}

static itOptions *parseArguments(int argc, char *argv[])
{
    int argnum;
    itOptions *options = psAlloc(sizeof(itOptions));
    bool gotLookupType = false;
    int gotTime       = 0;

    memset(options, 0, sizeof(*options));

    options->config = pmConfigRead(&argc, argv, NULL);
    if (!options->config) {
        psError(PS_ERR_UNKNOWN, false, "cannot find site file");
        psFree(options);
        return NULL;
    }
    if ((argnum = psArgumentGet(argc, argv, "-mjd_min"))) {
        psArgumentRemove(argnum, &argc, argv);
        if ((argnum) == argc) {
            psError(PS_ERR_UNKNOWN, true, "value required for -mjd_min");
            usage();
        }
        gotTime = 1;
        options->mjd_min = atof(argv[argnum]);
        psArgumentRemove(argnum, &argc, argv);
    }
    if ((argnum = psArgumentGet(argc, argv, "-mjd_max"))) {
        psArgumentRemove(argnum, &argc, argv);
        if ((argnum) == argc) {
            psError(PS_ERR_UNKNOWN, true, "value required for -mjd_max");
            usage();
        }
        gotTime++;
        options->mjd_max = atof(argv[argnum]);
        psArgumentRemove(argnum, &argc, argv);
    }
    if ((argnum = psArgumentGet(argc, argv, "-byid"))) {
        options->lookupType = BY_ID;
        psArgumentRemove(argnum, &argc, argv);
        getParams(options, argnum, &argc, argv);
        gotLookupType = true;
    }
    if ((argnum = psArgumentGet(argc, argv, "-byexp"))) {
        if (gotLookupType) {
            psError(PS_ERR_UNKNOWN, false, "only one of -byid, -byexp, and -bycoord may be used\n");
            usage();
        }
        gotLookupType = true;
        options->lookupType = BY_EXP;
        psArgumentRemove(argnum, &argc, argv);
        getParams(options, argnum, &argc, argv);
    }
    if ((argnum = psArgumentGet(argc, argv, "-bycoord"))) {
        if (gotLookupType) {
            psError(PS_ERR_UNKNOWN, false, "only one of -byid, -byexp, and -bycoord may be used\n");
            usage();
        }
        if (gotTime < 2) {
            psError(PS_ERR_UNKNOWN, true, "-mjd_min and -mjd_max required for -bycoord");
            usage();
        } else if (options->mjd_min != options->mjd_max) {
            psError(PS_ERR_UNKNOWN, true, "mjd_max ignored for now");
        }
        psArgumentRemove(argnum, &argc, argv);
        if (argc < 5) {
            psError(PS_ERR_UNKNOWN, true, "image_type, ra, and dec are required for bycoord");
            usage();
        }
        gotLookupType = true;
        options->lookupType = BY_COORD;

        options->imageType = parseImageType(argv[argnum]);
        psArgumentRemove(argnum, &argc, argv);

        options->ra = atof(argv[argnum]);
        psArgumentRemove(argnum, &argc, argv);

        options->dec = atof(argv[argnum]);
        psArgumentRemove(argnum, &argc, argv);
        // XXX TODO: handle class_id
        switch (options->imageType) {
        case RAW_IMAGE:
        case CHIP_IMAGE:
            if (argc < 2) {
                psError(PS_ERR_UNKNOWN, true, "class_id required for bycoord raw|chip");
                usage();
            }
            options->class_id = argv[argnum];
            psArgumentRemove(argnum, &argc, argv);
            break;
        default:
            break;
        }
    }

    if (!gotLookupType) {
        psError(PS_ERR_BAD_PARAMETER_NULL , true, "one of -byid, -byexp, or -bycoord must be supplied\n");
        usage();
    }

    if ((argnum = psArgumentGet(argc, argv, "-verbose"))) {
        options->verbose = true;
        psArgumentRemove(argnum, &argc, argv);
    }

    if ((argnum = psArgumentGet(argc, argv, "-simple"))) {
        options->simple = true;
        psArgumentRemove(argnum, &argc, argv);
    }

    // XXX: Arguments to add for bycoord searches FILTER, DATE_BEGIN, DATE_END

    if (argc > 1) {
        fprintf(stderr, "unknown arguments supplied: ");

        fprintf(stderr, "%s", argv[1]);
        for (int i=2; i<argc; i++) {
            fprintf(stderr, ", %s", argv[i]);
        }
        fprintf(stderr, "\n");
        usage();
    }

    return options;
}

static bool setupDB(itOptions *options)
{
    // XXX: need to select the database name based on the project name specified in the input

    options->config->database = psMemIncrRefCounter(pmConfigDB(options->config));

    if (!options->config->database) {
        psError(PS_ERR_UNKNOWN, false, "Can't configure database");
        return false;
    }

    return true;
}


// return a string with the contents of a unsigned 64 bit value
static psString idToStr(psU64 id)
{
    psString idstr = psStringAlloc(64);
    sprintf(idstr, "%" PRIu64, id);

    return idstr;
}


// runQuery
//
// run the given database query and return an array of strings containing the values of target
// the type for the values returned by the query is either a string or psU64.

static psArray *runQuery(itOptions *options, psString query, psString target,
                psString key1, psString key2, bool targetIsString, psArray *output)
{
    // query string must be in the form
    //  "SELECT %s from sometable WHERE somecolumn = %s and someothercolumn = %s
    //        target                                key1                       key2
    //
    //  or if key2 is null
    //
    //  "SELECT %s from sometable WHERE somecolumn = %s"
    //         target                               key1
    //
    //
    // we return an array containing strings for the values

    if (!p_psDBRunQuery(options->config->database, query, target, key1, key2)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return NULL;
    }

    psArray *queryOutput = p_psDBFetchResult(options->config->database);
    if (!queryOutput) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return NULL;
    }

    psU64 len = psArrayLength(queryOutput);
    if (!len) {
        psTrace("pstampparse", PS_LOG_INFO, "no rows found");
        psFree(queryOutput);
        return NULL;
    }

    if (output == NULL) {
        output = psArrayAlloc(0);
    }

    for (int i=0; i < len; i++) {
        psMetadata *row = queryOutput->data[i];
        if (targetIsString) {
            psString val = psMetadataLookupStr(NULL, row, target);
            if (val != 0) {
                output = psArrayAdd(output, len - i, val);
            }
        } else {
            psU64 val = psMetadataLookupU64(NULL, row, target);
            if (val != 0) {
                output = psArrayAdd(output, len - i, idToStr(val));
            }
        }
    }

    psFree(queryOutput);

    return output;
}

static psArray *findURIs(itOptions *options, psString table, psString id_type, psString id, psString class_id)
{
    psString query = NULL;

    psStringAppend(&query, "SELECT %%s from %s WHERE %s = %%s", table, id_type);
    // append class_id unless it is set to the special value "null"
    if (class_id && strcmp("null", class_id)) {
        psStringAppend(&query, "%s", " and class_id = '%s'" );
    }

    psArray *results = runQuery(options, query, "uri", id, class_id, true, NULL);

    psFree(query);

    return results;
}

// given an array of strings return the last entry

static psString lastID(psArray *array)
{
    if (array == NULL) {
        return 0;
    }
    psU64 len = psArrayLength(array);

    psString id = NULL;
    if (len > 0) {
        id = (psString) array->data[len-1];
        psMemIncrRefCounter(id);
    }

    // XXX: Hmm if we're doing lastID(runQuery(....)) we need to free the
    // result array, however freeing this in here seems like a potentially dangerous API
    // but hey we're a static function

    psFree(array);

    return id;
}



#define chipIDForExp(_opt, _id) \
            lastID(runQuery((_opt), "SELECT %s from chipRun  WHERE exp_id = %s", "chip_id", \
                            (_id), NULL, false, NULL))

#define camIDForChip(_opt, _id) \
    lastID(runQuery((_opt), "SELECT %s from camRun  WHERE chip_id = %s", "cam_id", \
                        (_id), NULL, false, NULL))

#define fakeIDForCam(_opt, _id) \
        lastID(runQuery((_opt), "SELECT %s from fakeRun  WHERE cam_id = %s", "fake_id", \
                            (_id), NULL, false, NULL))

#define warpIDForFake(_opt, _id) \
        lastID(runQuery((_opt), "SELECT %s from warpRun  WHERE fake_id = %s", "warp_id", \
                            (_id), NULL, false, NULL))



static psArray *findChipURIs(itOptions *options, psString exp_id, psString class_id)
{
    psString chip_id = chipIDForExp(options, exp_id);

    if (chip_id == NULL)
        return NULL;

    psString query = NULL;
    psStringAppend(&query, "SELECT %s from chipProcessedImfile WHERE chip_id = %s", "%s", "%s");
    if (class_id && (strcmp(class_id, "null") != 0)) {
        psStringAppend(&query, " AND class_id = '%s'", "%s");
    }

    psArray *chip_files = runQuery(options, query,
                        "uri", chip_id, class_id, true, NULL);
    psFree(query);
    psFree(chip_id);

    return chip_files;
}

static psArray *findWarpURIs(itOptions *options, psString exp_id)
{
    psString chip_id = chipIDForExp(options, exp_id);

    if (chip_id == NULL)
        return NULL;

    psString cam_id = camIDForChip(options, chip_id);
    if (cam_id == NULL)
        return NULL;

    psString fake_id = fakeIDForCam(options, cam_id);
    if (fake_id == NULL)
        return NULL;

    psString warp_id = warpIDForFake(options, fake_id);
    if (warp_id == NULL)
        return NULL;

    psArray *warp_files = runQuery(options,
                        "SELECT %s from warpSkyfile WHERE warp_id = %s"
                        " AND uri IS NOT NULL and (fault = 0 or fault is NULL)",
                           "uri",   warp_id, NULL, true, NULL);

    psFree(warp_id);
    psFree(fake_id);
    psFree(cam_id);
    psFree(chip_id);

    return warp_files;
}

static psArray *findStackURIs(itOptions *options, psString exp_id)
{
    psString chip_id = chipIDForExp(options, exp_id);

    if (chip_id == NULL)
        return NULL;

    psString cam_id = camIDForChip(options, chip_id);
    if (cam_id == NULL)
        return NULL;

    psString fake_id = fakeIDForCam(options, cam_id);
    if (fake_id == NULL)
        return NULL;

    psString warp_id = warpIDForFake(options, fake_id);
    if (warp_id == NULL)
        return NULL;

    psArray *stackIDs = runQuery(options, "SELECT %s FROM stackInputSkyfile WHERE warp_id = %s",
                            "stack_id", warp_id, NULL, false, NULL);

    psFree(warp_id);
    psFree(fake_id);
    psFree(cam_id);
    psFree(chip_id);

    if (stackIDs == NULL) {
        return NULL;
    }

    psArray *uris = psArrayAlloc(0);

    psU64 len = psArrayLength(stackIDs);

    // loop over the stack's for this exposure and add the images
    for (int i = 0; i < len ; i++ ) {
        psString stack_id = (psString) stackIDs->data[i];
        uris = runQuery(options, "SELECT %s FROM stackSumSkyfile where stack_id = %s",
                    "uri", stack_id, NULL, true, uris);
    }
    psFree(stackIDs);

    return uris;
}

static psArray *findDiffURIs(itOptions *options, psString exp_id)
{
    psString chip_id = chipIDForExp(options, exp_id);

    if (chip_id == NULL)
        return NULL;

    psString cam_id = camIDForChip(options, chip_id);
    if (cam_id == NULL)
        return NULL;

    psString fake_id = fakeIDForCam(options, cam_id);
    if (fake_id == NULL)
        return NULL;

    psString warp_id = warpIDForFake(options, fake_id);
    if (warp_id == NULL)
        return NULL;

    psArray *diffIDs = runQuery(options,
        "SELECT %s FROM diffInputSkyfile WHERE warp_id = %s",
            "diff_id", warp_id, NULL, false, NULL);

    psFree(warp_id);
    psFree(fake_id);
    psFree(cam_id);
    psFree(chip_id);

    if (diffIDs == NULL) {
        return NULL;
    }

    psArray *uris = psArrayAlloc(0);

    psU64 len = psArrayLength(diffIDs);

    for (int i = 0; i < len ; i++ ) {
        psString diff_id = (psString) diffIDs->data[i];
        uris = runQuery(options, "SELECT %s FROM diffSkyfile where diff_id = %s",
                    "uri", diff_id, NULL, true, uris);
    }

    psFree(diffIDs);

    return uris;
}

static psArray * parseByID(itOptions *options)
{
    psString id = options->id;
    psString class_id = options->class_id;
    psArray *uris = NULL;

    if (id == NULL) {
        fprintf(stderr, "parseByID: need id\n");
        return NULL;
    }

    switch (options->imageType) {
    case RAW_IMAGE:
        uris = findURIs(options, "rawImfile", "exp_id", id, class_id);
        break;
    case CHIP_IMAGE:
        uris = findURIs(options, "chipProcessedImfile", "chip_id", id, class_id);
        break;
    case WARP_IMAGE:
        uris = findURIs(options, "warpSkyfile", "warp_id", id, NULL);
        break;
    case DIFF_IMAGE:
        uris = findURIs(options, "diffSkyfile", "diff_id", id, NULL);
        break;
    case STACK_IMAGE:
        uris = findURIs(options, "stackSumSkyfile", "stack_id", id, NULL);
        break;
    default:
        // note: parseImageType prints a message
        return NULL;
    }

#ifdef VERBOSE
        for (int i=0; i<psArrayLength(uris); i++) {
            psString uri = (psString) uris->data[i];
            printf("uri: %s\n", uri ? (psString) uri : "NULL uri");
        }
#endif


    return uris;
}

psString exposureNameToID(itOptions *options, psString exposure)
{
    psArray *results;

    results = runQuery(options, "SELECT %s from rawExp WHERE exp_name = '%s'",
                        "exp_id", exposure, NULL, false, NULL);

    return lastID(results);
}

static psArray * parseByExp(itOptions *options)
{
    psString class_id = options->class_id;
    psString exposure = options->id;
    psArray *uris = NULL;

    if (exposure == NULL) {
        psError(PS_ERR_UNKNOWN, true, "parseByExp: need exposure name");
        return NULL;
    }

    psString exp_id = exposureNameToID(options, exposure);
    if (exp_id == 0) {
        return NULL;
    }

    switch (options->imageType) {
    case RAW_IMAGE:
        if (class_id == NULL) {
            // XXX: we could relax this requirement if roi is specified in sky coords and
            // process the whole fpa. That might even be a useful feature.
            psError(PS_ERR_UNKNOWN, true, "stamps from raw images require CLASS_ID");
            return NULL;
        }
        uris = findURIs(options, "rawImfile", "exp_id", exp_id, class_id);
        break;
    case CHIP_IMAGE:
        if (class_id == NULL) {
            // XXX: we could relax this requirement if roi is specified in sky coords and
            // process the whole fpa. That might even be a useful feature.
            psError(PS_ERR_UNKNOWN, true, "stamps from chip processed images require CLASS_ID");
            return NULL;
        }
        uris = findChipURIs(options, exp_id, class_id);
        break;
    case WARP_IMAGE:
        uris = findWarpURIs(options, exp_id);
        break;
    case DIFF_IMAGE:
        uris = findDiffURIs(options, exp_id);
        break;
    case STACK_IMAGE:
        uris = findStackURIs(options, exp_id);
        break;
    default:
        // note: parseImageType prints an error message
        return NULL;
    }

    psFree(exp_id);

    return uris;
}

/*
 * parseByCoord: first version. Looks for exposures with 
 *      (dateobs >= mjd_min) && (dateobs <= (mjd_min + exp_time))
 * I think that this meets the requirements that MOPS has for the postage stamp lookup
 */

static psArray * parseByCoord(itOptions *options)
{
    if (!options->mjd_min) {
        psError(PS_ERR_UNKNOWN, false, "mjd_min is null\n");
        return NULL;
    }

    psArray *output = psArrayAlloc(0);

    time_t sec = ohana_mjd_to_sec(options->mjd_min);

    // set time zone to utc so that sec --> dateobs are compared properly
    // XXX: TODO: make sure that I need to do this
    if (!p_psDBRunQuery(options->config->database, "set time_zone='+00:00'")) {
        psError(PS_ERR_UNKNOWN, false, "database error seting time zone");
        return NULL;
    }
    psString query = NULL;
    psStringAppend(&query, "SELECT exp_name FROM rawExp"
                " WHERE dateobs >= FROM_UNIXTIME(%ld) AND dateobs <= FROM_UNIXTIME(%ld + exp_time)",
                                    sec, sec);

    if (!p_psDBRunQuery(options->config->database, query)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return NULL;
    }

    psArray *queryOutput = p_psDBFetchResult(options->config->database);
    if (!queryOutput) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return NULL;
    }

    psU64 len = psArrayLength(queryOutput);
    if (!len) {
        psTrace("pstampparse", PS_LOG_INFO, "no rows found");
        psFree(queryOutput);
        return NULL;
    }

    // For each exp_name found send it to parseByExp and collect the results
    for (int i=0; i < len; i++) {
        psMetadata *row = queryOutput->data[i];
        psString val = psMetadataLookupStr(NULL, row, "exp_name");
        if (val != NULL) {
            options->id = val;
            psArray *uris = parseByExp(options);
            if (uris) {
                for (int i=0; i<psArrayLength(uris); i++) {
                    psString uri = (psString) uris->data[i];
                    psArrayAdd(output, 0, uri);
                }
            }
        }
    }

    psFree(queryOutput);

    return output;
}

static bool
go(itOptions *options)
{
    // how are we to select the images ?
    if (options->lookupType == UNKNOWN_LOOKUP_TYPE) {
        psError(PS_ERR_UNKNOWN, true, "request file has no REQ_TYPE");
        return false;
    }

    psArray *uris = NULL;

    switch (options->lookupType) {
    case BY_ID:
        // directly by exp_id, chip_id, warp_id etc
        uris = parseByID(options);
        break;
    case BY_EXP:
        // images that resulted from a given exposure
        uris = parseByExp(options);
        break;
    case BY_COORD:
        // images from a particular region of the sky
        uris = parseByCoord(options);
        break;
    default:
        psError(PS_ERR_PROGRAMMING, true, "unknown Lookup type: %d\n", options->lookupType);
        return false;
    }

    if (uris == NULL) {
        // XXX we need a finer grained status code: did something go wrong with the system
        // or are there simply no images matching the request
        fprintf(stderr, "No matching images found.\n");
        return false;
    }

    int numURIs = psArrayLength(uris);
    // Just list the images that would have jobs queued for them
    for (int i=0; i<numURIs; i++) {
        fprintf(stdout, "%s\n", (psString) uris->data[i]);
    }

    return true;
}

int main(int argc, char *argv[])
{
    itOptions *options = parseArguments(argc, argv);

    if (!setupDB(options)) {
        psErrorStackPrint(stderr, "unable to set up data base connection");
        return 1;
    }

    if (!go(options)) {
        psErrorStackPrint(stderr, "error processing request");
        return 1;
    }

    return 0;
}
