#define MODULE "fwupdate"
#define DEBUG 1

#include "common.h"
#include "jtag.h"
#include "spiflash.h"
#include "fpga.h"
#include "ota.h"
#include "spz.h"
#include "httpd.h"
#include "fw.h"
#include "boardinfo_esp.h"
#include "matchver.h"

#include <unzipLIB.h>
#include <zlib.h>

/* Needed for struct inflate_state, due to unziplib hacks */
#include <zutil.h>
#include <inftrees.h>
#include <inflate.h>
#ifndef local
# define local static
#endif

#define BUFFER_SIZE		SPIFLASH_SECTOR_SIZE
#define FWUPDATE_STACK		8192
#define FWUPDATE_PRIORITY	3

static inline void spz_heap_info(bool always)
{
    if (DEBUG || always)
	heap_info();
}

static void *spz_malloc(void *opaque, unsigned int size)
{
    spz_stream *spz = opaque;
    MSG("spz_malloc(%u) = ", size);
    void *p = malloc(size);
    CMSG("%p\n", p);
    spz_heap_info(!p);
    if (!p) {
	spz->err = Z_MEM_ERROR;
	printf("[FWUP] Out of memory error!\n");
    }
    return p;
}
static void *spz_calloc(void *opaque, unsigned int items, unsigned int size)
{
    size_t bytes = items*size;
    void *p = spz_malloc(opaque, bytes);
    if (likely(p))
	memset(p, 0, bytes);

    return p;
}

static void spz_free(void *opaque, void *ptr)
{
    MSG("spz_free(%p)\n", ptr);
    (void)opaque;
    free(ptr);
    spz_heap_info(false);
}

int spz_read_data(spz_stream *spz, void *buf, size_t len)
{
    uint8_t *p = buf;

    while (len) {
	unsigned int avail = spz->zs.next_out - spz->optr;

	if (spz->err)
	    break;

	if (avail) {
	    if (avail > len)
		avail = len;

	    memcpy(p, spz->optr, avail);
	    p += avail;
	    spz->optr += avail;
	    len -= avail;
	} else {
	    spz->optr = spz->zs.next_out = spz->obuf;
	    spz->zs.avail_out = BUFFER_SIZE;

	    while (spz->zs.avail_out) {
		if (!spz->zs.avail_in && !spz->eoi) {
		    int rlen;

		    spz->zs.next_in = spz->ibuf;
		    rlen = spz->read_data(spz->token, spz->ibuf, BUFFER_SIZE);
		    if (rlen < 0) {
			if (!spz->err)
			    spz->err = rlen;
			rlen = 0;
		    }
		    spz->eoi = !rlen;
		    spz->zs.avail_in = rlen;
		}

		int rv = inflate(&spz->zs, Z_SYNC_FLUSH);
		if (rv == Z_OK || (rv == Z_BUF_ERROR && !spz->eoi))
		    continue;

		spz->eoi = true;
		if (rv != Z_STREAM_END && !spz->err)
		    spz->err = rv;
		break;
	    }
	}
    }
    return p - (uint8_t *)buf;
}

/*
 * spz needs to be initialized to zero except the read_data and cookie
 * fields.
 */
static int fwupdate_data_init(spz_stream *spz)
{
    spz->zs.zalloc = spz_calloc;
    spz->zs.zfree  = spz_free;
    spz->zs.opaque = spz;	/* For error reporting */
    spz->err = Z_OK;

    /* This is necessary due to unziplib damage */
    spz->zs.state = spz_calloc(spz, 1, sizeof(struct inflate_state));
    if (!spz->zs.state)
	goto err;

    for (int i = 0; i < SPZ_NBUF; i++) {
	spz->bufs[i] = spz_malloc(spz, BUFFER_SIZE);
	if (!spz->bufs[i])
	    goto err;
    }

    /* gzip, max window size */
    int rv = inflateInit2(&spz->zs, 16 + 15);
    printf("[FWUP] fwupdate_data_init: inflateInit2 returned %d\n", rv);
    if (rv != Z_OK && rv != Z_STREAM_END) {
	spz->err = rv;
	goto err;
    }
    spz->cleanup = true;

err:
    return spz->err;
}

