/*
 * <nvram.c>
 *
 * Open Hack'Ware BIOS NVRAM management routines.
 * 
 * Copyright (c) 2004-2005 Jocelyn Mayer
 * 
 *   This program is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU General Public License V2
 *   as published by the Free Software Foundation
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <stdlib.h>
#include <stdio.h>
#include "bios.h"

#define NVRAM_MAX_SIZE 0x2000
#define NVRAM_IO_BASE 0x0074

struct nvram_t {
    uint16_t io_base;
    uint16_t size;
};

/* NVRAM access */
static void NVRAM_set_byte (nvram_t *nvram, uint32_t addr, uint8_t value)
{
    NVRAM_write(nvram, addr, value);
}

static uint8_t NVRAM_get_byte (nvram_t *nvram, uint16_t addr)
{
    return NVRAM_read(nvram, addr);
}

static void NVRAM_set_word (nvram_t *nvram, uint16_t addr, uint16_t value)
{
    NVRAM_write(nvram, addr, value >> 8);
    NVRAM_write(nvram, addr + 1, value);
}

static uint16_t NVRAM_get_word (nvram_t *nvram, uint16_t addr)
{
    uint16_t tmp;

    tmp = NVRAM_read(nvram, addr) << 8;
    tmp |= NVRAM_read(nvram, addr + 1);

    return tmp;
}

static void NVRAM_set_lword (nvram_t *nvram, uint16_t addr, uint32_t value)
{
    NVRAM_write(nvram, addr, value >> 24);
    NVRAM_write(nvram, addr + 1, value >> 16);
    NVRAM_write(nvram, addr + 2, value >> 8);
    NVRAM_write(nvram, addr + 3, value);
}

static uint32_t NVRAM_get_lword (nvram_t *nvram, uint16_t addr)
{
    uint32_t tmp;

    tmp = NVRAM_read(nvram, addr) << 24;
    tmp |= NVRAM_read(nvram, addr + 1) << 16;
    tmp |= NVRAM_read(nvram, addr + 2) << 8;
    tmp |= NVRAM_read(nvram, addr + 3);

    return tmp;
}

static void NVRAM_set_string (nvram_t *nvram, uint32_t addr,
                              const unsigned char *str, uint32_t max)
{
    uint32_t i;

    for (i = 0; i < max && str[i] != '\0'; i++) {
        NVRAM_write(nvram, addr + i, str[i]);
    }
    NVRAM_write(nvram, addr + i, '\0');
}

static int NVRAM_get_string (nvram_t *nvram, uint8_t *dst,
                             uint16_t addr, int max)
{
    int i;

    memset(dst, 0, max);
    for (i = 0; i < max; i++) {
        dst[i] = NVRAM_get_byte(nvram, addr + i);
        if (dst[i] == '\0')
            break;
    }

    return i;
}

static uint16_t NVRAM_crc_update (uint16_t prev, uint16_t value)
{
    uint16_t tmp;
    uint16_t pd, pd1, pd2;

    tmp = prev >> 8;
    pd = prev ^ value;
    pd1 = pd & 0x000F;
    pd2 = ((pd >> 4) & 0x000F) ^ pd1;
    tmp ^= (pd1 << 3) | (pd1 << 8);
    tmp ^= pd2 | (pd2 << 7) | (pd2 << 12);

    return tmp;
}

static uint16_t NVRAM_compute_crc (nvram_t *nvram,
                                   uint32_t start, uint32_t count)
{
    uint32_t i;
    uint16_t crc = 0xFFFF;
    int odd;

    odd = count & 1;
    count &= ~1;
    for (i = 0; i != count; i++) {
        crc = NVRAM_crc_update(crc, NVRAM_get_word(nvram, start + i));
    }
    if (odd) {
        crc = NVRAM_crc_update(crc, NVRAM_get_byte(nvram, start + i) << 8);
    }

    return crc;
}

