/*
 * BrightSign Mojito character LCD driver
 *
 * (C) 2007, 2008, 2009, 2010 BrightSign LLC
 *
 * Authors:
 *   Mike Crowe <mcrowe@brightsign.biz>
 *
 * This driver 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.
 *
 * This driver 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 driver; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA. Alternatively visit http://www.gnu.org/licenses/.
 *
 */

#include <linux/delay.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <asm/mach-pnx833x/gpio.h>
#include <asm/bs-mojito-lcd.h>

#define ENABLE_PNX833X_GPIO_REGS_DIRECT_ACCESS
#include <pnx833x.h>

//#define debugv printk
#define debugv(...) do {} while(0)

//#define debug printk
#define debug(...) do {} while(0)

//#define debugt printk
#define debugt(...) do {} while(0)

#define LCD_E 24
#define LCD_RW 50
#define LCD_RS 49
#define LCD_CONTRAST 17
#define LCD_DB 34
#define LCD_BUSY 46
#define LCD_BACKLIGHT 16

// About 65.3KHz (datasheet value/32bits)
#define CONTRAST_FREQ 8
#define CONTRAST_LEVEL 6

#define LCD_REAL_WIDTH 16U
#define LCD_REAL_HEIGHT 2U
#define LCD_SHADOW_WIDTH 2048U
#define LCD_SHADOW_HEIGHT LCD_REAL_HEIGHT

#define WANT_LCD_OPEN_COUNT 0

#if WANT_LCD_OPEN_COUNT
static int lcd_open_count;
#endif

static spinlock_t shared_gpio_lock;
static int lcd_contrast_level;
static int lcd_backlight_level;
static char lcd_shadow[LCD_SHADOW_HEIGHT][LCD_SHADOW_WIDTH];
static int lcd_line_origin[LCD_SHADOW_HEIGHT];
static int lcd_line_width[LCD_SHADOW_HEIGHT];
static struct lcd_point lcd_cursor;
static unsigned int lcd_scrolling_line;
static struct timer_list lcd_timer;
static int lcd_enable_busy_check;
static unsigned long lcd_skip_busy_check_jiffies;
DECLARE_MUTEX(lcd_mutex);

static inline void lcd_gpio_set(u32 pin)
{
	unsigned long flags;
	spin_lock_irqsave(&shared_gpio_lock, flags);
	gpio_write(1, pin);
	spin_unlock_irqrestore(&shared_gpio_lock, flags);
}

static inline void lcd_gpio_clear(u32 pin)
{
	unsigned long flags;
	spin_lock_irqsave(&shared_gpio_lock, flags);
	gpio_write(0, pin);
	spin_unlock_irqrestore(&shared_gpio_lock, flags);
}

static inline void lcd_gpio2_out(u32 bits, u32 value)
{
	unsigned long flags;
	u32 reg;
	
	spin_lock_irqsave(&shared_gpio_lock, flags);
	reg = PNX833X_PIO_OUT2;
	reg &= ~bits;
	reg |= value;
	PNX833X_PIO_OUT2 = reg;
	spin_unlock_irqrestore(&shared_gpio_lock, flags);
}

static inline void lcd_gpio2_dir(u32 bits, u32 value)
{
	unsigned long flags;
	u32 reg;
	
	spin_lock_irqsave(&shared_gpio_lock, flags);
	reg = PNX833X_PIO_DIR2;
	reg &= ~bits;
	reg |= value;
	PNX833X_PIO_DIR2 = reg;
	spin_unlock_irqrestore(&shared_gpio_lock, flags);
}

static inline void lcd_gpio_dir(u32 bits, u32 value)
{
	unsigned long flags;
	u32 reg;
	
	spin_lock_irqsave(&shared_gpio_lock, flags);
	reg = PNX833X_PIO_DIR;
	reg &= ~bits;
	reg |= value;
	PNX833X_PIO_DIR = reg;
	spin_unlock_irqrestore(&shared_gpio_lock, flags);
}

static inline void lcd_write_data(u8 data)
{
	lcd_gpio2_out(0xff << (LCD_DB - 32), data << (LCD_DB - 32));
}

#define CMD(x) ((1<<8)|(x))
#define IS_CMD(x) (!!((x) & (1<<8)))

#define CMD_FUNCTION_SET CMD(0x38)
#define CMD_CLEAR CMD(0x01)
#define CMD_HOME CMD(0x02)

