/*
 * 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"
#include "modlog/modlog.h"
#include "mfg/mfg.h"

/**
 * The "manufacturing meta region" is located at the end of the boot loader
 * flash area.  This region has the following structure.
 *
 *  0                   1                   2                   3
 *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 * |   TLV type    |   TLV size    | TLV data ("TLV size" bytes)   ~
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               ~
 * ~                                                               ~
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 * |   TLV type    |   TLV size    | TLV data ("TLV size" bytes)   ~
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               ~
 * ~                                                               ~
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 * |          Region size          |    Version    | 0xff padding  |
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 * |                       Magic (0x3bb2a269)                      |
 * +-+-+-+-+-+--+-+-+-+-end of boot loader area+-+-+-+-+-+-+-+-+-+-+
 *
 * The number of TLVs is variable; two are shown above for illustrative
 * purposes.
 *
 * Fields:
 * <TLVs>
 * 1. TLV type: Indicates the type of data to follow.
 * 2. TLV size: The number of bytes of data to follow.
 * 3. TLV data: "TLV size" bytes of data.
 *
 * <Footer>
 * 4. Region size: The size, in bytes, of the entire manufacturing meta region;
 *    includes TLVs and footer.
 * 5. Version: Manufacturing meta version number; always 0x02.
 * 6. Magic: Indicates the presence of the manufacturing meta region.
 */

#define MFG_META_MAGIC          0x3bb2a269
#define MFG_META_VERSION        2
#define MFG_META_FOOTER_SZ      (sizeof (struct mfg_meta_footer))
#define MFG_META_TLV_SZ         (sizeof (struct mfg_meta_tlv))

struct mfg_meta_footer {
    uint16_t size;
    uint8_t version;
    uint8_t pad8;
    uint32_t magic;
};

/** Represents an MMR after it has been read from flash. */
struct mfg_mmr {
    /* Flash area containing MMR. */
    uint8_t area_id;

    /* Offset within flash area of start of MMR. */
    uint32_t offset;

    /* Total size of MMR (TLVs + footer). */
    uint32_t size;
};

/** The full set of MMRs comprised by all installed mfgimages. */
static struct mfg_mmr mfg_mmrs[MYNEWT_VAL(MFG_MAX_MMRS)];
static int mfg_num_mmrs;

/** True if MMR detection has occurred. */
static bool mfg_initialized;

void
mfg_open(struct mfg_reader *out_reader)
{
    /* Ensure MMRs have been detected. */
    mfg_init();

    /* Start at MMR index 0. */
    *out_reader = (struct mfg_reader) { 0 };
}

/**
 * Seeks to the next mfg TLV.
 *
 * @param reader                The reader to seek with.
 *
 * @return                      0 if the next TLV was successfully seeked to.
 *                              SYS_EDONE if there are no additional TLVs
 *                                  available.
 *                              SYS_EAGAIN if the end of the current MMR is
 *                                  reached, but additional MMRs are available
 *                                  for reading.
 *                              Other MFG error code on failure.
 */
static int
mfg_seek_next_aux(struct mfg_reader *reader)
{
    const struct flash_area *fap;
    const struct mfg_mmr *mmr;
    int rc;

    if (reader->mmr_idx >= mfg_num_mmrs) {
        /* The reader is expired. */
        return SYS_EINVAL;
    }

    mmr = &mfg_mmrs[reader->mmr_idx];

    rc = flash_area_open(mmr->area_id, &fap);
    if (rc != 0) {
        return SYS_EIO;
    }

    if (reader->offset == 0) {
        /* First seek; advance to the start of the MMR. */
        reader->offset = mmr->offset;
    } else {
        /* Follow-up seek; skip the current TLV. */
        reader->offset += MFG_META_TLV_SZ + reader->cur_tlv.size;
    }

    if (reader->offset >= fap->fa_size - MFG_META_FOOTER_SZ) {
        /* Reached end of the MMR; advance to the next MMR if one exists. */
        if (reader->mmr_idx + 1 >= mfg_num_mmrs) {
            rc = SYS_EDONE;
        } else {
            reader->offset = 0;
            reader->mmr_idx++;
            rc = SYS_EAGAIN;
        }
        goto done;
    }

    /* Read current TLV header. */
    rc = flash_area_read(fap, reader->offset, &reader->cur_tlv,
                         MFG_META_TLV_SZ);
    if (rc != 0) {
        rc = SYS_EIO;
        goto done;
    }

done:
    flash_area_close(fap);
    return rc;
}

int
mfg_seek_next(struct mfg_reader *reader)
{
    int rc;

    do {
        rc = mfg_seek_next_aux(reader);
    } while (rc == SYS_EAGAIN);

    return rc;
}

int
mfg_seek_next_with_type(struct mfg_reader *reader, uint8_t type)
{
    int rc;

    while (1) {
        rc = mfg_seek_next(reader);
        if (rc != 0) {
            break;
        }

        if (reader->cur_tlv.type == type) {
            break;
        }

        /* Proceed to next TLV. */
    }

    return rc;
}

/**
 * Opens the flash area pointed to by the provided reader.
 */
