/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id:
 *
 * Copyright © 2009 Michael Sparmann
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#include "config.h"
#include "system.h"
#include "s5l87xx.h"
#include "spi-s5l8702.h"
#include "crypto-s5l8702.h"
#include "norboot-target.h"

static void bootflash_ce(int port, bool state)
{
    spi_ce(port, state);
}

void bootflash_init(int port)
{
    spi_init(port, true);
    bootflash_ce(port, false);
}

void bootflash_close(int port)
{
    spi_init(port, false);
}

static void bootflash_wait_ready(int port)
{
    while (true)
    {
        bootflash_ce(port, true);
        spi_write(port, 5);
        if (!(spi_write(port, 0xff) & 1)) break;
        bootflash_ce(port, false);
    }
    bootflash_ce(port, false);
}

static void bootflash_enable_writing(int port, bool state)
{
    if (!state)
    {
        bootflash_ce(port, true);
        spi_write(port, 4);
        bootflash_ce(port, false);
    }
    bootflash_ce(port, true);
    spi_write(port, 0x50);
    bootflash_ce(port, false);
    bootflash_ce(port, true);
    spi_write(port, 1);
    spi_write(port, state ? 0 : 0x1c);
    bootflash_ce(port, false);
    if (state)
    {
        bootflash_ce(port, true);
        spi_write(port, 6);
        bootflash_ce(port, false);
    }
}

void bootflash_read(int port, uint32_t addr, uint32_t size, void* buf)
{
    spi_prepare(port);
    bootflash_wait_ready(port);
    bootflash_ce(port, true);
    spi_write(port, 3);
    spi_write(port, (addr >> 16) & 0xff);
    spi_write(port, (addr >> 8) & 0xff);
    spi_write(port, addr & 0xff);
    spi_read(port, size, buf);
    bootflash_ce(port, false);
    spi_release(port);
}

static void bootflash_write_internal(int port, uint32_t addr, uint32_t size, void* buf)
{
    uint8_t* buffer = (uint8_t*)buf;
    bool first = true;
    spi_prepare(port);
    bootflash_wait_ready(port);
    bootflash_enable_writing(port, true);
    while (size)
    {
        bootflash_ce(port, true);
        spi_write(port, 0xad);
        if (first)
        {
            spi_write(port, (addr >> 16) & 0xff);
            spi_write(port, (addr >> 8) & 0xff);
            spi_write(port, addr & 0xff);
            first = false;
        }
        spi_write(port, *buffer++);
        spi_write(port, *buffer++);
        bootflash_ce(port, false);
        bootflash_wait_ready(port);
        size -= 2;
    }
    bootflash_enable_writing(port, false);
    spi_release(port);
}

static void bootflash_erase_internal(int port, uint32_t addr)
{
    spi_prepare(port);
    bootflash_wait_ready(port);
    bootflash_enable_writing(port, true);
    bootflash_ce(port, true);
    spi_write(port, 0x20);
    spi_write(port, (addr >> 16) & 0xff);
    spi_write(port, (addr >> 8) & 0xff);
    spi_write(port, addr & 0xff);
    bootflash_ce(port, false);
    bootflash_enable_writing(port, false);
    spi_release(port);
}

void bootflash_erase_blocks(int port, int first, int n)
{
    uint32_t offset = first << 12;
    while (n--)
    {
        bootflash_erase_internal(port, offset);
        offset += 0x1000;
    }
}

int bootflash_compare(int port, int offset, void* addr, int size)
{
    int i;
    int result = 0;
    uint8_t buf[32];
    spi_prepare(port);
    bootflash_wait_ready(port);
    bootflash_ce(port, true);
    spi_write(port, 3);
    spi_write(port, (offset >> 16) & 0xff);
    spi_write(port, (offset >> 8) & 0xff);
    spi_write(port, offset & 0xff);
    while (size > 0)
    {
        spi_read(port, MIN((int)sizeof(buf), size), buf);
        if (memcmp((uint8_t*)addr, buf, MIN((int)sizeof(buf), size)))
            result |= 1;
        for (i = 0; i < MIN((int)sizeof(buf), size); i += 2)
            if (buf[i] != 0xff) result |= 2;
        addr = (void*)(((uint32_t)addr) + sizeof(buf));
        size -= sizeof(buf);
    }
    bootflash_ce(port, false);
    spi_release(port);
    return result;
}

