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

#include <string.h>

#include "flash_map/flash_map.h"
#include "fcb/fcb.h"
#include "log/log.h"

/* Assume the flash alignment requirement is no stricter than 32. */
#define LOG_FCB_MAX_ALIGN   32

#define LOG_FCB_EXT_HDR_SIZE LOG_BASE_ENTRY_HDR_SIZE + LOG_IMG_HASHLEN + \
    LOG_FCB_MAX_ALIGN

/* Assuming the trailer fits in this, an arbitrary value */
#define LOG_FCB_FLAT_BUF_SIZE (LOG_FCB_EXT_HDR_SIZE > LOG_FCB_MAX_ALIGN * 2) ? \
    LOG_FCB_EXT_HDR_SIZE : LOG_FCB_MAX_ALIGN * 2

static int log_fcb_rtr_erase(struct log *log);

static int
fcb_get_fa_hdr(struct fcb *fcb, struct log *log, struct fcb_entry *fcb_entry, struct log_entry_hdr *hdr)
{
    int rc;

    rc = fcb_getnext(fcb, fcb_entry);
    if (rc == 0) {
        return log_read_hdr(log, fcb_entry, hdr);
    } else {
        return rc;
    }
}

/**
 * Helper function to find start point for walking, given an offset.
 * for a non-zero offset, instead of walking from the beginning,
 * walk from the last applicable area, check first entry and compare
 * against the given offset. Repeat this until an offset less than the
 * given offset is found, start walking from there.
 */
static int
fcb_walk_back_find_start(struct fcb *fcb, struct log *log,
                         struct log_offset *log_offset,
                         struct fcb_entry *fcb_entry)
{
    struct flash_area *fap;
    struct log_entry_hdr hdr;
    struct fcb_entry iter_entry = {0};
    int rc;

    /**
     * If the provided lo_index is less than the oldest entry,
     * simply return the oldest. Else, start from the active area and search back.
     */
    iter_entry.fe_area = fcb->f_oldest;
    rc = fcb_get_fa_hdr(fcb, log, &iter_entry, &hdr);
    if (rc != 0) {
        return rc;
    }

    if (hdr.ue_index >= log_offset->lo_index) {
        goto found_ent;
    }

    fap = fcb->f_active.fe_area;

    for (hdr.ue_index = log_offset->lo_index; hdr.ue_index >= log_offset->lo_index;) {
        memset(&iter_entry, 0, sizeof(iter_entry));
        iter_entry.fe_area = fap;
        rc = fcb_get_fa_hdr(fcb, log, &iter_entry, &hdr);
        if (rc != 0) {
            return rc;
        }
        /* Check if about to wrap around */
        if (fap == &fcb->f_sectors[0]) {
            fap = &(fcb->f_sectors[fcb->f_sector_cnt-1]);
        } else {
            fap--;
        }
    }

found_ent:
    /* copy the result back to caller */
    memcpy(fcb_entry, &iter_entry, sizeof(struct fcb_entry));
    return 0;
}

/**
 * Finds the first log entry whose "offset" is >= the one specified.  A log
 * offset consists of two parts:
 *     o timestamp
 *     o index
 *
 * The "timestamp" field is misnamed.  If it has a value of -1, then the offset
 * always points to the latest entry.  If this value is not -1, then it is
 * ignored; the "index" field is used instead.
 *
 * XXX: We should rename "timestamp" or make it an actual timestamp.
 *
 * The "index" field corresponds to a log entry index.
 *
 * If bookmark is found with the minimum difference in indices, min_diff contains
 * the difference else it will contain -1 if no bookmark is found. min_diff is 0
 * means an exact match.
 *
 * If bookmarks are enabled, this function uses them in the search.
 *
 * @return                      0 if an entry was found
 *                              SYS_ENOENT if there are no suitable entries.
 *                              Other error on failure.
 */
static int
log_fcb_find_gte(struct log *log, struct log_offset *log_offset,
                 struct fcb_entry *out_entry, int *min_diff)
{
#if MYNEWT_VAL(LOG_FCB_BOOKMARKS)
    const struct log_fcb_bmark *bmark;
#endif
    struct log_entry_hdr hdr;
    struct fcb_log *fcb_log;
    struct fcb *fcb;
    int rc;
    bool bmark_found = false;

    fcb_log = log->l_arg;
    fcb = &fcb_log->fl_fcb;

    /* Attempt to read the first entry.  If this fails, the FCB is empty. */
    memset(out_entry, 0, sizeof *out_entry);
    if (log_offset->lo_ts < 0) {
        out_entry->fe_step_back = true;
    }
    rc = fcb_getnext(fcb, out_entry);
    if (rc == FCB_ERR_NOVAR) {
        return SYS_ENOENT;
    } else if (rc != 0) {
        return SYS_EUNKNOWN;
    }

