/*
 * 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 <inttypes.h>
#include <string.h>
#include <assert.h>

#include "os/mynewt.h"
#include "hal/hal_bsp.h"
#include "hal/hal_flash.h"
#include "hal/hal_flash_int.h"
#if MYNEWT_VAL(FLASH_MAP_SUPPORT_MFG)
#include "mfg/mfg.h"
#endif
#include "flash_map/flash_map.h"

const struct flash_area *flash_map;
int flash_map_entries;

static int
flash_area_find_idx(uint8_t id)
{
    int i;

    if (flash_map == NULL) {
        return -1;
    }

    for (i = 0; i < flash_map_entries; i++) {
        if (flash_map[i].fa_id == id) {
            return i;
        }
    }

    return -1;
}

int
flash_area_open(uint8_t id, const struct flash_area **fap)
{
    int idx;

    if (flash_map == NULL) {
        return SYS_EACCES;
    }

    idx = flash_area_find_idx(id);
    if (idx == -1) {
        return SYS_ENOENT;
    }

    *fap = &flash_map[idx];

    return 0;
}

int
flash_area_to_sectors(int id, int *cnt, struct flash_area *ret)
{
    const struct flash_area *fa;
    const struct hal_flash *hf;
    uint32_t start;
    uint32_t size;
    int rc;
    int i;

    rc = flash_area_open(id, &fa);
    if (rc != 0) {
        return rc;
    }

    *cnt = 0;

    hf = hal_bsp_flash_dev(fa->fa_device_id);
    if (!hf) {
        flash_area_close(fa);
        return SYS_EINVAL;
    }

    for (i = 0; i < hf->hf_sector_cnt; i++) {
        hf->hf_itf->hff_sector_info(hf, i, &start, &size);
        if (start >= fa->fa_off && start < fa->fa_off + fa->fa_size) {
            if (ret) {
                ret->fa_id = id;
                ret->fa_device_id = fa->fa_device_id;
                ret->fa_off = start;
                ret->fa_size = size;
                ret++;
            }
            (*cnt)++;
        }
    }
    flash_area_close(fa);
    return 0;
}

static inline int
flash_range_end(const struct flash_sector_range *range)
{
    return range->fsr_flash_area.fa_off +
        range->fsr_sector_count * range->fsr_sector_size;
}

int
flash_area_to_sector_ranges(int id, int *cnt, struct flash_sector_range *ret)
{
    const struct flash_area *fa;
    const struct hal_flash *hf;
    struct flash_sector_range sr = { 0 };
    struct flash_sector_range *current;
    uint32_t start;      /* Sector start in flash */
    uint32_t size;       /* Sector size */
    uint32_t offset = 0; /* Address inside flash area */
    int rc;
    int i;
    int allowed_ranges = UINT16_MAX;
    int range_count = 0;
    int sector_in_ranges = 0;

    rc = flash_area_open(id, &fa);
    if (rc != 0) {
        return rc;
    }

    /* Respect maximum number of allowed ranges if specified and
     * ret was given. Otherwise count required number of sector
     * ranges.
     */
    if (*cnt > 0 && ret != NULL) {
        allowed_ranges = *cnt;
    }
    if (ret) {
        current = ret;
    } else {
        current = &sr;
    }

    hf = hal_bsp_flash_dev(fa->fa_device_id);
    if (!hf) {
        flash_area_close(fa);
        return SYS_EINVAL;
    }

    for (i = 0; i < hf->hf_sector_cnt; i++) {
        hf->hf_itf->hff_sector_info(hf, i, &start, &size);
        if (start >= fa->fa_off && start < fa->fa_off + fa->fa_size) {
            if (range_count) {
                /*
                 * Extend range if sector is adjacent to previous one.
                 */
                if (flash_range_end(current) == start &&
                    current->fsr_sector_size == size) {
                    current->fsr_flash_area.fa_size += size;
                    offset += size;
                    current->fsr_sector_count++;
                    sector_in_ranges++;
                    continue;
                } else if (ret != NULL) {
                    if (range_count < allowed_ranges) {
                        current = ++ret;
                    } else {
                        /* Found non-adjacent sector, but there is no
                         * space for it, just stop looking and return
                         * what was already found.
                         */
                        break;
                    }
                }
            }
            /* New sector range */
            range_count++;
            current->fsr_flash_area.fa_device_id = fa->fa_device_id;
            current->fsr_flash_area.fa_id = id;
            current->fsr_flash_area.fa_off = start;
            current->fsr_flash_area.fa_size = size;
            current->fsr_sector_size = size;
            current->fsr_sector_count = 1;
            current->fsr_first_sector = (uint16_t)sector_in_ranges;
            current->fsr_range_start = offset;
            current->fsr_align = hal_flash_align(fa->fa_device_id);
        }
    }
    *cnt = range_count;

    flash_area_close(fa);
    return 0;
}

