/*
 *  Copyright (c) 2016, The OpenThread Authors.
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *  3. Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @file
 *   This file implements the OpenThread platform abstraction for non-volatile storage of settings.
 *
 */

#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <assert.h>

#include <openthread-types.h>
#include <openthread-core-config.h>

#include <common/code_utils.hpp>
#include <platform/settings.h>

#include "flash.h"

#ifdef __cplusplus
extern "C" {
#endif

enum
{
    kBlockAddBeginFlag = 0x1,
    kBlockAddCompleteFlag = 0x02,
    kBlockDeleteFlag = 0x04,
    kBlockIndex0Flag = 0x08,
};

enum
{
    kSettingsFlagSize = 4,
    kSettingsBlockDataSize = 255,

    kSettingsInSwap = 0xbe5cc5ef,
    kSettingsInUse = 0xbe5cc5ee,
    kSettingsNotUse = 0xbe5cc5ec,
};

OT_TOOL_PACKED_BEGIN
struct settingsBlock
{
    uint16_t key;
    uint16_t flag;
    uint16_t length;
    uint16_t reserved;
} OT_TOOL_PACKED_END;


/**
 * @def SETTINGS_CONFIG_BASE_ADDRESS
 *
 * The base address of settings.
 *
 */
#ifndef SETTINGS_CONFIG_BASE_ADDRESS
#define SETTINGS_CONFIG_BASE_ADDRESS                 0x39000
#endif  // SETTINGS_CONFIG_BASE_ADDRESS

/**
 * @def SETTINGS_CONFIG_PAGE_SIZE
 *
 * The page size of settings.
 *
 */
#ifndef SETTINGS_CONFIG_PAGE_SIZE
#define SETTINGS_CONFIG_PAGE_SIZE                    0x800
#endif  // SETTINGS_CONFIG_PAGE_SIZE

/**
 * @def SETTINGS_CONFIG_PAGE_NUM
 *
 * The page number of settings.
 *
 */
#ifndef SETTINGS_CONFIG_PAGE_NUM
#define SETTINGS_CONFIG_PAGE_NUM                     2
#endif  // SETTINGS_CONFIG_PAGE_NUM

static uint32_t sSettingsBaseAddress;
static uint32_t sSettingsUsedSize;

static uint16_t getAlignLength(uint16_t length)
{
    return (length + 3) & 0xfffc;
}

static void setSettingsFlag(uint32_t aBase, uint32_t aFlag)
{
    utilsFlashWrite(aBase, reinterpret_cast<uint8_t *>(&aFlag), sizeof(aFlag));
}

static void initSettings(uint32_t aBase, uint32_t aFlag)
{
    uint32_t address = aBase;
    uint32_t settingsSize = SETTINGS_CONFIG_PAGE_NUM > 1 ?
                            SETTINGS_CONFIG_PAGE_SIZE * SETTINGS_CONFIG_PAGE_NUM / 2 :
                            SETTINGS_CONFIG_PAGE_SIZE;

    while (address < (aBase + settingsSize))
    {
        utilsFlashErasePage(address);
        utilsFlashStatusWait(1000);
        address += SETTINGS_CONFIG_PAGE_SIZE;
    }

    setSettingsFlag(aBase, aFlag);
}

static uint32_t swapSettingsBlock(otInstance *aInstance)
{
    uint32_t oldBase = sSettingsBaseAddress;
    uint32_t swapAddress = oldBase;
    uint32_t usedSize = sSettingsUsedSize;
    uint8_t pageNum = SETTINGS_CONFIG_PAGE_NUM;
    uint32_t settingsSize = pageNum > 1 ? SETTINGS_CONFIG_PAGE_SIZE * pageNum / 2 :
                            SETTINGS_CONFIG_PAGE_SIZE;

    (void)aInstance;

    VerifyOrExit(pageNum > 1, ;);

    sSettingsBaseAddress = (swapAddress == SETTINGS_CONFIG_BASE_ADDRESS) ?
                           (swapAddress + settingsSize) :
                           SETTINGS_CONFIG_BASE_ADDRESS;

    initSettings(sSettingsBaseAddress, static_cast<uint32_t>(kSettingsInSwap));
    sSettingsUsedSize = kSettingsFlagSize;
    swapAddress += kSettingsFlagSize;

    while (swapAddress < (oldBase + usedSize))
    {
        OT_TOOL_PACKED_BEGIN
        struct addSettingsBlock
        {
            struct settingsBlock block;
            uint8_t data[kSettingsBlockDataSize];
        } OT_TOOL_PACKED_END addBlock;
        bool valid = true;

        utilsFlashRead(swapAddress, reinterpret_cast<uint8_t *>(&addBlock.block), sizeof(struct settingsBlock));
        swapAddress += sizeof(struct settingsBlock);

        if (!(addBlock.block.flag & kBlockAddCompleteFlag) && (addBlock.block.flag & kBlockDeleteFlag))
        {
            uint32_t address = swapAddress + getAlignLength(addBlock.block.length);

            while (address < (oldBase + usedSize))
            {
                struct settingsBlock block;

                utilsFlashRead(address, reinterpret_cast<uint8_t *>(&block), sizeof(block));

                if (!(block.flag & kBlockAddCompleteFlag) && (block.flag & kBlockDeleteFlag) &&
                    !(block.flag & kBlockIndex0Flag) && (block.key == addBlock.block.key))
                {
                    valid = false;
                    break;
                }

                address += (getAlignLength(block.length) + sizeof(struct settingsBlock));
            }

            if (valid)
            {
                utilsFlashRead(swapAddress, addBlock.data, getAlignLength(addBlock.block.length));
                utilsFlashWrite(sSettingsBaseAddress + sSettingsUsedSize,
                                reinterpret_cast<uint8_t *>(&addBlock),
                                getAlignLength(addBlock.block.length) + sizeof(struct settingsBlock));
                sSettingsUsedSize += (sizeof(struct settingsBlock) + getAlignLength(addBlock.block.length));
            }
        }
        else if (addBlock.block.flag == 0xff)
        {
            break;
        }

        swapAddress += getAlignLength(addBlock.block.length);
    }

    setSettingsFlag(sSettingsBaseAddress, static_cast<uint32_t>(kSettingsInUse));
    setSettingsFlag(oldBase, static_cast<uint32_t>(kSettingsNotUse));

exit:
    return settingsSize - sSettingsUsedSize;
}

static ThreadError addSetting(otInstance *aInstance, uint16_t aKey, bool aIndex0, const uint8_t *aValue,
                              uint16_t aValueLength)
{
    ThreadError error = kThreadError_None;
    OT_TOOL_PACKED_BEGIN
    struct addSettingsBlock
    {
        struct settingsBlock block;
        uint8_t data[kSettingsBlockDataSize];
    } OT_TOOL_PACKED_END addBlock;
    uint32_t settingsSize = SETTINGS_CONFIG_PAGE_NUM > 1 ?
                            SETTINGS_CONFIG_PAGE_SIZE * SETTINGS_CONFIG_PAGE_NUM / 2 :
                            SETTINGS_CONFIG_PAGE_SIZE;

    addBlock.block.flag = 0xff;
    addBlock.block.key = aKey;

    if (aIndex0)
    {
        addBlock.block.flag &= (~kBlockIndex0Flag);
    }

    addBlock.block.flag &= (~kBlockAddBeginFlag);
    addBlock.block.length = aValueLength;

    if ((sSettingsUsedSize + getAlignLength(addBlock.block.length) + sizeof(struct settingsBlock)) >=
        settingsSize)
    {
        VerifyOrExit(swapSettingsBlock(aInstance) >= (getAlignLength(addBlock.block.length) + sizeof(struct settingsBlock)),
                     error = kThreadError_NoBufs);
    }

    utilsFlashWrite(sSettingsBaseAddress + sSettingsUsedSize,
                    reinterpret_cast<uint8_t *>(&addBlock.block),
                    sizeof(struct settingsBlock));

    memset(addBlock.data, 0xff, kSettingsBlockDataSize);
    memcpy(addBlock.data, aValue, addBlock.block.length);

    utilsFlashWrite(sSettingsBaseAddress + sSettingsUsedSize + sizeof(struct settingsBlock),
                    reinterpret_cast<uint8_t *>(addBlock.data), getAlignLength(addBlock.block.length));

    addBlock.block.flag &= (~kBlockAddCompleteFlag);
    utilsFlashWrite(sSettingsBaseAddress + sSettingsUsedSize,
                    reinterpret_cast<uint8_t *>(&addBlock.block),
                    sizeof(struct settingsBlock));
    sSettingsUsedSize += (sizeof(struct settingsBlock) + getAlignLength(addBlock.block.length));

exit:
    return error;
}

// settings API
void otPlatSettingsInit(otInstance *aInstance)
{
    uint8_t index;
    uint32_t settingsSize = SETTINGS_CONFIG_PAGE_NUM > 1 ?
                            SETTINGS_CONFIG_PAGE_SIZE * SETTINGS_CONFIG_PAGE_NUM / 2 :
                            SETTINGS_CONFIG_PAGE_SIZE;

    (void)aInstance;

    sSettingsBaseAddress = SETTINGS_CONFIG_BASE_ADDRESS;

    utilsFlashInit();

    for (index = 0; index < 2; index++)
    {
        uint32_t blockFlag;

        sSettingsBaseAddress += settingsSize * index;
        utilsFlashRead(sSettingsBaseAddress, reinterpret_cast<uint8_t *>(&blockFlag), sizeof(blockFlag));

        if (blockFlag == kSettingsInUse)
        {
            break;
        }
    }

    if (index == 2)
    {
        initSettings(sSettingsBaseAddress, static_cast<uint32_t>(kSettingsInUse));
    }

    sSettingsUsedSize = kSettingsFlagSize;

    while (sSettingsUsedSize < settingsSize)
    {
        struct settingsBlock block;

        utilsFlashRead(sSettingsBaseAddress + sSettingsUsedSize,
                       reinterpret_cast<uint8_t *>(&block), sizeof(block));

        if (!(block.flag & kBlockAddBeginFlag))
        {
            sSettingsUsedSize += (getAlignLength(block.length) + sizeof(struct settingsBlock));
        }
        else
        {
            break;
        }
    }
}

ThreadError otPlatSettingsBeginChange(otInstance *aInstance)
{
    (void)aInstance;
    return kThreadError_None;
}

ThreadError otPlatSettingsCommitChange(otInstance *aInstance)
{
    (void)aInstance;
    return kThreadError_None;
}

ThreadError otPlatSettingsAbandonChange(otInstance *aInstance)
{
    (void)aInstance;
    return kThreadError_None;
}

ThreadError otPlatSettingsGet(otInstance *aInstance, uint16_t aKey, int aIndex, uint8_t *aValue, uint16_t *aValueLength)
{
    ThreadError error = kThreadError_NotFound;
    uint32_t address = sSettingsBaseAddress + kSettingsFlagSize;
    int index = 0;

    (void)aInstance;

    while (address < (sSettingsBaseAddress + sSettingsUsedSize))
    {
        struct settingsBlock block;

        utilsFlashRead(address, reinterpret_cast<uint8_t *>(&block), sizeof(block));

        if (block.key == aKey)
        {
            if (!(block.flag & kBlockIndex0Flag))
            {
                index = 0;
            }

            if (!(block.flag & kBlockAddCompleteFlag) && (block.flag & kBlockDeleteFlag))
            {
                if (index == aIndex)
                {
                    error = kThreadError_None;

                    if (aValueLength)
                    {
                        *aValueLength = block.length;
                    }

                    if (aValue)
                    {
                        VerifyOrExit(aValueLength, error = kThreadError_InvalidArgs);
                        utilsFlashRead(address + sizeof(struct settingsBlock), aValue, block.length);
                    }
                }

                index++;
            }
        }

        address += (getAlignLength(block.length) + sizeof(struct settingsBlock));
    }

exit:
    return error;
}

ThreadError otPlatSettingsSet(otInstance *aInstance, uint16_t aKey, const uint8_t *aValue, uint16_t aValueLength)
{
    return addSetting(aInstance, aKey, true, aValue, aValueLength);
}

ThreadError otPlatSettingsAdd(otInstance *aInstance, uint16_t aKey, const uint8_t *aValue, uint16_t aValueLength)
{
    uint16_t length;
    bool index0;

    index0 = (otPlatSettingsGet(aInstance, aKey, 0, NULL, &length) == kThreadError_NotFound ? true : false);
    return addSetting(aInstance, aKey, index0, aValue, aValueLength);
}

ThreadError otPlatSettingsDelete(otInstance *aInstance, uint16_t aKey, int aIndex)
{
    ThreadError error = kThreadError_NotFound;
    uint32_t address = sSettingsBaseAddress + kSettingsFlagSize;
    int index = 0;

    (void)aInstance;

    while (address < (sSettingsBaseAddress + sSettingsUsedSize))
    {
        struct settingsBlock block;

        utilsFlashRead(address, reinterpret_cast<uint8_t *>(&block), sizeof(block));

        if (block.key == aKey)
        {
            if (!(block.flag & kBlockIndex0Flag))
            {
                index = 0;
            }

            if (!(block.flag & kBlockAddCompleteFlag) && (block.flag & kBlockDeleteFlag))
            {
                if (aIndex == index || aIndex == -1)
                {
                    error = kThreadError_None;
                    block.flag &= (~kBlockDeleteFlag);
                    utilsFlashWrite(address, reinterpret_cast<uint8_t *>(&block), sizeof(block));
                }

                if (index == 1 && aIndex == 0)
                {
                    block.flag &= (~kBlockIndex0Flag);
                    utilsFlashWrite(address, reinterpret_cast<uint8_t *>(&block), sizeof(block));
                }

                index++;
            }
        }

        address += (getAlignLength(block.length) + sizeof(struct settingsBlock));
    }

    return error;
}

void otPlatSettingsWipe(otInstance *aInstance)
{
    initSettings(sSettingsBaseAddress, static_cast<uint32_t>(kSettingsInUse));
    otPlatSettingsInit(aInstance);
}

#ifdef __cplusplus
};
#endif