static int fwupdate_data_cleanup(spz_stream *spz)
{
    int err = 0;

    if (!spz)
	return 0;

    err = spz->err;

    if (spz->cleanup)
	inflateEnd(&spz->zs);

    /* Don't reload the FPGA on error; it wedges the JTAG bus */
    if (spz->fpga_updated && !err)
	fpga_reset();

    for (int i = 0; i < SPZ_NBUF; i++) {
	if (spz->bufs[i])
	    free(spz->bufs[i]);
    }
    if (spz->zs.state)
	free(spz->zs.state);

    return err;
}

/*
 * Blash a full chunk of data as a JTAG SHIFT_DR transaction
 */
int jtag_shift_spz(spz_stream *spz, enum jtag_io_flags flags)
{
    unsigned int data_left = spz->header.len;
    int err = 0;

    if (!data_left)
	return 0;

    while (data_left) {
	unsigned int bytes = data_left;
	int rv;

	if (bytes > BUFFER_SIZE)
	    bytes = BUFFER_SIZE;

	rv = spz_read_data(spz, spz->dbuf, bytes);
	if (rv < 1) {
	    err = Z_DATA_ERROR;
	    break;
	}

	data_left -= rv;
	jtag_io(rv << 3, data_left ? 0 : flags, spz->dbuf, NULL);
    }

    return err;
}

static void *fwupdate_read_chunk_str(spz_stream *spz)
{
    int rv;

    if (spz->header.len >= BUFFER_SIZE) {
	spz->err = Z_DATA_ERROR;
	return NULL;
    }

    rv = spz_read_data(spz, spz->dbuf, spz->header.len);
    if (spz->err) {
	return NULL;
    }
    if (rv != (int)spz->header.len) {
	spz->err = Z_DATA_ERROR;
	return NULL;
    }
    spz->dbuf[spz->header.len] = '\0';
    return spz->dbuf;
}

/* Skip a data chunk */
static int fwupdate_skip_chunk(spz_stream *spz)
{
    unsigned int skip = spz->header.len;

    while (skip) {
	unsigned int block = skip;
	if (block > BUFFER_SIZE)
	    block = BUFFER_SIZE;

	int rv = spz_read_data(spz, spz->dbuf, block);
	if (spz->err)
	    return spz->err;
	if (rv != (int)block) {
	    return spz->err = Z_DATA_ERROR;
	}
	skip -= block;
    }

    return 0;
}

static int fwupdate_boardinfo(spz_stream *spz)
{
    uint8_t *board_info_data = spz_malloc(spz, BOARDINFO_SIZE);
    int rv = Z_OK;

    MSG("updating FPGA board_info\n");

    if (!board_info_data)
	return spz->err;

    if (board_info.len >= 16 &&
	board_info.len <= sizeof board_info &&
	board_info.len <= BOARDINFO_SIZE) {
	memcpy(board_info_data, &board_info, board_info.len);
	memset(board_info_data + board_info.len, 0xff,
	       BOARDINFO_SIZE - board_info.len);

	rv = spiflash_write_spz(spz, board_info_data, BOARDINFO_SIZE);
    }

    free(board_info_data);
    fwupdate_skip_chunk(spz);
    if (rv)
	spz->err = rv;
    return spz->err;
}

/* Get a piece of the chunk header */
static int fwupdate_get_header_data(spz_stream *spz, void *buf, size_t len)
{
    int rv;

    rv = spz_read_data(spz, buf, len);
    if (spz->err)
	return spz->err;
    else if (!rv)
	return Z_STREAM_END;
    else if (rv != len)
	return spz->err = Z_STREAM_ERROR;
    else
	return Z_OK;
}

/* Get and validate a chunk header */
static int fwupdate_get_header(spz_stream *spz)
{
    struct fw_header * const hdr = &spz->header;
    uint8_t *hptr = (uint8_t *)hdr;
    int rv;
    unsigned int hlen;

    memset(hdr, 0, sizeof *hdr);
    hdr->vmax = -1;

    rv = fwupdate_get_header_data(spz, hptr, FW_HDR_LEN_V1);
    if (rv)
	return rv;

    switch (hdr->magic) {
    case FW_MAGIC_V1:
	hlen = FW_HDR_LEN_V1;
	break;

    case FW_MAGIC_V2:
	hlen = FW_HDR_LEN_V2;
	break;

    default:
	MSG("bad chunk header magic 0x%08x\n", hdr->magic);
	hlen = 0;
	rv = Z_DATA_ERROR;
	break;
    }

    if (hlen > FW_HDR_LEN_V1) {
	rv = fwupdate_get_header_data(spz, hptr + FW_HDR_LEN_V1,
				      hlen - FW_HDR_LEN_V1);

	if (rv == Z_STREAM_END)	/* Only valid for the first chunk */
	    rv = Z_STREAM_ERROR;
    }

    return spz->err = rv;
}

