#include "common.h"
#include "zlib.h"
#include "spiflash.h"
#include "esp.h"
#include "matchver.h"
#include "boardinfo_fpga.h"

#if 1
# include "console.h"
# define MSG(...) con_printf(__VA_ARGS__)
#else
# define MSG(...) ((void)0)
#endif

struct spz_stream;
typedef struct spz_stream spz_stream;

#define NBUF 4
struct spz_stream {
    z_stream zs;
    const struct spiflash *flash;
    uint8_t *optr;		    /* Output data pointer into obuf */
    /* Note: available output data ends at zs->next_out */
    union {
	uint8_t *bufs[NBUF];
	struct {
	    uint8_t *ibuf;	    /* Input buffer if compressed */
	    uint8_t *obuf;	    /* Output buffer */
	    uint8_t *dbuf;	    /* Block data buffer */
	    uint8_t *vbuf;	    /* Readback/verify buffer */
	};
    };
    struct fw_header header;	    /* Header of currently processed chunk */
    struct fw_header vmatch;	    /* Matched firmware version string */
    int err;			    /* Error code to return */
    bool eoi;			    /* Reached end of input */
    bool cleanup;		    /* Call inflateEnd() */
};

static void *spz_malloc(spz_stream *spz, size_t bytes)
{
    void *p = malloc(bytes);
    if (!p && !spz->err) {
	spz->err = Z_MEM_ERROR;
    }
    return p;
}

