/*
 * Copyright (c) 2008 Pavel Rimsky
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/** @addtogroup sparc64
 * @{
 */
/**
 * @file
 * @brief SGCN driver.
 */

#include <arch.h>
#include <arch/drivers/sgcn.h>
#include <arch/drivers/kbd.h>
#include <genarch/ofw/ofw_tree.h>
#include <debug.h>
#include <string.h>
#include <print.h>
#include <mm/page.h>
#include <proc/thread.h>
#include <console/chardev.h>
#include <console/console.h>
#include <sysinfo/sysinfo.h>
#include <synch/spinlock.h>

#define POLL_INTERVAL		10000

/*
 * Physical address at which the SBBC starts. This value has been obtained
 * by inspecting (using Simics) memory accesses made by OBP. It is valid
 * for the Simics-simulated Serengeti machine. The author of this code is
 * not sure whether this value is valid generally. 
 */
#define SBBC_START		0x63000000000

/* offset of SRAM within the SBBC memory */
#define SBBC_SRAM_OFFSET	0x900000

/* size (in bytes) of the physical memory area which will be mapped */
#define MAPPED_AREA_SIZE	(128 * 1024)

/* magic string contained at the beginning of SRAM */
#define SRAM_TOC_MAGIC		"TOCSRAM"

/*
 * Key into the SRAM table of contents which identifies the entry
 * describing the OBP console buffer. It is worth mentioning
 * that the OBP console buffer is not the only console buffer
 * which can be used. It is, however, used because when the kernel
 * is running, the OBP buffer is not used by OBP any more but OBP
 * has already made necessary arrangements so that the output will
 * be read from the OBP buffer and input will go to the OBP buffer.
 * Therefore HelenOS needs to make no such arrangements any more.
 */
#define CONSOLE_KEY		"OBPCONS"

/* magic string contained at the beginning of the console buffer */
#define SGCN_BUFFER_MAGIC	"CON"

/*
 * Returns a pointer to the object of a given type which is placed at the given
 * offset from the SRAM beginning.
 */
#define SRAM(type, offset)	((type *) (sram_begin + (offset)))

/* Returns a pointer to the SRAM table of contents. */
#define SRAM_TOC		(SRAM(iosram_toc_t, 0))

/*
 * Returns a pointer to the object of a given type which is placed at the given
 * offset from the console buffer beginning.
 */
#define SGCN_BUFFER(type, offset) \
	((type *) (sgcn_buffer_begin + (offset)))

/** Returns a pointer to the console buffer header. */
#define SGCN_BUFFER_HEADER	(SGCN_BUFFER(sgcn_buffer_header_t, 0))

/** starting address of SRAM, will be set by the init_sram_begin function */
static uintptr_t sram_begin;

/**
 * starting address of the SGCN buffer, will be set by the
 * init_sgcn_buffer_begin function
 */
static uintptr_t sgcn_buffer_begin;

/* true iff the kernel driver should ignore pressed keys */
static bool kbd_disabled;

/* 
 * Ensures that writing to the buffer and consequent update of the write pointer
 * are together one atomic operation.
 */
SPINLOCK_INITIALIZE(sgcn_output_lock);

/* 
 * Prevents the input buffer read/write pointers from getting to inconsistent
 * state. 
 */
SPINLOCK_INITIALIZE(sgcn_input_lock);


/* functions referenced from definitions of I/O operations structures */
static void sgcn_putchar(outdev_t *, const wchar_t, bool);

/** SGCN output device operations */
static outdev_operations_t sgcnout_ops = {
	.write = sgcn_putchar
};

static outdev_t sgcnout;	/**< SGCN output device. */

/**
 * Set some sysinfo values (SRAM address and SRAM size).
 */
static void register_sram(uintptr_t sram_begin_physical)
{
	sysinfo_set_item_val("sram.area.size", NULL, MAPPED_AREA_SIZE);
	sysinfo_set_item_val("sram.address.physical", NULL,
	    sram_begin_physical);
}