    /*
     * if timestamp for request is < 0, return last log entry
     */
    if (log_offset->lo_ts < 0) {
        *out_entry = fcb->f_active;
        out_entry->fe_step_back = true;
        return 0;
    }

    /* If the requested index is beyond the end of the log, there is nothing to
     * retrieve.
     */
    rc = log_read_hdr(log, &fcb->f_active, &hdr);
    if (rc != 0) {
        return rc;
    }
    if (log_offset->lo_index > hdr.ue_index) {
        return SYS_ENOENT;
    }

#if MYNEWT_VAL(LOG_FCB_BOOKMARKS)
    bmark = log_fcb_closest_bmark(fcb_log, log_offset->lo_index, min_diff);
    if (bmark != NULL) {
        *out_entry = bmark->lfb_entry;
        bmark_found = true;
    }
#endif

    /**
     * For non-zero indices, we walk back from the latest fe_area,
     * compare the ue_index with lo_index for the first entry of each
     * of these areas. If we find one that is less than the lo_index,
     * use that. This covers a case if we are looking for a an entry
     * GTE to any random non-zero value. If bookmark is set, it is expected
     * that the log is walked from there.
     */
    if ((bmark_found == false) && (log_offset->lo_index != 0)) {
        rc = fcb_walk_back_find_start(fcb, log, log_offset, out_entry);
        if (rc != 0) {
            return rc;
        }
    }

    do {
        rc = log_read_hdr(log, out_entry, &hdr);
        if (rc != 0) {
            return rc;
        }

        if (hdr.ue_index >= log_offset->lo_index) {
            return 0;
        }
    } while (fcb_getnext(fcb, out_entry) == 0);

    return SYS_ENOENT;
}

static int
log_fcb_start_append(struct log *log, int len, struct fcb_entry *loc)
{
    struct fcb_log *fcb_log = (struct fcb_log *)log->l_arg;
    struct fcb *fcb = &fcb_log->fl_fcb;
    struct flash_area *old_fa;
    int rc = 0;
#if MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
    int active_id;
    uint32_t idx;
    struct log_fcb_bset *bset = &fcb_log->fl_bset;
#endif

#if MYNEWT_VAL(LOG_STATS)
    int cnt;
#endif

    /* Cache active ID before appending */
#if MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
    active_id = fcb->f_active_id;
#endif
    while (1) {
        rc = fcb_append(fcb, len, loc);
        if (rc == 0) {
            break;
        }

        if (rc != FCB_ERR_NOSPACE) {
            goto err;
        }

        if (fcb_log->fl_entries) {
            rc = log_fcb_rtr_erase(log);
            if (rc) {
                goto err;
            }
            continue;
        }

        old_fa = fcb->f_oldest;
        (void)old_fa; /* to avoid #ifdefs everywhere... */

#if MYNEWT_VAL(LOG_STATS)
        rc = fcb_area_info(fcb, NULL, &cnt, NULL);
        if (rc == 0) {
            LOG_STATS_INCN(log, lost, cnt);
        }
#endif

        /* Notify upper layer that a rotation is about to occur */
        if (log->l_rotate_notify_cb != NULL) {
            fcb_append_to_scratch(fcb);
            log->l_rotate_notify_cb(log);
        }

#if MYNEWT_VAL(LOG_FCB_BOOKMARKS) && !MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
        /* The FCB needs to be rotated. For sector bookmarks
         * we just re-initialize the bookmarks
         */
        log_fcb_rotate_bmarks(fcb_log);
#endif

        rc = fcb_rotate(fcb);
        if (rc) {
            goto err;
        }

#if MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
        /* The FCB needs to be rotated, reinit previously allocated
         * bookmarks
         */
        log_fcb_init_bmarks(fcb_log, bset->lfs_bmarks, bset->lfs_cap,
                            bset->lfs_en_sect_bmarks);
#endif

#if MYNEWT_VAL(LOG_STORAGE_WATERMARK)
        /*
         * FCB was rotated successfully so let's check if watermark was within
         * oldest flash area which was erased. If yes, then move watermark to
         * beginning of current oldest area.
         */
        if ((fcb_log->fl_watermark_off >= old_fa->fa_off) &&
            (fcb_log->fl_watermark_off < old_fa->fa_off + old_fa->fa_size)) {
            fcb_log->fl_watermark_off = fcb->f_oldest->fa_off;
        }
#endif
    }

#if MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
    /* Add bookmark if entry is added to a new sector */
    if (!rc && log->l_log->log_type != LOG_TYPE_STREAM) {
        /* If sector bookmarks are enabled, add a bookmark if the entry is
         * added to a new sector (active_id change) or if the the first sector
         * bookmark is not set yet.
         */
        if (bset->lfs_en_sect_bmarks &&
            (!bset->lfs_bmarks[bset->lfs_sect_cap - 1].lfb_entry.fe_area ||
             fcb->f_active_id != active_id)) {
#if MYNEWT_VAL(LOG_GLOBAL_IDX)
            idx = g_log_info.li_next_index;
#else
            idx = log->l_idx;
#endif
            if (!bset->lfs_bmarks[bset->lfs_sect_cap - 1].lfb_entry.fe_area) {
                bset->lfs_next_sect = bset->lfs_sect_cap - 1;
            }

            log_fcb_add_bmark(fcb_log, loc, idx, true);
        }
    }
#endif

err:
    return (rc);
}

