/*****************************************************************************
 * rrd_hw_update.c  Functions for updating a Holt-Winters RRA
 ****************************************************************************/

#include "rrd_tool.h"
#include "rrd_format.h"
#include "rrd_hw_math.h"
#include "rrd_hw_update.h"

static void init_slope_intercept(
    unival *coefs,
    unsigned short CDP_scratch_idx)
{
#ifdef DEBUG
    fprintf(stderr, "Initialization of slope/intercept\n");
#endif
    coefs[CDP_hw_intercept].u_val = coefs[CDP_scratch_idx].u_val;
    coefs[CDP_hw_last_intercept].u_val = coefs[CDP_scratch_idx].u_val;
    /* initialize the slope to 0 */
    coefs[CDP_hw_slope].u_val = 0.0;
    coefs[CDP_hw_last_slope].u_val = 0.0;
    /* initialize null count to 1 */
    coefs[CDP_null_count].u_cnt = 1;
    coefs[CDP_last_null_count].u_cnt = 1;
}

static int hw_is_violation(
    rrd_value_t observed,
    rrd_value_t prediction,
    rrd_value_t deviation,
    rrd_value_t delta_pos,
    rrd_value_t delta_neg)
{
    return (observed > prediction + delta_pos * deviation
            || observed < prediction - delta_neg * deviation);
}

int update_hwpredict(
    rrd_t *rrd,
    unsigned long cdp_idx,
    unsigned long rra_idx,
    unsigned long ds_idx,
    unsigned short CDP_scratch_idx,
    hw_functions_t * functions)
{
    rrd_value_t prediction;
    unsigned long dependent_rra_idx, seasonal_cdp_idx;
    unival   *coefs = rrd->cdp_prep[cdp_idx].scratch;
    rra_def_t *current_rra = &(rrd->rra_def[rra_idx]);
    rrd_value_t seasonal_coef;

    /* save coefficients from current prediction */
    coefs[CDP_hw_last_intercept].u_val = coefs[CDP_hw_intercept].u_val;
    coefs[CDP_hw_last_slope].u_val = coefs[CDP_hw_slope].u_val;
    coefs[CDP_last_null_count].u_cnt = coefs[CDP_null_count].u_cnt;

    /* retrieve the current seasonal coef */
    dependent_rra_idx = current_rra->par[RRA_dependent_rra_idx].u_cnt;
    seasonal_cdp_idx = dependent_rra_idx * (rrd->stat_head->ds_cnt) + ds_idx;

    seasonal_coef = (dependent_rra_idx < rra_idx)
        ? rrd->cdp_prep[seasonal_cdp_idx].scratch[CDP_hw_last_seasonal].u_val
        : rrd->cdp_prep[seasonal_cdp_idx].scratch[CDP_hw_seasonal].u_val;

    /* compute the prediction */
    if (isnan(coefs[CDP_hw_intercept].u_val)
        || isnan(coefs[CDP_hw_slope].u_val)
        || isnan(seasonal_coef)) {
        prediction = DNAN;

        /* bootstrap initialization of slope and intercept */
        if (isnan(coefs[CDP_hw_intercept].u_val) &&
            !isnan(coefs[CDP_scratch_idx].u_val)) {
            init_slope_intercept(coefs, CDP_scratch_idx);
        }
        /* if seasonal coefficient is NA, then don't update intercept, slope */
    } else {
        prediction = functions->predict(coefs[CDP_hw_intercept].u_val,
                                        coefs[CDP_hw_slope].u_val,
                                        coefs[CDP_null_count].u_cnt,
                                        seasonal_coef);
#ifdef DEBUG
        fprintf(stderr,
                "computed prediction: %f (intercept %f, slope %f, season %f)\n",
                prediction, coefs[CDP_hw_intercept].u_val,
                coefs[CDP_hw_slope].u_val, seasonal_coef);
#endif
        if (isnan(coefs[CDP_scratch_idx].u_val)) {
            /* NA value, no updates of intercept, slope;
             * increment the null count */
            (coefs[CDP_null_count].u_cnt)++;
        } else {
            /* update the intercept */
            coefs[CDP_hw_intercept].u_val =
                functions->intercept(current_rra->par[RRA_hw_alpha].u_val,
                                     coefs[CDP_scratch_idx].u_val,
                                     seasonal_coef, coefs);

            /* update the slope */
            coefs[CDP_hw_slope].u_val =
                functions->slope(current_rra->par[RRA_hw_beta].u_val, coefs);

            /* reset the null count */
            coefs[CDP_null_count].u_cnt = 1;
#ifdef DEBUG
            fprintf(stderr, "Updating intercept = %f, slope = %f\n",
                    coefs[CDP_hw_intercept].u_val, coefs[CDP_hw_slope].u_val);
#endif
        }
    }

    /* store the prediction for writing */
    coefs[CDP_scratch_idx].u_val = prediction;
    return 0;
}

