/*
 * pxtool.c
 *
 * Copyright (C) 2008  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 <string.h>

#include "pxtools.h"

bool pxIsValidState(const char *state)
{
    PS_ASSERT_PTR_NON_NULL(state, false);

    if (!strcmp(state, "new")) return true;
    if (!strcmp(state, "reg")) return true;
    if (!strcmp(state, "full")) return true;
    if (!strcmp(state, "drop")) return true;
    if (!strcmp(state, "wait")) return true;
    if (!strcmp(state, "goto_cleaned")) return true;
    if (!strcmp(state, "error_cleaned")) return true;
    if (!strcmp(state, "goto_purged")) return true;
    if (!strcmp(state, "error_purged")) return true;
    if (!strcmp(state, "goto_scrubbed")) return true;
    if (!strcmp(state, "error_scrubbed")) return true;
    if (!strcmp(state, "cleaned")) return true;
    if (!strcmp(state, "update")) return true;
    if (!strcmp(state, "purged")) return true;
    if (!strcmp(state, "scrubbed")) return true;
    return false;
}

psString pxMergeCodeVersions(psString version1, psString version2) {
  psString out = NULL;

  bool mod1 = false;
  bool mod2 = false;

  psS32 num1;
  psS32 num2;
  psS32 numO;

  if (!version1) {
    psStringAppend(&out,version2);
    return(out);
  }
  if (!version2) {
    psStringAppend(&out,version1);
    return(out);
  }
  
  if (strchr(version1,'M')) {
    psStringSubstitute(&version1,"M","");
    mod1 = true;
  }
  if (strchr(version2,'M')) {
    psStringSubstitute(&version2,"M","");
    mod2 = true;
  }

  num1 = strtol(version1,NULL,10);
  num2 = strtol(version2,NULL,10);

  if (num1 >= num2) {
    numO = num1;
  }
  else {
    numO = num2;
  }
  
  psStringAppend(&out,"%" PRId32,numO);
  if (mod1 || mod2) {
    psStringAppend(&out,"M");
  }
  return(out);
}

bool pxCoalesceRunStatus(pxConfig *config, const psString dbQFile, psS64 stage_id, psString *software_ver,
			 psS32 *maskfrac_npix, psF32 *maskfrac_static, psF32 *maskfrac_dynamic,
			 psF32 *maskfrac_magic, psF32 *maskfrac_advisory) {
  psString query = pxDataGet(dbQFile);
/*   psString text_id = NULL; */
/*   psStringAppend(&text_id," %" PRId64,stage_id); */
/*   psStringSubstitute(&query,text_id,"@STAGE_ID@"); */
/*   psFree(text_id); */
  if (!p_psDBRunQueryF(config->dbh, query, stage_id)) {
    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);
  }
  
  for (long i = 0; i < psArrayLength(output); i++) {
    psMetadata *row = output->data[i];

    psS32 this_npix = psMetadataLookupS32(NULL, row, "maskfrac_npix");
    psF32 this_static = psMetadataLookupF32(NULL, row, "maskfrac_static");
    psF32 this_dynamic = psMetadataLookupF32(NULL, row, "maskfrac_dynamic");
    psF32 this_magic = psMetadataLookupF32(NULL, row, "maskfrac_magic");
    psF32 this_advisory = psMetadataLookupF32(NULL, row, "maskfrac_advisory");
    psString this_version = psMetadataLookupStr(NULL, row, "software_ver");

    *software_ver = pxMergeCodeVersions(*software_ver,this_version);

    *maskfrac_static = ((*maskfrac_static * *maskfrac_npix) + (this_npix * this_static)) / (this_npix + *maskfrac_npix);
    *maskfrac_dynamic = ((*maskfrac_dynamic * *maskfrac_npix) + (this_npix * this_dynamic)) / (this_npix + *maskfrac_npix);
    *maskfrac_magic = ((*maskfrac_magic * *maskfrac_npix) + (this_npix * this_magic)) / (this_npix + *maskfrac_npix);
    *maskfrac_advisory = ((*maskfrac_advisory * *maskfrac_npix) + (this_npix * this_advisory)) / (this_npix + *maskfrac_npix);
    *maskfrac_npix += this_npix;
  }
  psFree(output);
  return(true);
}

