/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include "os/mynewt.h"

#if !MYNEWT_VAL(FAULT_STUB)

#include "modlog/modlog.h"
#include "fault/fault.h"
#include "fault_priv.h"

struct fault_domain {
    uint8_t success_delta;
    uint8_t failure_delta;

#if MYNEWT_VAL(FAULT_DOMAIN_NAMES)
    const char *name;
#endif
};

uint8_t fault_chronic_counts[MYNEWT_VAL(FAULT_MAX_DOMAINS)];
static struct fault_domain fault_domains[MYNEWT_VAL(FAULT_MAX_DOMAINS)];

static fault_thresh_fn *fault_thresh_cb;

void
fault_configure_cb(fault_thresh_fn *cb)
{
    fault_thresh_cb = cb;
}

static struct fault_domain *
fault_get_domain(int domain_id)
{
    if (domain_id < 0 || domain_id >= MYNEWT_VAL(FAULT_MAX_DOMAINS)) {
        return NULL;
    }

    return &fault_domains[domain_id];
}

bool
fault_domain_is_registered(int domain_id)
{
    const struct fault_domain *dom;

    dom = fault_get_domain(domain_id);
    if (dom == NULL) {
        return false;
    }

    return dom->failure_delta != 0;
}

static struct fault_domain *
fault_get_registered_domain(int domain_id)
{
    struct fault_domain *dom;

    dom = fault_get_domain(domain_id);
    if (dom == NULL) {
        return NULL;
    }

    if (!fault_domain_is_registered(domain_id)) {
        return NULL;
    }

    return dom;
}

static bool
fault_recorder_is_saturated(const struct fault_recorder *recorder)
{
    return debouncer_val(&recorder->deb) == recorder->deb.max;
}

static int
fault_recorder_state(const struct fault_recorder *recorder)
{
    if (fault_recorder_is_saturated(recorder)) {
        return FAULT_STATE_ERROR;
    } else if (debouncer_state(&recorder->deb)) {
        return FAULT_STATE_WARN;
    } else {
        return FAULT_STATE_GOOD;
    }
}

static int
fault_adjust_chronic_count(int domain_id, int delta)
{
    int count;

    /* Increase and persist the chronic failure count if it would not wrap. */
    count = fault_chronic_counts[domain_id] + delta;
    if (count < 0) {
        count = 0;
    } else if (count > UINT8_MAX) {
        count = UINT8_MAX;
    }

    return fault_set_chronic_count(domain_id, count);
}

static int
fault_decrease_chronic_count(int domain_id)
{
    const struct fault_domain *dom;

    dom = fault_get_registered_domain(domain_id);
    assert(dom != NULL);

    return fault_adjust_chronic_count(domain_id, -dom->success_delta);
}

static int
fault_increase_chronic_count(int domain_id)
{
    const struct fault_domain *dom;

    dom = fault_get_registered_domain(domain_id);
    assert(dom != NULL);

    return fault_adjust_chronic_count(domain_id, dom->failure_delta);
}

/**
 * @brief processes the result of a fault-capable operation.
 *
 * @param recorder              The fault recorder associated with the
 *                                  operation.
 * @param success               Whether the operation was successful.
 */
int
fault_process(struct fault_recorder *recorder, bool is_failure)
{
    int prev_state;
    int state;
    int delta;

    prev_state = fault_recorder_state(recorder);

    /* There is nothing to do if we are already at the maximum fault count. */
    if (is_failure && prev_state == FAULT_STATE_ERROR) {
        return FAULT_STATE_ERROR;
    }

    if (is_failure) {
        delta = 1;
    } else {
        delta = -1;
    }

    debouncer_adjust(&recorder->deb, delta);
    state = fault_recorder_state(recorder);

    if (state == FAULT_STATE_GOOD) {
        /* The domain seems to be working; decrease chronic failure count. */
        fault_decrease_chronic_count(recorder->domain_id);
    } else {
        if (state == FAULT_STATE_ERROR) {
            /* Fault detected; trigger a crash in debug builds. */
            DEBUG_PANIC();

            /* Increase chronic fail count and persist. */
            fault_increase_chronic_count(recorder->domain_id);
        }
    }

    /* Notify the application if the fault state changed. */
    if (prev_state != state && fault_thresh_cb != NULL) {
        fault_thresh_cb(recorder->domain_id, prev_state, state, recorder->arg);
    }

    return state;
}

