/*
 * 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(CONFIG_FCB)

#include <fcb/fcb.h>
#include <string.h>

#include "config/config.h"
#include "config/config_store.h"
#include "config/config_fcb.h"
#include "config/config_generic_kv.h"
#include "config_priv.h"
#include "hal/hal_watchdog.h"

#define CONF_FCB_VERS		1

struct conf_fcb_load_cb_arg {
    conf_store_load_cb cb;
    void *cb_arg;
};

struct conf_kv_load_cb_arg {
    const char *name;
    char *value;
    size_t len;
};

static int conf_fcb_load(struct conf_store *, conf_store_load_cb cb,
                         void *cb_arg);
static int conf_fcb_save(struct conf_store *, const char *name,
                         const char *value);

static struct conf_store_itf conf_fcb_itf = {
    .csi_load = conf_fcb_load,
    .csi_save = conf_fcb_save,
};

int
conf_fcb_src(struct conf_fcb *cf)
{
    int rc;

    cf->cf_fcb.f_version = CONF_FCB_VERS;
    if (cf->cf_fcb.f_sector_cnt > 1) {
        cf->cf_fcb.f_scratch_cnt = 1;
    } else {
        cf->cf_fcb.f_scratch_cnt = 0;
    }
    while (1) {
        rc = fcb_init(&cf->cf_fcb);
        if (rc) {
            return OS_INVALID_PARM;
        }

        /*
         * Check if system was reset in middle of emptying a sector. This
         * situation is recognized by checking if the scratch block is missing.
         */
        if (cf->cf_fcb.f_scratch_cnt &&
            fcb_free_sector_cnt(&cf->cf_fcb) < 1) {
            flash_area_erase(cf->cf_fcb.f_active.fe_area, 0,
              cf->cf_fcb.f_active.fe_area->fa_size);
        } else {
            break;
        }
    }

    cf->cf_store.cs_itf = &conf_fcb_itf;
    conf_src_register(&cf->cf_store);

    return OS_OK;
}

int
conf_fcb_dst(struct conf_fcb *cf)
{
    cf->cf_store.cs_itf = &conf_fcb_itf;
    conf_dst_register(&cf->cf_store);

    return OS_OK;
}

static int
conf_fcb_load_cb(struct fcb_entry *loc, void *arg)
{
    struct conf_fcb_load_cb_arg *argp;
    char buf[CONF_MAX_NAME_LEN + CONF_MAX_VAL_LEN + 32];
    char *name_str;
    char *val_str;
    int rc;
    int len;

    argp = (struct conf_fcb_load_cb_arg *)arg;

    len = loc->fe_data_len;
    if (len >= sizeof(buf)) {
        len = sizeof(buf) - 1;
    }

    rc = flash_area_read(loc->fe_area, loc->fe_data_off, buf, len);
    if (rc) {
        return 0;
    }
    buf[len] = '\0';

    rc = conf_line_parse(buf, &name_str, &val_str);
    if (rc) {
        return 0;
    }
    argp->cb(name_str, val_str, argp->cb_arg);
    return 0;
}

static int
conf_fcb_load(struct conf_store *cs, conf_store_load_cb cb, void *cb_arg)
{
    struct conf_fcb *cf = (struct conf_fcb *)cs;
    struct conf_fcb_load_cb_arg arg;
    int rc;

    arg.cb = cb;
    arg.cb_arg = cb_arg;
    rc = fcb_walk(&cf->cf_fcb, 0, conf_fcb_load_cb, &arg);
    if (rc) {
        return OS_EINVAL;
    }
    return OS_OK;
}

static int
conf_fcb_var_read(struct fcb_entry *loc, char *buf, char **name, char **val)
{
    int rc;

    rc = flash_area_read(loc->fe_area, loc->fe_data_off, buf, loc->fe_data_len);
    if (rc) {
        return rc;
    }
    buf[loc->fe_data_len] = '\0';
    rc = conf_line_parse(buf, name, val);
    return rc;
}

