/*
 * 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 <string.h>

#include "os/mynewt.h"

#if MYNEWT_VAL(LOG_FCB_BOOKMARKS)
#include "log/log.h"
#include "log/log_fcb.h"

#if MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
static int
log_fcb_init_sector_bmarks(struct fcb_log *fcb_log)
{
    int rc = 0;
    int i = 0;
    int j = 0;
    struct log_fcb_bset *bset = &fcb_log->fl_bset;
    struct log_entry_hdr ueh = {0};
#if MYNEWT_VAL(LOG_FCB)
    struct fcb_entry loc = {0};
    struct flash_area *fa = NULL;

    rc = fcb_getnext(&fcb_log->fl_fcb, &loc);
    if (rc) {
        return SYS_EOK;
    }
#else
    struct flash_sector_range *range = NULL;
    struct fcb2_entry loc = {0};

    rc = fcb2_getnext(&fcb_log->fl_fcb, &loc);
    if (rc) {
        return SYS_EOK;
    }
#endif

    /* Start adding a bookmark from the end of array just before
     * non-sector bookmarks
     */
    bset->lfs_next_sect = bset->lfs_sect_cap - 1;
    for (i = 0; i < bset->lfs_sect_cap; i++) {
        rc = log_read_hdr(fcb_log->fl_log, &loc, &ueh);
        if (rc) {
            /* Read failed, don't add a bookmark, done adding bookmarks */
            rc = SYS_EOK;
            break;
        }

        rc = log_fcb_add_bmark(fcb_log, &loc, ueh.ue_index, true);
        if (rc) {
            return rc;
        }

#if MYNEWT_VAL(LOG_FCB)
        j = 0;
        fa = loc.fe_area;
        /* Keep skipping sectors until lfs_sect_bmark_itvl is reached */
        do {
            fa = fcb_getnext_area(&fcb_log->fl_fcb, fa);
            if (!fa) {
                break;
            }
            j++;
        } while (j < bset->lfs_sect_bmark_itvl);

        if (!fa) {
            break;
        }

        /* First entry in the next area */
        rc = fcb_getnext_in_area(&fcb_log->fl_fcb, fa, &loc);
        if (rc) {
            rc = SYS_EOK;
            break;
        }
#else
        j = 0;

        /* Keep skipping rangess until lfs_sect_bmark_itvl is reached */
        do {
            /* Get next range */
            range = fcb2_getnext_range(&fcb_log->fl_fcb, &loc);
            if (!range) {
                break;
            }
            j++;
        } while (j < bset->lfs_sect_bmark_itvl);

        if (!range) {
            break;
        }

        /* First entry in the next area */
        rc = fcb2_getnext_in_area(&fcb_log->fl_fcb, range, &loc);
        if (rc) {
            rc = SYS_EOK;
            break;
        }
#endif
    }

    return rc;
}
#endif

int
log_fcb_init_bmarks(struct fcb_log *fcb_log,
                    struct log_fcb_bmark *buf, int avl_bmark_cnt,
                    bool en_sect_bmarks)
{
    struct log_fcb_bset *bset = &fcb_log->fl_bset;
    /* Required bookmark count */
    int reqd_bmark_cnt = MYNEWT_VAL(LOG_FCB_NUM_ABS_BOOKMARKS);

    (void)reqd_bmark_cnt;
    if (!bset || !buf || !avl_bmark_cnt) {
        return SYS_EINVAL;
    }

    memset(buf, 0, sizeof(struct log_fcb_bmark) * avl_bmark_cnt);

    *bset = (struct log_fcb_bset) {
        .lfs_bmarks = buf,
        .lfs_cap = avl_bmark_cnt,
        .lfs_en_sect_bmarks = en_sect_bmarks
    };

#if MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
    if (en_sect_bmarks) {
        /* Default sector bookmark interval is 1 */
        bset->lfs_sect_bmark_itvl = 1;
        reqd_bmark_cnt += fcb_log->fl_fcb.f_sector_cnt;
        /* Make sure we have allocated the exact number of bookmarks
         * compare available bookmark count Vs required bookmark count
         */
        if (avl_bmark_cnt < reqd_bmark_cnt) {
            /* Not enough space allocated for sector bookmarks,
             * add bookmarks at sector intervals
             */
            bset->lfs_sect_bmark_itvl =
                fcb_log->fl_fcb.f_sector_cnt / avl_bmark_cnt;
            bset->lfs_sect_cap = avl_bmark_cnt -
                                 MYNEWT_VAL(LOG_FCB_NUM_ABS_BOOKMARKS);
        } else {
            bset->lfs_sect_cap = fcb_log->fl_fcb.f_sector_cnt;
        }

        return log_fcb_init_sector_bmarks(fcb_log);
    }
#endif

    return 0;
}

