/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2005-2013 European Southern Observatory
 *
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

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

/*----------------------------------------------------------------------------*
 *                              Includes                                      *
 *----------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <cpl.h>

#include "muse_processing.h"

#include "muse_utils.h"

/*---------------------------------------------------------------------------*/
/**
 * @addtogroup muse_processing
 */
/*---------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*
 *                          Static variables                                  *
 *----------------------------------------------------------------------------*/
/*---------------------------------------------------------------------------*/
/**
  Structure to hold the extended processing information.
 */
/*---------------------------------------------------------------------------*/
typedef struct muse_processinginfo_s {
  struct muse_processinginfo_s *prev;
  struct muse_processinginfo_s *next;
  cpl_recipe *plugin;
  cpl_recipeconfig *recipeconfig;
  /** @brief Function to check the FITS header and insert the comments */
  muse_processing_prepare_header_func *prepare_header;
  /** @brief Function to get the processing level of a frame */
  muse_processing_get_frame_level_func *get_frame_level;
  /** @brief Function to get the processing level of a frame */
  muse_processing_get_frame_mode_func *get_frame_mode;
} muse_processinginfo_t;

/*---------------------------------------------------------------------------*/
/**
  NULL-terminated array of all registered processing information
 */
/*---------------------------------------------------------------------------*/
static muse_processinginfo_t *muse_processinginfo = NULL;

/*---------------------------------------------------------------------------*/
/**
  @brief Get processinginfo for a certain recipe
 */
/*---------------------------------------------------------------------------*/

static muse_processinginfo_t *
muse_processinginfo_get(const cpl_recipe *aRecipe) {
  if (muse_processinginfo != NULL) {
    muse_processinginfo_t *m;
    for (m = muse_processinginfo; m != NULL; m = m->next) {
      if (m->plugin == aRecipe) {
        return m;
      }
    }
  }
  return NULL;
}

/*---------------------------------------------------------------------------*/
/**
  @brief Register extended functionalities for MUSE recipes
  @param plugin            the CPL recipe
  @param recipeconfig      the recipe configuration
  @param prepare_header    function to declare the properties of a frame
  @param get_frame_level   function that returns the processing level of
  @param get_frame_mode    function that returns the output mode of a frame

  This functions registers all extensions that are used to use our
  framework.
 */
/*---------------------------------------------------------------------------*/
void
muse_processinginfo_register
( cpl_recipe *plugin,
  cpl_recipeconfig *recipeconfig,
  muse_processing_prepare_header_func *prepare_header,
  muse_processing_get_frame_level_func *get_frame_level,
  muse_processing_get_frame_mode_func *get_frame_mode) {

  muse_processinginfo_t *m = muse_processinginfo;
  if (muse_processinginfo == NULL) {
    muse_processinginfo = cpl_calloc(1, sizeof(muse_processinginfo_t));
    m = muse_processinginfo;
  } else {
    while (m->next != NULL) {
      m = m->next;
    }
    m->next = cpl_calloc(1, sizeof(muse_processinginfo_t));
    m->next->prev = m;
    m = m->next;
  }
  m->plugin = plugin;
  m->recipeconfig = recipeconfig;
  m->prepare_header = prepare_header;
  m->get_frame_level = get_frame_level;
  m->get_frame_mode = get_frame_mode;
}

/*---------------------------------------------------------------------------*/
/**
  @brief Clear all information from the processing info and from the recipe
         config.
  @param aRecipe     the CPL recipe

  To be called before closing the program.
 */
/*---------------------------------------------------------------------------*/
void
muse_processinginfo_delete(cpl_recipe *aRecipe) {
  muse_processinginfo_t *m = muse_processinginfo_get(aRecipe);
  if (m == NULL) {
    return;
  }
  if (m == muse_processinginfo) {
    muse_processinginfo = m->next;
    if (muse_processinginfo != NULL) {
      muse_processinginfo->prev = NULL;
    }
  } else {
    m->prev->next = m->next;
    if (m->next != NULL) {
      m->next->prev = m->prev;
    }
  }
  cpl_recipeconfig_delete(m->recipeconfig);
  cpl_free(m);
}