static int
log_fcb_hdr_bytes(uint16_t align, uint16_t len)
{
    uint16_t mod;

    /* Assume power-of-two alignment for faster modulo calculation. */
    assert((align & (align - 1)) == 0);

    mod = len & (align - 1);
    if (mod == 0) {
        return 0;
    }

    return align - mod;
}

static int
log_fcb_append_body(struct log *log, const struct log_entry_hdr *hdr,
                    const void *body, int body_len)
{
    uint8_t buf[LOG_FCB_FLAT_BUF_SIZE] = {0};
    struct fcb *fcb;
    struct fcb_entry loc = {};
    struct fcb_log *fcb_log;
    const uint8_t *u8p;
    int hdr_alignment;
    int chunk_sz = 0;
    int rc;
    uint16_t hdr_len;
#if MYNEWT_VAL(LOG_FLAGS_TRAILER)
    int trailer_alignment = 0;
    uint16_t trailer_len = 0;
#endif
    uint16_t padding = 0;
    uint16_t offset = 0;

    fcb_log = (struct fcb_log *)log->l_arg;
    fcb = &fcb_log->fl_fcb;

    (void)offset;
    (void)padding;

    if (fcb->f_align > LOG_FCB_MAX_ALIGN) {
        return SYS_ENOTSUP;
    }

    hdr_len = log_hdr_len(hdr);

    /* Append the first chunk (header + x-bytes of body, where x is however
     * many bytes are required to increase the chunk size up to a multiple of
     * the flash alignment). If the hash flag is set, we have to account for
     * appending the hash right after the header.
     */
    hdr_alignment = log_fcb_hdr_bytes(fcb->f_align, hdr_len);
    if (hdr_alignment > body_len) {
        chunk_sz = hdr_len + body_len;
    } else {
        chunk_sz = hdr_len + hdr_alignment;
    }

    /*
     * Based on the flags being set in the log header, we need to write
     * specific fields to the flash
     */

    u8p = body;

    memcpy(buf, hdr, LOG_BASE_ENTRY_HDR_SIZE);
    if (hdr->ue_flags & LOG_FLAGS_IMG_HASH) {
        memcpy(buf + LOG_BASE_ENTRY_HDR_SIZE, hdr->ue_imghash, LOG_IMG_HASHLEN);
    }

    memcpy(buf + hdr_len, u8p, hdr_alignment);

#if MYNEWT_VAL(LOG_FLAGS_TRAILER)
    if (hdr->ue_flags & LOG_FLAGS_TRAILER && log->l_tr_om) {
        /* Calculate trailer alignment */
        trailer_alignment = log_fcb_hdr_bytes(fcb->f_align,
                                              chunk_sz + body_len -
                                              hdr_alignment);
        /* If trailer is set, we need to write the trailer length and
         * trailer data after the body.
         * ------------------------------------------------------
         * | hdr + align | body | align + trailer | trailer len |
         * ------------------------------------------------------
         */
        trailer_len = os_mbuf_len(log->l_tr_om);
        rc = log_fcb_start_append(log, hdr_len + body_len + trailer_len +
                                  trailer_alignment + LOG_TRAILER_LEN_SIZE,
                                  &loc);
    } else
#endif
    {
        /* If trailer is not set, we just write the header + body */
        rc = log_fcb_start_append(log, hdr_len + body_len, &loc);
    }

    if (rc != 0) {
        return rc;
    }

    rc = fcb_write(fcb, &loc, buf, chunk_sz);
    if (rc != 0) {
        return rc;
    }

    /* Append the remainder of the message body. */

    u8p += hdr_alignment;
    if (body_len > hdr_alignment) {
        body_len -= hdr_alignment;
    }

#if MYNEWT_VAL(LOG_FLAGS_TRAILER)
    if (hdr->ue_flags & LOG_FLAGS_TRAILER && log->l_tr_om) {
        memset(buf, 0, sizeof(buf));
        padding = trailer_alignment ? fcb->f_align - trailer_alignment : 0;
        /* This writes padding + trailer_alignment if needed */
        if (body_len > padding) {
            /* Writes body - padding bytes */
            rc = fcb_write(fcb, &loc, u8p, body_len - padding);
            u8p += (body_len - padding);
            memcpy(buf, u8p, padding);
            offset = padding;
        } else {
            /* Just write the entire body since its less than padding */
            rc = fcb_write(fcb, &loc, u8p, body_len);
            u8p += body_len;
        }

        if (rc != 0) {
            return rc;
        }

        offset += trailer_alignment;

        rc = os_mbuf_copydata(log->l_tr_om, 0, trailer_len, buf + offset);
        if (rc) {
            return rc;
        }
        offset += trailer_len;
        memcpy(buf + offset, &trailer_len, LOG_TRAILER_LEN_SIZE);
        offset += LOG_TRAILER_LEN_SIZE;

        /* Writes the following:
         * ------------------------------------------------------------------
         * | body: [padding] | trailer_alignment | trailer | trailer length |
         * ------------------------------------------------------------------
         *
         * Padding is optional based on whether body_len > padding.
         */
        rc = fcb_write(fcb, &loc, buf, offset);
    } else
#endif
    {
        if (body_len > 0) {
            rc = fcb_write(fcb, &loc, u8p, body_len);
        }
    }

    if (rc != 0) {
        return rc;
    }

    rc = fcb_append_finish(fcb, &loc);
    if (rc != 0) {
        return rc;
    }

    return 0;
}