bool pxSetRunSoftware(pxConfig *config, const psString tableName, const psString stage_id_name, const psS64 stage_id,
		      psString software_ver) {
  char *query = "UPDATE %s SET software_ver = '%s' WHERE %s = %" PRId64;
  printf(query,tableName,software_ver,stage_id_name,stage_id);
  if (!p_psDBRunQueryF(config->dbh,query,tableName,software_ver,stage_id_name,stage_id)) {
    psError(PS_ERR_UNKNOWN, false,
	    "failed to set software version for %s %" PRId64,stage_id_name,stage_id);
    return(false);
  }
  
  return(true);
}
bool pxSetRunMaskfrac(pxConfig *config, const psString tableName, const psString stage_id_name, const psS64 stage_id,
		      psS32 maskfrac_npix, psF32 maskfrac_static, psF32 maskfrac_dynamic,
		      psF32 maskfrac_magic, psF32 maskfrac_advisory) {
  char *query = "UPDATE %s SET maskfrac_npix = %d, maskfrac_static = %f, maskfrac_dynamic = %f, maskfrac_magic = %f, maskfrac_advisory = %f WHERE %s = %" PRId64;
  if (!p_psDBRunQueryF(config->dbh,query,tableName,maskfrac_npix,maskfrac_static,
		       maskfrac_dynamic, maskfrac_magic,maskfrac_advisory,stage_id_name,stage_id)) {
    psError(PS_ERR_UNKNOWN, false,
	    "failed to set maskfrac stats for %s %" PRId64,stage_id_name,stage_id);
    return(false);
  }
  


  return(true);
}

// 'scrubbed' is no longer a virtual state equivalent to cleaned, but allows files to be removed
// even if the config files is missing.  This change was prompted as files that are cleaned can
// be regenerated, but that is not certain after being scrubbed.


// change the value for tableName.columName from 'full' to 'cleaned' if necessary
bool pxSetStateCleaned(const psString tableName, const psString columnName, psArray *rows)
{
    for (long i = 0; i < psArrayLength(rows); i++) {
        psMetadata *row = rows->data[i];
        psString state = psMetadataLookupStr(NULL, row, columnName);
        if (!state) {
            psError(PS_ERR_PROGRAMMING, false, "%s not found in row %ld of table %s",
                    columnName, i, tableName);
            return false;
        }
        if (!strcmp("full", state)) {
            // change full to cleaned
            psMetadataAddStr(row, PS_LIST_TAIL, columnName, PS_META_REPLACE, "", "cleaned");
        } else if (strcmp("cleaned", state)) {
            // if state isn't cleaned or full we can't set it to cleaned
            psError(PS_ERR_PROGRAMMING, true, "%s with state %s may not be exported cleaned",
                    tableName, state);
            return false;
        }
    }
    return true;
}

// XXX verify data type?
bool pxAddLabelSearchArgs (pxConfig *config, psMetadata *where, char *name, char *field, char *op) {

    psMetadataItem *item = psMetadataLookup(config->args, name);
    if (!item) {
        psError(PS_ERR_UNKNOWN, false, "failed to lookup value for %s", name);
        return false;
    }
    psAssert (item->type == PS_DATA_METADATA_MULTI, "%s should be a multi container", name);
    psAssert (item->data.list->n, "%s should at least have a place-holder", name);
    psMetadataItem *entry = (psMetadataItem *)item->data.list->head->data;
    psAssert (entry, "%s should at least have a place-holder", name);
    if (entry->data.str) {
        psListIterator *iter = psListIteratorAlloc (item->data.list, PS_LIST_HEAD, true);
        psMetadataItem *item = NULL;
        while ((item = psListGetAndIncrement(iter))) {
            // need to change the name and comment
            psFree (item->name);
            item->name = psStringCopy (field);
            psFree (item->comment);
            item->comment = psStringCopy (op);
            if (!psMetadataAddItem(where, item, PS_LIST_TAIL, PS_META_DUPLICATE_OK)) {
                psError(PS_ERR_UNKNOWN, false, "failed to add item %s", field);
                psFree(where);
                return false;
            }
        }
        psFree (iter);
    }
    return true;
}