/* Process a data chunk; return a nonzero value if done */
static int fwupdate_process_chunk(spz_stream *spz)
{
    int rv;
    char *str;

    rv = fwupdate_get_header(spz);
    if (rv)
	return rv;

    if (spz->header.type != FDT_NOTE &&
	spz->header.type != FDT_TARGET &&
	spz->header.type != FDT_END &&
	!(spz->header.flags & FDF_PRETARGET)) {
	if (!spz->vmatch.magic) {
	    /* No matching firmware target support */
	    return spz->err = FWUPDATE_ERR_NOT_MINE;
	}

	if (spz->header.vmin > spz->vmatch.vmax ||
	    spz->header.vmax < spz->vmatch.vmin ||
	    ((spz->header.vmatch ^ spz->vmatch.vmatch) & spz->header.vmask)) {
	    /* Chunk not applicable to this target */
	    return fwupdate_skip_chunk(spz);
	}
    }

    switch (spz->header.type) {
    case FDT_END:
	return Z_STREAM_END;	/* End of data - not an error */
    case FDT_DATA:
	MSG("updating FPGA flash\n");
	return spiflash_write_spz(spz, NULL, 0);
    case FDT_BOARDINFO:
	return fwupdate_boardinfo(spz);
    case FDT_TARGET:
    {
	bool match;
	str = fwupdate_read_chunk_str(spz);
	match = match_version(board_info.version_str, str);
	if (match || spz->header.magic == FW_MAGIC_V1)
	    spz->vmatch = spz->header;

	MSG("firmware file supports: %s%s\n",
	    str, match ? " (match)" : "");
	return Z_OK;
    }
    case FDT_NOTE:
	str = fwupdate_read_chunk_str(spz);
	MSG("%s\n", str);
	return Z_OK;
    case FDT_ESP_OTA:
	MSG("updating ESP32... ");
	spz->esp_updated = true;
	rv = esp_update((read_func_t)spz_read_data, (token_t)spz,
			spz->header.len);
	CMSG("done.\n");
	return rv;
    case FDT_FPGA_INIT:
	MSG("initializing FPGA for flash programming... ");
	spz->fpga_updated = true;
	rv = fpga_program_spz(spz);
	CMSG("done\n");
	return rv;
    case FDT_ESP_PART:
    case FDT_ESP_SYS:
    case FDT_ESP_TOOL:
	/* Not applicable to this update method */
	return fwupdate_skip_chunk(spz);
    default:
	if (spz->header.flags & FDF_OPTIONAL) {
	    return fwupdate_skip_chunk(spz);
	} else {
	    MSG("unknown chunk type: %u\n", spz->header.type);
	    return spz->err = Z_DATA_ERROR;
	}
    }
}