static int
log_fcb_append(struct log *log, void *buf, int len)
{
    int hdr_len;

    hdr_len = log_hdr_len(buf);

    return log_fcb_append_body(log, buf, (uint8_t *)buf + hdr_len,
                               len - hdr_len);
}

static int
log_fcb_write_mbuf(struct fcb_entry *loc, struct os_mbuf *om)
{
    int rc;

    while (om) {
        rc = flash_area_write(loc->fe_area, loc->fe_data_off, om->om_data,
                              om->om_len);
        if (rc != 0) {
            return SYS_EIO;
        }

        loc->fe_data_off += om->om_len;
        om = SLIST_NEXT(om, om_next);
    }

    return 0;
}

static int
log_fcb_append_mbuf_body(struct log *log, const struct log_entry_hdr *hdr,
                         struct os_mbuf *om)
{
    struct fcb *fcb;
    struct fcb_entry loc = {};
    struct fcb_log *fcb_log;
    int len;
    int rc;
    uint16_t buflen = 0;
#if MYNEWT_VAL(LOG_FLAGS_TRAILER)
    int trailer_alignment = 0;
    uint8_t pad[LOG_FCB_MAX_ALIGN] = {0};
    uint16_t trailer_len = 0;
#endif

    fcb_log = (struct fcb_log *)log->l_arg;
    fcb = &fcb_log->fl_fcb;

    /* This function expects to be able to write each mbuf without any
     * buffering.
     */
    if (fcb->f_align != 1) {
        return SYS_ENOTSUP;
    }

    buflen = os_mbuf_len(om);
    len = log_hdr_len(hdr) + buflen;

#if MYNEWT_VAL(LOG_FLAGS_TRAILER)
    if (hdr->ue_flags & LOG_FLAGS_TRAILER) {
        /* The trailer gets appended after the trailer_alignment
         * Trailers start from updated loc.fe_data_off. Write everything
         * together
         * Writes the following:
         * -----------------------------------------------------------------
         * | body: body_len | trailer_alignment | trailer | trailer length |
         * -----------------------------------------------------------------
         * part of body len + trailer_alignment = fcb->f_align
         * So, we just pad trailer_alignment agt the end of the mbuf chain
         */
        if (log->l_tr_om) {
            trailer_len = os_mbuf_len(log->l_tr_om);
            /* Calculate trailer alignment */
            trailer_alignment = log_fcb_hdr_bytes(fcb->f_align, trailer_len);
            len += (trailer_alignment + trailer_len + LOG_TRAILER_LEN_SIZE);
        }
    }
#endif

    rc = log_fcb_start_append(log, len, &loc);
    if (rc != 0) {
        return rc;
    }

    rc = flash_area_write(loc.fe_area, loc.fe_data_off, hdr,
                          LOG_BASE_ENTRY_HDR_SIZE);
    if (rc != 0) {
        return rc;
    }
    loc.fe_data_off += LOG_BASE_ENTRY_HDR_SIZE;