void bootflash_write(int port, int offset, void* addr, int size)
{
    int i;
    bool needswrite;
    while (size > 0)
    {
        int remainder = MIN(0x1000 - (offset & 0xfff), size);
        int contentinfo = bootflash_compare(port, offset, addr, remainder);
        if (contentinfo & 1)
        {
            if (contentinfo & 2)
                bootflash_erase_internal(port, offset & ~0xfff);
            needswrite = false;
            for (i = 0; i < remainder; i += 1)
                if (((uint8_t*)addr)[i] != 0xff)
                {
                    needswrite = true;
                    break;
                }
            if (needswrite)
                bootflash_write_internal(port, offset, remainder, addr);
        }
        addr = (void*)(((uint32_t)addr) + remainder);
        offset += remainder;
        size -= remainder;
    }
}

/*
 * SysCfg
 */
ssize_t syscfg_read(struct SysCfg* syscfg)
{
    const size_t syscfg_hdr_size = sizeof(struct SysCfgHeader);
    const size_t syscfg_entry_size = sizeof(struct SysCfgEntry);

    bootflash_init(SPI_PORT);
    bootflash_read(SPI_PORT, 0, syscfg_hdr_size, &syscfg->header);

    if (syscfg->header.magic != SYSCFG_MAGIC) {
        bootflash_close(SPI_PORT);
        return -1;
    }

    const size_t calculated_syscfg_size = syscfg_hdr_size + syscfg_entry_size * syscfg->header.num_entries;

    if (syscfg->header.size != calculated_syscfg_size) {
        bootflash_close(SPI_PORT);
        return calculated_syscfg_size;
    }

    const size_t syscfg_num_entries = MIN(syscfg->header.num_entries, SYSCFG_MAX_ENTRIES);
    const size_t syscfg_entries_size = syscfg_entry_size * syscfg_num_entries;

    bootflash_read(SPI_PORT, syscfg_hdr_size, syscfg_entries_size, &syscfg->entries);
    bootflash_close(SPI_PORT);

    return 0;
}

/*
 * IM3
 */
static inline uint32_t get_uint32le(unsigned char *p)
{
    return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24);
}

// TODO: rename to bootflash_im3_sz(), norboot_im3_sz() or nb_im3_sz()
/* return full IM3 size aligned to NOR sector size */
unsigned im3_nor_sz(struct Im3Info* hinfo)
{
    return ALIGN_UP(IM3HDR_SZ + get_uint32le(hinfo->data_sz), 0x1000);
}

// TODO: norboot_im3_read(), on s5l8720 we could set nandboot_im3_read(), then norboot_ and nandboot_ could go to boot-s5l8702.c
int im3_read(uint32_t offset, struct Im3Info *hinfo, void *fw_addr)
{
    unsigned char hash[SIGN_SZ];
    uint32_t fw_size;

    /* header */
    bootflash_init(SPI_PORT);
    bootflash_read(SPI_PORT, offset, IM3HDR_SZ, hinfo);
    bootflash_close(SPI_PORT);

    if (memcmp(hinfo, IM3_IDENT, 4) != 0)
        return -1; /* OF not found */

    im3_sign(HWKEYAES_UKEY, hinfo, IM3INFOSIGN_SZ, hash);
    if (memcmp(hash, hinfo->info_sign, SIGN_SZ) != 0)
        return -2; /* corrupt header */

    fw_size = get_uint32le(hinfo->data_sz);
    if ((fw_size > NORBOOT_MAXSZ - IM3HDR_SZ) ||
                (get_uint32le(hinfo->entry) > fw_size))
        return -3; /* wrong info */

    if (hinfo->enc_type != 1 && hinfo->enc_type != 2)
        return -4; /* encrypt type not supported */

    if (fw_addr)
    {
        /* body */
        bootflash_init(SPI_PORT);
        bootflash_read(SPI_PORT, offset + IM3HDR_SZ, fw_size, fw_addr);
        bootflash_close(SPI_PORT);
        if (hinfo->enc_type == 1)
            im3_crypt(HWKEYAES_DECRYPT, hinfo, fw_addr);
        im3_sign(HWKEYAES_UKEY, fw_addr, fw_size, hash);
        if (memcmp(hash, hinfo->u.enc12.data_sign, SIGN_SZ) != 0)
            return -5; /* corrupt data */
    }

    return 0;
}

