/*
 *  linux/drivers/serial/bdh_console.c
 *
 *  Based on drivers/serial/8250.c by Russell King.
 *
 *  Author:	KoHo
 *  Created:	Jan 20, 2011
 *  Copyright:	(C) 2011 SHARP Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */

#if defined(CONFIG_SERIAL_BDH_CONSOLE) && defined(CONFIG_MAGIC_SYSRQ)
#define SUPPORT_SYSRQ
#endif

#include <linux/module.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/console.h>
#include <linux/sysrq.h>
#include <linux/serial_reg.h>
#include <linux/circ_buf.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/tty.h>
#include <linux/tty_flip.h>
#include <linux/serial_core.h>

#include <asm/io.h>
#include <asm/irq.h>
#include <asm/emma3/emma3.h>


/*
 * Configuration:
 */
static unsigned int initmem = 0;

/*
 * Debugging.
 */
#define VcError(fmt, ...)						\
	printk(KERN_ERR " <%04d> %s " fmt,			\
		   __LINE__, __func__, ## __VA_ARGS__)
#if 0
#define VcLog(fmt, ...)							\
	printk(KERN_INFO " <%04d> %s " fmt,			\
		   __LINE__, __func__, ## __VA_ARGS__)
#else
#define VcLog(fmt, ...)
#endif

#define VCON_BUFFER_FULL 1

struct vconbuf {
	volatile char			*buf;
	unsigned int			bufsz;
	volatile unsigned int	r;
	volatile unsigned int	w;
};

struct uart_bdh_port {
	struct uart_port	port;
	struct timer_list	timer;
	struct vconbuf		*r;
	struct vconbuf		*w;
	char				*name;
};

static int serial_bdh_vconsole_sw(void)
{
#if defined(CONFIG_SERIAL_BDH_CONSOLE)
	return *((uint32_t *)CONFIG_SERIAL_BDH_CONSOLE_SW_ADDR);
#else
	return 1;
#endif
}

static void serial_bdh_enable_ms(struct uart_port *port)
{
	VcLog("enter\n");
}

static void serial_bdh_stop_tx(struct uart_port *port)
{
	VcLog("enter\n");
}

static void serial_bdh_stop_rx(struct uart_port *port)
{
	VcLog("enter\n");
}

static inline void receive_chars(struct uart_bdh_port *up)
{
	struct tty_struct *tty = up->port.info->tty;
	unsigned int ch, flag;
	int max_count = 256;
	struct vconbuf *rbuf = up->r;

	VcLog("enter\n");
	while ((rbuf->r != rbuf->w) && (max_count-- > 0)) {
		/* error handling */
		if (rbuf->r >= rbuf->bufsz) {
			rbuf->r = 0;
		}
		if (rbuf->w >= rbuf->bufsz) {
			break;
		}
		ch = rbuf->buf[rbuf->r];
		flag = TTY_NORMAL;
		up->port.icount.rx++;

		if (uart_handle_sysrq_char(&up->port, ch))
			goto ignore_char;

		tty_insert_flip_char(tty, ch, flag);

	ignore_char:
		rbuf->r = rbuf->r < rbuf->bufsz - 1 ? rbuf->r + 1 : 0;
	}
	tty_flip_buffer_push(tty);
}

static int vcon_buffer_full(struct vconbuf *wbuf)
{
	unsigned int r;

	/* error handling */
	if (wbuf->w >= wbuf->bufsz) {
		wbuf->w = 0;
	}
	/* buffer full check */
	r = wbuf->r; /* wbuf->r may be changed by sub CPU */
	if ((wbuf->w + 1 == r) || (wbuf->w + 1 == r + wbuf->bufsz)) {
		return -VCON_BUFFER_FULL;
	}
	return 0;
}

static int vcon_buffer_putchar(struct vconbuf *wbuf, int ch)
{
	int ret;

	ret = vcon_buffer_full(wbuf);
	if (ret) {
		return ret;
	}

	if (!serial_bdh_vconsole_sw()) return 0;

	/* put one character */
	wbuf->buf[wbuf->w] = (char)ch;
	wbuf->w = wbuf->w < wbuf->bufsz - 1 ? wbuf->w + 1 : 0;
	return 0;
}

static void transmit_chars(struct uart_bdh_port *up)
{
	struct circ_buf *xmit = &up->port.info->xmit;
	int count;
	struct vconbuf *wbuf = up->w;

	VcLog("enter\n");
	if (up->port.x_char) {
		if (vcon_buffer_putchar(wbuf, up->port.x_char)) {
			return;
		}
		up->port.icount.tx++;
		up->port.x_char = 0;
		if (serial_bdh_vconsole_sw()) {
			emma3_out32(EMMA3_BHIF_SW1_INT, 1 << EMMA3_SW_IRQ_VCON_BIT);
		}
		return;
	}
	if (uart_circ_empty(xmit) || uart_tx_stopped(&up->port)) {
		serial_bdh_stop_tx(&up->port);
		return;
	}

	count = up->port.fifosize / 2;
	do {
		if (vcon_buffer_putchar(wbuf, xmit->buf[xmit->tail])) {
			break;
		}
		xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);
		up->port.icount.tx++;
		--count;
		if ((!(count & 0xf)) && (wbuf->w != wbuf->r)) {
			if (serial_bdh_vconsole_sw()) {
				emma3_out32(EMMA3_BHIF_SW1_INT, 1 << EMMA3_SW_IRQ_VCON_BIT);
			}
		}

		if (uart_circ_empty(xmit))
			break;
	} while (count > 0);

	if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)
		uart_write_wakeup(&up->port);

	if (uart_circ_empty(xmit))
		serial_bdh_stop_tx(&up->port);

	if ((wbuf->w != wbuf->r) && (count < up->port.fifosize / 2)) {
		if (serial_bdh_vconsole_sw()) {
			emma3_out32(EMMA3_BHIF_SW1_INT, 1 << EMMA3_SW_IRQ_VCON_BIT);
		}
	}
}