int
fault_success(struct fault_recorder *recorder)
{
    return fault_process(recorder, false);
}

int
fault_failure(struct fault_recorder *recorder)
{
    return fault_process(recorder, true);
}

void
fault_fatal(int domain_id, void *arg, bool is_failure)
{
    struct fault_recorder recorder;
    int rc;

    rc = fault_recorder_init(&recorder, domain_id, 1, 1, arg);
    assert(rc == 0);

    fault_process(&recorder, is_failure);
}

void
fault_fatal_success(int domain_id, void *arg)
{
    fault_fatal(domain_id, arg, false);
}

void
fault_fatal_failure(int domain_id, void *arg)
{
    fault_fatal(domain_id, arg, true);
}

int
fault_get_chronic_count(int domain_id, uint8_t *out_count)
{
    if (fault_get_registered_domain(domain_id) == NULL) {
        return SYS_EINVAL;
    }

    *out_count = fault_chronic_counts[domain_id];
    return 0;
}

int
fault_set_chronic_count(int domain_id, uint8_t count)
{
    int rc;

    if (fault_get_registered_domain(domain_id) == NULL) {
        return SYS_EINVAL;
    }

    if (fault_chronic_counts[domain_id] == count) {
        return 0;
    }

    fault_chronic_counts[domain_id] = count;

    rc = fault_conf_persist_chronic_counts();
    DEBUG_ASSERT(rc == 0);

    return rc;
}

int
fault_recorder_init(struct fault_recorder *recorder,
                    int domain_id,
                    uint16_t warn_thresh,
                    uint16_t error_thresh,
                    void *arg)
{
    int rc;

    if (fault_get_registered_domain(domain_id) == NULL) {
        return SYS_EINVAL;
    }

    if (warn_thresh > error_thresh) {
        return SYS_EINVAL;
    }

    rc = debouncer_init(&recorder->deb, 0, warn_thresh, error_thresh);
    if (rc != 0) {
        return rc;
    }

    /* If this fault recorder is associated with a chronically-failing domain,
     * start the recorder with an initial acute failure count.  For recorders
     * with high failure thresholds, this ensures that at least a few successes
     * are required before the domain is considered stable.
     */
    if (fault_chronic_counts[domain_id] > 0) {
        debouncer_set(&recorder->deb, warn_thresh / 2);
    }

    recorder->domain_id = domain_id;
    recorder->arg = arg;

    return 0;
}

const char *
fault_domain_name(int domain_id)
{
#if MYNEWT_VAL(FAULT_DOMAIN_NAMES)
    const struct fault_domain *domain;

    domain = fault_get_registered_domain(domain_id);
    if (domain != NULL) {
        return domain->name;
    }
#endif

    return NULL;
}

int
fault_register_domain_priv(int domain_id, uint8_t success_delta,
                           uint8_t failure_delta, const char *name)
{
    struct fault_domain *dom;

    if (failure_delta == 0) {
        return SYS_EINVAL;
    }

    dom = fault_get_domain(domain_id);
    if (dom == NULL) {
        return SYS_EINVAL;
    }

    if (fault_domain_is_registered(domain_id)) {
        return SYS_EALREADY;
    }

    *dom = (struct fault_domain) {
        .success_delta = success_delta,
        .failure_delta = failure_delta,
#if MYNEWT_VAL(FAULT_DOMAIN_NAMES)
        .name = name,
#endif
    };

    return 0;
}

void
fault_init(void)
{
}

#endif /* !MYNEWT_VAL(FAULT_STUB) */