    if (hdr->ue_flags & LOG_FLAGS_IMG_HASH) {
        /* Write LOG_IMG_HASHLEN bytes of image hash */
        rc = flash_area_write(loc.fe_area, loc.fe_data_off, hdr->ue_imghash,
                              LOG_IMG_HASHLEN);
        if (rc != 0) {
            return rc;
        }
        loc.fe_data_off += LOG_IMG_HASHLEN;
    }

#if MYNEWT_VAL(LOG_FLAGS_TRAILER)
    if (hdr->ue_flags & LOG_FLAGS_TRAILER) {
        /* The trailer gets appended after the padding + trailer_alignment
         * Trailers start from updated loc.fe_data_off. Write everything
         * together
         * Writes the following:
         * -----------------------------------------------------------------
         * | body: body_len | trailer_alignment | trailer | trailer length |
         * -----------------------------------------------------------------
         * part of body_len + trailer_alignment = f_align
         */

        if (log->l_tr_om) {
            if (trailer_alignment) {
                /* Copy padding for trailer alignment */
                rc = os_mbuf_copyinto(om, buflen, pad, trailer_alignment);
                if (rc) {
                    return rc;
                }
            }

            /* Append from the trailer */
            rc = os_mbuf_appendfrom(om, log->l_tr_om, 0, trailer_len);
            if (rc) {
                return rc;
            }

            om = os_mbuf_pullup(om, LOG_TRAILER_LEN_SIZE);
            /* Copy the trailer length */
            rc = os_mbuf_copyinto(om, buflen + trailer_alignment + trailer_len,
                                  &trailer_len, LOG_TRAILER_LEN_SIZE);
            if (rc) {
                return rc;
            }
        }
    }
#endif

    rc = log_fcb_write_mbuf(&loc, om);
    if (rc != 0) {
        return rc;
    }

    rc = fcb_append_finish(fcb, &loc);
    if (rc != 0) {
        return rc;
    }

    return 0;
}

static int
log_fcb_append_mbuf(struct log *log, struct os_mbuf *om)
{
    int rc;
    uint16_t mlen;
    uint16_t hdr_len;
    struct log_entry_hdr hdr;

    mlen = os_mbuf_len(om);
    if (mlen < LOG_BASE_ENTRY_HDR_SIZE) {
        return SYS_ENOMEM;
    }

    /*
     * We do a pull up twice, once so that the base header is
     * contiguous, so that we read the flags correctly, second
     * time is so that we account for the image hash as well.
     */
    om = os_mbuf_pullup(om, LOG_BASE_ENTRY_HDR_SIZE);

    /*
     * We can just pass the om->om_data ptr as the log_entry_hdr
     * because the log_entry_hdr is a packed struct and does not
     * cause any alignment or padding issues
     */
    hdr_len = log_hdr_len((struct log_entry_hdr *)om->om_data);

    om = os_mbuf_pullup(om, hdr_len);

    memcpy(&hdr, om->om_data, hdr_len);

    os_mbuf_adj(om, hdr_len);

    rc = log_fcb_append_mbuf_body(log, &hdr, om);

    os_mbuf_prepend(om, hdr_len);

    memcpy(om->om_data, &hdr, hdr_len);

    return rc;
}

static uint16_t
log_fcb_read_entry_len(struct log *log, const void *dptr)
{
    struct fcb_entry *loc;

    loc = (struct fcb_entry *)dptr;

    if (!log || !dptr) {
        return 0;
    }

    return loc->fe_data_len;
}

static int
log_fcb_read(struct log *log, const void *dptr, void *buf, uint16_t offset,
  uint16_t len)
{
    struct fcb_entry *loc;
    int rc;

    loc = (struct fcb_entry *)dptr;

    if (offset + len > loc->fe_data_len) {
        len = loc->fe_data_len - offset;
    }
    rc = flash_area_read(loc->fe_area, loc->fe_data_off + offset, buf, len);
    if (rc == 0) {
        return len;
    } else {
        return 0;
    }
}

static int
log_fcb_read_mbuf(struct log *log, const void *dptr, struct os_mbuf *om,
                  uint16_t offset, uint16_t len)
{
    struct fcb_entry *loc;
    uint8_t data[128];
    uint16_t read_len;
    uint16_t rem_len;
    int rc;

    loc = (struct fcb_entry *)dptr;

    if (offset + len > loc->fe_data_len) {
        len = loc->fe_data_len - offset;
    }

    rem_len = len;

    while (rem_len > 0) {
        read_len = min(rem_len, sizeof(data));
        rc = flash_area_read(loc->fe_area, loc->fe_data_off + offset, data,
                             read_len);
        if (rc) {
            goto done;
        }
        rc = os_mbuf_append(om, data, read_len);
        if (rc) {
            goto done;
        }

        rem_len -= read_len;
        offset += read_len;
    }

done:
    return len - rem_len;
}

/**
 * @brief Common function for walking a single area or the full logs
 *
 * @param      The log
 * @param[in]  The walk function
 * @param      The log offset
 * @param[in]  Reading either a single area or the full log
 *
 * @return     { description_of_the_return_value }
 */