static int
mfg_open_flash_area(const struct mfg_reader *reader,
                    const struct flash_area **fap)
{
    const struct mfg_mmr *mmr;
    int rc;

    assert(reader->mmr_idx < mfg_num_mmrs);
    mmr = &mfg_mmrs[reader->mmr_idx];

    rc = flash_area_open(mmr->area_id, fap);
    if (rc != 0) {
        return SYS_EIO;
    }

    return 0;
}

static int
mfg_read_tlv_body(const struct mfg_reader *reader, void *dst, int max_size)
{
    const struct flash_area *fap;
    int read_sz;
    int rc;

    rc = mfg_open_flash_area(reader, &fap);
    if (rc != 0) {
        return rc;
    }

    memset(dst, 0, max_size);

    read_sz = min(max_size, reader->cur_tlv.size);
    rc = flash_area_read(fap, reader->offset + MFG_META_TLV_SZ, dst, read_sz);
    flash_area_close(fap);

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

    return 0;
}

int
mfg_read_tlv_flash_area(const struct mfg_reader *reader,
                        struct mfg_meta_flash_area *out_mfa)
{
    return mfg_read_tlv_body(reader, out_mfa, sizeof *out_mfa);
}

int
mfg_read_tlv_mmr_ref(const struct mfg_reader *reader,
                     struct mfg_meta_mmr_ref *out_mr)
{
    return mfg_read_tlv_body(reader, out_mr, sizeof *out_mr);
}

int
mfg_read_tlv_hash(const struct mfg_reader *reader, void *out_hash)
{
    return mfg_read_tlv_body(reader, out_hash, MFG_HASH_SZ);
}

/**
 * Reads an MMR from the end of the specified flash area.
 */
static int
mfg_read_mmr(uint8_t area_id, struct mfg_mmr *out_mmr)
{
    const struct flash_area *fap;
    struct mfg_meta_footer ftr;
    int rc;

    rc = flash_area_open(area_id, &fap);
    if (rc != 0) {
        return SYS_EIO;
    }

    /* Read the MMR footer. */
    rc = flash_area_read(fap, fap->fa_size - sizeof ftr, &ftr, sizeof ftr);
    flash_area_close(fap);

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

    if (ftr.magic != MFG_META_MAGIC) {
        return SYS_ENODEV;
    }

    if (ftr.version != MFG_META_VERSION) {
        return SYS_ENOTSUP;
    }

    if (ftr.size > fap->fa_size) {
        return SYS_ENODEV;
    }

    *out_mmr = (struct mfg_mmr) {
        .area_id = area_id,
        .offset = fap->fa_size - ftr.size,
        .size = ftr.size,
    };

    return 0;
}

/**
 * Reads an MMR from the end of the specified flash area.  On success, the
 * global MMR list is populated with the result for subsequent reading.
 */
static int
mfg_read_next_mmr(uint8_t area_id)
{
    int rc;
    int i;

    /* Detect if this MMR has already been read. */
    for (i = 0; i < mfg_num_mmrs; i++) {
        if (mfg_mmrs[i].area_id == area_id) {
            return SYS_EALREADY;
        }
    }

    if (mfg_num_mmrs >= MYNEWT_VAL(MFG_MAX_MMRS)) {
        return SYS_ENOMEM;
    }

    rc = mfg_read_mmr(area_id, &mfg_mmrs[mfg_num_mmrs]);
    if (rc != 0) {
        return rc;
    }

    mfg_num_mmrs++;
    return 0;
}

/**
 * Reads all MMR ref TLVs in the specified MMR.  The global MMR list is
 * populated with the results for subsequent reading.
 */
static int
mfg_read_mmr_refs(void)
{
    struct mfg_meta_mmr_ref mmr_ref;
    struct mfg_reader reader;
    int rc;

    mfg_open(&reader);

    /* Repeatedly find and read the next MMR ref TLV.  As new MMRs are read,
     * they are added to the global list and become available in this loop.
     */
    while (true) {
        rc = mfg_seek_next_with_type(&reader, MFG_META_TLV_TYPE_MMR_REF);
        switch (rc) {
        case 0:
            /* Found an MMR ref TLV.  Read it below. */
            break;

        case SYS_EDONE:
            /* No more MMR ref TLVs. */
            return 0;

        default:
            return rc;
        }

        rc = mfg_read_tlv_mmr_ref(&reader, &mmr_ref);
        if (rc != 0) {
            return rc;
        }

        rc = mfg_read_next_mmr(mmr_ref.area_id);
        if (rc != 0 && rc != SYS_EALREADY) {
            return rc;
        }
    }

    return 0;
}

/**
 * Locates the manufacturing meta region in flash.  This function must be
 * called before any TLVs can be read.  No-op if this function has already
 * executed successfully.
 */
void
mfg_init(void)
{
    int rc;

    if (mfg_initialized) {
        return;
    }
    mfg_initialized = true;

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

    /* Read the first MMR from the boot loader area. */
    rc = mfg_read_next_mmr(FLASH_AREA_BOOTLOADER);
    if (rc != 0) {
        goto err;
    }

    /* Read all MMR references. */
    rc = mfg_read_mmr_refs();
    if (rc != 0) {
        goto err;
    }

    return;

err:
    MFG_LOG_ERROR("failed to read MMRs: rc=%d", rc);
}