/*---------------------------------------------------------------------------*/
/**
  @brief Prepare and check a FITS header for a certain frame tag.
  @param aRecipe     the CPL recipe
  @param aFrametag   the tag of the output frame
  @param aHeader     the FITS header of the output frame
  @return CPL_ERROR_NONE if the header was OK
 */
/*---------------------------------------------------------------------------*/
cpl_error_code muse_processing_prepare_header(const cpl_recipe *aRecipe,
                                              const char *aFrametag,
                                              cpl_propertylist *aHeader) {
  muse_processinginfo_t *m = muse_processinginfo_get(aRecipe);
  return (m != NULL)? (* m->prepare_header)(aFrametag, aHeader): CPL_ERROR_NONE;
}

/*---------------------------------------------------------------------------*/
/**
  @brief Get the level for a product frame with a certain tag
  @param aRecipe     the CPL recipe
  @param aFrametag   the tag of the output frame
  @return its frame level, CPL_FRAME_LEVEL_NONE if the tag was not registered.
 */
/*---------------------------------------------------------------------------*/
cpl_frame_level
muse_processing_get_frame_level(const cpl_recipe *aRecipe,
                                const char *aFrametag) {
  muse_processinginfo_t *m = muse_processinginfo_get(aRecipe);
  return (m != NULL)? (* m->get_frame_level)(aFrametag): CPL_FRAME_LEVEL_NONE;
}

/*---------------------------------------------------------------------------*/
/**
  @brief Get the mode for a product frame with a certain tag
  @param aRecipe     the CPL recipe
  @param aFrametag   the tag of the output frame
  @return its frame level, MUSE_FRAME_MODE_ALL if the tag was not registered.
 */
/*---------------------------------------------------------------------------*/
int
muse_processing_get_frame_mode(const cpl_recipe *aRecipe,
                               const char *aFrametag) {
  muse_processinginfo_t *m = muse_processinginfo_get(aRecipe);
  return (m != NULL)?(* m->get_frame_mode)(aFrametag): MUSE_FRAME_MODE_ALL;
}

/*---------------------------------------------------------------------------*/
/**
  @brief Get the recipe (frame) configuration.
  @param aRecipe Pointer to the recipe
  @return the recipe config, or NULL if not found.
 */
/*---------------------------------------------------------------------------*/
cpl_recipeconfig *
muse_processing_get_recipeconfig(cpl_recipe *aRecipe) {
  muse_processinginfo_t *m = muse_processinginfo_get(aRecipe);
  return (m != NULL)?m->recipeconfig: NULL;
}