/**
 * Initializes the starting address of SRAM.
 *
 * The SRAM starts 0x900000 + C bytes behind the SBBC start in the
 * physical memory, where C is the value read from the "iosram-toc"
 * property of the "/chosen" OBP node. The sram_begin variable will
 * be set to the virtual address which maps to the SRAM physical
 * address.
 */
static void init_sram_begin(void)
{
	ofw_tree_node_t *chosen;
	ofw_tree_property_t *iosram_toc;
	uintptr_t sram_begin_physical;

	chosen = ofw_tree_lookup("/chosen");
	if (!chosen)
		panic("Cannot find '/chosen'.");

	iosram_toc = ofw_tree_getprop(chosen, "iosram-toc");
	if (!iosram_toc)
		panic("Cannot find property 'iosram-toc'.");
	if (!iosram_toc->value)
		panic("Cannot find SRAM TOC.");

	sram_begin_physical = SBBC_START + SBBC_SRAM_OFFSET
	    + *((uint32_t *) iosram_toc->value);
	sram_begin = hw_map(sram_begin_physical, MAPPED_AREA_SIZE);
	
	register_sram(sram_begin_physical);
}

/**
 * Initializes the starting address of the SGCN buffer.
 *
 * The offset of the SGCN buffer within SRAM is obtained from the
 * SRAM table of contents. The table of contents contains
 * information about several buffers, among which there is an OBP
 * console buffer - this one will be used as the SGCN buffer. 
 *
 * This function also writes the offset of the SGCN buffer within SRAM
 * under the sram.buffer.offset sysinfo key.
 */
static void sgcn_buffer_begin_init(void)
{
	static bool initialized;
	
	if (initialized)
		return;

	init_sram_begin();
		
	ASSERT(str_cmp(SRAM_TOC->magic, SRAM_TOC_MAGIC) == 0);
	
	/* lookup TOC entry with the correct key */
	uint32_t i;
	for (i = 0; i < MAX_TOC_ENTRIES; i++) {
		if (str_cmp(SRAM_TOC->keys[i].key, CONSOLE_KEY) == 0)
			break;
	}
	ASSERT(i < MAX_TOC_ENTRIES);
	
	sgcn_buffer_begin = sram_begin + SRAM_TOC->keys[i].offset;
	
	sysinfo_set_item_val("sram.buffer.offset", NULL,
	    SRAM_TOC->keys[i].offset);
	
	initialized = true;
}

/**
 * Writes a single character to the SGCN (circular) output buffer
 * and updates the output write pointer so that SGCN gets to know
 * that the character has been written.
 */
static void sgcn_do_putchar(const char c)
{
	uint32_t begin = SGCN_BUFFER_HEADER->out_begin;
	uint32_t end = SGCN_BUFFER_HEADER->out_end;
	uint32_t size = end - begin;
	
	/* we need pointers to volatile variables */
	volatile char *buf_ptr = (volatile char *)
	    SGCN_BUFFER(char, SGCN_BUFFER_HEADER->out_wrptr);
	volatile uint32_t *out_wrptr_ptr = &(SGCN_BUFFER_HEADER->out_wrptr);
	volatile uint32_t *out_rdptr_ptr = &(SGCN_BUFFER_HEADER->out_rdptr);

	/*
	 * Write the character and increment the write pointer modulo the
	 * output buffer size. Note that if we are to rewrite a character
	 * which has not been read by the SGCN controller yet (i.e. the output
	 * buffer is full), we need to wait until the controller reads some more
	 * characters. We wait actively, which means that all threads waiting
	 * for the lock are blocked. However, this situation is
	 *   1) rare - the output buffer is big, so filling the whole
	 *             output buffer is improbable
	 *   2) short-lasting - it will take the controller only a fraction
	 *             of millisecond to pick the unread characters up
	 *   3) not serious - the blocked threads are those that print something
	 *             to user console, which is not a time-critical operation
	 */
	uint32_t new_wrptr = (((*out_wrptr_ptr) - begin + 1) % size) + begin;
	while (*out_rdptr_ptr == new_wrptr)
		;
	*buf_ptr = c;
	*out_wrptr_ptr = new_wrptr;
}