/* Format NVRAM for PREP target */
static int NVRAM_prep_format (nvram_t *nvram)
{
#define NVRAM_PREP_OSAREA_SIZE 512
#define NVRAM_PREP_CONFSIZE    1024
    uint16_t crc;
    
    /* NVRAM header */
    /* 0x00: NVRAM size in kB */
    NVRAM_set_word(nvram, 0x00, nvram->size >> 10);
    /* 0x02: NVRAM version */
    NVRAM_set_byte(nvram, 0x02, 0x01);
    /* 0x03: NVRAM revision */
    NVRAM_set_byte(nvram, 0x03, 0x01);
    /* 0x08: last OS */
    NVRAM_set_byte(nvram, 0x08, 0x00); /* Unknown */
    /* 0x09: endian */
    NVRAM_set_byte(nvram, 0x09, 'B');  /* Big-endian */
    /* 0x0A: OSArea usage */
    NVRAM_set_byte(nvram, 0x0A, 0x00); /* Empty */
    /* 0x0B: PM mode */
    NVRAM_set_byte(nvram, 0x0B, 0x00); /* Normal */
    /* Restart block description record */
    /* 0x0C: restart block version */
    NVRAM_set_word(nvram, 0x0C, 0x01);
    /* 0x0E: restart block revision */
    NVRAM_set_word(nvram, 0x0E, 0x01);
    /* 0x20: restart address */
    NVRAM_set_lword(nvram, 0x20, 0x00);
    /* 0x24: save area address */
    NVRAM_set_lword(nvram, 0x24, 0x00);
    /* 0x28: save area length */
    NVRAM_set_lword(nvram, 0x28, 0x00);
    /* 0x1C: checksum of restart block */
    crc = NVRAM_compute_crc(nvram, 0x0C, 32);
    NVRAM_set_word(nvram, 0x1C, crc);

    /* Security section */
    /* Set all to zero */
    /* 0xC4: pointer to global environment area */
    NVRAM_set_lword(nvram, 0xC4, 0x0100);
    /* 0xC8: size of global environment area */
    NVRAM_set_lword(nvram, 0xC8, nvram->size - NVRAM_PREP_OSAREA_SIZE -
                    NVRAM_PREP_CONFSIZE - 0x0100);
    /* 0xD4: pointer to configuration area */
    NVRAM_set_lword(nvram, 0xD4, nvram->size - NVRAM_PREP_CONFSIZE);
    /* 0xD8: size of configuration area */
    NVRAM_set_lword(nvram, 0xD8, NVRAM_PREP_CONFSIZE);
    /* 0xE8: pointer to OS specific area */
    NVRAM_set_lword(nvram, 0xE8, nvram->size - NVRAM_PREP_CONFSIZE
                    - NVRAM_PREP_OSAREA_SIZE);
    /* 0xD8: size of OS specific area */
    NVRAM_set_lword(nvram, 0xEC, NVRAM_PREP_OSAREA_SIZE);

    /* Configuration area */

    /* 0x04: checksum 0 => OS area   */
    crc = NVRAM_compute_crc(nvram, 0x00, nvram->size - NVRAM_PREP_CONFSIZE -
                            NVRAM_PREP_OSAREA_SIZE);
    NVRAM_set_word(nvram, 0x04, crc);
    /* 0x06: checksum of config area */
    crc = NVRAM_compute_crc(nvram, nvram->size - NVRAM_PREP_CONFSIZE,
                            NVRAM_PREP_CONFSIZE);
    NVRAM_set_word(nvram, 0x06, crc);

    return 0;
}

static uint8_t NVRAM_chrp_chksum (nvram_t *nvram, uint16_t pos)
{
    uint16_t sum, end;

    end = pos + 0x10;
    sum = NVRAM_get_byte(nvram, pos);
    for (pos += 2; pos < end; pos++) {
        sum += NVRAM_get_byte(nvram, pos);
    }
    while (sum > 0xFF) {
        sum = (sum & 0xFF) + (sum >> 8);
    }

    return sum;
}