void
log_fcb_rotate_bmarks(struct fcb_log *fcb_log)
{
    int i;
    struct log_fcb_bmark *bmark;
    struct log_fcb_bset *bset;

    bset = &fcb_log->fl_bset;

    for (i = 0; i < bset->lfs_size; i++) {
        bmark = &bset->lfs_bmarks[i];
#if MYNEWT_VAL(LOG_FCB)
        if (bmark->lfb_entry.fe_area != fcb_log->fl_fcb.f_oldest) {
            continue;
        }
#endif
#if MYNEWT_VAL(LOG_FCB2)
        if (bmark->lfb_entry.fe_sector != fcb_log->fl_fcb.f_oldest_sec) {
            continue;
        }
#endif
        if (i != bset->lfs_size - 1) {
            *bmark = bset->lfs_bmarks[bset->lfs_size - 1];
            i--;
        }
        bset->lfs_size--;
    }
}

void
log_fcb_clear_bmarks(struct fcb_log *fcb_log)
{
    fcb_log->fl_bset.lfs_size = 0;
    fcb_log->fl_bset.lfs_next_non_sect = 0;
    fcb_log->fl_bset.lfs_non_sect_size = 0;
#if MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
    if (fcb_log->fl_bset.lfs_en_sect_bmarks) {
        fcb_log->fl_bset.lfs_next_sect = fcb_log->fl_bset.lfs_sect_cap - 1;
    }
#endif
    memset(fcb_log->fl_bset.lfs_bmarks, 0,
           sizeof(struct log_fcb_bmark) *
           fcb_log->fl_bset.lfs_cap);
}

struct log_fcb_bmark *
log_fcb_get_bmarks(struct log *log, uint32_t *bmarks_size)
{
    struct fcb_log *fcb_log = (struct fcb_log *)log->l_arg;

    *bmarks_size = fcb_log->fl_bset.lfs_cap;

    return fcb_log->fl_bset.lfs_bmarks;
}

const struct log_fcb_bmark *
log_fcb_closest_bmark(const struct fcb_log *fcb_log, uint32_t index,
                      int *min_diff)
{
    const struct log_fcb_bmark *closest;
    const struct log_fcb_bmark *bmark;
    uint32_t diff;
    int i = 0;
    uint32_t start_idx = 0;

    *min_diff = -1;
    closest = NULL;

    if (!fcb_log->fl_bset.lfs_bmarks) {
        return closest;
    }

#if MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
#if MYNEWT_VAL(LOG_FCB)
    /* Reason for this is that we want to iterate to the end of the
     * bmarks sector section of the array where a bookmark is valid,
     * this is only for the case where the sector bookmarks have not
     * been fully filled up, if they are filled up, we can iterate
     * normally to the end of the array
     */
    if (!fcb_log->fl_bset.lfs_bmarks[i].lfb_entry.fe_area &&
        fcb_log->fl_bset.lfs_next_sect < (fcb_log->fl_bset.lfs_sect_cap - 1)) {
        start_idx = fcb_log->fl_bset.lfs_next_sect + 1;
    }
#endif
#endif

    /* This works for both sector as well as non-sector bmarks
     * because we calculate the min diff and iterate to the end
     * of the bmarks array keeping track of min diff
     */
    for (i = start_idx; i < (start_idx + fcb_log->fl_bset.lfs_size); i++) {
        bmark = &fcb_log->fl_bset.lfs_bmarks[i];
#if MYNEWT_VAL(LOG_FCB)
        if (!fcb_log->fl_bset.lfs_bmarks[i].lfb_entry.fe_area) {
#if !MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
            /* Empty non-sector bookmark, nothing more to do
             * Previous closest bookmark is the closest one */
            break;
#endif
        }
#elif MYNEWT_VAL(LOG_FCB2)
        if (!fcb_log->fl_bset.lfs_bmarks[i].lfb_entry.fe_range) {
#if !MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
            /* Empty non-sector bookmark, nothing more to do
             * Previous closest bookmark is the closest one */
            break;
#endif
        }
#endif
        if (bmark->lfb_index <= index) {
            diff = index - bmark->lfb_index;
            if (diff < *min_diff) {
                *min_diff = diff;
                closest = bmark;
                MODLOG_DEBUG(LOG_MODULE_DEFAULT,
                             "index: %u, closest bmark idx: %u, \n",
                             (unsigned int)index,
                             (unsigned int)bmark->lfb_index);
                /* We found the exact match, no need to keep searching for a
                 * better match
                 */
                if (*min_diff == 0) {
                    break;
                }
            }
        }
    }

    return closest;
}

#if MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
static int
log_fcb_insert_sect_bmark(struct fcb_log *fcb_log, struct fcb_entry *entry,
                          uint32_t index)
{
    struct log_fcb_bset *bset;

    bset = &fcb_log->fl_bset;

    if (bset->lfs_size < fcb_log->fl_bset.lfs_sect_cap) {
        bset->lfs_bmarks[bset->lfs_next_sect] = (struct log_fcb_bmark) {
            .lfb_entry = *entry,
            .lfb_index = index,
        };

        bset->lfs_size++;
        bset->lfs_next_sect--;
        if (bset->lfs_next_sect >= fcb_log->fl_bset.lfs_sect_cap) {
            bset->lfs_next_sect = fcb_log->fl_bset.lfs_sect_cap - 1;
        }
    }

    return 0;
}
#endif

