/*
 * camtool.c
 *
 * Copyright (C) 2006  Joshua Hoblitt
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * program; see the file COPYING. If not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>

#include "pxtools.h"
#include "camtool.h"

static bool queueMode(pxConfig *config);
static bool pendingexpMode(pxConfig *config);
static bool pendingimfileMode(pxConfig *config);
static bool addprocessedexpMode(pxConfig *config);
static bool processedexpMode(pxConfig *config);
static bool revertprocessedexpMode(pxConfig *config);
static bool updateprocessedexpMode(pxConfig *config);
static bool blockMode(pxConfig *config);
static bool maskedMode(pxConfig *config);
static bool unblockMode(pxConfig *config);

# define MODECASE(caseName, func) \
    case caseName: \
    if (!func(config)) { \
        goto FAIL; \
    } \
    break;

int main(int argc, char **argv)
{
    psLibInit(NULL);

    pxConfig *config = camtoolConfig(NULL, argc, argv);
    if (!config) {
        psError(PXTOOLS_ERR_CONFIG, false, "failed to configure");
        goto FAIL;
    }

    switch (config->mode) {
        MODECASE(CAMTOOL_MODE_QUEUE,                queueMode);
        MODECASE(CAMTOOL_MODE_PENDINGEXP,           pendingexpMode);
        MODECASE(CAMTOOL_MODE_PENDINGIMFILE,        pendingimfileMode);
        MODECASE(CAMTOOL_MODE_ADDPROCESSEDEXP,      addprocessedexpMode);
        MODECASE(CAMTOOL_MODE_PROCESSEDEXP,         processedexpMode);
        MODECASE(CAMTOOL_MODE_REVERTPROCESSEDEXP,   revertprocessedexpMode);
        MODECASE(CAMTOOL_MODE_UPDATEPROCESSEDEXP,   updateprocessedexpMode);
        MODECASE(CAMTOOL_MODE_BLOCK,                blockMode);
        MODECASE(CAMTOOL_MODE_MASKED,               maskedMode);
        MODECASE(CAMTOOL_MODE_UNBLOCK,              unblockMode);
        default:
            psAbort("invalid option (this should not happen)");
    }

    psFree(config);
    pmConfigDone();
    psLibFinalize();

    exit(EXIT_SUCCESS);

FAIL:
    psErrorStackPrint(stderr, "\n");
    int exit_status = pxerrorGetExitStatus();

    psFree(config);
    pmConfigDone();
    psLibFinalize();

    exit(exit_status);
}

#define ADDRENAMEPARAMSTR(from, to, oldname, newname, comment) \
{ \
    bool status = false; \
    psString str = psMetadataLookupStr(&status, from, "-" oldname); \
    if (!status) { \
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -" oldname); \
        return false; \
    } \
    if (str) { \
        if (!psMetadataAddStr(to, PS_LIST_TAIL, newname, PS_META_DUPLICATE_OK, comment, str)) { \
            psError(PS_ERR_UNKNOWN, false, "failed to add item " newname); \
            psFree(to); \
            return false; \
        } \
    } \
}

#define ADDPARAMSTR(from, to, name) \
  ADDRENAMEPARAMSTR(from, to, name, name, "==")

#define ADDRENAMEPARAMF(from, to, type, oldname, newname, comment) \
{ \
    bool status = false; \
    ps##type var = psMetadataLookup##type(&status, from, "-" oldname); \
    if (!status) { \
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -" oldname); \
        return false; \
    } \
    if (!isnan(var)) { \
        if (!psMetadataAdd##type(to, PS_LIST_TAIL, newname, PS_META_DUPLICATE_OK, comment, var)) { \
            psError(PS_ERR_UNKNOWN, false, "failed to add item " newname); \
            psFree(to); \
            return false; \
        } \
    } \
}

#define ADDPARAMF(from, to, type, name) \
  ADDRENAMEPARAMF(from, to, type, name, name, "==")

static bool queueMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, NULL);

    psMetadata *where = psMetadataAlloc();
    // convert chip_id into a psS64
{
    bool status = false;
    psString var = psMetadataLookupStr(&status, config->args, "-chip_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -chip_id");
        return false;
    }
    if (var) {
        if (!psMetadataAddS64(where, PS_LIST_TAIL, "chip_id", 0, "==", (psS64)atoll(var))) {
            psError(PS_ERR_UNKNOWN, false, "failed to add item chip_id");
            psFree(where);
            return false;
        }
    }
}
    // convert exp_id into a psS64
{
    bool status = false;
    psString var = psMetadataLookupStr(&status, config->args, "-exp_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -exp_id");
        return false;
    }
    if (var) {
        if (!psMetadataAddS64(where, PS_LIST_TAIL, "exp_id", 0, "==", (psS64)atoll(var))) {
            psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
            psFree(where);
            return false; 
        }
    }
}
    ADDPARAMSTR(config->args, where, "exp_name");
    ADDRENAMEPARAMSTR(config->args, where, "inst", "camera", "==");
    ADDPARAMSTR(config->args, where, "telescope");
    ADDPARAMSTR(config->args, where, "dateobs_begin");
    ADDPARAMSTR(config->args, where, "dateobs_end");
    ADDPARAMSTR(config->args, where, "exp_tag");
    ADDPARAMSTR(config->args, where, "exp_type");
    ADDPARAMSTR(config->args, where, "filelevel");
    ADDPARAMSTR(config->args, where, "reduction");
    ADDPARAMSTR(config->args, where, "filter");
    ADDRENAMEPARAMF(config->args, where, F64, "airmass_min", "airmass", ">=");
    ADDRENAMEPARAMF(config->args, where, F64, "airmass_max", "airmass", "<");
    ADDRENAMEPARAMF(config->args, where, F64, "ra_min", "ra", ">=");
    ADDRENAMEPARAMF(config->args, where, F64, "ra_max", "ra", "<");
    ADDRENAMEPARAMF(config->args, where, F64, "decl_min", "decl", ">=");
    ADDRENAMEPARAMF(config->args, where, F64, "decl_max", "decl", "<");
    ADDRENAMEPARAMF(config->args, where, F32, "exp_time_min", "exp_time", ">=");
    ADDRENAMEPARAMF(config->args, where, F32, "exp_time_max", "exp_time", "<");
    ADDRENAMEPARAMF(config->args, where, F32, "sat_pixel_frac_min", "sat_pixel_frac", ">=");
    ADDRENAMEPARAMF(config->args, where, F32, "sat_pixel_frac_max", "sat_pixel_frac", "<");
    ADDRENAMEPARAMF(config->args, where, F64, "bg_min", "bt", ">=");
    ADDRENAMEPARAMF(config->args, where, F64, "bg_max", "bt", "<");
    ADDRENAMEPARAMF(config->args, where, F64, "bg_stdev_min", "bg_stdev", ">=");
    ADDRENAMEPARAMF(config->args, where, F64, "bg_stdev_max", "bg_stdev", "<");
    ADDRENAMEPARAMF(config->args, where, F64, "bg_mean_stdev_min", "bg_mean_stdev", ">=");
    ADDRENAMEPARAMF(config->args, where, F64, "bg_mean_stdev_max", "bg_mean_stdev", "<");
    ADDRENAMEPARAMF(config->args, where, F64, "alt_min", "alt", ">=");
    ADDRENAMEPARAMF(config->args, where, F64, "alt_max", "alt", "<");
    ADDRENAMEPARAMF(config->args, where, F64, "az_min", "az", ">=");
    ADDRENAMEPARAMF(config->args, where, F64, "az_max", "az", "<");
    ADDRENAMEPARAMF(config->args, where, F32, "ccd_temp_min", "ccd_temp", ">=");
    ADDRENAMEPARAMF(config->args, where, F32, "ccd_temp_max", "ccd_temp", "<");
    ADDRENAMEPARAMF(config->args, where, F64, "posang_min", "posang", ">=");
    ADDRENAMEPARAMF(config->args, where, F64, "posang_max", "posang", "<");
    ADDPARAMSTR(config->args, where, "object");
    ADDRENAMEPARAMF(config->args, where, F32, "solang_min", "solang", ">=");
    ADDRENAMEPARAMF(config->args, where, F32, "solang_max", "solang", "<");

    if (where->list->n < 1) {
        psFree(where);
        where = NULL;
    }

    bool status = false;
    psString workdir = psMetadataLookupStr(&status, config->args, "-set_workdir");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -set_workdir");
        return false;
    }

    psString label = psMetadataLookupStr(&status, config->args, "-set_label");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -set_label");
        return false;
    }

    psString reduction = psMetadataLookupStr(&status, config->args, "-set_reduction");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -set_reduction");
        return false;
    }

    psString expgroup = psMetadataLookupStr(&status, config->args, "-set_expgroup");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -set_expgroup");
        return false;
    }

    psString dvodb = psMetadataLookupStr(&status, config->args, "-set_dvodb");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -set_dvodb");
        return false;
    }

    // find the exp_id of all the exposures that we want to queue up.
    psString query = pxDataGet("camtool_find_chip_id.sql");
    if (!query) {
        psError(PXTOOLS_ERR_DATA, false, "failed to retreive SQL statement");
        return false;
    }

    if (where) {
        psString whereClause = psDBGenerateWhereConditionSQL(where, "");
        psFree(where);
        psStringAppend(&query, " AND %s", whereClause);
        psFree(whereClause);
    }

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

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("camtool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    // start a transaction so we don't end up with an exp without any associted
    // imfiles
    if (!psDBTransaction(config->dbh)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        psFree(output);
        return false;
    }

    // would could do this "all in the database" if we didn't want the option
    // of changing the label/reduction/expgroup/dvodb/etc.  So we're pulling the
    // data out so we have the option of changing these values or leaving the
    // old values in place (i.e., passing the values through).

    // loop over our list of chipRun rows
    for (long i = 0; i < psArrayLength(output); i++) {
        psMetadata *md = output->data[i];

        chipRunRow *row = chipRunObjectFromMetadata(md);
        if (!row) {
            psError(PS_ERR_UNKNOWN, false, "failed to convert metadata into chipRun");
            psFree(output);
            return false;
        }

        // queue the exp
        if (!pxcamQueueByChipID(config,
                    row->chip_id,
                    workdir     ? workdir   : row->workdir,
                    label       ? label     : row->label,
                    reduction      ? reduction    : row->reduction,
                    expgroup    ? expgroup  : row->expgroup,
                    dvodb       ? dvodb     : row->dvodb
        )) {
            if (!psDBRollback(config->dbh)) {
                psError(PS_ERR_UNKNOWN, false, "database error");
            }
            psError(PS_ERR_UNKNOWN, false,
                    "failed to trying to queue chip_id: %" PRId64, row->chip_id);
            psFree(row);
            psFree(output);
            return false;
        }
        psFree(row);
    }
    psFree(output);

    if (!psDBCommit(config->dbh)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }

    return true;
}

static bool pendingexpMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psU64 limit = psMetadataLookupU64(&status, config->args, "-limit");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -limit");
        return false;
    }

    psString query = pxDataGet("camtool_find_pendingexp.sql");
    if (!query) {
        psError(PXTOOLS_ERR_DATA, false, "failed to retreive SQL statement");
        return false;
    }

    if (config->where) {
        psString whereClause = psDBGenerateWhereConditionSQL(config->where, "camPendingExp");
        psStringAppend(&query, " AND %s", whereClause);
        psFree(whereClause);
    }

    // treat limit == 0 as "no limit"
    if (limit) {
        psString limitString = psDBGenerateLimitSQL(limit);
        psStringAppend(&query, " %s", limitString);
        psFree(limitString);
    }

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

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("camtool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    bool simple = false;
    {
        bool status = false;
        simple = psMetadataLookupBool(&status, config->args, "-simple");
        if (!status) {
            psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -simple");
            return false;
        }
    }

    if (!convertIdToStr(output)) {
        psError(PS_ERR_UNKNOWN, false, "failed to convert id fields into a strings");
        psFree(output);
        return false;
    }

    // negate simple so the default is true
    if (!ippdbPrintMetadatas(stdout, output, "camPendingExp", !simple)) {
        psError(PS_ERR_UNKNOWN, false, "failed to print array");
        psFree(output);
        return false;
    }

    psFree(output);

    return true;
}

static bool pendingimfileMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    psString query = pxDataGet("camtool_find_pendingimfile.sql");
    if (!query) {
        psError(PXTOOLS_ERR_DATA, false, "failed to retreive SQL statement");
        return false;
    }

    if (config->where) {
        psString whereClause = psDBGenerateWhereSQL(config->where, NULL);
        psStringAppend(&query, " %s", whereClause);
        psFree(whereClause);
    }

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

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("camtool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    bool simple = false;
    {
        bool status = false;
        simple = psMetadataLookupBool(&status, config->args, "-simple");
        if (!status) {
            psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -simple");
            return false;
        }
    }

    if (!convertIdToStr(output)) {
        psError(PS_ERR_UNKNOWN, false, "failed to convert id fields into a strings");
        psFree(output);
        return false;
    }

    // negate simple so the default is true
    if (!ippdbPrintMetadatas(stdout, output, "chipProcessedImfile", !simple)) {
        psError(PS_ERR_UNKNOWN, false, "failed to print array");
        psFree(output);
        return false;
    }

    psFree(output);

    return true;
}

static bool addprocessedexpMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psString cam_id = psMetadataLookupStr(&status, config->args, "-cam_id");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -cam_id");
        return false;
    }
    if (!cam_id) {
        psError(PS_ERR_UNKNOWN, true, "-cam_id is required");
        return false;
    }

    psString uri = psMetadataLookupStr(&status, config->args, "-uri");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -uri");
        return false;
    }
    if (!uri) {
        psError(PS_ERR_UNKNOWN, true, "-uri is required");
        return false;
    }

    psF64 bg = psMetadataLookupF64(&status, config->args, "-bg");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -bg");
        return false;
    }

    psF64 bg_stdev = psMetadataLookupF64(&status, config->args, "-bg_stdev");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -bg_stdev");
        return false;
    }

    psF64 bg_mean_stdev = psMetadataLookupF64(&status, config->args, "-bg_mean_stdev");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -bg_mean_stdev");
        return false;
    }

    psF32 sigma_ra = psMetadataLookupF32(&status, config->args, "-sigma_ra");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -sigma_ra");
        return false;
    }
    //if (isnan(sigma_ra)) {
    //  psError(PS_ERR_UNKNOWN, true, "-sigma_ra is required");
    //  return false;
    //}

    psF32 sigma_dec = psMetadataLookupF32(&status, config->args, "-sigma_dec");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -sigma_dec");
        return false;
    }
    //if (isnan(sigma_dec)) {
    //  psError(PS_ERR_UNKNOWN, true, "-sigma_dec is required");
    //  return false;
    //}

    psF32 zp_mean = psMetadataLookupF32(&status, config->args, "-zp_mean");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -zp_mean");
        return false;
    }
    //if (isnan(zp_mean)) {
    //  psError(PS_ERR_UNKNOWN, true, "-zp_mean is required");
    //  return false;
    //}

    psF32 zp_stdev = psMetadataLookupF32(&status, config->args, "-zp_stdev");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -zp_stdev");
        return false;
    }
    //if (isnan(zp_stdev)) {
    //  psError(PS_ERR_UNKNOWN, true, "-zp_stdev is required");
    //  return false;
    //}

    psF32 fwhm = psMetadataLookupF32(&status, config->args, "-fwhm");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -fwhm");
        return false;
    }
    //if (isnan(fwhm)) {
    //  psError(PS_ERR_UNKNOWN, true, "-fwhm is required");
    //  return false;
    //}

    psF32 fwhm_range = psMetadataLookupF32(&status, config->args, "-fwhm_range");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -fwhm_range");
        return false;
    }
    //if (isnan(fwhm_range)) {
    //  psError(PS_ERR_UNKNOWN, true, "-fwhm_range is required");
    //  return false;
    //}

    psS32 n_stars = psMetadataLookupS32(&status, config->args, "-n_stars");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -n_stars");
        return false;
    }
    if (n_stars < 0) {
        psError(PS_ERR_UNKNOWN, true, "-n_stars is required");
        return false;
    }

    psS32 n_extended = psMetadataLookupS32(&status, config->args, "-n_extended");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -n_extended");
        return false;
    }
    if (n_extended < 0) {
        psError(PS_ERR_UNKNOWN, true, "-n_extended is required");
        return false;
    }

    psS32 n_cr = psMetadataLookupS32(&status, config->args, "-n_cr");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -n_cr");
        return false;
    }
    if (n_cr < 0) {
        psError(PS_ERR_UNKNOWN, true, "-n_cr is required");
        return false;
    }

    psS32 n_astrom = psMetadataLookupS32(&status, config->args, "-n_astrom");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -n_astrom");
        return false;
    }
    if (n_astrom < 0) {
        psError(PS_ERR_UNKNOWN, true, "-n_astrom is required");
        return false;
    }

    // optional
    psString path_base = psMetadataLookupStr(&status, config->args, "-path_base");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -path_base");
        return false;
    }

    // default
    psS16 code = psMetadataLookupS16(&status, config->args, "-code");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -code");
        return false;
    }

    psString query = pxDataGet("camtool_find_pendingexp.sql");
    if (!query) {
        psError(PXTOOLS_ERR_DATA, false, "failed to retreive SQL statement");
        return false;
    }

    {
        // build a query to search by cam_id
        psMetadata *where = psMetadataAlloc();
        if (cam_id) {
            if (!psMetadataAddS64(where, PS_LIST_TAIL, "cam_id", 0, "==", (psS64)atoll(cam_id))) {
                psError(PS_ERR_UNKNOWN, false, "failed to add item cam_id");
                psFree(where);
                psFree(query);
                return false;
            }
        }

        psString whereClaus = psDBGenerateWhereSQL(where, NULL);
        psFree(where);
        if (whereClaus) {
            psStringAppend(&query, " %s", whereClaus);
            psFree(whereClaus);
        }

    }

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

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("camtool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    if (!psDBTransaction(config->dbh)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        psFree(output);
        return false;
    }

    camRunRow *pendingRow = camRunObjectFromMetadata(output->data[0]);
    psFree(output);
    // create a new camProcessedImfile object
    camProcessedExpRow *row = camProcessedExpRowAlloc(
        pendingRow->cam_id,
        pendingRow->chip_id,
        uri,
        bg,
        bg_stdev,
        bg_mean_stdev,
        sigma_ra,
        sigma_dec,
        zp_mean,
        zp_stdev,
        fwhm,
        fwhm_range,
        n_stars,
        n_extended,
        n_cr,
        n_astrom,
        path_base,
        code
    );
    psFree(pendingRow);

    // insert the new row into the camProcessedImfile table
    if (!camProcessedExpInsertObject(config->dbh, row)) {
        // rollback
        if (!psDBRollback(config->dbh)) {
            psError(PS_ERR_UNKNOWN, false, "database error");
        }
        psError(PS_ERR_UNKNOWN, false, "database error");
        psFree(row);
        return false;
    }

    // since there is only one exp per 'run' set camRun.state = 'stop'
    if (!pxcamRunSetState(config, row->cam_id, "stop")) {
        psError(PS_ERR_UNKNOWN, false, "failed to change camRun.state for cam_id: %" PRId64, row->cam_id);
        psFree(row);
        return false;
    }

    psFree(row);

    if (!psDBCommit(config->dbh)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }

    return true;
}


static bool processedexpMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psU64 limit = psMetadataLookupU64(&status, config->args, "-limit");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -limit");
        return false;
    }

    bool faulted = psMetadataLookupU64(&status, config->args, "-faulted");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -faulted");
        return false;
    }

    psString query = pxDataGet("camtool_find_processedexp.sql");
    if (!query) {
        psError(PXTOOLS_ERR_DATA, false, "failed to retreive SQL statement");
        return false;
    }

    if (config->where) {
        psString whereClause = psDBGenerateWhereConditionSQL(config->where, NULL);
        psStringAppend(&query, " AND %s", whereClause);
        psFree(whereClause);
    }

    if (faulted) {
        // list only faulted rows
        psStringAppend(&query, " %s", "AND camProcessedExp.fault != 0");
    } else {
        // don't list faulted rows
        psStringAppend(&query, " %s", "AND camProcessedExp.fault = 0");
    }

    // treat limit == 0 as "no limit"
    if (limit) {
        psString limitString = psDBGenerateLimitSQL(limit);
        psStringAppend(&query, " %s", limitString);
        psFree(limitString);
    }

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

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("camtool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    bool simple = false;
    {
        bool status = false;
        simple = psMetadataLookupBool(&status, config->args, "-simple");
        if (!status) {
            psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -simple");
            return false;
        }
    }

    if (!convertIdToStr(output)) {
        psError(PS_ERR_UNKNOWN, false, "failed to convert id fields into a strings");
        psFree(output);
        return false;
    }

    // negate simple so the default is true
    if (!ippdbPrintMetadatas(stdout, output, "camProcessedExp", !simple)) {
        psError(PS_ERR_UNKNOWN, false, "failed to print array");
        psFree(output);
        return false;
    }

    psFree(output);

    return true;
}


static bool revertprocessedexpMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    psString query = pxDataGet("camtool_revertprocessedexp.sql");
    if (!query) {
        psError(PXTOOLS_ERR_DATA, false, "failed to retreive SQL statement");
        return false;
    }

    if (config->where) {
        psString whereClause = psDBGenerateWhereConditionSQL(config->where, "camProcessedExp");
        psStringAppend(&query, " AND %s", whereClause);
        psFree(whereClause);
    }

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

    if (psDBAffectedRows(config->dbh) < 1) {
        psError(PS_ERR_UNKNOWN, false, "should have affected atleast 1 row");
        return false;
    }

    return true;
}


static bool updateprocessedexpMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psS16 code = psMetadataLookupS16(&status, config->args, "-code");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -code");
        return false;
    }
    if (code == INT16_MAX) {
        psError(PS_ERR_UNKNOWN, true, "-code is required");
        return false;
    }

    if (!pxSetFaultCode(config->dbh, "camProcessedExp", config->where, code)) {
        psError(PS_ERR_UNKNOWN, false, "failed to set set fault flag");
        return false;
    }

    return true;
}


static bool blockMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psString label = psMetadataLookupStr(&status, config->args, "-label");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -label");
        return false;
    }
    if (!label) {
        psError(PS_ERR_UNKNOWN, true, "-label is required");
        return false;
    }

    if (!camMaskInsert(config->dbh, label)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }

    return true;
}


static bool maskedMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    psString query = psStringCopy("SELECT * FROM camMask");

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

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psTrace("camtool", PS_LOG_INFO, "no rows found");
        psFree(output);
        return true;
    }

    bool simple = false;
    {
        bool status = false;
        simple = psMetadataLookupBool(&status, config->args, "-simple");
        if (!status) {
            psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -simple");
            return false;
        }
    }

    if (!convertIdToStr(output)) {
        psError(PS_ERR_UNKNOWN, false, "failed to convert id fields into a strings");
        psFree(output);
        return false;
    }

    // negative simple so the default is true
    if (!ippdbPrintMetadatas(stdout, output, "camMask", !simple)) {
        psError(PS_ERR_UNKNOWN, false, "failed to print array");
        psFree(output);
        return false;
    }

    psFree(output);

    return true;
}


static bool unblockMode(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    bool status = false;
    psString label = psMetadataLookupStr(&status, config->args, "-label");
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for -label");
        return false;
    }
    if (!label) {
        psError(PS_ERR_UNKNOWN, true, "-label is required");
        return false;
    }

    char *query = "DELETE FROM camMask WHERE label = '%s'";

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

    return true;
}