static int NVRAM_chrp_format (unused nvram_t *nvram)
{
    uint8_t chksum;

    /* Mark NVRAM as free */
    NVRAM_set_byte(nvram, 0x00, 0x5A);
    NVRAM_set_byte(nvram, 0x01, 0x00);
    NVRAM_set_word(nvram, 0x02, 0x2000);
    NVRAM_set_string(nvram, 0x04, "wwwwwwwwwwww", 12);
    chksum = NVRAM_chrp_chksum(nvram, 0x00);
    NVRAM_set_byte(nvram, 0x01, chksum);

    return 0;
}

#if 0
static uint16_t NVRAM_mac99_chksum (nvram_t *nvram,
                                   uint16_t start, uint16_t len)
	int cnt;
	u32 low, high;

   	buffer += CORE99_ADLER_START;
	low = 1;
	high = 0;
	for (cnt=0; cnt<(NVRAM_SIZE-CORE99_ADLER_START); cnt++) {
		if ((cnt % 5000) == 0) {
			high  %= 65521UL;
			high %= 65521UL;
		}
		low += buffer[cnt];
		high += low;
	}
	low  %= 65521UL;
	high %= 65521UL;

	return (high << 16) | low;
{
    uint16_t pos;
    uint8_t tmp, sum;

    sum = 0;
    for (pos = start; pos < (start + len); pos++) {
        tmp = sum + NVRAM_get_byte(nvram, pos);
        if (tmp < sum)
            tmp++;
        sum = tmp;
    }

    return sum;
}
#endif

static int NVRAM_mac99_format (nvram_t *nvram)
{
    uint8_t chksum;

    /* Mark NVRAM as free */
    NVRAM_set_byte(nvram, 0x00, 0x5A);
    NVRAM_set_byte(nvram, 0x01, 0x00);
    NVRAM_set_word(nvram, 0x02, 0x2000);
    NVRAM_set_string(nvram, 0x04, "wwwwwwwwwwww", 12);
    chksum = NVRAM_chrp_chksum(nvram, 0x00);
    NVRAM_set_byte(nvram, 0x01, chksum);

    return 0;
}

static int NVRAM_pop_format (unused nvram_t *nvram)
{
    /* TODO */
    return -1;
}

/* Interface */
uint8_t NVRAM_read (nvram_t *nvram, uint32_t addr)
{
    outb(nvram->io_base + 0x00, addr);
    outb(nvram->io_base + 0x01, addr >> 8);

    return inb(NVRAM_IO_BASE + 0x03);
}

void NVRAM_write (nvram_t *nvram, uint32_t addr, uint8_t value)
{
    outb(nvram->io_base + 0x00, addr);
    outb(nvram->io_base + 0x01, addr >> 8);
    outb(nvram->io_base + 0x03, value);
}

uint16_t NVRAM_get_size (nvram_t *nvram)
{
    return nvram->size;
}

int NVRAM_format (nvram_t *nvram)
{
    int ret;

    {
        uint16_t pos;
        
        for (pos = 0; pos < nvram->size; pos += 4)
            NVRAM_set_lword(nvram, pos, 0);
    }
    switch (arch) {
    case ARCH_PREP:
        ret = NVRAM_prep_format(nvram);
        break;
    case ARCH_CHRP:
        ret = NVRAM_chrp_format(nvram);
        break;
    case ARCH_MAC99:
    case ARCH_HEATHROW: /* XXX: may be incorrect */
        ret = NVRAM_mac99_format(nvram);
        break;
    case ARCH_POP:
        ret = NVRAM_pop_format(nvram);
        break;
    default:
        ret = -1;
        break;
    }

    return ret;
}

/* HACK... */
extern int vga_width, vga_height, vga_depth;

static nvram_t global_nvram;