int update_seasonal(
    rrd_t *rrd,
    unsigned long cdp_idx,
    unsigned long rra_idx,
    unsigned long ds_idx,
    unsigned short CDP_scratch_idx,
    rrd_value_t *seasonal_coef,
    hw_functions_t * functions)
{
/* TODO: extract common if subblocks in the wake of I/O optimization */
    rrd_value_t intercept, seasonal;
    rra_def_t *current_rra = &(rrd->rra_def[rra_idx]);
    rra_def_t *hw_rra =
        &(rrd->rra_def[current_rra->par[RRA_dependent_rra_idx].u_cnt]);

    /* obtain cdp_prep index for HWPREDICT */
    unsigned long hw_cdp_idx = (current_rra->par[RRA_dependent_rra_idx].u_cnt)
        * (rrd->stat_head->ds_cnt) + ds_idx;
    unival   *coefs = rrd->cdp_prep[hw_cdp_idx].scratch;

    /* update seasonal coefficient in cdp prep areas */
    seasonal = rrd->cdp_prep[cdp_idx].scratch[CDP_hw_seasonal].u_val;
    rrd->cdp_prep[cdp_idx].scratch[CDP_hw_last_seasonal].u_val = seasonal;
    rrd->cdp_prep[cdp_idx].scratch[CDP_hw_seasonal].u_val =
        seasonal_coef[ds_idx];

    if (isnan(rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val)) {
        /* no update, store the old value unchanged,
         * doesn't matter if it is NA */
        rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val = seasonal;
        return 0;
    }

    /* update seasonal value for disk */
    if (current_rra->par[RRA_dependent_rra_idx].u_cnt < rra_idx) {
        /* associated HWPREDICT has already been updated */
        /* check for possible NA values */
        if (isnan(coefs[CDP_hw_last_intercept].u_val)
            || isnan(coefs[CDP_hw_last_slope].u_val)) {
            /* this should never happen, as HWPREDICT was already updated */
            rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val = DNAN;
        } else if (isnan(seasonal)) {
            /* initialization: intercept is not currently being updated */
#ifdef DEBUG
            fprintf(stderr, "Initialization of seasonal coef %lu\n",
                    rrd->rra_ptr[rra_idx].cur_row);
#endif
            rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val =
                functions->init_seasonality(rrd->cdp_prep[cdp_idx].
                                            scratch[CDP_scratch_idx].u_val,
                                            coefs[CDP_hw_last_intercept].
                                            u_val);
        } else {
            intercept = coefs[CDP_hw_intercept].u_val;

            rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val =
                functions->seasonality(current_rra->par[RRA_seasonal_gamma].
                                       u_val,
                                       rrd->cdp_prep[cdp_idx].
                                       scratch[CDP_scratch_idx].u_val,
                                       intercept, seasonal);
#ifdef DEBUG
            fprintf(stderr,
                    "Updating seasonal = %f (params: gamma %f, new intercept %f, old seasonal %f)\n",
                    rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val,
                    current_rra->par[RRA_seasonal_gamma].u_val,
                    intercept, seasonal);
#endif
        }
    } else {
        /* SEASONAL array is updated first, which means the new intercept
         * hasn't be computed; so we compute it here. */

        /* check for possible NA values */
        if (isnan(coefs[CDP_hw_intercept].u_val)
            || isnan(coefs[CDP_hw_slope].u_val)) {
            /* Initialization of slope and intercept will occur.
             * force seasonal coefficient to 0 or 1. */
            rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val =
                functions->identity;
        } else if (isnan(seasonal)) {
            /* initialization: intercept will not be updated
             * CDP_hw_intercept = CDP_hw_last_intercept; just need to 
             * subtract/divide by this baseline value. */
#ifdef DEBUG
            fprintf(stderr, "Initialization of seasonal coef %lu\n",
                    rrd->rra_ptr[rra_idx].cur_row);
#endif
            rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val =
                functions->init_seasonality(rrd->cdp_prep[cdp_idx].
                                            scratch[CDP_scratch_idx].u_val,
                                            coefs[CDP_hw_intercept].u_val);
        } else {
            /* Note that we must get CDP_scratch_idx from SEASONAL array, as CDP_scratch_idx
             * for HWPREDICT array will be DNAN. */
            intercept = functions->intercept(hw_rra->par[RRA_hw_alpha].u_val,
                                             rrd->cdp_prep[cdp_idx].
                                             scratch[CDP_scratch_idx].u_val,
                                             seasonal, coefs);

            rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val =
                functions->seasonality(current_rra->par[RRA_seasonal_gamma].
                                       u_val,
                                       rrd->cdp_prep[cdp_idx].
                                       scratch[CDP_scratch_idx].u_val,
                                       intercept, seasonal);
        }
    }
#ifdef DEBUG
    fprintf(stderr, "seasonal coefficient set= %f\n",
            rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val);
#endif
    return 0;
}

