/*
 * Copyright (c) 2020 Embedded Planet
 * Copyright (c) 2020 ARM Limited
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed 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 <assert.h>
#include <cstring>
#include "flash_map_backend/flash_map_backend.h"
#include "flash_map_backend/secondary_bd.h"
#include "sysflash/sysflash.h"

#include "blockdevice/BlockDevice.h"
#include "FlashIAP/FlashIAPBlockDevice.h"

#include "mcuboot_config/mcuboot_logging.h"

#include "bootutil_priv.h"

#define FLASH_DEVICE_INTERNAL_FLASH 0
#define FLASH_AREAS 3

/** Application defined secondary block device */
mbed::BlockDevice* mcuboot_secondary_bd = get_secondary_bd();

/** Internal application block device */
static FlashIAPBlockDevice mcuboot_primary_bd(MCUBOOT_PRIMARY_SLOT_START_ADDR, MCUBOOT_SLOT_SIZE);

#if MCUBOOT_SWAP_USING_SCRATCH
/** Scratch space is at the end of internal flash, after the main application */
static FlashIAPBlockDevice mcuboot_scratch_bd(MCUBOOT_SCRATCH_START_ADDR, MCUBOOT_SCRATCH_SIZE);
#endif

static mbed::BlockDevice* flash_map_bd[FLASH_AREAS] = {
        (mbed::BlockDevice*) &mcuboot_primary_bd,       /** Primary (loadable) image area */
        mcuboot_secondary_bd,                           /** Secondary (update candidate) image area */
#if MCUBOOT_SWAP_USING_SCRATCH
        (mbed::BlockDevice*) &mcuboot_scratch_bd        /** Scratch space for swapping images */
#else
        nullptr
#endif
};

static struct flash_area flash_areas[FLASH_AREAS];

static unsigned int open_count[FLASH_AREAS] = {0};

int flash_area_open(uint8_t id, const struct flash_area** fapp) {

    *fapp = &flash_areas[id];
    struct flash_area* fap = (struct flash_area*)*fapp;

    // The offset of the slot is from the beginning of the flash device.
    switch (id) {
        case PRIMARY_ID:
            fap->fa_off = MCUBOOT_PRIMARY_SLOT_START_ADDR;
            break;
        case SECONDARY_ID:
#if MCUBOOT_DIRECT_XIP
            fap->fa_off = MBED_CONF_MCUBOOT_XIP_SECONDARY_SLOT_ADDRESS;
#else
            fap->fa_off = 0;
#endif
            break;
#if MCUBOOT_SWAP_USING_SCRATCH
        case SCRATCH_ID:
            fap->fa_off = MCUBOOT_SCRATCH_START_ADDR;
            break;
#endif
        default:
            MCUBOOT_LOG_ERR("flash_area_open, unknown id %d", id);
            return -1;
    }

    open_count[id]++;
    MCUBOOT_LOG_DBG("flash area %d open count: %d (+)", id, open_count[id]);

    fap->fa_id = id;
    fap->fa_device_id = 0; // not relevant

    mbed::BlockDevice* bd = flash_map_bd[id];

    /* Only initialize if this isn't a nested call to open the flash area */
    if (open_count[id] == 1) {
        MCUBOOT_LOG_DBG("initializing flash area %d...", id);
        int ret = bd->init();
        if (ret)
        {
            MCUBOOT_LOG_ERR("initializing flash area failed %d", ret);
            return ret;
        }
    } else {
        return 0;
    }

    fap->fa_size = (uint32_t) bd->size();
    return 0;
}

void flash_area_close(const struct flash_area* fap) {
    uint8_t id = fap->fa_id;
    /* No need to close an unopened flash area, avoid an overflow of the counter */
    if (!open_count[id]) {
        return;
    }

    open_count[id]--;
    MCUBOOT_LOG_DBG("flash area %d open count: %d (-)", id, open_count[id]);
    if (!open_count[id]) {
        /* mcuboot is not currently consistent in opening/closing flash areas only once at a time
         * so only deinitialize the BlockDevice if all callers have closed the flash area. */
        MCUBOOT_LOG_DBG("deinitializing flash area block device %d...", id);
        mbed::BlockDevice* bd = flash_map_bd[id];
        bd->deinit();
    }
}