// *) Write command=0x0C (Display control Bit 3=1, Bit 2(D)=1 for display on, bit 1(C)=0 for cursor off,  Bit 0(B)=0 for blinking off
#define CMD_DISPLAY_ON CMD(0x0c)

#define CMD_ENTRY_MODE_NORMAL CMD(0x06)


static void lcd_send(u32 value)
{
	unsigned int wait_us;

	// If the last write to the LCD was a long time ago then we
	// can just skip the busy check altogether.
	if (lcd_enable_busy_check && time_before(jiffies, lcd_skip_busy_check_jiffies)) {
		int i;
		int busy = 1;
		
		lcd_gpio_set(LCD_RW);
		for(i = 0; busy && (i != 500); ++i) {
			lcd_gpio_set(LCD_E);
			ndelay(450);
			busy = gpio_read(LCD_BUSY);
			lcd_gpio_clear(LCD_E);
			ndelay(550);
		}
		
		if (busy)
			pr_err("LCD is still busy!\n");
		else
		        debugv("LCD busy check succeeded after %d for %u\n", i, value);
		lcd_gpio_clear(LCD_RW);	
	} else {
		debugv("Skipping LCD busy check for %u\n", value);
	}
	
	if (IS_CMD(value)) {
		debugv("Send COMMAND %02x\n", value & 0xff);
		lcd_gpio_clear(LCD_RS);
	} else {
		debugv("Send DATA %02x\n", value & 0xff);
		lcd_gpio_set(LCD_RS);
	}

	lcd_write_data(value & 0xff);
	ndelay(100);
	lcd_gpio_set(LCD_E);
	ndelay(450);
	lcd_gpio_clear(LCD_E);
	ndelay(450);
	lcd_gpio_clear(LCD_RS);

	switch (value)
	{
	case CMD_CLEAR:
	case CMD_HOME:
		wait_us = 1640;
		break;
	default:
		wait_us = 40;
		break;
	}
	
	if (lcd_enable_busy_check)
		lcd_skip_busy_check_jiffies = jiffies + usecs_to_jiffies(wait_us);
	else
		udelay(wait_us);
}

static void lcd_setpos(unsigned int x, unsigned int y)
{
	lcd_send(CMD(0x80) + 0x40 * (y & 3) + (x % 0x40));
}

static void lcd_set_contrast(int bits)
{
	// Arrange for the word to contain the number of bits we
	// requested set.
	u32 pattern = (1<<bits) - 1;

	debug("Setting contrast pattern to %08x\n", pattern);
	PNX833X_FG6_PRESET = pattern;
}

static void lcd_set_backlight(int level)
{
	debug("Setting backlight to %d\n", level);
	if (level)
		lcd_gpio_clear(LCD_BACKLIGHT);
	else
		lcd_gpio_set(LCD_BACKLIGHT);
}

static void lcd_check_workaround(void)
{
	// On REVA boards GPIO49 and GPIO50 are tied together. Let's
	// try and work out if that is the case.
	gpio_select_input(LCD_RW);

	// On REVB boards LCD_RW is pulled up so let's force LCD_RS
	// low and see if LCD_RW follows. If it stays high then it
	// must be independent.
	lcd_gpio_clear(LCD_RS);
	if (!gpio_read(LCD_RW)) {
		pr_err("Mojito LCD has shorted RW pin\n");
		return;
	}

	pr_info("Mojito LCD has independent RW pin.\n");

	// Right, that means we can write to LCD_RW and do proper busy
	// checks then.
	gpio_select_output(LCD_RW);
	lcd_enable_busy_check = 1;
}