static void
conf_fcb_compress_internal(struct fcb *fcb,
                           int (*copy_or_not)(const char *name, const char *val,
                                              void *cn_arg),
                           void *cn_arg)
{
    int rc;
    char buf1[CONF_MAX_NAME_LEN + CONF_MAX_VAL_LEN + 32];
    char buf2[CONF_MAX_NAME_LEN + CONF_MAX_VAL_LEN + 32];
    struct fcb_entry loc1 = {};
    struct fcb_entry loc2 = {};
    char *name1, *val1;
    char *name2, *val2;
    int copy;

    rc = fcb_append_to_scratch(fcb);
    if (rc) {
        return; /* XXX */
    }

    loc1.fe_area = NULL;
    loc1.fe_elem_off = 0;
    while (fcb_getnext(fcb, &loc1) == 0) {
        if (loc1.fe_area != fcb->f_oldest) {
            break;
        }
        hal_watchdog_tickle();
        rc = conf_fcb_var_read(&loc1, buf1, &name1, &val1);
        if (rc) {
            continue;
        }
        if (!val1) {
            continue;
        }
        loc2 = loc1;
        copy = 1;
        while (fcb_getnext(fcb, &loc2) == 0) {
            hal_watchdog_tickle();
            rc = conf_fcb_var_read(&loc2, buf2, &name2, &val2);
            if (rc) {
                continue;
            }
            if (!strcmp(name1, name2)) {
                copy = 0;
                break;
            }
        }
        if (!copy) {
            continue;
        }

        if (copy_or_not) {
            if (copy_or_not(name1, val1, cn_arg)) {
                /* Copy rejected */
                continue;
            }
        }
        /*
         * Can't find one. Must copy.
         */
        rc = flash_area_read(loc1.fe_area, loc1.fe_data_off, buf1,
          loc1.fe_data_len);
        if (rc) {
            continue;
        }
        rc = fcb_append(fcb, loc1.fe_data_len, &loc2);
        if (rc) {
            continue;
        }
        rc = flash_area_write(loc2.fe_area, loc2.fe_data_off, buf1,
          loc1.fe_data_len);
        if (rc) {
            continue;
        }
        fcb_append_finish(fcb, &loc2);
    }
    rc = fcb_rotate(fcb);
    if (rc) {
        /* XXXX */
        ;
    }
}

static int
conf_fcb_append(struct fcb *fcb, char *buf, int len)
{
    int rc;
    int i;
    struct fcb_entry loc = {};

    for (i = 0; i < 10; i++) {
        rc = fcb_append(fcb, len, &loc);
        if (rc != FCB_ERR_NOSPACE) {
            break;
        }
        if (fcb->f_scratch_cnt == 0) {
            return OS_ENOMEM;
        }
        conf_fcb_compress_internal(fcb, NULL, NULL);
    }
    if (rc) {
        return OS_EINVAL;
    }
    rc = flash_area_write(loc.fe_area, loc.fe_data_off, buf, len);
    if (rc) {
        return OS_EINVAL;
    }
    fcb_append_finish(fcb, &loc);
    return OS_OK;
}

static int
conf_fcb_save(struct conf_store *cs, const char *name, const char *value)
{
    struct conf_fcb *cf = (struct conf_fcb *)cs;

    return conf_fcb_kv_save(&cf->cf_fcb, name, value);
}

void
conf_fcb_compress(struct conf_fcb *cf,
                  int (*copy_or_not)(const char *name, const char *val,
                                     void *cn_arg),
                  void *cn_arg)
{
    conf_fcb_compress_internal(&cf->cf_fcb, copy_or_not, cn_arg);
}

static int
conf_kv_load_cb(struct fcb_entry *loc, void *arg)
{
    struct conf_kv_load_cb_arg *cb_arg = arg;
    char buf[CONF_MAX_NAME_LEN + CONF_MAX_VAL_LEN + 32];
    char *name_str;
    char *val_str;
    int rc;
    int len;

    len = loc->fe_data_len;
    if (len >= sizeof(buf)) {
        len = sizeof(buf) - 1;
    }

    rc = flash_area_read(loc->fe_area, loc->fe_data_off, buf, len);
    if (rc) {
        return 0;
    }
    buf[len] = '\0';

    rc = conf_line_parse(buf, &name_str, &val_str);
    if (rc) {
        return 0;
    }

    if (strcmp(name_str, cb_arg->name)) {
        return 0;
    }

    strncpy(cb_arg->value, val_str, cb_arg->len);
    cb_arg->value[cb_arg->len - 1] = '\0';

    return 0;
}

int
conf_fcb_kv_load(struct fcb *fcb, const char *name, char *value, size_t len)
{
    struct conf_kv_load_cb_arg arg;
    int rc;

    arg.name = name;
    arg.value = value;
    arg.len = len;

    rc = fcb_walk(fcb, 0, conf_kv_load_cb, &arg);
    if (rc) {
        return OS_EINVAL;
    }

    return OS_OK;
}

int
conf_fcb_kv_save(struct fcb *fcb, const char *name, const char *value)
{
    char buf[CONF_MAX_NAME_LEN + CONF_MAX_VAL_LEN + 32];
    int len;

    if (!name) {
        return OS_INVALID_PARM;
    }

    len = conf_line_make(buf, sizeof(buf), name, value);
    if (len < 0 || len + 2 > sizeof(buf)) {
        return OS_INVALID_PARM;
    }
    return conf_fcb_append(fcb, buf, len);
}

#endif