/* Base timer interval for polling */
static inline int poll_timeout(int timeout)
{
	return timeout > 6 ? (timeout / 2 - 2) : 1;
}

/*
 * This function is used to handle ports that do not have an
 * interrupt for tx. (Although at the expense of much CPU overhead).
 */
static void serial_bdh_timeout(unsigned long data)
{
	struct uart_bdh_port *up = (struct uart_bdh_port *)data;
	struct circ_buf *xmit = &up->port.info->xmit;

	VcLog("enter\n");
	transmit_chars(up);
	if (uart_circ_empty(xmit)) {
		del_timer_sync(&up->timer);
	} else {
		up->timer.data = (unsigned long)up;
		mod_timer(&up->timer, jiffies + poll_timeout(up->port.timeout));
	}
}

static void serial_bdh_start_tx(struct uart_port *port)
{
	struct uart_bdh_port *up = (struct uart_bdh_port *)port;
	struct circ_buf *xmit = &up->port.info->xmit;

	VcLog("enter\n");
	transmit_chars(up);
	if (!uart_circ_empty(xmit)) {
		up->timer.data = (unsigned long)up;
		mod_timer(&up->timer, jiffies + poll_timeout(up->port.timeout));
	}
}

/*
 * This handles the interrupt from one port.
 */
static inline irqreturn_t serial_bdh_irq(int irq, void *dev_id)
{
	struct uart_bdh_port *up = dev_id;

	VcLog("enter\n");
	if (irq == EMMA3_SW_IRQ_VCON) {
		receive_chars(up);
	} else {
		/* for future extension of tx empty interrupt */
		transmit_chars(up);
	}
	return IRQ_HANDLED;
}

static unsigned int serial_bdh_tx_empty(struct uart_port *port)
{
	struct uart_bdh_port *up = (struct uart_bdh_port *)port;
	struct vconbuf *wbuf = up->w;
	int retval;

	VcLog("enter\n");
	retval = wbuf->w == wbuf->r ? TIOCSER_TEMT : 0;

	return retval;
}

static unsigned int serial_bdh_get_mctrl(struct uart_port *port)
{
	VcLog("enter\n");
	return 0;
}

static void serial_bdh_set_mctrl(struct uart_port *port, unsigned int mctrl)
{
	VcLog("enter\n");
}

static void serial_bdh_break_ctl(struct uart_port *port, int break_state)
{
	VcLog("enter\n");
}

static int serial_bdh_startup(struct uart_port *port)
{
	struct uart_bdh_port *up = (struct uart_bdh_port *)port;
	int retval;

	VcLog("enter\n");
	/*
	 * Allocate the IRQ
	 */
	retval = request_irq(up->port.irq, serial_bdh_irq, 0, up->name, up);
	if (retval)
		return retval;

	return 0;
}

static void serial_bdh_shutdown(struct uart_port *port)
{
	struct uart_bdh_port *up = (struct uart_bdh_port *)port;

	VcLog("enter\n");
	free_irq(up->port.irq, up);
}

static void
serial_bdh_set_termios(struct uart_port *port, struct ktermios *termios,
					   struct ktermios *old)
{
	VcLog("enter\n");
	/*
	 * Update the per-port timeout.
	 */
	uart_update_timeout(port, termios->c_cflag, 115200);
}