static void lcd_hw_init(void)
{
	// Configure our GPIOs how we need them.
	u64 outputs = (1ULL<<LCD_E)|(1ULL<<LCD_RS)|(1ULL<<LCD_BACKLIGHT)|(0xffULL<<LCD_DB);
	lcd_gpio2_dir(outputs >> 32, outputs >> 32);
	lcd_gpio_dir(outputs & 0xffffffff, outputs & 0xffffffff);

	// The contrast is driven by PWM
	gpio_select_function_alt(LCD_CONTRAST);

	lcd_check_workaround();
	
	{
		unsigned long flags;
		u32 reg;
		
		spin_lock_irqsave(&shared_gpio_lock, flags);
		
		// Set up the waveform generator clock
#if LCD_CONTRAST == 17
		reg = PNX833X_FG_FREQ_SEL2;
		reg &= 0x1f;
		reg |= CONTRAST_FREQ & 0x1f;
		PNX833X_FG_FREQ_SEL2 = reg;
#else
#error Unsupported contrast pin
#endif
		// Set up the data source to feed in from the shift register.
		reg = PNX833X_FG_DATA_SEL;
		reg &= (3 << (LCD_CONTRAST - 11)*2);
		PNX833X_FG_DATA_SEL = reg;
		
		spin_unlock_irqrestore(&shared_gpio_lock, flags);

		lcd_contrast_level = CONTRAST_LEVEL;
		lcd_backlight_level = 1;
		lcd_set_contrast(lcd_contrast_level);
		lcd_set_backlight(lcd_backlight_level);
	}
	
	// *) Write command=0x38 (Function set Bit 5=1, Bit 4(DL)=1 for 8 bit, bit 3(N)=1 for 2 line,  Bit 2(F)=0 for 5X8 dots
	lcd_send(CMD_FUNCTION_SET);
	udelay(4100-40);
	lcd_send(CMD_FUNCTION_SET);
	udelay(100-40);
	lcd_send(CMD_FUNCTION_SET); // We can rely on the busy check from now onwards
	udelay(100-40);
	lcd_send(CMD_DISPLAY_ON);

	// Move to the home position - this command takes a while so
	// the busy line should go high for long enough for us to
	// notice so we can rely on that to indicate that the display
	// is present. Ideally we'd clear the screen because that
	// takes even longer but that won't look as nice for the user
	// because it will leave the screen blank.
	lcd_send(CMD_HOME);
	lcd_send(CMD_ENTRY_MODE_NORMAL);

	// We don't put anything on the screen because the bootloader
	// has probably already done that.
}

static void lcd_repaint(struct lcd_point pos, unsigned int count)
{
	unsigned int line_origin = lcd_line_origin[pos.y];
	unsigned int x;
	unsigned int start, finish;

	debug("lcd_repaint([%u,%u], %u)\n", pos.x, pos.y, count);
	
	// Are any changes off the screen to the right?
	if (pos.x > line_origin + LCD_REAL_WIDTH)
		return;

	// or the left?
	if (pos.x + count <= line_origin)
		return;

	if (pos.x <= line_origin)
		start = 0;
	else
		start = pos.x;

	if (pos.x + count >= line_origin + LCD_REAL_WIDTH)
		finish = LCD_REAL_WIDTH;
	else
		finish = pos.x + count;

	debug(" repainting from %u to %u\n", start, finish);
	lcd_setpos(start, pos.y);
	for(x = start; x < finish; ++x)
		lcd_send(lcd_shadow[pos.y][line_origin + x]);
}

static int lcd_scroll_line(unsigned int line)
{
	struct lcd_point pt;

	debug("lcd_scroll_line(%u)\n", line);
	
	if (lcd_line_width[line] <= LCD_REAL_WIDTH) {
		debug(" current line width is insufficient: %d\n", lcd_line_width[line]);
		return 1;
	}
	
	if (++lcd_line_origin[line] > lcd_line_width[line] - LCD_REAL_WIDTH) {
		debug(" reset to beginning\n");
		lcd_line_origin[line] = 0;
	}

	pt.x = lcd_line_origin[line];
	pt.y = line;
	lcd_repaint(pt, lcd_line_width[line]);

	if (lcd_line_origin[line] == lcd_line_width[line] - LCD_REAL_WIDTH) {
		debug("  at end of line\n");
		return 1;
	}
	else
		return 0;
}

static void lcd_reset_line(unsigned int line)
{
	struct lcd_point pt = { .x = 0, .y = line };
	lcd_line_origin[line] = 0;
	lcd_repaint(pt, lcd_line_width[line]);
}

#define JIFFY_EOL_INCREMENT HZ
#define JIFFY_BOL_INCREMENT HZ/2
#define JIFFY_CHAR_INCREMENT HZ/4
#define JIFFY_LOCKED_INCREMENT HZ/10