bool im3_write(int offset, void *im3_addr)
{
    bool res;
    struct Im3Info *hinfo = (struct Im3Info *)im3_addr;
    uint32_t im3_size = get_uint32le(hinfo->data_sz) + IM3HDR_SZ;
    bootflash_init(SPI_PORT);
    bootflash_write(SPI_PORT, offset, im3_addr, im3_size);
    /* check if the IM3 image was written correctly */
    res = !(bootflash_compare(SPI_PORT, offset, im3_addr, im3_size) & 1);
    bootflash_close(SPI_PORT);
    return res;
}


/*
 * flsh FS
 */
unsigned flsh_get_unused(void)
{
    unsigned tail = DIR_OFF;
    bootflash_init(SPI_PORT);
    for (int i = 0; i < MAX_ENTRY; i++)
    {
        image_t entry;
        bootflash_read(SPI_PORT, DIR_OFF + ENTRY_SZ*i, ENTRY_SZ, &entry);
        if (memcmp(entry.type, "hslf", 4))
            break; /* no more entries */
        if (entry.devOffset < tail)
            tail = entry.devOffset;
    }
    bootflash_close(SPI_PORT);
    return tail - (NORBOOT_OFF + NORBOOT_MAXSZ);
}

int flsh_find_file(char *name, image_t *entry)
{
    int found = 0;
    bootflash_init(SPI_PORT);
    for (int off = 0; off < ENTRY_SZ*MAX_ENTRY; off += ENTRY_SZ)
    {
        bootflash_read(SPI_PORT, DIR_OFF+off, ENTRY_SZ, entry);
        if (memcmp(&(entry->type), "hslf", 4))
            break; /* no more entries */
        if (!memcmp(&(entry->id), name, 4)) {
            found = 1;
            break;
        }
    }
    bootflash_close(SPI_PORT);
    return found;
}

#define KEYIDX_OFF      0x8 /* TBC */
#define DATALEN_OFF     0x14
#define DATASHA_OFF     0x1c
#define HEADSHA_OFF     0x1d4

int flsh_load_file(image_t *entry, void *hdr, void *data)
{
    uint8_t orig_hash[SHA1_SZ];
    uint8_t calc_hash[SHA1_SZ];
    uint32_t data_len;

    /* header */
    bootflash_init(SPI_PORT);
    bootflash_read(SPI_PORT, entry->devOffset, FILEHDR_SZ, hdr);
    bootflash_close(SPI_PORT);
    memcpy(orig_hash, hdr + HEADSHA_OFF, SHA1_SZ);
    memset(hdr+HEADSHA_OFF, 0, SHA1_SZ);
    sha1(hdr, FILEHDR_SZ, calc_hash);
    if (memcmp(calc_hash, orig_hash, SHA1_SZ))
        return -1; /* corrupt header */
    memcpy(hdr+HEADSHA_OFF, orig_hash, SHA1_SZ);
    data_len = ALIGN_UP(*(uint32_t*)(hdr+DATALEN_OFF), 0x10);
    if (data_len > ALIGN_UP(entry->len, 0x10))
        return -2; /* bad size */

    if (data)
    {
        bootflash_init(SPI_PORT);
        bootflash_read(SPI_PORT, entry->devOffset+FILEHDR_SZ, data_len, data);
        bootflash_close(SPI_PORT);
        hwkeyaes(HWKEYAES_DECRYPT, *(uint8_t*)(hdr+KEYIDX_OFF), data, data_len);
        sha1(data, data_len, calc_hash);
        if (memcmp(calc_hash, hdr+DATASHA_OFF, SHA1_SZ))
            return -3; /* corrupt data */
    }

    return 0;
}