static int spiflash_read_data(spz_stream *spz, void *buf, unsigned int 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 = SPIFLASH_BLOCK_SIZE;

	    while (spz->zs.avail_out) {
		if (!spz->zs.avail_in && !spz->eoi) {
		    int (*read_data)(void *, void *, unsigned int);
		    read_data = spz->flash->read_data;
		    spz->zs.next_in = spz->ibuf;
		    int rlen = read_data(spz->flash->cookie,
					 spz->ibuf, SPIFLASH_BLOCK_SIZE);

		    spz->eoi = rlen < SPIFLASH_BLOCK_SIZE;
		    if (rlen < 0) {
			if (!spz->err)
			    spz->err = rlen;
			rlen = 0;
		    }
		    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 flash, zs.next_in,
 * and zs.avail_in fields.
 */
static int spiflash_data_init(spz_stream *spz)
{
    int rv = Z_OK;
    uint8_t *rdbuf = NULL;
    int rlen;
    uint32_t header_crc;

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

    spz->eoi = !spz->flash->read_data;
    /* gzip, max window size */
    rv = inflateInit2(&spz->zs, 16 + 15);
    if (rv != Z_OK && rv != Z_STREAM_END) {
	spz->err = rv;
	goto err;
    }
    spz->cleanup = true;

err:
    return spz->err;
}

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

    if (!spz)
	return 0;

    err = spz->err;

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

    for (int i = 0; i < NBUF; i++) {
	if (spz->bufs[i])
	    free(spz->bufs[i]);
    }
    return err;
}

/*
 * Set up a command header with an address according to the SPI
 * addressing mode. Returns a pointer to the first byte past the
 * address.
 */
static void *spiflash_setup_addrcmd(const struct spiflash *flash,
				    uint32_t addr,
				    uint8_t cmd24, uint8_t cmd32,
				    void *cmdbuf)
{
    enum spiflash_addr_mode mode = flash->param->addr;
    uint8_t *cmd = cmdbuf;

    if (!mode)
	mode = addr < (1 << 24) ? SPIFLASH_ADDR_24BIT : SPIFLASH_ADDR_32BIT;

    if (mode == SPIFLASH_ADDR_24BIT) {
	*cmd++ = cmd24;
    } else {
	*cmd++ = cmd32;
	*cmd++ = addr >> 24;
    }
    *cmd++ = addr >> 16;
    *cmd++ = addr >> 8;
    *cmd++ = addr;

    return cmd;
}

static int spiflash_get_status(const struct spiflash *flash,
			       uint8_t cmd, uint8_t *sr)
{
    return flash->ops->spi_read(flash->cookie, &cmd, 1, sr, 1,
				flash->param->tshsl);
}

/* This needs a timeout function */
static int spiflash_wait_status(const struct spiflash *flash,
				int delay, uint8_t mask, uint8_t val)
{
    uint8_t sr1;
    int rv;

    do {
	if (flash->ops->yield)
	    flash->ops->yield(flash->cookie, delay);

	rv = spiflash_get_status(flash, ROM_READ_SR1, &sr1);
	if (rv)
	    return rv;
    } while ((sr1 & mask) != val); /* Waiting... */

    return 0;
}

int spiflash_read(const struct spiflash *flash,
		  uint32_t addr, void *buffer, size_t len)
{
    uint8_t cmdbuf[6];
    uint8_t *cmd;

    cmd = spiflash_setup_addrcmd(flash, addr,
				 ROM_FAST_READ, ROM_FAST_READ_32BIT,
				 cmdbuf);
    *cmd++ = 0;			/* Dummy cycles */

    return flash->ops->spi_read(flash->cookie, cmdbuf, cmd - cmdbuf,
				buffer, len, flash->param->tshsl1);
}

static int spiflash_simple_command(const struct spiflash *flash, uint8_t cmd)
{
    return flash->ops->spi_write(flash->cookie, &cmd, 1, NULL, 0,
				 flash->param->tshsl);
}

static int spiflash_write_enable(const struct spiflash *flash)
{
    uint8_t sr1;
    int rv;

    rv = spiflash_wait_status(flash, 0, 1, 0);
    if (rv)
	return rv;

    rv = spiflash_simple_command(flash, ROM_WRITE_ENABLE);
    if (rv)
	return rv;

    return spiflash_wait_status(flash, 0, 3, 2);
}

static int spiflash_program(const struct spiflash *flash,
			    uint32_t addr, const void *buffer, size_t len)
{
    uint8_t cmdbuf[5];
    uint8_t *cmd;
    int rv;

    rv = spiflash_write_enable(flash);
    if (rv)
	return rv;

    cmd = spiflash_setup_addrcmd(flash, addr,
				 ROM_PAGE_PROGRAM, ROM_PAGE_PROGRAM_32BIT,
				 cmdbuf);
    rv = flash->ops->spi_write(flash->cookie, cmdbuf, cmd - cmdbuf,
			       buffer, len, flash->param->tshsl2);
    if (rv)
	return rv;

    return spiflash_wait_status(flash, flash->param->tpp, 3, 0);
}

/*
 * Erase up to (long bits) sectors, using block erase if possible.
 */
static int spiflash_erase(const struct spiflash *flash,
			  uint32_t addr, unsigned long sector_mask)
{
    uint8_t cmdbuf[5];
    uint8_t *cmd;
    uint8_t cmd24, cmd32;
    uint32_t erasesize;
    int rv;
    int delay;
    const uint32_t block_mask = SPIFLASH_BLOCK_SIZE - 1;
    const unsigned long block_sector_mask
	= block_mask >> SPIFLASH_SECTOR_SHIFT;

    if (!sector_mask) {
	MSG("update: nothing to erase\n");
	return 0;
    }

    while (sector_mask) {
	if (!(addr & block_mask) &&
	    ((sector_mask & block_sector_mask) == block_sector_mask)) {
		cmd24 = ROM_ERASE_64K;
		cmd32 = ROM_ERASE_64K_32BIT;
		delay = flash->param->tbe2;
		erasesize = SPIFLASH_BLOCK_SIZE;
	} else {
	    cmd24 = ROM_ERASE_4K;
	    cmd32 = ROM_ERASE_4K_32BIT;
	    delay = flash->param->tse;
	    erasesize = SPIFLASH_SECTOR_SIZE;
	}

	if (sector_mask & 1) {
	    rv = spiflash_write_enable(flash);
	    if (rv)
		return rv;

	    cmd = spiflash_setup_addrcmd(flash, addr, cmd24, cmd32, cmdbuf);
	    rv = flash->ops->spi_write(flash->cookie, cmdbuf, cmd - cmdbuf,
				       NULL, 0, flash->param->tshsl2);
	    if (rv)
		return rv;

	    rv = spiflash_wait_status(flash, delay, 3, 0);
	    if (rv)
		return rv;
	}

	addr += erasesize;
	sector_mask >>= (erasesize >> SPIFLASH_SECTOR_SHIFT);
    }

    MSG("ok\n");
    return 0;
}

/*
 * from: current flash contents
 * to:   desired flash contents
 *
 * These are assumed to be aligned full block buffers
 */
enum flashmem_status {
    FMS_DONE,			/* All done, no programming needed */
    FMS_PROGRAM,		/* Can be programmed */
    FMS_ERASE,			/* Needs erase before programming */
    FMS_NOTCHECKED		/* Not checked yet */
};
static enum flashmem_status
spiflash_memcmp(const void *from, const void *to, size_t len)
{
    const uint32_t *pf = from;
    const uint32_t *pt = to;
    const uint32_t *pfend = (const uint32_t *)((const char *)from + len);
    uint32_t doprog  = 0;
    uint32_t doerase = 0;

    while (pf < pfend) {
	uint32_t f = *pf++;
	uint32_t t = *pt++;

	doprog  |=  f ^ t;	/* Need programming if any data mismatch */
	doerase |= ~f & t;	/* Need erasing if any 0 -> 1 */
    }

    return doerase ? FMS_ERASE : doprog ? FMS_PROGRAM : FMS_DONE;
}

/*
 * Check a block for sectors which need erasing and pages which need
 * programming; the prog_mask is 256 bits long and so span multiple words.
 *
 * The desired input is spz->dbuf and the existing flash content should be
 * already read into spz->vbuf.
 *
 */
static void spiflash_check_block(spz_stream *spz, uint32_t addr,
				uint32_t *erase_mask, uint32_t *prog_mask)
{
    const uint8_t *p, *q;
    unsigned int page;

    *erase_mask = 0;
    memset(prog_mask, 0, SPIFLASH_BLOCK_SIZE/SPIFLASH_PAGE_SIZE/8);

    p = spz->vbuf;
    q = spz->dbuf;

    for (page = 0; page < SPIFLASH_BLOCK_SIZE/SPIFLASH_PAGE_SIZE; page++) {
	enum flashmem_status status;

	switch (spiflash_memcmp(p, q, SPIFLASH_PAGE_SIZE)) {
	case FMS_ERASE:
	    *erase_mask |= UINT32_C(1) <<
		(page >> (SPIFLASH_SECTOR_SHIFT-SPIFLASH_PAGE_SHIFT));
	    break;
	case FMS_PROGRAM:
	    prog_mask[page >> 5] |= UINT32_C(1) << (page & 31);
	    break;
	default:
	    /* Nothing to do! */
	    break;
	}

	p += SPIFLASH_PAGE_SIZE;
	q += SPIFLASH_PAGE_SIZE;
    }
}

static int spiflash_flash_chunk(spz_stream *spz)
{
    unsigned int data_left = spz->header.len;
    unsigned int addr      = spz->header.addr;
    int rv;

    while (data_left && !spz->err) {
	unsigned int pre_padding  = addr & (SPIFLASH_BLOCK_SIZE-1);
	unsigned int post_padding;
	unsigned int bytes;

	bytes = SPIFLASH_BLOCK_SIZE - pre_padding;
	post_padding = 0;
	if (bytes > data_left) {
	    post_padding = bytes - data_left;
	    bytes = data_left;
	}
	addr -= pre_padding;

	/* Read the current content of this block into vbuf */
	rv = spiflash_read(spz->flash, addr, spz->vbuf, SPIFLASH_BLOCK_SIZE);
	if (rv)
	    goto err;

	/* Copy any invariant chunk */
	if (pre_padding)
	    memcpy(spz->dbuf, spz->vbuf, pre_padding);
	if (post_padding)
	    memcpy(spz->dbuf+SPIFLASH_BLOCK_SIZE-post_padding,
		   spz->vbuf+SPIFLASH_BLOCK_SIZE-post_padding,
		   post_padding);

	rv = spiflash_read_data(spz, spz->dbuf+pre_padding, bytes);
	if (rv != (int)bytes) {
	    MSG("needed %u bytes got %d, \n", bytes, rv);
	    rv = Z_DATA_ERROR;
	    goto err;
	}

	uint32_t erase_mask;
	uint32_t prog_mask[SPIFLASH_BLOCK_SIZE >> (SPIFLASH_PAGE_SHIFT+5)];

	spiflash_check_block(spz, addr, &erase_mask, prog_mask);
	if (erase_mask) {
	    MSG("flash: erasing at 0x%06x mask %04x... ", addr, erase_mask);

	    rv = spiflash_erase(spz->flash, addr, erase_mask);
	    if (rv)
		goto err;

	    /* Verify that the sector did erase */
	    rv = spiflash_read(spz->flash, addr, spz->vbuf, SPIFLASH_BLOCK_SIZE);
	    if (rv) {
		MSG("readback ");
		goto err;
	    }

	    spiflash_check_block(spz, addr, &erase_mask, prog_mask);
	    if (erase_mask) {
		spz->err = FWUPDATE_ERR_ERASE_FAILED;
		MSG("%04x left, ", erase_mask);
		goto err;
	    }
	    MSG("ok\n");
	}

	unsigned int page;
	bool programmed = false;

	for (page = 0; page < (SPIFLASH_BLOCK_SIZE >> SPIFLASH_PAGE_SHIFT);
	     page++) {
	    uint32_t page_offs = page << SPIFLASH_PAGE_SHIFT;

	    if (!(prog_mask[page >> 5] & (UINT32_C(1) << (page & 31))))
		continue;	/* No need to program */

	    programmed = true;

	    udelay(100);
	    MSG("flash: writing at 0x%06x... ", addr + page_offs);

	    rv = spiflash_program(spz->flash, addr + page_offs,
				  spz->dbuf + page_offs,
				  SPIFLASH_PAGE_SIZE);
	    if (rv)
		goto err;

	    /* Verify that the page did write */
	    rv = spiflash_read(spz->flash, addr + page_offs,
			       spz->vbuf + page_offs,
			       SPIFLASH_PAGE_SIZE);
	    if (rv) {
		MSG("readback ");
		goto err;
	    }

	    if (memcmp(spz->dbuf + page_offs, spz->vbuf + page_offs,
		       SPIFLASH_PAGE_SIZE)) {
		MSG("verify ");
		spz->err = FWUPDATE_ERR_PROGRAM_FAILED;
		goto err;
	    }
	    MSG("ok\n");
	}
	if (programmed)
	    MSG("ok\n");
	else
	    MSG("unchanged\n");

	addr += pre_padding + bytes;
	data_left -= bytes;
    }
    return spz->err;

err:
    MSG("failed\n");

    if (!spz->err)
	spz->err = rv;

    return spz->err;
}

/* Serial Flash Discoverable Parameter Table, see JESD216 */
static int spiflash_get_sfdp(const struct spiflash *flash, void *sfdp)
{
    static const uint8_t cmd_read_sfdp[] = { ROM_READ_SFDP, 0, 0, 0, 0 };

    return flash->ops->spi_read(flash->cookie, cmd_read_sfdp,
				sizeof cmd_read_sfdp, sfdp, SPIFLASH_SFDP_SIZE,
				flash->param->tshsl);
}

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

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

    rv = spiflash_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 spiflash_skip_chunk(spz_stream *spz)
{
    unsigned int skip = spz->header.len;

    while (skip) {
	unsigned int block = min(skip, SPIFLASH_BLOCK_SIZE);
	int rv = spiflash_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;
}

/* Read a chunk into malloc()'d storage */
static int spiflash_load_chunk(spz_stream *spz, void **dptr)
{
    void *data;
    int len = spz->header.len;

    *dptr = NULL;
    data = spz_malloc(spz, len);
    if (!data) {
	spiflash_skip_chunk(spz);
	return spz->err;
    } else {
	int rv = spiflash_read_data(spz, data, len);
	if (!spz->err && rv != len)
	    spz->err = Z_DATA_ERROR;
	if (spz->err) {
	    free(data);
	    return spz->err;
	}
	*dptr = data;
	return rv;
    }
}

static int esp_ota_chunk(spz_stream *spz)
{
    void *data;
    int len = spiflash_load_chunk(spz, &data);
    if (data) {
	esp_ota(data, len);
	free(data);
    }
    return spz->err;
}

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

    rv = spiflash_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("update: 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 spiflash_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 */
	    goto skip;
	}
    }

    con_printf("update: chunk type %u size %u addr 0x%08x\n",
	       spz->header.type, spz->header.len, spz->header.addr);

    switch (spz->header.type) {
    case FDT_END:
	return Z_STREAM_END;	/* End of data - not an error */
    case FDT_DATA:
	if (!spz->flash->ops)
	    goto skip;
	return spiflash_flash_chunk(spz);
    case FDT_TARGET:
    {
	bool match;
	str = spiflash_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("update: firmware file supports: %s%s\n",
	    str, match ? " (match)" : "");
	return Z_OK;
    }
    case FDT_NOTE:
	str = spiflash_read_chunk_str(spz);
	MSG("update: %s\n", str);
	break;
    case FDT_ESP_OTA:
	if (!spz->flash->ops)
	    goto skip;
	return esp_ota_chunk(spz);
    case FDT_FPGA_INIT:
    case FDT_ESP_PART:
    case FDT_ESP_SYS:
    case FDT_ESP_TOOL:
    case FDT_BOARDINFO:
	/* Used only when flashing from ESP32 */
	goto skip;
    default:
	if (spz->header.flags & FDF_OPTIONAL)
	    goto skip;

	MSG("update: unknown chunk type: %u\n", spz->header.type);
	return spz->err = Z_DATA_ERROR;
    }
    return spz->err;

skip:
    return spiflash_skip_chunk(spz);
}

int spiflash_flash_file(const struct spiflash *flash, void *buf, size_t buflen)
{
    spz_stream _spz;
    spz_stream * const spz = &_spz; /* For consistency in notation */
    int err = 0;

    memset(spz, 0, sizeof *spz);
    spz->zs.avail_in = buflen;
    spz->zs.next_in  = buf;
    spz->flash = flash;

    err = spiflash_data_init(spz);
    if (err)
	return err;

    while (!spiflash_process_chunk(spz)) {
	/* Process data chunks until end */
    }

    err = spiflash_data_cleanup(spz);
    if (err)
	MSG("failed (err %d)\n", err);

    return err;
}

/*
 * Read unique serial number from flash. Note: returns id in
 * bigendian ("network") byte order.
 */
int spiflash_read_id(const struct spiflash *flash, void *id)
{
    static const uint8_t read_unique_id[] = { ROM_READ_UNIQUE_ID, 0, 0, 0, 0 };

    return flash->ops->spi_read(flash->cookie, read_unique_id,
				sizeof read_unique_id,
				id, SPIFLASH_ID_LEN, flash->param->tshsl);
}

/*
 * Read vendor and device ID from flash.
 */
int spiflash_read_vdid(const struct spiflash *flash, void *vdid)
{
    static const uint8_t read_vdid[] = { ROM_MANUFACTURER_DEVICE_ID, 0, 0, 0 };

    return flash->ops->spi_read(flash->cookie, read_vdid,
				sizeof read_vdid,
				vdid, SPIFLASH_VDID_LEN, flash->param->tshsl);
}

/*
 * Write an absolute region to flash
 */
int spiflash_flash_data(const struct spiflash *flash, uint32_t addr,
			const void *data, size_t len)
{
    spz_stream _spz;
    spz_stream * const spz = &_spz; /* For consistency in notation */
    int err = 0;

    memset(spz, 0, sizeof *spz);

    /* No ibuf or obuf */
    for (int i = 2; i < NBUF; i++) {
	spz->bufs[i] = spz_malloc(spz, SPIFLASH_BLOCK_SIZE);
	if (!spz->bufs[i])
		goto err;
    }

    spz->flash       = flash;
    spz->optr        = (uint8_t *)data; /* OK to lose const here */
    spz->obuf        = (uint8_t *)data; /* OK to lose const here */
    spz->zs.next_out = (uint8_t *)data + len;
    spz->eoi         = true;
    spz->header.len  = len;
    spz->header.addr = addr;

    spiflash_flash_chunk(spz);

err:
    for (int i = 2; i < NBUF; i++)
	if (spz->bufs[i])
	    free(spz->bufs[i]);

    return spz->err;
}