static void
serial_bdh_pm(struct uart_port *port, unsigned int state,
			  unsigned int oldstate)
{
	VcLog("enter\n");
	if (!state)
		udelay(1);
}

static void serial_bdh_release_port(struct uart_port *port)
{
	VcLog("enter\n");
}

static int serial_bdh_request_port(struct uart_port *port)
{
	VcLog("enter\n");
	return 0;
}

static void serial_bdh_config_port(struct uart_port *port, int flags)
{
	struct uart_bdh_port *up = (struct uart_bdh_port *)port;
	VcLog("enter\n");
	up->port.type = PORT_BDH;
}

static int
serial_bdh_verify_port(struct uart_port *port, struct serial_struct *ser)
{
	VcLog("enter\n");
	/* we don't want the core code to modify any port params */
	return -EINVAL;
}

static const char *
serial_bdh_type(struct uart_port *port)
{
	struct uart_bdh_port *up = (struct uart_bdh_port *)port;
	VcLog("enter\n");
	return up->name;
}

#ifdef CONFIG_SERIAL_BDH_CONSOLE

static struct uart_bdh_port serial_bdh_ports[];
static struct uart_driver serial_bdh_reg;

#define BOTH_EMPTY 0

/*
 *	Wait for transmitter & holding register to empty
 */
static inline void wait_for_xmitr(struct uart_bdh_port *up)
{
	unsigned int tmout = 10000;
	struct vconbuf *wbuf = up->w;

	/* Wait up to 10ms for the character(s) to be sent. */
	while (vcon_buffer_full(wbuf)) {
		if (--tmout == 0)
			break;
		udelay(1);
	}
}

static void serial_bdh_console_putchar(struct uart_port *port, int ch)
{
	struct uart_bdh_port *up = (struct uart_bdh_port *)port;
	struct vconbuf *wbuf = up->w;

	wait_for_xmitr(up);
	vcon_buffer_putchar(wbuf, ch);
}

/*
 * Print a string to the serial port trying not to disturb
 * any possible real use of the port...
 *
 *	The console_lock must be held when we get here.
 */
static void
serial_bdh_console_write(struct console *co, const char *s, unsigned int count)
{
	struct uart_bdh_port *up = &serial_bdh_ports[co->index];

	if (!serial_bdh_vconsole_sw()) return;

	/*
	 *	First save the IER then disable the interrupts
	 */

	uart_console_write(&up->port, s, count, serial_bdh_console_putchar);
	emma3_out32(EMMA3_BHIF_SW1_INT, 1 << EMMA3_SW_IRQ_VCON_BIT);

	/*
	 *	Finally, wait for transmitter to become empty
	 *	and restore the IER
	 */
	wait_for_xmitr(up);
}

static int __init
serial_bdh_console_setup(struct console *co, char *options)
{
	struct uart_bdh_port *up;
	int baud = 9600;
	int bits = 8;
	int parity = 'n';
	int flow = 'n';

	VcLog("enter\n");
	if (co->index == -1 || co->index >= serial_bdh_reg.nr)
		co->index = 0;
	up = &serial_bdh_ports[co->index];

	if (options)
		uart_parse_options(options, &baud, &parity, &bits, &flow);

	return uart_set_options(&up->port, co, baud, parity, bits, flow);
}

static struct console serial_bdh_console = {
	.name		= "ttyS",
	.write		= serial_bdh_console_write,
	.device		= uart_console_device,
	.setup		= serial_bdh_console_setup,
	.flags		= CON_PRINTBUFFER | CON_CONSDEV,
	.index		= -1,
	.data		= &serial_bdh_reg,
};

#define BDH_CONSOLE	&serial_bdh_console
#else
#define BDH_CONSOLE	NULL
#endif

struct uart_ops serial_bdh_pops = {
	.tx_empty	= serial_bdh_tx_empty,
	.set_mctrl	= serial_bdh_set_mctrl,
	.get_mctrl	= serial_bdh_get_mctrl,
	.stop_tx	= serial_bdh_stop_tx,
	.start_tx	= serial_bdh_start_tx,
	.stop_rx	= serial_bdh_stop_rx,
	.enable_ms	= serial_bdh_enable_ms,
	.break_ctl	= serial_bdh_break_ctl,
	.startup	= serial_bdh_startup,
	.shutdown	= serial_bdh_shutdown,
	.set_termios	= serial_bdh_set_termios,
	.pm		= serial_bdh_pm,
	.type		= serial_bdh_type,
	.release_port	= serial_bdh_release_port,
	.request_port	= serial_bdh_request_port,
	.config_port	= serial_bdh_config_port,
	.verify_port	= serial_bdh_verify_port,
};