static int
log_fcb_walk_impl(struct log *log, log_walk_func_t walk_func,
             struct log_offset *log_offset, bool area)
{
    struct fcb *fcb;
    struct fcb_log *fcb_log;
    struct fcb_entry loc = {};
    struct flash_area *fap;
    int rc;
    struct fcb_entry_cache cache;
    int min_diff = -1;

    fcb_log = log->l_arg;
    fcb = &fcb_log->fl_fcb;

    /* Locate the starting point of the walk. */
    rc = log_fcb_find_gte(log, log_offset, &loc, &min_diff);
    switch (rc) {
    case 0:
        /* Found a starting point. */
        break;
    case SYS_ENOENT:
        /* No entries match the offset criteria; nothing to walk. */
        return 0;
    default:
        return rc;
    }
    fap = loc.fe_area;

    if (log_offset->lo_walk_backward) {
        loc.fe_step_back = true;
        if (MYNEWT_VAL(FCB_BIDIRECTIONAL_CACHE)) {
            fcb_cache_init(fcb, &cache, 50);
            loc.fe_cache = &cache;
        }
    }

#if MYNEWT_VAL(LOG_FCB_BOOKMARKS)
    /* If a minimum index was specified (i.e., we are not just retrieving the
     * last entry), add a bookmark pointing to this walk's start location.
     * Only add a bmark if the index is non-zero and an exactly matching bmark
     * was not found. If an exactly matching bmark was found, min_diff is 0,
     * else it stays -1 or is great than 0.
     */
    if ((log_offset->lo_ts >= 0 && log_offset->lo_index > 0) && min_diff != 0) {
        log_fcb_add_bmark(fcb_log, &loc, log_offset->lo_index, false);
    }
#endif

    rc = 0;
    do {
        if (area) {
            if (fap != loc.fe_area) {
                break;
            }
        }

        rc = walk_func(log, log_offset, &loc, loc.fe_data_len);
        if (rc != 0) {
            if (rc > 0) {
                rc = 0;
            }
            break;
        }
    } while (fcb_getnext(fcb, &loc) == 0);

    if (log_offset->lo_walk_backward && MYNEWT_VAL(FCB_BIDIRECTIONAL_CACHE)) {
        fcb_cache_free(fcb, &cache);
    }

    return rc;
}

static int
log_fcb_walk(struct log *log, log_walk_func_t walk_func,
             struct log_offset *log_offset)
{
    return log_fcb_walk_impl(log, walk_func, log_offset, false);
}

static int
log_fcb_walk_area(struct log *log, log_walk_func_t walk_func,
             struct log_offset *log_offset)
{
    return log_fcb_walk_impl(log, walk_func, log_offset, true);
}

static int
log_fcb_flush(struct log *log)
{
    struct fcb_log *fcb_log;
    struct fcb *fcb;
    int rc;

    fcb_log = (struct fcb_log *)log->l_arg;
    fcb = &fcb_log->fl_fcb;

    rc = fcb_clear(fcb);

#if MYNEWT_VAL(LOG_FCB_BOOKMARKS)
#if MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
    /* Reinit previously allocated bookmarks */
    log_fcb_init_bmarks(fcb_log, fcb_log->fl_bset.lfs_bmarks,
                        fcb_log->fl_bset.lfs_cap,
                        fcb_log->fl_bset.lfs_en_sect_bmarks);
#else
    log_fcb_clear_bmarks(fcb_log);
#endif
#endif

    return rc;
}

static int
log_fcb_registered(struct log *log)
{
    struct fcb_log *fl = (struct fcb_log *)log->l_arg;

    fl->fl_log = log;
#if MYNEWT_VAL(LOG_STORAGE_WATERMARK)
#if MYNEWT_VAL(LOG_PERSIST_WATERMARK)
    struct fcb *fcb;
    struct fcb_entry loc;
#endif

#if MYNEWT_VAL(LOG_PERSIST_WATERMARK)
    fcb = &fl->fl_fcb;

    /* Set watermark to first element */
    memset(&loc, 0, sizeof(loc));

    if (fcb_getnext(fcb, &loc)) {
        fl->fl_watermark_off = loc.fe_area->fa_off + loc.fe_elem_off;
    } else {
        fl->fl_watermark_off = fcb->f_oldest->fa_off;
    }
#else
    /* Initialize watermark to designated unknown value*/
    fl->fl_watermark_off = 0xffffffff;
#endif
#endif
    return 0;
}