// shared code for updating the various strings for a Run
bool pxUpdateRun(pxConfig *config, psMetadata *where, psString *pQuery, psString runTable, psString idColumn, psString fileTable, bool has_dist_group)
{
    PS_ASSERT_PTR_NON_NULL(config, false);
    PS_ASSERT_PTR_NON_NULL(where, false);
    PS_ASSERT_PTR_NON_NULL(pQuery, false);
    PS_ASSERT_PTR_NON_NULL(*pQuery, false);
    PS_ASSERT_PTR_NON_NULL(runTable, false);
    PS_ASSERT_PTR_NON_NULL(idColumn, false);
    PS_ASSERT_PTR_NON_NULL(fileTable, false);

    // make sure that -state is not the only selection parameter
    PXOPT_LOOKUP_STR(where_state, config->args, "-state", false, false);
    if (where_state && (psListLength(where->list) < 2)) {
        psError(PXTOOLS_ERR_CONFIG, true, "selection by -state alone is not allowed");
        return false;
    }

    PXOPT_LOOKUP_STR(state, config->args,       "-set_state", false, false);
    PXOPT_LOOKUP_STR(label, config->args,       "-set_label", false, false);
    PXOPT_LOOKUP_STR(data_group, config->args,  "-set_data_group", false, false);
    PXOPT_LOOKUP_STR(note, config->args,        "-set_note", false, false);

    psString dist_group = NULL;
    if (has_dist_group) {
        PXOPT_LOOKUP_STR(tmp_dist_group, config->args,  "-set_dist_group", false, false);
        dist_group = tmp_dist_group;
    }

    if ((!state) && (!label) && (!data_group) && (has_dist_group && !dist_group) && !(note)) {
        psError(PXTOOLS_ERR_CONFIG, false, "parameters are required");
        return false;
    }

    if (state && ! pxIsValidState(state)) {
        psError(PXTOOLS_ERR_CONFIG, false, "pxIsValidState failed");
        return false;
    }

    // first paramter is added with "SET param = 'value'"
    // subseqent ones with ", param = 'value'"
    char *separator = " SET ";
    char *comma = ",";

#   define addColumn(_tab, _val) \
        do { \
            if (_val) { \
                psStringAppend(pQuery, "%s %s.%s = '%s'", separator, _tab, #_val, _val); \
                separator = comma; \
            } \
        } while (0)

    addColumn(runTable, state);
    addColumn(runTable, data_group);
    if (has_dist_group) {
        addColumn(runTable, dist_group);
    }
    addColumn(runTable, note);
    addColumn(runTable, label);

    psString joinHook = psStringCopy("");
    psString fileWhere = NULL;
    if (state && !strcmp(state, "update")) {
        psStringAppend(&joinHook, "\n JOIN %s USING(%s)", fileTable, idColumn);
        psStringAppend(pQuery, ", %s.data_state = 'update'", fileTable);
        psStringAppend(&fileWhere, "AND %s.data_state = 'cleaned'", fileTable);
    }

    psString whereClause =  psDBGenerateWhereSQL(where, NULL);
    psStringAppend(pQuery, " %s", whereClause);
    psFree(whereClause);
    if (fileWhere) {
        psStringAppend(pQuery, "%s", fileWhere);
    }

    bool mdok;                          // Status of MD lookup
    if (psMetadataLookupBool(&mdok, config->args, "-pretend")) {
        psLogMsg("pxtools", PS_LOG_INFO, "Query to run: %s\n", *pQuery);
        return true;
    }

    if (!p_psDBRunQueryF(config->dbh, *pQuery, joinHook)) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }

    return true;
}