int update_devpredict(
    rrd_t *rrd,
    unsigned long cdp_idx,
    unsigned long rra_idx,
    unsigned long ds_idx,
    unsigned short CDP_scratch_idx)
{
    /* there really isn't any "update" here; the only reason this information
     * is stored separately from DEVSEASONAL is to preserve deviation predictions
     * for a longer duration than one seasonal cycle. */
    unsigned long seasonal_cdp_idx =
        (rrd->rra_def[rra_idx].par[RRA_dependent_rra_idx].u_cnt)
        * (rrd->stat_head->ds_cnt) + ds_idx;

    if (rrd->rra_def[rra_idx].par[RRA_dependent_rra_idx].u_cnt < rra_idx) {
        /* associated DEVSEASONAL array already updated */
        rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val
            =
            rrd->cdp_prep[seasonal_cdp_idx].
            scratch[CDP_last_seasonal_deviation].u_val;
    } else {
        /* associated DEVSEASONAL not yet updated */
        rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val
            =
            rrd->cdp_prep[seasonal_cdp_idx].scratch[CDP_seasonal_deviation].
            u_val;
    }
    return 0;
}

int update_devseasonal(
    rrd_t *rrd,
    unsigned long cdp_idx,
    unsigned long rra_idx,
    unsigned long ds_idx,
    unsigned short CDP_scratch_idx,
    rrd_value_t *seasonal_dev,
    hw_functions_t * functions)
{
    rrd_value_t prediction = 0, seasonal_coef = DNAN;
    rra_def_t *current_rra = &(rrd->rra_def[rra_idx]);

    /* obtain cdp_prep index for HWPREDICT */
    unsigned long hw_rra_idx = current_rra->par[RRA_dependent_rra_idx].u_cnt;
    unsigned long hw_cdp_idx = hw_rra_idx * (rrd->stat_head->ds_cnt) + ds_idx;
    unsigned long seasonal_cdp_idx;
    unival   *coefs = rrd->cdp_prep[hw_cdp_idx].scratch;

    rrd->cdp_prep[cdp_idx].scratch[CDP_last_seasonal_deviation].u_val =
        rrd->cdp_prep[cdp_idx].scratch[CDP_seasonal_deviation].u_val;
    /* retrieve the next seasonal deviation value, could be NA */
    rrd->cdp_prep[cdp_idx].scratch[CDP_seasonal_deviation].u_val =
        seasonal_dev[ds_idx];

    /* retrieve the current seasonal_coef (not to be confused with the
     * current seasonal deviation). Could make this more readable by introducing
     * some wrapper functions. */
    seasonal_cdp_idx =
        (rrd->rra_def[hw_rra_idx].par[RRA_dependent_rra_idx].u_cnt)
        * (rrd->stat_head->ds_cnt) + ds_idx;
    if (rrd->rra_def[hw_rra_idx].par[RRA_dependent_rra_idx].u_cnt < rra_idx)
        /* SEASONAL array already updated */
        seasonal_coef =
            rrd->cdp_prep[seasonal_cdp_idx].scratch[CDP_hw_last_seasonal].
            u_val;
    else
        /* SEASONAL array not yet updated */
        seasonal_coef =
            rrd->cdp_prep[seasonal_cdp_idx].scratch[CDP_hw_seasonal].u_val;

    /* compute the abs value of the difference between the prediction and
     * observed value */
    if (hw_rra_idx < rra_idx) {
        /* associated HWPREDICT has already been updated */
        if (isnan(coefs[CDP_hw_last_intercept].u_val) ||
            isnan(coefs[CDP_hw_last_slope].u_val) || isnan(seasonal_coef)) {
            /* one of the prediction values is unitialized */
            rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val = DNAN;
            return 0;
        } else {
            prediction =
                functions->predict(coefs[CDP_hw_last_intercept].u_val,
                                   coefs[CDP_hw_last_slope].u_val,
                                   coefs[CDP_last_null_count].u_cnt,
                                   seasonal_coef);
        }
    } else {
        /* associated HWPREDICT has NOT been updated */
        if (isnan(coefs[CDP_hw_intercept].u_val) ||
            isnan(coefs[CDP_hw_slope].u_val) || isnan(seasonal_coef)) {
            /* one of the prediction values is unitialized */
            rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val = DNAN;
            return 0;
        } else {
            prediction = functions->predict(coefs[CDP_hw_intercept].u_val,
                                            coefs[CDP_hw_slope].u_val,
                                            coefs[CDP_null_count].u_cnt,
                                            seasonal_coef);
        }
    }

    if (isnan(rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val)) {
        /* no update, store existing value unchanged, doesn't
         * matter if it is NA */
        rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val =
            rrd->cdp_prep[cdp_idx].scratch[CDP_last_seasonal_deviation].u_val;
    } else
        if (isnan
            (rrd->cdp_prep[cdp_idx].scratch[CDP_last_seasonal_deviation].
             u_val)) {
        /* initialization */
#ifdef DEBUG
        fprintf(stderr, "Initialization of seasonal deviation\n");
#endif
        rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val =
            functions->init_seasonal_deviation(prediction,
                                               rrd->cdp_prep[cdp_idx].
                                               scratch[CDP_scratch_idx].
                                               u_val);
    } else {
        /* exponential smoothing update */
        rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val =
            functions->seasonal_deviation(rrd->rra_def[rra_idx].
                                          par[RRA_seasonal_gamma].u_val,
                                          prediction,
                                          rrd->cdp_prep[cdp_idx].
                                          scratch[CDP_scratch_idx].u_val,
                                          rrd->cdp_prep[cdp_idx].
                                          scratch
                                          [CDP_last_seasonal_deviation].
                                          u_val);
    }
    return 0;
}

