#include "config.h"
#include "libopenbios/bindings.h"
#include "drivers/drivers.h"
#include "libc/byteorder.h"
#include "libc/vsprintf.h"

#include "macio.h"
#include "cuda.h"

//#define DEBUG_CUDA
#ifdef DEBUG_CUDA
#define CUDA_DPRINTF(fmt, args...) \
	do { printk("CUDA - %s: " fmt, __func__ , ##args); } while (0)
#else
#define CUDA_DPRINTF(fmt, args...) do { } while (0)
#endif

#define IO_CUDA_OFFSET	0x00016000
#define IO_CUDA_SIZE	0x00002000

/* VIA registers - spaced 0x200 bytes apart */
#define RS              0x200           /* skip between registers */
#define B               0               /* B-side data */
#define A               RS              /* A-side data */
#define DIRB            (2*RS)          /* B-side direction (1=output) */
#define DIRA            (3*RS)          /* A-side direction (1=output) */
#define T1CL            (4*RS)          /* Timer 1 ctr/latch (low 8 bits) */
#define T1CH            (5*RS)          /* Timer 1 counter (high 8 bits) */
#define T1LL            (6*RS)          /* Timer 1 latch (low 8 bits) */
#define T1LH            (7*RS)          /* Timer 1 latch (high 8 bits) */
#define T2CL            (8*RS)          /* Timer 2 ctr/latch (low 8 bits) */
#define T2CH            (9*RS)          /* Timer 2 counter (high 8 bits) */
#define SR              (10*RS)         /* Shift register */
#define ACR             (11*RS)         /* Auxiliary control register */
#define PCR             (12*RS)         /* Peripheral control register */
#define IFR             (13*RS)         /* Interrupt flag register */
#define IER             (14*RS)         /* Interrupt enable register */
#define ANH             (15*RS)         /* A-side data, no handshake */

/* Bits in B data register: all active low */
#define TREQ            0x08            /* Transfer request (input) */
#define TACK            0x10            /* Transfer acknowledge (output) */
#define TIP             0x20            /* Transfer in progress (output) */

/* Bits in ACR */
#define SR_CTRL         0x1c            /* Shift register control bits */
#define SR_EXT          0x0c            /* Shift on external clock */
#define SR_OUT          0x10            /* Shift out if 1 */

/* Bits in IFR and IER */
#define IER_SET         0x80            /* set bits in IER */
#define IER_CLR         0               /* clear bits in IER */
#define SR_INT          0x04            /* Shift register full/empty */

#define CUDA_BUF_SIZE 16

#define ADB_PACKET      0
#define CUDA_PACKET     1

/* CUDA commands (2nd byte) */
#define CUDA_GET_TIME			0x03
#define CUDA_SET_TIME			0x09
#define CUDA_POWERDOWN                  0x0a
#define CUDA_RESET_SYSTEM               0x11

static uint8_t cuda_readb (cuda_t *dev, int reg)
{
	    return *(volatile uint8_t *)(dev->base + reg);
}

static void cuda_writeb (cuda_t *dev, int reg, uint8_t val)
{
	    *(volatile uint8_t *)(dev->base + reg) = val;
}

static void cuda_wait_irq (cuda_t *dev)
{
    int val;

//    CUDA_DPRINTF("\n");
    for(;;) {
        val = cuda_readb(dev, IFR);
        cuda_writeb(dev, IFR, val & 0x7f);
        if (val & SR_INT)
            break;
    }
}



static int cuda_request (cuda_t *dev, uint8_t pkt_type, const uint8_t *buf,
                         int buf_len, uint8_t *obuf)
{
    int i, obuf_len, val;

    cuda_writeb(dev, ACR, cuda_readb(dev, ACR) | SR_OUT);
    cuda_writeb(dev, SR, pkt_type);
    cuda_writeb(dev, B, cuda_readb(dev, B) & ~TIP);
    if (buf) {
        //CUDA_DPRINTF("Send buf len: %d\n", buf_len);
        /* send 'buf' */
        for(i = 0; i < buf_len; i++) {
            cuda_wait_irq(dev);
            cuda_writeb(dev, SR, buf[i]);
            cuda_writeb(dev, B, cuda_readb(dev, B) ^ TACK);
        }
    }
    cuda_wait_irq(dev);
    cuda_writeb(dev, ACR, cuda_readb(dev, ACR) & ~SR_OUT);
    cuda_readb(dev, SR);
    cuda_writeb(dev, B, cuda_readb(dev, B) | TIP | TACK);

    obuf_len = 0;
    if (obuf) {
        cuda_wait_irq(dev);
        cuda_readb(dev, SR);
        cuda_writeb(dev, B, cuda_readb(dev, B) & ~TIP);
        for(;;) {
            cuda_wait_irq(dev);
            val = cuda_readb(dev, SR);
            if (obuf_len < CUDA_BUF_SIZE)
                obuf[obuf_len++] = val;
            if (cuda_readb(dev, B) & TREQ)
                break;
            cuda_writeb(dev, B, cuda_readb(dev, B) ^ TACK);
        }
        cuda_writeb(dev, B, cuda_readb(dev, B) | TIP | TACK);

        cuda_wait_irq(dev);
        cuda_readb(dev, SR);
    }
//    CUDA_DPRINTF("Got len: %d\n", obuf_len);

    return obuf_len;
}



static int cuda_adb_req (void *host, const uint8_t *snd_buf, int len,
                         uint8_t *rcv_buf)
{
    uint8_t buffer[CUDA_BUF_SIZE], *pos;

 //   CUDA_DPRINTF("len: %d %02x\n", len, snd_buf[0]);
    len = cuda_request(host, ADB_PACKET, snd_buf, len, buffer);
    if (len > 1 && buffer[0] == ADB_PACKET) {
        /* We handle 2 types of ADB packet here:
               Normal: <type> <status> <data> ...
               Error : <type> <status> <cmd> (<data> ...)
           Ideally we should use buffer[1] (status) to determine whether this
           is a normal or error packet but this requires a corresponding fix
           in QEMU <= 2.4. Hence we temporarily handle it this way to ease
           the transition. */
        if (len > 2 && buffer[2] == snd_buf[0]) {
            /* Error */
            pos = buffer + 3;
            len -= 3;
        } else {
            /* Normal */
            pos = buffer + 2;
            len -= 2;
        }
    } else {
        pos = buffer + 1;
        len -= 1;
    }
    memcpy(rcv_buf, pos, len);

    return len;
}


DECLARE_UNNAMED_NODE(ob_cuda, 0, sizeof(int));

static cuda_t *main_cuda;

static void
ppc32_reset_all(void)
{
        uint8_t cmdbuf[1], obuf[64];

        cmdbuf[0] = CUDA_RESET_SYSTEM;
        cuda_request(main_cuda, CUDA_PACKET, cmdbuf, sizeof(cmdbuf), obuf);
}

static void
ppc32_poweroff(void)
{
        uint8_t cmdbuf[1], obuf[64];

        cmdbuf[0] = CUDA_POWERDOWN;
        cuda_request(main_cuda, CUDA_PACKET, cmdbuf, sizeof(cmdbuf), obuf);
}

static void
ob_cuda_open(int *idx)
{
	RET(-1);
}

static void
ob_cuda_close(int *idx)
{
}

NODE_METHODS(ob_cuda) = {
	{ "open",		ob_cuda_open		},
	{ "close",		ob_cuda_close		},
};

DECLARE_UNNAMED_NODE(rtc, 0, sizeof(int));

static void
rtc_open(int *idx)
{
	RET(-1);
}

/*
 * get-time ( -- second minute hour day month year )
 *
 */

static const int days_month[12] =
	{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
static const int days_month_leap[12] =
	{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

static inline int is_leap(int year)
{
	return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}

static  void
rtc_get_time(int *idx)
{
        uint8_t cmdbuf[1], obuf[64];
	ucell second, minute, hour, day, month, year;
	uint32_t now;
	int current;
	const int *days;

        cmdbuf[0] = CUDA_GET_TIME;
        cuda_request(main_cuda, CUDA_PACKET, cmdbuf, sizeof(cmdbuf), obuf);

	/* seconds since 01/01/1904 */

	now = (obuf[3] << 24) + (obuf[4] << 16) + (obuf[5] << 8) + obuf[6];

	second =  now % 60;
	now /= 60;

	minute = now % 60;
	now /= 60;

	hour = now % 24;
	now /= 24;

	year = now * 100 / 36525;
	now -= year * 36525 / 100;
	year += 1904;

	days = is_leap(year) ?  days_month_leap : days_month;

	current = 0;
	month = 0;
	while (month < 12) {
		if (now <= current + days[month]) {
			break;
		}
		current += days[month];
		month++;
	}
	month++;

	day = now - current;

	PUSH(second);
	PUSH(minute);
	PUSH(hour);
	PUSH(day);
	PUSH(month);
	PUSH(year);
}

/*
 * set-time ( second minute hour day month year -- )
 *
 */

static  void
rtc_set_time(int *idx)
{
        uint8_t cmdbuf[5], obuf[3];
	ucell second, minute, hour, day, month, year;
	const int *days;
	uint32_t now;
	unsigned int nb_days;
	int i;

	year = POP();
	month = POP();
	day = POP();
	hour = POP();
	minute = POP();
	second = POP();

	days = is_leap(year) ?  days_month_leap : days_month;
	nb_days = (year - 1904) * 36525 / 100 + day;
	for (i = 0; i < month - 1; i++)
		nb_days += days[i];

	now = (((nb_days * 24) + hour) * 60 + minute) * 60 + second;

        cmdbuf[0] = CUDA_SET_TIME;
	cmdbuf[1] = now >> 24;
	cmdbuf[2] = now >> 16;
	cmdbuf[3] = now >> 8;
	cmdbuf[4] = now;

        cuda_request(main_cuda, CUDA_PACKET, cmdbuf, sizeof(cmdbuf), obuf);
}

NODE_METHODS(rtc) = {
	{ "open",		rtc_open		},
	{ "get-time",		rtc_get_time		},
	{ "set-time",		rtc_set_time		},
};

static void
rtc_init(char *path)
{
	phandle_t aliases;
	char buf[128];

	push_str(path);
	fword("find-device");

	fword("new-device");

	push_str("rtc");
	fword("device-name");

	push_str("rtc");
	fword("device-type");

	push_str("rtc");
	fword("encode-string");
	push_str("compatible");
	fword("property");

	BIND_NODE_METHODS(get_cur_dev(), rtc);
	fword("finish-device");

	aliases = find_dev("/aliases");
	snprintf(buf, sizeof(buf), "%s/rtc", path);
	set_property(aliases, "rtc", buf, strlen(buf) + 1);
}

static void
powermgt_init(char *path)
{
	push_str(path);
	fword("find-device");

	fword("new-device");

	push_str("power-mgt");
	fword("device-name");

	push_str("power-mgt");
	fword("device-type");

	push_str("min-consumption-pwm-led");
	fword("encode-string");
	push_str("mgt-kind");
	fword("property");

	push_str("cuda");
	fword("encode-string");
	push_str("compatible");
	fword("property");

	BIND_NODE_METHODS(get_cur_dev(), rtc);
	fword("finish-device");
}

cuda_t *cuda_init (const char *path, phys_addr_t base)
{
	cuda_t *cuda;
	char buf[64];
	phandle_t ph, aliases;
	int props[2];

	base += IO_CUDA_OFFSET;
	CUDA_DPRINTF(" base=" FMT_plx "\n", base);
	cuda = malloc(sizeof(cuda_t));
	if (cuda == NULL)
	    return NULL;

	fword("new-device");

	push_str("via-cuda");
	fword("device-name");

	push_str("via-cuda");
	fword("device-type");

	push_str("cuda");
	fword("encode-string");
	push_str("compatible");
	fword("property");

	PUSH(1);
	fword("encode-int");
	push_str("#address-cells");
	fword("property");

	PUSH(0);
	fword("encode-int");
	push_str("#size-cells");
	fword("property");

	PUSH(IO_CUDA_OFFSET);
	fword("encode-int");
	PUSH(IO_CUDA_SIZE);
	fword("encode-int");
	fword("encode+");
	push_str("reg");
	fword("property");

	ph = get_cur_dev();

	/* on newworld machines the cuda is on interrupt 0x19 */
	props[0] = 0x19;
	props[1] = 0;
	NEWWORLD(set_property(ph, "interrupts", (char *)props, sizeof(props)));
	NEWWORLD(set_int_property(ph, "#interrupt-cells", 2));

	/* we emulate an oldworld hardware, so we must use
	 * non-standard oldworld property (needed by linux 2.6.18)
	 */
	OLDWORLD(set_int_property(ph, "AAPL,interrupts", 0x12));

	BIND_NODE_METHODS(get_cur_dev(), ob_cuda);

	aliases = find_dev("/aliases");
	snprintf(buf, sizeof(buf), "%s/via-cuda", path);
	set_property(aliases, "via-cuda", buf, strlen(buf) + 1);

	cuda->base = base;
	cuda_writeb(cuda, B, cuda_readb(cuda, B) | TREQ | TIP);
#ifdef CONFIG_DRIVER_ADB
	cuda->adb_bus = adb_bus_new(cuda, &cuda_adb_req);
	if (cuda->adb_bus == NULL) {
	    free(cuda);
	    return NULL;
	}
	adb_bus_init(buf, cuda->adb_bus);
#endif

	rtc_init(buf);
	powermgt_init(buf);

	main_cuda = cuda;

	fword("finish-device");

	bind_func("ppc32-power-off", ppc32_poweroff);
	feval("['] ppc32-power-off to power-off");
	bind_func("ppc32-reset-all", ppc32_reset_all);
	feval("['] ppc32-reset-all to reset-all");

	return cuda;
}