bool pxLookupVersion(pxConfig *config, psArray **pArray)
{
    const char *query = "SELECT * FROM dbversion";

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

    psArray *output = p_psDBFetchResult(config->dbh);
    if (!output) {
        psError(PS_ERR_UNKNOWN, false, "database error");
        return false;
    }
    if (!psArrayLength(output)) {
        psFree(output);
        psError(PS_ERR_UNKNOWN, true, "no rows in dbversion");
        return false;
    }
    if (psArrayLength(output) > 1) {
        psError(PS_ERR_UNKNOWN, true, "unexpected number of rows found in dbversion: %ld",
                psArrayLength(output));
        return false;
    }
    *pArray = output;

    return true;
}

psString pxGetDBVersion(pxConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, NULL);

    psArray *array = NULL;
    if (!pxLookupVersion(config, &array)) {
        psError(PS_ERR_UNKNOWN, false, "pxLookupVersion failed");
        return NULL;
    }
    psMetadata *md = array->data[0];
    if (!md) {
        psError(PS_ERR_UNKNOWN, true, "output of pxLookupVersion is null");
        return NULL;
    }

    psString version = psMetadataLookupStr(NULL, md, "schema_version");

    return version;
}

bool pxExportVersion(pxConfig *config, FILE *file)
{
    PS_ASSERT_PTR_NON_NULL(config, NULL);
    PS_ASSERT_PTR_NON_NULL(file, NULL);

    psArray *array = NULL;
    if (!pxLookupVersion(config, &array) || !array) {
        psError(PS_ERR_UNKNOWN, false, "pxLookupVersion failed");
        return false;
    }
    if (!ippdbPrintMetadatas(file, array, "dbversion", true)) {
        psError(PS_ERR_UNKNOWN, false, "failed to print array");
        psFree(array);
        return false;
    }
    return true;
}

bool pxCheckImportVersion(pxConfig *config, psMetadata *input)
{
    PS_ASSERT_PTR_NON_NULL(config, NULL);
    PS_ASSERT_PTR_NON_NULL(input, NULL);

    // This code was adapted from the way camtool parses the structures.
    // Is this really the way to do it?
    psMetadataItem *multi_item =  psMetadataLookup(input, "dbversion");
    if (!multi_item || (multi_item->type != PS_DATA_METADATA_MULTI)) {
        psError(PS_ERR_UNKNOWN, true, "dbversion multi not found in input");
        return false;
    }

    psMetadataItem *dbversion = psListGet(multi_item->data.list, 0);
    if (!dbversion) {
        psError(PS_ERR_UNKNOWN, true, "dbversion not found in input");
        return false;
    }

    if (!strcmp(dbversion->name, "dbversion")) {
        // horray
        psMetadata *md = dbversion->data.md;
        psString schema_version = pxGetDBVersion(config);
        if (!schema_version) {
            psError(PS_ERR_UNKNOWN, false, "pxGetDBVersion failed");
            return false;
        }

        psString import_version = psMetadataLookupStr(NULL, md, "schema_version");
        if (import_version && strcmp(import_version, schema_version)) {
            psError(PS_ERR_UNKNOWN, true, "input file schema_version: %s does not match data base: %s",
                import_version, schema_version);
            return false;
        } else if (!import_version) {
            psError(PS_ERR_UNKNOWN, true, "input file schema_version is NULL");
            return false;
        } else {
            // YIPPEE this file is the same version
        }
    } else {
        psError(PS_ERR_UNKNOWN, true, "Unexpected config dump format");
        return false;
    }

    return true;
}