/* Check for a failure based on a threshold # of violations within the specified
 * window. */
int update_failures(
    rrd_t *rrd,
    unsigned long cdp_idx,
    unsigned long rra_idx,
    unsigned long ds_idx,
    unsigned short CDP_scratch_idx,
    hw_functions_t * functions)
{
    /* detection of a violation depends on 3 RRAs:
     * HWPREDICT, SEASONAL, and DEVSEASONAL */
    rra_def_t *current_rra = &(rrd->rra_def[rra_idx]);
    unsigned long dev_rra_idx = current_rra->par[RRA_dependent_rra_idx].u_cnt;
    rra_def_t *dev_rra = &(rrd->rra_def[dev_rra_idx]);
    unsigned long hw_rra_idx = dev_rra->par[RRA_dependent_rra_idx].u_cnt;
    rra_def_t *hw_rra = &(rrd->rra_def[hw_rra_idx]);
    unsigned long seasonal_rra_idx = hw_rra->par[RRA_dependent_rra_idx].u_cnt;
    unsigned long temp_cdp_idx;
    rrd_value_t deviation = DNAN;
    rrd_value_t seasonal_coef = DNAN;
    rrd_value_t prediction = DNAN;
    char      violation = 0;
    unsigned short violation_cnt = 0, i;
    char     *violations_array;

    /* usual checks to determine the order of the RRAs */
    temp_cdp_idx = dev_rra_idx * (rrd->stat_head->ds_cnt) + ds_idx;
    if (rra_idx < seasonal_rra_idx) {
        /* DEVSEASONAL not yet updated */
        deviation =
            rrd->cdp_prep[temp_cdp_idx].scratch[CDP_seasonal_deviation].u_val;
    } else {
        /* DEVSEASONAL already updated */
        deviation =
            rrd->cdp_prep[temp_cdp_idx].scratch[CDP_last_seasonal_deviation].
            u_val;
    }
    if (!isnan(deviation)) {

        temp_cdp_idx = seasonal_rra_idx * (rrd->stat_head->ds_cnt) + ds_idx;
        if (rra_idx < seasonal_rra_idx) {
            /* SEASONAL not yet updated */
            seasonal_coef =
                rrd->cdp_prep[temp_cdp_idx].scratch[CDP_hw_seasonal].u_val;
        } else {
            /* SEASONAL already updated */
            seasonal_coef =
                rrd->cdp_prep[temp_cdp_idx].scratch[CDP_hw_last_seasonal].
                u_val;
        }
        /* in this code block, we know seasonal coef is not DNAN, because deviation is not
         * null */

        temp_cdp_idx = hw_rra_idx * (rrd->stat_head->ds_cnt) + ds_idx;
        if (rra_idx < hw_rra_idx) {
            /* HWPREDICT not yet updated */
            prediction =
                functions->predict(rrd->cdp_prep[temp_cdp_idx].
                                   scratch[CDP_hw_intercept].u_val,
                                   rrd->cdp_prep[temp_cdp_idx].
                                   scratch[CDP_hw_slope].u_val,
                                   rrd->cdp_prep[temp_cdp_idx].
                                   scratch[CDP_null_count].u_cnt,
                                   seasonal_coef);
        } else {
            /* HWPREDICT already updated */
            prediction =
                functions->predict(rrd->cdp_prep[temp_cdp_idx].
                                   scratch[CDP_hw_last_intercept].u_val,
                                   rrd->cdp_prep[temp_cdp_idx].
                                   scratch[CDP_hw_last_slope].u_val,
                                   rrd->cdp_prep[temp_cdp_idx].
                                   scratch[CDP_last_null_count].u_cnt,
                                   seasonal_coef);
        }

        /* determine if the observed value is a violation */
        if (!isnan(rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val)) {
            if (hw_is_violation
                (rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val,
                 prediction, deviation, current_rra->par[RRA_delta_pos].u_val,
                 current_rra->par[RRA_delta_neg].u_val)) {
                violation = 1;
            }
        } else {
            violation = 1;  /* count DNAN values as violations */
        }

    }

    /* determine if a failure has occurred and update the failure array */
    violation_cnt = violation;
    violations_array = (char *) ((void *) rrd->cdp_prep[cdp_idx].scratch);
    for (i = current_rra->par[RRA_window_len].u_cnt; i > 1; i--) {
        /* shift */
        violations_array[i - 1] = violations_array[i - 2];
        violation_cnt += violations_array[i - 1];
    }
    violations_array[0] = violation;

    if (violation_cnt < current_rra->par[RRA_failure_threshold].u_cnt)
        /* not a failure */
        rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val = 0.0;
    else
        rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val = 1.0;

    return (rrd->cdp_prep[cdp_idx].scratch[CDP_scratch_idx].u_val);
}