/**
 * SGCN output operation. Prints a single character to the SGCN. Newline
 * character is converted to CRLF.
 */
static void sgcn_putchar(outdev_t *od, const wchar_t ch, bool silent)
{
	if (!silent) {
		spinlock_lock(&sgcn_output_lock);
		
		if (ascii_check(ch)) {
			if (ch == '\n')
				sgcn_do_putchar('\r');
			sgcn_do_putchar(ch);
		} else
			sgcn_do_putchar(U_SPECIAL);
		
		spinlock_unlock(&sgcn_output_lock);
	}
}

/**
 * Grabs the input for kernel.
 */
void sgcn_grab(void)
{
	kbd_disabled = false;
}

/**
 * Releases the input so that userspace can use it.
 */
void sgcn_release(void)
{
	kbd_disabled = true;
}

/**
 * Function regularly called by the keyboard polling thread. Finds out whether
 * there are some unread characters in the input queue. If so, it picks them up
 * and sends them to the upper layers of HelenOS.
 */
static void sgcn_poll(sgcn_instance_t *instance)
{
	uint32_t begin = SGCN_BUFFER_HEADER->in_begin;
	uint32_t end = SGCN_BUFFER_HEADER->in_end;
	uint32_t size = end - begin;

	if (kbd_disabled)
		return;

	spinlock_lock(&sgcn_input_lock);
	
	/* we need pointers to volatile variables */
	volatile char *buf_ptr = (volatile char *)
	    SGCN_BUFFER(char, SGCN_BUFFER_HEADER->in_rdptr);
	volatile uint32_t *in_wrptr_ptr = &(SGCN_BUFFER_HEADER->in_wrptr);
	volatile uint32_t *in_rdptr_ptr = &(SGCN_BUFFER_HEADER->in_rdptr);
	
	while (*in_rdptr_ptr != *in_wrptr_ptr) {
		buf_ptr = (volatile char *)
		    SGCN_BUFFER(char, SGCN_BUFFER_HEADER->in_rdptr);
		char c = *buf_ptr;
		*in_rdptr_ptr = (((*in_rdptr_ptr) - begin + 1) % size) + begin;
			
		indev_push_character(instance->srlnin, c);	
	}	

	spinlock_unlock(&sgcn_input_lock);
}

/**
 * Polling thread function.
 */
static void ksgcnpoll(void *instance) {
	while (1) {
		if (!silent) 
			sgcn_poll(instance);
		thread_usleep(POLL_INTERVAL);
	}
}

/**
 * A public function which initializes input from the Serengeti console.
 */
sgcn_instance_t *sgcnin_init(void)
{
	sgcn_buffer_begin_init();
	
	sgcn_instance_t *instance =
	    malloc(sizeof(sgcn_instance_t), FRAME_ATOMIC);
	
	if (instance) {
		instance->srlnin = NULL;
		instance->thread = thread_create(ksgcnpoll, instance, TASK, 0,
		    "ksgcnpoll", true);
		
		if (!instance->thread) {
			free(instance);
			return NULL;
		}
	}
	
	return instance;
}

void sgcnin_wire(sgcn_instance_t *instance, indev_t *srlnin)
{
	ASSERT(instance);
	ASSERT(srlnin);

	instance->srlnin = srlnin;
	thread_ready(instance->thread);

	sysinfo_set_item_val("kbd", NULL, true);
}

/**
 * A public function which initializes output to the Serengeti console.
 */
void sgcnout_init(void)
{
	sgcn_buffer_begin_init();

	sysinfo_set_item_val("fb.kind", NULL, 4);

	outdev_initialize("sgcnout", &sgcnout, &sgcnout_ops);	
	stdout = &sgcnout;
}

/** @}
 */