static void lcd_timer_expired(unsigned long data)
{
	static int at_end_last_time = 0;

	if (down_trylock(&lcd_mutex) == 0) {
		if (at_end_last_time) {
			// Just redraw the current line at the beginning
			lcd_reset_line(lcd_scrolling_line);
			
			if (++lcd_scrolling_line >= LCD_REAL_HEIGHT) {
				debugt("lcd_scrolling_line reset\n");
				lcd_scrolling_line = 0;
			}
			debugt("BOL_INCREMENT\n");
			lcd_timer.expires = jiffies + JIFFY_BOL_INCREMENT;
			at_end_last_time = 0;
		} else {
			if (lcd_scroll_line(lcd_scrolling_line)) {
				lcd_timer.expires = jiffies + JIFFY_EOL_INCREMENT;
				debugt("EOL_INCREMENT\n");
				at_end_last_time = 1;
			} else {
				lcd_timer.expires = jiffies + JIFFY_CHAR_INCREMENT;
				debugt("CHAR_INCREMENT\n");
			}
		}

		up(&lcd_mutex);
	} else {
		lcd_timer.expires = jiffies + JIFFY_LOCKED_INCREMENT;
		debugt("LOCKED_INCREMENT\n");
	}
	
	add_timer(&lcd_timer);
}

static void enable_disable_timer(void)
{
	int scrolling_required = 0;
	unsigned int line;
	debugt("enable_disable_timer\n");
	
	for (line = 0; line < LCD_SHADOW_HEIGHT; ++line) {
		if (lcd_line_width[line] > LCD_REAL_WIDTH) {
			debugt("scrolling required on line %u\n", line);
			scrolling_required = 1;
			break;
		}
	}

	if (timer_pending(&lcd_timer)) {
		debugt("timer is running\n");
		if (!scrolling_required) {
			debugt("Disabling timer\n");
			del_timer_sync(&lcd_timer);
			lcd_scrolling_line = 0;
		}
	} else {
		debugt("timer is not running\n");
		if (scrolling_required) {
			debugt("Enabling timer\n");
			lcd_timer.expires = jiffies + JIFFY_BOL_INCREMENT;
			add_timer(&lcd_timer);
		}
	}
}

static int lcd_clear(void)
{
	unsigned int y;

	debug("lcd_clear\n");
	if (down_interruptible(&lcd_mutex))
		return -EINTR;
	
	lcd_cursor.x = lcd_cursor.y = 0;
	for(y = 0; y < LCD_SHADOW_HEIGHT; ++y) {
		lcd_line_width[y] = 0;
		lcd_line_origin[y] = 0;
	}
	enable_disable_timer();
	memset(&lcd_shadow[0][0], ' ', LCD_SHADOW_WIDTH * LCD_SHADOW_HEIGHT);
	lcd_send(CMD_CLEAR);

	up(&lcd_mutex);
	return 0;
}

// Must be called with mutex held.
static void __lcd_clear_eol(void)
{
	struct lcd_point pt = { 0, lcd_cursor.y };
	
	lcd_line_width[lcd_cursor.y] = lcd_cursor.x;
	lcd_line_origin[lcd_cursor.y] = 0;
	if (lcd_cursor.x < LCD_SHADOW_WIDTH)
		memset(&lcd_shadow[lcd_cursor.y][lcd_cursor.x], ' ', LCD_SHADOW_WIDTH - lcd_cursor.x);
	lcd_repaint(pt, LCD_REAL_WIDTH);

	enable_disable_timer();
}

static inline int lcd_clear_eol(void)
{
	if (down_interruptible(&lcd_mutex))
		return -EINTR;
	__lcd_clear_eol();
	up(&lcd_mutex);
	return 0;
}

static int lcd_open(struct inode *inode, struct file *filp)
{
#if WANT_LCD_OPEN_COUNT
	if (lcd_open_count)
		return -EBUSY;

	++lcd_open_count;
#endif // WANT_LCD_OPEN_COUNT

	return 0;
}

static int lcd_release(struct inode *inode, struct file *filp)
{
#if WANT_LCD_OPEN_COUNT
    --lcd_open_count;
#endif // WANT_LCD_OPEN_COUNT

    return 0;
}