const char *firmware_errstr(int err)
{
    static char unknown_err[32];
    static const char * const errstr[] = {
	[-Z_STREAM_ERROR]	         = "Decompression error",
	[-Z_DATA_ERROR]		         = "Invalid data stream",
	[-Z_MEM_ERROR]		         = "Out of memory",
	[-Z_BUF_ERROR]		         = "Decompression error",
	[-FWUPDATE_ERR_IN_PROGRESS]      =
	    "Firmware update already in progress",
	[-FWUPDATE_ERR_BAD_CHUNK]        = "Invalid firmware chunk header",
	[-FWUPDATE_ERR_ERASE_FAILED]     = "FPGA flash erase failed",
	[-FWUPDATE_ERR_PROGRAM_FAILED]   = "FGPA flash program failed",
	[-FWUPDATE_ERR_WRITE_PROTECT]    = "FPGA flash write protected",
	[-FWUPDATE_ERR_NOT_READY]        = "FPGA flash stuck at not ready",
	[-FWUPDATE_ERR_FPGA_JTAG]        =
	    "FPGA JTAG bus stuck, check for JTAG adapter or power cycle board",
	[-FWUPDATE_ERR_FPGA_MISMATCH]    =
	    "Bad FPGA IDCODE, check for JTAG adapter or power cycle board",
	[-FWUPDATE_ERR_FPGA_FAILED]      = "FPGA reboot failed",
	[-FWUPDATE_ERR_UNKNOWN]          = "Unidentified error",
	[-FWUPDATE_ERR_ESP_NO_PARTITION] = "No available ESP partition",
	[-FWUPDATE_ERR_ESP_BAD_OTA]      = "ESP OTA information corrupt",
	[-FWUPDATE_ERR_ESP_FLASH_FAILED] = "ESP flash program failed",
	[-FWUPDATE_ERR_ESP_BAD_DATA]     = "ESP firmware image corrupt",
	[-FWUPDATE_ERR_CONFIG_READ]      = "Configuration upload failure",
	[-FWUPDATE_ERR_CONFIG_SAVE]      = "Error saving configuration",
	[-FWUPDATE_ERR_NOT_MINE]         = "Firmware file is not compatible"
    };

    switch (err) {
    case Z_OK:
	return errstr[-FWUPDATE_ERR_UNKNOWN];
    case Z_ERRNO:
	return strerror(errno);
    case -ARRAY_SIZE(errstr)+1 ... Z_STREAM_ERROR:
	if (errstr[-err])
	    return errstr[-err];
	/* fall through */
    default:
	snprintf(unknown_err, sizeof unknown_err, "error %d", -err);
	return unknown_err;
    }
}

static TaskHandle_t fwupdate_task;
static spz_stream *fwupdate_spz;
static SemaphoreHandle_t fwupdate_done;
static bool do_reboot;

static void firmware_update_task(void *pvt)
{
    spz_stream *spz = pvt;

    fpga_service_enable(false);

    printf("[FWUP] fwupdate_data_init()\n");

    spz->err = fwupdate_data_init(spz);
    if (spz->err)
	goto fail;

    printf("[FWUP] fwupdate_process_chunk loop\n");

    int err;
    while (!(err = fwupdate_process_chunk(spz))) {
	/* Process data chunks until end */
    }

    if (!spz->err && err != Z_STREAM_END)
	spz->err = err;

    printf("[FWUP] fwupdate_data_cleanup\n");

    err = fwupdate_data_cleanup(spz);
    if (err)
	spz->err = err;

fail:
    if (spz->err)
	MSG("failed (err %d)\n", spz->err);
    xSemaphoreGive(fwupdate_done);

    if (do_reboot) {
	printf("[FWUP] rebooting in %d seconds\n", reboot_delayed());
	while (1)
	    vTaskSuspend(NULL);
    } else {
	exit_task();
    }
}

static int firmware_update_cleanup(void)
{
    int err = Z_OK;

    fwupdate_task = NULL;

    if (fwupdate_done) {
	SemaphoreHandle_t done = fwupdate_done;
	fwupdate_done = NULL;
	vSemaphoreDelete(done);
    } else {
	err = Z_MEM_ERROR;
    }
    if (fwupdate_spz) {
	struct spz_stream *spz = fwupdate_spz;
	if (spz->err)
	    err = spz->err;
	fwupdate_spz = NULL;
	free(spz);
    } else {
	err = Z_MEM_ERROR;
    }

    return err;
}

int firmware_update_start(read_func_t read_data, token_t token, bool autoreboot)
{
    do_reboot = autoreboot;

    if (fwupdate_spz)
	return FWUPDATE_ERR_IN_PROGRESS;

    fwupdate_spz = calloc(1, sizeof *fwupdate_spz);
    if (!fwupdate_spz)
	goto err;

    fwupdate_spz->read_data = read_data;
    fwupdate_spz->token     = token;

    fwupdate_done = xSemaphoreCreateBinary();
    if (!fwupdate_done)
	goto err;

    if (xTaskCreate(firmware_update_task, "fwupdate",
		    FWUPDATE_STACK, fwupdate_spz,
		    FWUPDATE_PRIORITY, &fwupdate_task) != pdPASS) {
	xSemaphoreGive(fwupdate_done);
    }
    return Z_OK;

err:
    return firmware_update_cleanup();
}

int firmware_update_wait(TickType_t delay)
{
    if (!fwupdate_done)
	return Z_MEM_ERROR;

    if (!xSemaphoreTake(fwupdate_done, delay))
	return FWUPDATE_ERR_IN_PROGRESS;

    return firmware_update_cleanup();
}