#if MYNEWT_VAL(LOG_FCB)
static int
log_fcb_replace_non_sect_bmark(struct fcb_log *fcb_log, struct fcb_entry *entry,
                               uint32_t index, int pos)
#elif MYNEWT_VAL(LOG_FCB2)
static int
log_fcb_replace_non_sect_bmark(struct fcb_log *fcb_log, struct fcb2_entry *entry,
                               uint32_t index, int pos)
#endif
{
    int i = 0;
    struct log_fcb_bset *bset = &fcb_log->fl_bset;

#if MYNEWT_VAL(LOG_FCB) && MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
    if (bset->lfs_en_sect_bmarks) {
        for (i = bset->lfs_sect_cap;
             i < (bset->lfs_non_sect_size + bset->lfs_sect_cap);
             i++) {
            if (index == bset->lfs_bmarks[i].lfb_index) {
                /* If index matches, no need to replace */
                return SYS_EALREADY;
            }
        }
    } else
#endif
    {
        for (i = 0; i < bset->lfs_non_sect_size; i++) {
            if (index == bset->lfs_bmarks[i].lfb_index) {
                /* If index matches, no need to replace */
                return SYS_EALREADY;
            }
        }
    }

    bset->lfs_bmarks[pos] = (struct log_fcb_bmark) {
        .lfb_entry = *entry,
        .lfb_index = index,
    };

    return SYS_EOK;
}

#if MYNEWT_VAL(LOG_FCB)
int
log_fcb_add_bmark(struct fcb_log *fcb_log, struct fcb_entry *entry,
                  uint32_t index, bool sect_bmark)
#elif MYNEWT_VAL(LOG_FCB2)
int
log_fcb_add_bmark(struct fcb_log *fcb_log, struct fcb2_entry *entry,
                  uint32_t index, bool sect_bmark)
#endif
{
    struct log_fcb_bset *bset = &fcb_log->fl_bset;
    int rc = 0;

    if (bset->lfs_cap == 0) {
        return SYS_ENOMEM;
    }

#if MYNEWT_VAL(LOG_FCB) && MYNEWT_VAL(LOG_FCB_SECTOR_BOOKMARKS)
    if (sect_bmark & bset->lfs_en_sect_bmarks) {
        rc = log_fcb_insert_sect_bmark(fcb_log, entry, index);
        if (rc) {
            MODLOG_DEBUG(LOG_MODULE_DEFAULT, "insert bmark failure: %u\n",
                         index);
        }
        MODLOG_DEBUG(LOG_MODULE_DEFAULT, "insert bmark index: %u, pos: %u\n",
                     index, bset->lfs_next_sect);
    } else {
        /* Replace oldest non-sector bmark */
        rc = log_fcb_replace_non_sect_bmark(fcb_log, entry, index,
                                            bset->lfs_next_non_sect +
                                            (bset->lfs_en_sect_bmarks ?
                                             bset->lfs_sect_cap : 0));
        if (rc == SYS_EOK) {
            MODLOG_DEBUG(LOG_MODULE_DEFAULT, "replace bmark index: %u, pos: %u\n",
                         index, bset->lfs_next_non_sect +
                         (bset->lfs_en_sect_bmarks ?
                          bset->lfs_sect_cap : 0));

            if (bset->lfs_non_sect_size < MYNEWT_VAL(LOG_FCB_NUM_ABS_BOOKMARKS)) {
                bset->lfs_non_sect_size++;
                bset->lfs_size++;
            }

            bset->lfs_next_non_sect = (bset->lfs_next_non_sect + 1) %
                                      MYNEWT_VAL(LOG_FCB_NUM_ABS_BOOKMARKS);
        }
    }
#else
    if (!sect_bmark) {
        if (bset->lfs_size >= MYNEWT_VAL(LOG_FCB_NUM_ABS_BOOKMARKS)) {
            /* Replace oldest non-sector bmark */
            rc = log_fcb_replace_non_sect_bmark(fcb_log, entry, index,
                                                bset->lfs_next_non_sect);
            if (rc == SYS_EOK) {
                MODLOG_DEBUG(LOG_MODULE_DEFAULT, "replace bmark index: %u, pos: %u\n",
                             index, bset->lfs_next_non_sect);
                bset->lfs_next_non_sect = (bset->lfs_next_non_sect + 1) %
                                          MYNEWT_VAL(LOG_FCB_NUM_ABS_BOOKMARKS);
            }
        } else {
            rc = log_fcb_replace_non_sect_bmark(fcb_log, entry, index,
                                                bset->lfs_size);
            if (rc == SYS_EOK) {
                MODLOG_DEBUG(LOG_MODULE_DEFAULT, "replace bmark index: %u, pos: %u\n",
                             index, bset->lfs_size);
                if (!bset->lfs_size) {
                    /* First non-sector bmark position */
                    bset->lfs_next_non_sect = 0;
                }
                bset->lfs_size++;
            }
        }

        assert(bset->lfs_size <= MYNEWT_VAL(LOG_FCB_NUM_ABS_BOOKMARKS));
    }
#endif
    return SYS_EOK;
}

#endif /* MYNEWT_VAL(LOG_FCB_BOOKMARKS) */