int
flash_area_getnext_sector(int id, int *sec_id, struct flash_area *ret)
{
    const struct flash_area *fa;
    const struct hal_flash *hf;
    uint32_t start;
    uint32_t size;
    int rc;
    int i;

    rc = flash_area_open(id, &fa);
    if (rc) {
        return rc;
    }
    if (!ret || *sec_id < -1) {
        rc = SYS_EINVAL;
        goto end;
    }
    hf = hal_bsp_flash_dev(fa->fa_device_id);
    if (!hf) {
        rc = SYS_EINVAL;
        goto end;
    }

    i = *sec_id + 1;
    for (; i < hf->hf_sector_cnt; i++) {
        hf->hf_itf->hff_sector_info(hf, i, &start, &size);
        if (start >= fa->fa_off && start < fa->fa_off + fa->fa_size) {
            ret->fa_id = id;
            ret->fa_device_id = fa->fa_device_id;
            ret->fa_off = start;
            ret->fa_size = size;
            *sec_id = i;
            rc = 0;
            goto end;
        }
    }
    rc = SYS_ENOENT;
end:
    flash_area_close(fa);
    return rc;
}

int
flash_area_read(const struct flash_area *fa, uint32_t off, void *dst,
    uint32_t len)
{
    if (off > fa->fa_size || off + len > fa->fa_size) {
        return -1;
    }
    return hal_flash_read(fa->fa_device_id, fa->fa_off + off, dst, len);
}

int
flash_area_write(const struct flash_area *fa, uint32_t off, const void *src,
    uint32_t len)
{
    if (off > fa->fa_size || off + len > fa->fa_size) {
        return -1;
    }
    return hal_flash_write(fa->fa_device_id, fa->fa_off + off,
                           (void *)src, len);
}

int
flash_area_erase(const struct flash_area *fa, uint32_t off, uint32_t len)
{
    if (off > fa->fa_size || off + len > fa->fa_size) {
        return -1;
    }
    return hal_flash_erase(fa->fa_device_id, fa->fa_off + off, len);
}

uint32_t
flash_area_align(const struct flash_area *fa)
{
    return hal_flash_align(fa->fa_device_id);
}

uint32_t
flash_area_erased_val(const struct flash_area *fa)
{
    return hal_flash_erased_val(fa->fa_device_id);
}


/**
 * Checks if a flash area has been erased. Returns false if there are any
 * non non-erased bytes.
 *
 * @param fa                    An opened flash area to iterate.
 *                                  the count of flash area TLVs in the meta
 *                                  region is greater than this number, this
 *                                  function fails.
 * @param empty (out)           On success, the is_empty result gets written
 *                                  here.
 * @return                      0 on success; nonzero on failure.
 */
int
flash_area_is_empty(const struct flash_area *fa, bool *empty)
{
    int rc;

    *empty = false;
    rc = hal_flash_isempty_no_buf(fa->fa_device_id, fa->fa_off, fa->fa_size);
    if (rc < 0) {
        return rc;
    } else if (rc == 1) {
        *empty = true;
    }

    return 0;
}

int
flash_area_read_is_empty(const struct flash_area *fa, uint32_t off, void *dst,
                         uint32_t len)
{
    return hal_flash_isempty(fa->fa_device_id, fa->fa_off + off, dst, len);
}

/**
 * Converts the specified image slot index to a flash area ID.  If the
 * specified value is not a valid image slot index (0 or 1), a crash is
 * triggered.
 */
int
flash_area_id_from_image_slot(int slot)
{
    switch (slot) {
    case 0:
        return FLASH_AREA_IMAGE_0;
#ifdef FLASH_AREA_IMAGE_1
    case 1:
        return FLASH_AREA_IMAGE_1;
#endif
    default:
        assert(0);
        return FLASH_AREA_IMAGE_0;
    }
}

/**
 * Converts the specified flash area ID to an image slot index (0 or 1).  If
 * the area ID does not correspond to an image slot, -1 is returned.
 */
int
flash_area_id_to_image_slot(int area_id)
{
    switch (area_id) {
    case FLASH_AREA_IMAGE_0:
        return 0;
#ifdef FLASH_AREA_IMAGE_1
    case FLASH_AREA_IMAGE_1:
        return 1;
#endif
    default:
        return -1;
    }
}

/**
 * Reads the flash map layout from the manufacturing meta region.  This
 * function requires that the flash map be populated with a
 * FLASH_AREA_BOOTLOADER entry, as the meta region is stored at the end of the
 * boot loader area.
 *
 * @param max_areas             The maximum number of flash areas to read.  If
 *                                  the count of flash area TLVs in the meta
 *                                  region is greater than this number, this
 *                                  function fails.
 * @param out_areas (out)       An array of flash areas.  On success, the flash
 *                                  map stored in the meta region gets written
 *                                  here.
 * @param out_num_areas (out)   On success, the number of flash areas read gets
 *                                  written here.
 *
 * @return                      0 on success; nonzero on failure.
 */
#if MYNEWT_VAL(FLASH_MAP_SUPPORT_MFG)
static int
flash_map_read_mfg(int max_areas,
                   struct flash_area *out_areas, int *out_num_areas)
{
    struct mfg_meta_flash_area meta_flash_area;
    struct mfg_reader reader;
    struct flash_area *fap;
    int rc;

    *out_num_areas = 0;