/*
 * Read/write/erase. Offset is relative from beginning of flash area.
 */
int flash_area_read(const struct flash_area* fap, uint32_t off, void* dst, uint32_t len) {
    mbed::BlockDevice* bd = flash_map_bd[fap->fa_id];

    /* Note: The address must be aligned to bd->get_read_size(). If MCUBOOT_READ_GRANULARITY
       is defined, the length does not need to be aligned. */
#ifdef MCUBOOT_READ_GRANULARITY
    uint32_t read_size = bd->get_read_size();
    if (read_size == 0) {
        MCUBOOT_LOG_ERR("Invalid read size: must be non-zero");
        return -1;
    }
    if (MCUBOOT_READ_GRANULARITY < read_size) {
        MCUBOOT_LOG_ERR("Please increase MCUBOOT_READ_GRANULARITY (currently %u) to be at least %u",
                    MCUBOOT_READ_GRANULARITY, read_size);
        return -1;
    }

    uint32_t remainder = len % read_size;
    len -= remainder;
    if (len != 0) {
#endif
        if (!bd->is_valid_read(off, len)) {
            MCUBOOT_LOG_ERR("Invalid read: fa_id %d offset 0x%x len 0x%x", fap->fa_id,
                    (unsigned int) off, (unsigned int) len);
            return -1;
        }
        else {
            int ret = bd->read(dst, off, len);
            if (ret != 0) {
                MCUBOOT_LOG_ERR("Read failed: fa_id %d offset 0x%x len 0x%x", fap->fa_id,
                        (unsigned int) off, (unsigned int) len);
                return ret;
            }
        }
#ifdef MCUBOOT_READ_GRANULARITY
    }

    if (remainder) {
        if (!bd->is_valid_read(off + len, read_size)) {
            MCUBOOT_LOG_ERR("Invalid read: fa_id %d offset 0x%x len 0x%x", fap->fa_id,
                    (unsigned int) (off + len), (unsigned int) read_size);
            return -1;
        }
        else {
            uint8_t buffer[MCUBOOT_READ_GRANULARITY];
            int ret = bd->read(buffer, off + len, read_size);
            if (ret != 0) {
                MCUBOOT_LOG_ERR("Read failed: %d", ret);
                return ret;
            }
            memcpy((uint8_t *)dst + len, buffer, remainder);
        }
    }
#endif

    return 0;
}

int flash_area_write(const struct flash_area* fap, uint32_t off, const void* src, uint32_t len) {
    mbed::BlockDevice* bd = flash_map_bd[fap->fa_id];
    return bd->program(src, off, len);
}

int flash_area_erase(const struct flash_area* fap, uint32_t off, uint32_t len) {
    mbed::BlockDevice* bd = flash_map_bd[fap->fa_id];
    return bd->erase(off, len);
}

uint32_t flash_area_align(const struct flash_area* fap) {
    mbed::BlockDevice* bd = flash_map_bd[fap->fa_id];
    return bd->get_program_size();
}

uint8_t flash_area_erased_val(const struct flash_area* fap) {
    mbed::BlockDevice* bd = flash_map_bd[fap->fa_id];
    return bd->get_erase_value();
}

int flash_area_get_sectors(int fa_id, uint32_t* count, struct flash_sector* sectors) {
    mbed::BlockDevice* bd = flash_map_bd[fa_id];

    /* Loop through sectors and collect information on them */
    bd_addr_t offset = 0;
    *count = 0;
    while (*count < MCUBOOT_MAX_IMG_SECTORS && bd->is_valid_read(offset, bd->get_read_size())) {

        sectors[*count].fs_off = offset;
        bd_size_t erase_size = bd->get_erase_size(offset);
        sectors[*count].fs_size = erase_size;

        offset += erase_size;
        *count += 1;
    }

    return 0;
}

int flash_area_id_from_image_slot(int slot) {
    return slot;
}

/**
 * Multi images support not implemented yet
 */
int flash_area_id_from_multi_image_slot(int image_index, int slot)
{
    assert(image_index == 0);
    return slot;
}

int flash_area_id_to_multi_image_slot(int image_index, int area_id)
{
    assert(image_index == 0);
    return area_id;
}