#if MYNEWT_VAL(LOG_STORAGE_INFO)
static int
log_fcb_storage_info(struct log *log, struct log_storage_info *info)
{
    struct fcb_log *fl;
    struct fcb *fcb;
    struct flash_area *fa;
    uint32_t el_min;
    uint32_t el_max;
    uint32_t fa_min;
    uint32_t fa_max;
    uint32_t fa_size;
    uint32_t fa_used;
    int rc;

    fl = (struct fcb_log *)log->l_arg;
    fcb = &fl->fl_fcb;

    rc = os_mutex_pend(&fcb->f_mtx, OS_WAIT_FOREVER);
    if (rc && rc != OS_NOT_STARTED) {
        return FCB_ERR_ARGS;
    }

    /*
     * Calculate location of 1st entry.
     * We assume 1st log entry starts at beginning of oldest sector in FCB.
     * This is because even if 1st entry is in the middle of sector (is this
     * even possible?) we will never use free space before it thus that space
     * can be also considered used.
     */
    el_min = fcb->f_oldest->fa_off;

    /* Calculate end location of last entry */
    el_max = fcb->f_active.fe_area->fa_off + fcb->f_active.fe_elem_off;

    /* Sectors assigned to FCB are guaranteed to be contiguous */
    fa = &fcb->f_sectors[0];
    fa_min = fa->fa_off;
    fa = &fcb->f_sectors[fcb->f_sector_cnt - 1];
    fa_max = fa->fa_off + fa->fa_size;
    fa_size = fa_max - fa_min;

    /* Calculate used size */
    fa_used = el_max - el_min;
    if ((int32_t)fa_used < 0) {
        fa_used += fa_size;
    }

    info->size = fa_size;
    info->used = fa_used;

#if MYNEWT_VAL(LOG_STORAGE_WATERMARK)
    /* Calculate used size */
    fa_used = el_max - fl->fl_watermark_off;

    if (fl->fl_watermark_off == 0xffffffff){
        fa_used = 0xffffffff;
    }
    else if ((int32_t)fa_used < 0) {
        fa_used += fa_size;
    }

    info->used_unread = fa_used;
#endif

    os_mutex_release(&fcb->f_mtx);

    return 0;
}
#endif

#if MYNEWT_VAL(LOG_STORAGE_WATERMARK)
static int
log_fcb_new_watermark_index(struct log *log, struct log_offset *log_offset,
                            const void *dptr, uint16_t len)
{
    struct fcb_entry *loc;
    struct fcb_log *fl;
    struct log_entry_hdr ueh;
    int rc;

    loc = (struct fcb_entry*)dptr;
    fl = (struct fcb_log *)log->l_arg;

    rc = log_fcb_read(log, loc, &ueh, 0, sizeof(ueh));

    if (rc != sizeof(ueh)) {
        return -1;
    }
    /* Set log watermark to end of this element */
    if (ueh.ue_index >= log_offset->lo_index) {
        fl->fl_watermark_off = loc->fe_area->fa_off + loc->fe_data_off +
                               loc->fe_data_len;
        return 1;
    } else {
        return 0;
    }
}

static int
log_fcb_set_watermark(struct log *log, uint32_t index)
{
    int rc;
    struct log_offset log_offset = {};
    struct fcb_log *fl;
    struct fcb *fcb;

    fl = (struct fcb_log *)log->l_arg;
    fcb = &fl->fl_fcb;

    log_offset.lo_arg = NULL;
    log_offset.lo_ts = 0;
    log_offset.lo_index = index;
    log_offset.lo_data_len = 0;

    /* Find where to start the walk, and set watermark accordingly */
    rc = log_fcb_walk(log, log_fcb_new_watermark_index, &log_offset);
    if (rc != 0) {
        goto done;
    }

    /* If there are no entries to read and the watermark has not been set */
    if (fl->fl_watermark_off == 0xffffffff) {
        fl->fl_watermark_off = fcb->f_oldest->fa_off;
    }

    return (0);
done:
    return (rc);
}
#endif

/**
 * Copies one log entry from source fcb to destination fcb
 *
 * @param log      Log this operation applies to
 * @param entry    FCB2 location for the entry being copied
 * @param dst_fcb  FCB2 area where data is getting copied to.
 *
 * @return 0 on success; non-zero on error
 */
static int
log_fcb_copy_entry(struct log *log, struct fcb_entry *entry,
                   struct fcb *dst_fcb)
{
    struct log_entry_hdr ueh;
    char data[MYNEWT_VAL(LOG_FCB_COPY_MAX_ENTRY_LEN) + LOG_BASE_ENTRY_HDR_SIZE +
              LOG_IMG_HASHLEN];
    uint16_t hdr_len;
    int dlen;
    int rc;
    struct fcb_log fcb_log_tmp;
    struct fcb_log *fcb_log_ptr;