/*---------------------------------------------------------------------------*/
/**
  @brief Prepare and check the specified property
  @param aHeader the header to be prepared
  @param aName the property name
  @param aType the property type
  @param aDescription the description (comment) of this property
  @return CPL error code

  Check the specified property for its existence and correct type and
  append the comment to it.

  @error{return CPL_ERROR_DATA_NOT_FOUND, the property was not found}
  @error{return CPL_ERROR_TYPE_MISMATCH, the property has different type}
  @error{return CPL_ERROR_NULL_INPUT, the header or the name was NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_processing_prepare_property(cpl_propertylist *aHeader, const char *aName,
                                 cpl_type aType, const char *aDescription)
{
  cpl_ensure_code(aHeader, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aName, CPL_ERROR_NULL_INPUT);

  cpl_error_code rc = CPL_ERROR_NONE;

  cpl_propertylist *list = cpl_propertylist_new();
  cpl_propertylist_copy_property_regexp(list, aHeader, aName, 0);
  if (cpl_propertylist_is_empty(list)) {
    cpl_propertylist_delete(list);

    // The tag MUSE PRIVATE DOCUMENTATION is set in xmldoc.c and checked
    // here:
    // If it is not set (the normal case in a recipe), just an error
    // message is generated in case of a missing header.
    // If the description contains the string "(optional)", the property may not
    // be present, but this failure should be ignored.
    if (!cpl_propertylist_has(aHeader, "MUSE PRIVATE DOCUMENTATION") &&
        (aDescription && !strstr(aDescription, "(optional)"))) {
      cpl_msg_warning(__func__, "Property %s (%s) not used", aName,
                      aDescription);
      return CPL_ERROR_DATA_NOT_FOUND;
    }
    // If this property is set (if the function is used from
    // muse_xmldoc), then we add missing headers with some default
    // values.
    // (For floating-point keywords, use "curious" values, since the better
    // choice of NAN cannot be written to FITS headers and causes an error.)
    switch(aType) {
    case CPL_TYPE_FLOAT:
        cpl_propertylist_append_float(aHeader, aName, -99.);
        break;
      case CPL_TYPE_DOUBLE:
        cpl_propertylist_append_double(aHeader, aName, -999.);
        break;
      case CPL_TYPE_STRING:
        cpl_propertylist_append_string(aHeader, aName, "");
        break;
      case CPL_TYPE_INT:
        cpl_propertylist_append_int(aHeader, aName, INT_MAX);
        break;
      case CPL_TYPE_LONG:
        cpl_propertylist_append_long(aHeader, aName, LONG_MAX);
        break;
      case CPL_TYPE_BOOL:
        cpl_propertylist_append_bool(aHeader, aName, FALSE);
        break;
      default:
        return CPL_ERROR_INVALID_TYPE;
    }

    /* XXX ugly: copied code from below for the moment to keep the case *
     *     of the documentation working                                 */
    cpl_property *property = cpl_propertylist_get_property(aHeader, aName);
    if (aDescription != NULL && strlen(aDescription)>0) {
      rc = cpl_property_set_comment(property, aDescription);
    }
    cpl_type type = cpl_property_get_type(property);
    if (type != aType) {
      cpl_msg_warning(__func__, "Type of property %s is %s but should be %s",
                      aName, cpl_type_get_name(type), cpl_type_get_name(aType));
      return CPL_ERROR_TYPE_MISMATCH;
    }
    return CPL_ERROR_NONE;
  } /* empty list of properties of regexp search */

  /* loop through the properties that matched the regular expressions */
  int i;
  for (i = 0; i < cpl_propertylist_get_size(list); i++) {
    cpl_property *prop = cpl_propertylist_get(list, i);
    cpl_property *property =
      cpl_propertylist_get_property(aHeader, cpl_property_get_name(prop));
    if (aDescription != NULL && strlen(aDescription)>0) {
      rc = cpl_property_set_comment(property, aDescription);
    }
    cpl_type type = cpl_property_get_type(property);
    if (type != aType) {
      cpl_msg_warning(__func__, "Type of property %s is %s but should be %s",
                      aName, cpl_type_get_name(type), cpl_type_get_name(aType));
      cpl_propertylist_delete(list);
      return CPL_ERROR_TYPE_MISMATCH;
    }
  } /* for i (all regexp-found properties) */
  cpl_propertylist_delete(list);
  return rc;
}

/*---------------------------------------------------------------------------*/
/**
  @brief  Output main pipeline configuration, inputs, and parameters.
  @param  aPlugin   the CPL plugin of the recipe in question

  Output pipeline name (PACKAGE_NAME) and version (PACKAGE_VERSION) and if
  aPlugin is non-NULL, give all input frames (filenames and tags), non-default
  parameters, and relevant (MUSE and OpenMP) environment variables.
  @note Only known environment variables are tested.

  This function is meant to be called at the beginning of a MUSE recipe.
 */