static long lcd_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	debug("In lcd_ioctl(%u)\n", cmd);
	switch (cmd) {
	case LCDIOC_SCONTRAST:
	{
		if (get_user(lcd_contrast_level, (int __user *)arg))
			return -EFAULT;
		lcd_set_contrast(lcd_contrast_level);
		return 0;
	}
	
	case LCDIOC_GCONTRAST:
		return put_user(lcd_contrast_level, (int __user *)arg);
		
	case LCDIOC_SBACKLIGHT:
	{
		if (get_user(lcd_backlight_level, (int __user *)arg))
			return -EFAULT;
		lcd_set_backlight(lcd_backlight_level);
		return 0;
	}
	
	case LCDIOC_GBACKLIGHT:
		return put_user(lcd_backlight_level, (int __user *)arg);

	case LCDIOC_CLEAR:
		return lcd_clear();

	case LCDIOC_CLEAR_TO_LINE_END:
		return lcd_clear_eol();

	case LCDIOC_SETPOS:
	case LCDIOC_SETPOS_CLEAR_TO_LINE_END:
	{
		struct lcd_point pt;
		if (copy_from_user(&pt, (struct lcd_point __user *)arg, sizeof(pt)))
			return -EFAULT;

		if ((pt.x >= LCD_SHADOW_WIDTH) || (pt.y >= LCD_SHADOW_HEIGHT))
			return -EINVAL;
		
		if (down_interruptible(&lcd_mutex))
			return -EINTR;
		lcd_cursor = pt;
		if (cmd == LCDIOC_SETPOS_CLEAR_TO_LINE_END)
			__lcd_clear_eol();
		up(&lcd_mutex);
		return 0;
	}

	default:
		return -EINVAL;
	}
}

static ssize_t lcd_write(struct file *filp, const char *buf, size_t count, loff_t *ppos)
{
	size_t written = count;

	if (down_interruptible(&lcd_mutex))
		return -EINTR;
	
	debug("lcd_write with %u characters at [%u, %u]\n", count, lcd_cursor.x, lcd_cursor.y);
	if (lcd_cursor.x + count > LCD_SHADOW_WIDTH)
		written = LCD_SHADOW_WIDTH - lcd_cursor.x;

	memcpy(&lcd_shadow[lcd_cursor.y][lcd_cursor.x], buf, written);
	lcd_repaint(lcd_cursor, written);
	lcd_cursor.x += written;

	debug(" cursor position = %u, line width is %u\n", lcd_cursor.x, lcd_line_width[lcd_cursor.y]);
	if (lcd_cursor.x > lcd_line_width[lcd_cursor.y]) {
		lcd_line_width[lcd_cursor.y] = lcd_cursor.x;
		debug(" new cursor position = %u, line width is %u\n", lcd_cursor.x, lcd_line_width[lcd_cursor.y]);
		enable_disable_timer();
	}

	up(&lcd_mutex);

	// Say we wrote it all to stop userspace getting into a loop.
	return count;
}

static const struct file_operations lcd_fops = {
	.open = lcd_open,
	.release = lcd_release,
	.unlocked_ioctl = lcd_ioctl,
	.write = lcd_write
};

static struct miscdevice lcd_dev = {
        MISC_DYNAMIC_MINOR,
        "lcd",
        &lcd_fops
};

static int __devinit lcd_probe(struct platform_device *dev)
{
	// Make sure our opinion of the screen is empty. It may not be
	// true but at least it isn't gibberish.
	memset(&lcd_shadow[0][0], ' ', LCD_SHADOW_WIDTH * LCD_SHADOW_HEIGHT);
	lcd_hw_init();
	init_timer(&lcd_timer);
	lcd_timer.data = 0;
	lcd_timer.function = lcd_timer_expired;
	return misc_register(&lcd_dev);
}

static void lcd_shutdown(struct platform_device *dev)
{
	del_timer_sync(&lcd_timer);
	misc_deregister(&lcd_dev);
}

static int __devexit lcd_remove(struct platform_device *dev)
{
	lcd_shutdown(dev);
        return 0;
}

static struct platform_driver lcd_platform_driver = {
        .driver         = {
                .name   = "bs-mojito-lcd",
                .owner  = THIS_MODULE,
        },
        .probe          = lcd_probe,
        .remove         = lcd_remove,
        .shutdown       = lcd_shutdown,
};

static struct platform_device *lcd_platform_device;

static int __init lcd_init(void)
{
	int err;
	spin_lock_init(&shared_gpio_lock);
	
	err = platform_driver_register(&lcd_platform_driver);
	if (err == 0) {
		lcd_platform_device = platform_device_alloc("bs-mojito-lcd", -1);
		if (lcd_platform_device) {
			err = platform_device_add(lcd_platform_device);
			if (err == 0) {
				return 0;
			} else {
				platform_device_put(lcd_platform_device);
			}
		} else {
			err = -ENOMEM;
		}
		
		platform_driver_unregister(&lcd_platform_driver);
	}
	return err;
}

static void __exit lcd_exit(void)
{
	platform_device_del(lcd_platform_device);
	platform_driver_unregister(&lcd_platform_driver);
}

module_init(lcd_init);
module_exit(lcd_exit);