    rc = log_fcb_read(log, entry, &ueh, 0, LOG_BASE_ENTRY_HDR_SIZE);

    if (rc != LOG_BASE_ENTRY_HDR_SIZE) {
        goto err;
    }

    hdr_len = log_hdr_len(&ueh);

    dlen = min(entry->fe_data_len, MYNEWT_VAL(LOG_FCB_COPY_MAX_ENTRY_LEN) +
               hdr_len);

    rc = log_fcb_read(log, entry, data, 0, dlen);
    if (rc < 0) {
        goto err;
    }

    /* Cache fcb_log pointer */
    fcb_log_ptr = (struct fcb_log *)log->l_arg;

    /* Cache the fcb log, so that we preserve original fcb pointer */
    fcb_log_tmp = *fcb_log_ptr;
    fcb_log_tmp.fl_fcb = *dst_fcb;
    log->l_arg = &fcb_log_tmp;
    rc = log_fcb_append(log, data, dlen);

    /* Restore the original fcb_log pointer */
    log->l_arg = fcb_log_ptr;
    *dst_fcb = fcb_log_tmp.fl_fcb;

    if (rc) {
        goto err;
    }

err:
    return (rc);
}

/**
 * Copies log entries from source fcb to destination fcb
 *
 * @param log      Log this operation applies to
 * @param src_fcb  FCB area which is the source of data
 * @param dst_fcb  FCB area which is the target
 * @param entry    Location in src_fcb to start copy from
 *
 * @return 0 on success; non-zero on error
 */
static int
log_fcb_copy(struct log *log, struct fcb *src_fcb, struct fcb *dst_fcb,
             struct fcb_entry *entry)
{
    int rc;

    rc = 0;
    while (!fcb_getnext(src_fcb, entry)) {
        rc = log_fcb_copy_entry(log, entry, dst_fcb);
        if (rc) {
            break;
        }
    }

    return (rc);
}

/**
 * Flushes the log while keeping the specified number of entries
 * using image scratch
 *
 * @param log      Log this operation applies to
 *
 * @return 0 on success; non-zero on error
 */
static int
log_fcb_rtr_erase(struct log *log)
{
    struct fcb_log *fcb_log;
    struct fcb fcb_scratch;
    struct fcb *fcb;
    const struct flash_area *ptr;
    struct fcb_entry entry = {};
    int rc;
    struct flash_area sector;

    rc = 0;
    if (!log) {
        rc = -1;
        goto err;
    }

    fcb_log = log->l_arg;
    fcb = &fcb_log->fl_fcb;

    memset(&fcb_scratch, 0, sizeof(fcb_scratch));

    if (flash_area_open(FLASH_AREA_IMAGE_SCRATCH, &ptr)) {
        goto err;
    }
    sector = *ptr;
    fcb_scratch.f_sectors = &sector;
    fcb_scratch.f_sector_cnt = 1;
    fcb_scratch.f_magic = 0x7EADBADF;
    fcb_scratch.f_version = g_log_info.li_version;

    flash_area_erase(&sector, 0, sector.fa_size);
    rc = fcb_init(&fcb_scratch);
    if (rc) {
        goto err;
    }

    /* Calculate offset of n-th last entry */
    rc = fcb_offset_last_n(fcb, fcb_log->fl_entries, &entry);
    if (rc) {
        goto err;
    }

    /* Copy to scratch */
    rc = log_fcb_copy(log, fcb, &fcb_scratch, &entry);
    if (rc) {
        goto err;
    }

    /* Flush log */
    rc = log_fcb_flush(log);
    if (rc) {
        goto err;
    }

    memset(&entry, 0, sizeof(entry));
    /* Copy back from scratch */
    rc = log_fcb_copy(log, &fcb_scratch, fcb, &entry);

err:
    return (rc);
}

const struct log_handler log_fcb_handler = {
    .log_type             = LOG_TYPE_STORAGE,
    .log_read             = log_fcb_read,
    .log_read_mbuf        = log_fcb_read_mbuf,
    .log_append           = log_fcb_append,
    .log_append_body      = log_fcb_append_body,
    .log_append_mbuf      = log_fcb_append_mbuf,
    .log_append_mbuf_body = log_fcb_append_mbuf_body,
    .log_walk             = log_fcb_walk,
    .log_walk_sector      = log_fcb_walk_area,
    .log_flush            = log_fcb_flush,
    .log_read_entry_len   = log_fcb_read_entry_len,
#if MYNEWT_VAL(LOG_STORAGE_INFO)
    .log_storage_info     = log_fcb_storage_info,
#endif
#if MYNEWT_VAL(LOG_STORAGE_WATERMARK)
    .log_set_watermark    = log_fcb_set_watermark,
#endif
    .log_registered       = log_fcb_registered,
};

#endif