/*---------------------------------------------------------------------------*/
void
muse_processing_recipeinfo(cpl_plugin *aPlugin)
{
  cpl_msg_set_threadid_off();
  cpl_msg_info(__func__, "%s v%s", PACKAGE_NAME, PACKAGE_VERSION);
  if (!aPlugin) {
    return;
  }
  cpl_recipe *recipe = (cpl_recipe *)aPlugin;
  cpl_errorstate state = cpl_errorstate_get();
  cpl_size nframes = cpl_frameset_get_size(recipe->frames);
  if (!cpl_errorstate_is_equal(state)) {
    cpl_errorstate_set(state); /* swallow error about missing frames */
  }
  cpl_msg_debug(__func__, "%"CPL_SIZE_FORMAT" input frames:", nframes);

  cpl_msg_indent_more();
  cpl_size iframe;
  for (iframe = 0; iframe < nframes; iframe++) {
    cpl_frame *frame = cpl_frameset_get_position(recipe->frames, iframe);
    cpl_msg_debug(__func__, "%s\t%s", cpl_frame_get_filename(frame),
                  cpl_frame_get_tag(frame));
  } /* for iframe (all frames) */
  cpl_msg_indent_less();

  // print non-default parameters
  cpl_msg_debug(__func__, "non-default parameters:");
  cpl_msg_indent_more();
  int n = 0; /* count non-default parameters */
  const cpl_parameter *p = cpl_parameterlist_get_first(recipe->parameters);
  for ( ; p; p = cpl_parameterlist_get_next(recipe->parameters)) {
    /* the alias is the short name, and we always set the *
     * same one for both command line and configuration   */
    const char *name = cpl_parameter_get_alias(p, CPL_PARAMETER_MODE_CLI);
    cpl_type type = cpl_parameter_get_type(p);

     switch (type) {
     case CPL_TYPE_BOOL: {
       cpl_boolean value = cpl_parameter_get_bool(p),
                   dval = cpl_parameter_get_default_bool(p);
       if (value != dval) {
         cpl_msg_debug(__func__, "--%s=%s [%s]", name,
                       value ? "true" : "false", dval ? "true" : "false");
         n++;
       }
       break;
     }
     case CPL_TYPE_INT: {
       int value = cpl_parameter_get_int(p),
           dval = cpl_parameter_get_default_int(p);
       if (value != dval) {
         cpl_msg_debug(__func__, "--%s=%d [%d]", name, value, dval);
         n++;
       }
       break;
     }
     case CPL_TYPE_DOUBLE: {
       double value = cpl_parameter_get_double(p),
              dval = cpl_parameter_get_default_double(p);
       if (value != dval) {
         cpl_msg_debug(__func__, "--%s=%g [%g]", name, value, dval);
         n++;
       }
       break;
     }
     case CPL_TYPE_STRING: {
       const char *value = cpl_parameter_get_string(p),
                  *dval = cpl_parameter_get_default_string(p);
       if (value && dval && strncmp(value, dval, strlen(dval) + 1)) {
         cpl_msg_debug(__func__, "--%s=\"%s\" [\"%s\"]", name, value, dval);
         n++;
       }
       break;
     }
     default:
       cpl_msg_debug(__func__, "--%s: parameter of unknown type!", name);
       break;
     } /* switch */
  } /* for ipar (all parameters) */
  if (!n) { /* if no non-default parameters were found */
    cpl_msg_debug(__func__, "none");
  }
  cpl_msg_indent_less();

  /* print known MUSE* environment variables, keep this in sync with README */
  cpl_msg_debug(__func__, "relevant MUSE environment variables:");
  cpl_msg_indent_more();
  const char *env[] = { "MUSE_AIT_HACK_SLICE_NUMBER",
                        "MUSE_BASICPROC_SKIP_GAIN_OVERRIDE",
                        "MUSE_BASICPROC_SKIP_NONLIN_CORR",
                        "MUSE_TWILIGHT_SCALES", "MUSE_TWILIGHT_SKIP",
                        "MUSE_GEOMETRY_SKIP", "MUSE_GEOMETRY_NO_INVERSION",
                        "MUSE_GEOMETRY_MASK_ROTATION",
                        "MUSE_GEOMETRY_PINHOLE_DY", "MUSE_GEOMETRY_STD_GAP",
                        "MUSE_GEOMETRY_HORI_OFFSETS", "MUSE_DAR_CORRECT_METHOD",
                        "MUSE_AUTOCAL_EXCLUDE_RAMAN",
                        "MUSE_PIXTABLE_SAVE_AS_TABLE", "MUSE_COLLAPSE_PIXTABLE",
                        "MUSE_COLLAPSE_USE_VARIANCE",
                        "MUSE_CPL_ERRORSTATE_NDUMP", "MUSE_EXPERT_USER",
                        "MUSE_DEBUG_QUADRANTS",
                        "MUSE_DEBUG_TRACE", "MUSE_DEBUG_WAVECAL",
                        "MUSE_DEBUG_IGNORE_READOUT", "MUSE_DEBUG_IGNORE_INSMODE",
                        "MUSE_DEBUG_LSF_FIT", "MUSE_DEBUG_SKY",
                        "MUSE_PLOT_TRACE", "MUSE_DEBUG_DCR", "MUSE_DEBUG_FLUX",
                        "MUSE_DEBUG_PIXTABLE_LIMITS",
                        "MUSE_DEBUG_GEO_VERIFY_DY", "MUSE_DEBUG_GEO_VERTICAL",
                        "MUSE_DEBUG_MEMORY_PROGRAM", "MUSE_DEBUG_WCS",
                        "MUSE_DEBUG_GRID_CONVERSION", "MUSE_DEBUG_NEAREST",
                        "MUSE_DEBUG_WEIGHTED", "MUSE_DEBUG_WEIGHTED_X",
                        "MUSE_DEBUG_WEIGHTED_Y", "MUSE_DEBUG_WEIGHTED_Z",
                        "MUSE_DEBUG_WEIGHTED_GRID", "MUSE_DEBUG_WEIGHT_CUBE",
                        "MUSE_DEBUG_CRREJECT", "MUSE_DEBUG_CRREJECT_X",
                        "MUSE_DEBUG_CRREJECT_Y", "MUSE_DEBUG_CRREJECT_Z",
                        "MUSE_DEBUG_AUTOCALIB", "MUSE_SUPERFLAT_POS",
                        NULL };
  int ienv = 0;
  while (env[ienv]) {
    char *value = getenv(env[ienv]);
    if (value) {
      cpl_msg_debug(__func__, "%s=%s", env[ienv], value);
    } /* if */
    ienv++;
  } /* while */
  cpl_msg_indent_less();

  /* print OpenMP-related variables */
  cpl_msg_debug(__func__, "relevant OpenMP environment variables:");
  cpl_msg_indent_more();
  /* list all variables from OpenMP spec v3.1, section 4, add    *
   * those that are also part of OpenMP v4.0 and v4.5 at the end */
  const char *envomp[] = { "OMP_SCHEDULE", "OMP_NUM_THREADS", "OMP_DYNAMIC",
                           "OMP_PROC_BIND", "OMP_NESTED", "OMP_STACKSIZE",
                           "OMP_WAIT_POLICY", "OMP_MAX_ACTIVE_LEVELS",
                           "OMP_THREAD_LIMIT", /* v3.1 */
                           "OMP_PLACES", "OMP_CANCELLATION", "OMP_DISPLAY_ENV",
                           "OMP_DEFAULT_DEVICE", /* v4.0 */
                           "OMP_MAX_TASK_PRIORITY", /* v4.5 */ NULL };
  ienv = 0;
  while (envomp[ienv]) {
    char *value = getenv(envomp[ienv]);
    if (value) {
      cpl_msg_debug(__func__, "%s=%s", envomp[ienv], value);
    } /* if */
    ienv++;
  } /* while */
  cpl_msg_indent_less();
} /* muse_processing_recipeinfo() */

/**@}*/