nvram_t *NVRAM_get_config (uint32_t *RAM_size, int *boot_device,
                           void **boot_image, uint32_t *boot_size,
                           void **cmdline, uint32_t *cmdline_size,
                           void **ramdisk, uint32_t *ramdisk_size)
{
    unsigned char sign[16];
    nvram_t *nvram;
    uint32_t lword;
    uint16_t NVRAM_size, crc;
    uint8_t byte;

#if 0
    nvram = malloc(sizeof(nvram_t));
    if (nvram == NULL)
        return NULL;
#else
    nvram = &global_nvram;
#endif
    nvram->io_base = NVRAM_IO_BASE;
    /* Pre-initialised NVRAM is not supported any more */
    if (NVRAM_get_string(nvram, sign, 0x00, 0x10) <= 0 ||
        strcmp(sign, "QEMU_BIOS") != 0) {
        ERROR("Wrong NVRAM signature %s\n", sign);
        return NULL;
    }
    /* Check structure version */
    lword = NVRAM_get_lword(nvram, 0x10);
    if (lword != 0x00000002) {
        ERROR("Wrong NVRAM structure version: %0x\n", lword);
        return NULL;
    }
    /* Check CRC */
    crc = NVRAM_compute_crc(nvram, 0x00, 0xF8);
    if (NVRAM_get_word(nvram, 0xFC) != crc) {
        ERROR("Invalid NVRAM structure CRC: %0x <=> %0x\n", crc,
              NVRAM_get_word(nvram, 0xFC));
        return NULL;
    }
    NVRAM_size = NVRAM_get_word(nvram, 0x14);
    if ((NVRAM_size & 0x100) != 0x00 || NVRAM_size < 0x400 ||
        NVRAM_size > 0x2000) {
        ERROR("Invalid NVRAM size: %d\n", NVRAM_size);
        return NULL;
    }
    nvram->size = NVRAM_size;
    if (NVRAM_get_string(nvram, sign, 0x20, 0x10) < 0) {
        ERROR("Unable to get architecture from NVRAM\n");
        return NULL;
    }
    if (strcmp(sign, "PREP") == 0) {
        arch = ARCH_PREP;
    } else if (strcmp(sign, "CHRP") == 0) {
        arch = ARCH_CHRP;
    } else if (strcmp(sign, "MAC99") == 0) {
        arch = ARCH_MAC99;
    } else if (strcmp(sign, "POP") == 0) {
        arch = ARCH_POP;
    } else if (strcmp(sign, "HEATHROW") == 0) {
        arch = ARCH_HEATHROW;
    } else {
        ERROR("Unknown PPC architecture: '%s'\n", sign);
        return NULL;
    }
    lword = NVRAM_get_lword(nvram, 0x30);
    *RAM_size = lword;
    byte = NVRAM_get_byte(nvram, 0x34);
    *boot_device = byte;
    /* Preloaded boot image */
    lword = NVRAM_get_lword(nvram, 0x38);
    *boot_image = (void *)lword;
    lword = NVRAM_get_lword(nvram, 0x3C);
    *boot_size = lword;
    /* Preloaded cmdline */
    lword = NVRAM_get_lword(nvram, 0x40);
    *cmdline = (void *)lword;
    lword = NVRAM_get_lword(nvram, 0x44);
    *cmdline_size = lword;
    /* Preloaded RAM disk */
    lword = NVRAM_get_lword(nvram, 0x48);
    *ramdisk = (void *)lword;
    lword = NVRAM_get_lword(nvram, 0x4C);
    *ramdisk_size = lword;
    /* Preloaded NVRAM image */
    lword = NVRAM_get_lword(nvram, 0x50);
    /* Display init geometry */
    lword = NVRAM_get_word(nvram, 0x54);
    vga_width = lword;
    lword = NVRAM_get_word(nvram, 0x56);
    vga_height = lword;
    lword = NVRAM_get_word(nvram, 0x58);
    vga_depth = lword;
    /* TODO: write it into NVRAM */

    return nvram;
}