static struct uart_bdh_port serial_bdh_ports[] = {
	{
		.name	= "bdh_console",
		.port	= {
			.type		= PORT_BDH,
			.irq		= EMMA3_SW_IRQ_VCON,
			.ops		= &serial_bdh_pops,
			.flags		= UPF_BOOT_AUTOCONF,
			.membase	= NULL,
			.mapbase	= 0x80000000,
			.iotype		= UPIO_MEM,
			.fifosize	= 0x1000,
			.line		= 0,
		},
	}
};

static int __init
serial_bdh_console_init(void)
{
	int pn;
	struct uart_bdh_port *up;

	VcLog("enter\n");
	if (!initmem) {
		return -ENODEV;
	}
	for (pn = 0; pn < ARRAY_SIZE(serial_bdh_ports); pn++) {
		up = &serial_bdh_ports[pn];
		up->port.membase = (unsigned char __iomem*)initmem;
		up->port.mapbase = initmem;
		up->r = ((struct vconbuf **)(up->port.membase))[0];
		up->w = ((struct vconbuf **)(up->port.membase))[1];
		if ((((unsigned int)up->r & 0xd0000000) != 0x80000000) ||
			(((unsigned int)up->w & 0xd0000000) != 0x80000000)) {
			VcError("invalid initmem 0x%x\n", initmem);
			initmem = 0;
			return -ENODEV;
		}
		up->r->r = 0;
		up->w->w = 0;
		up->port.fifosize = up->w->bufsz;
		init_timer(&up->timer);
		up->timer.function = serial_bdh_timeout;
	}

	register_console(&serial_bdh_console);
	return 0;
}

console_initcall(serial_bdh_console_init);

static struct uart_driver serial_bdh_reg = {
	.owner		= THIS_MODULE,
	.driver_name	= "serial",
	.dev_name	= "ttyS",
	.major		= TTY_MAJOR,
	.minor		= 64,
	.nr		= ARRAY_SIZE(serial_bdh_ports),
	.cons		= BDH_CONSOLE,
};

static int serial_bdh_suspend(struct platform_device *dev, pm_message_t state)
{
	struct uart_bdh_port *sport = platform_get_drvdata(dev);

	VcLog("enter\n");
	if (sport)
		uart_suspend_port(&serial_bdh_reg, &sport->port);

	return 0;
}

static int serial_bdh_resume(struct platform_device *dev)
{
	struct uart_bdh_port *sport = platform_get_drvdata(dev);

	VcLog("enter\n");
	if (sport)
		uart_resume_port(&serial_bdh_reg, &sport->port);

	return 0;
}

static int serial_bdh_probe(struct platform_device *dev)
{
	VcLog("enter\n");
	serial_bdh_ports[dev->id].port.dev = &dev->dev;
	uart_add_one_port(&serial_bdh_reg, &serial_bdh_ports[dev->id].port);
	platform_set_drvdata(dev, &serial_bdh_ports[dev->id]);
	return 0;
}

static int serial_bdh_remove(struct platform_device *dev)
{
	struct uart_bdh_port *sport = platform_get_drvdata(dev);

	VcLog("enter\n");
	platform_set_drvdata(dev, NULL);

	if (sport)
		uart_remove_one_port(&serial_bdh_reg, &sport->port);

	return 0;
}

static struct platform_driver serial_bdh_driver = {
	.probe		= serial_bdh_probe,
	.remove		= serial_bdh_remove,
	.suspend	= serial_bdh_suspend,
	.resume		= serial_bdh_resume,
	.driver		= {
		.name	= "serial_bdh_virtual",
		.owner	= THIS_MODULE,
	},
};

int __init serial_bdh_init(void)
{
	int ret;
	struct uart_bdh_port *up = &serial_bdh_ports[0];

	VcLog("enter\n");
	if (!initmem) {
		return -ENODEV;
	}
	up->port.membase = (unsigned char __iomem*)initmem;
	up->port.mapbase = initmem;
	ret = uart_register_driver(&serial_bdh_reg);
	if (ret != 0)
		return ret;

	ret = platform_driver_register(&serial_bdh_driver);
	if (ret != 0)
		uart_unregister_driver(&serial_bdh_reg);

	up->port.line = 0;
	up->port.ops = &serial_bdh_pops;
	uart_add_one_port(&serial_bdh_reg, &up->port);

	return ret;
}

void __exit serial_bdh_exit(void)
{
	VcLog("enter\n");
	platform_driver_unregister(&serial_bdh_driver);
	uart_unregister_driver(&serial_bdh_reg);
}

module_init(serial_bdh_init);
module_exit(serial_bdh_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("BDH virtual console driver");

module_param(initmem, uint, 0644);
MODULE_PARM_DESC(initmem, "Setup Buffer address of BDH console.");