    /* Ensure manufacturing meta region has been located in flash. */
    mfg_init();

    mfg_open(&reader);

    while (1) {
        if (*out_num_areas >= max_areas) {
            return -1;
        }

        rc = mfg_seek_next_with_type(&reader, MFG_META_TLV_TYPE_FLASH_AREA);
        switch (rc) {
        case 0:
            break;
        case SYS_EDONE:
            return 0;
        default:
            return rc;
        }

        rc = mfg_read_tlv_flash_area(&reader, &meta_flash_area);
        if (rc != 0) {
            return rc;
        }

        fap = out_areas + *out_num_areas;
        fap->fa_id = meta_flash_area.area_id;
        fap->fa_device_id = meta_flash_area.device_id;
        fap->fa_off = meta_flash_area.offset;
        fap->fa_size = meta_flash_area.size;

        (*out_num_areas)++;
    }
}
#endif

/**
 * Determines if the specified flash area overlaps any areas in the flash map.
 *
 * @param area1                 The flash area to compare against the flash
 *                                  map.
 *
 * @return                      True if there is an overlap; false otherwise.
 */
static bool
flash_map_area_overlaps(const struct flash_area *area1)
{
    const struct flash_area *area2;
    uint32_t end1;
    uint32_t end2;
    int i;

    for (i = 0; i < flash_map_entries; i++) {
        area2 = &flash_map[i];

        if (area1->fa_device_id == area2->fa_device_id) {
            end1 = area1->fa_off + area1->fa_size;
            end2 = area2->fa_off + area2->fa_size;

            if (end1 > area2->fa_off && area1->fa_off < end2) {
                return true;
            }
        }
    }

    return false;
}

/**
 * Adds areas from the hardcoded flash map that aren't present in, and don't
 * overlap with, the manufacturing flash map.
 */
static void
flash_map_add_new_dflt_areas(void)
{
    static const int num_dflt_entries =
        sizeof sysflash_map_dflt / sizeof sysflash_map_dflt[0];

    const struct flash_area *dflt_area;
    struct flash_area *dst_area;
    int i;
    
    for (i = 0; i < num_dflt_entries; i++) {
        dflt_area = &sysflash_map_dflt[i];

        /* If there is already a manufacturing area with this ID, discard the
         * default area.
         */
        if (flash_area_find_idx(dflt_area->fa_id) == -1) {
            /* Default flash map contains a new entry. */
            if (flash_map_entries >= MYNEWT_VAL(FLASH_MAP_MAX_AREAS)) {
                DFLT_LOG_DEBUG("failed to add default flash area: "
                               "no room: id=%d",
                               dflt_area->fa_id);
                DEBUG_PANIC();
                return;
            } 

            /* Add the default entry if it doesn't cause any overlaps. */
            if (flash_map_area_overlaps(dflt_area)) {
                DFLT_LOG_DEBUG("failed to add default flash area: "
                               "overlap: id=%d",
                               dflt_area->fa_id);
            } else {
                /* Cast away const. */
                dst_area = (struct flash_area *) &flash_map[flash_map_entries];

                *dst_area = *dflt_area;
                flash_map_entries++;
            }
        }
    }
}

void
flash_map_init(void)
{
#if MYNEWT_VAL(FLASH_MAP_SUPPORT_MFG)
    static struct flash_area mfg_areas[MYNEWT_VAL(FLASH_MAP_MAX_AREAS)];
    int num_areas;
#endif

    int rc;

    /* Ensure this function only gets called by sysinit. */
    SYSINIT_ASSERT_ACTIVE();

    rc = hal_flash_init();
    SYSINIT_PANIC_ASSERT(rc == 0);

    /* Use the hardcoded default flash map.  This is done for two reasons:
     * 1. A minimal flash map configuration is required to boot strap the
     *    process of reading the flash map from the manufacturing meta regions.
     *    In particular, a FLASH_AREA_BOOTLOADER entry is required for the boot
     *    MMR, as well as an entry for each extended MMR.
     * 2. If we fail to read the flash map from the MMRs, the system continues
     *    to use the default flash map.
     */
    flash_map = sysflash_map_dflt;
    flash_map_entries = sizeof sysflash_map_dflt / sizeof sysflash_map_dflt[0];

#if MYNEWT_VAL(FLASH_MAP_SUPPORT_MFG)
    /* Attempt to read the flash map from the manufacturing meta regions.  On
     * success, use the new flash map instead of the default hardcoded one.
     */
    rc = flash_map_read_mfg(sizeof mfg_areas / sizeof mfg_areas[0],
                            mfg_areas, &num_areas);
    if (rc != 0 || num_areas == 0) {
        return;
    }
    flash_map = mfg_areas;
    flash_map_entries = num_areas;
#endif

    /* The hardcoded flash map may contain new areas that aren't present in the
     * manufacturing flash map.  Try including them if they don't overlap with
     * any mfg areas.
     */
    flash_map_add_new_dflt_areas();
}

#if MYNEWT_VAL(SELFTEST)

void
flash_map_add_new_dflt_areas_extern(void)
{
    flash_map_add_new_dflt_areas();
}

#endif
