/** \file server/drivers/sed1520.c
 * LCDd \c sed1520 driver for graphic displays based on the SED1520.
 *
 * This is a driver for 122x32 pixel graphic displays based on the SED1520
 * controller connected to the parallel port. This Controller has no
 * built in character generator. Therefore all fonts and pixels are generated
 * by this driver.
 */

/*-
 * Copyright (C) 2001 Robin Adams <robin@adams-online.de>
 *		 2011 Markus Dolze <bsdfan@nurfuerspam.de>
 *
 * This file is released under the GNU General Public License. Refer to the
 * COPYING file distributed with this package.
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include "lcd.h"
#include "sed1520.h"
#include "glcd_font5x8.h"
#include "sed1520fm.h"
#include "shared/report.h"
#include "port.h"
#include "timing.h"
#include "lpt-port.h"

#define uPause timing_uPause

#ifndef DEFAULT_PORT
# define DEFAULT_PORT	0x378
#endif

#define CELLWIDTH	6
#define CELLHEIGHT	8

#define PIXELWIDTH	122
#define PIXELHEIGHT	32

#define WIDTH		((int) (PIXELWIDTH / CELLWIDTH))	/* 20 */
#define HEIGHT		((int) (PIXELHEIGHT / CELLHEIGHT))	/*  4 */

#define A0	nSEL		/* pin 17 */
#define CS1	nLF		/* pin 14, EN1 for 68-style connection */
#define CS2	INIT		/* pin 16, EN2 for 68-style connection */
#define WR	nSTRB		/* pin 1 */

/** private data for the \c sed1520 driver */
typedef struct sed1520_private_data {
    unsigned short port;
    int interface;
    int delayMult;
    int haveInverter;
    unsigned char colStartAdd;

    unsigned char *framebuf;
} PrivateData;


/* Vars for the server core */
MODULE_EXPORT char *api_version = API_VERSION;
MODULE_EXPORT int stay_in_foreground = 0;
MODULE_EXPORT int supports_multiple = 0;
MODULE_EXPORT char *symbol_prefix = "sed1520_";

/**
 * Writes command value to one or both SED1520 selected by chip.
 * \param p      Pointer to private data structure
 * \param value  Command byte to write
 * \param chip   Bitmap of controllers to send value to
 *
 * \note It is a little code duplication having writecommand and writedata
 *       both, but I leave it that way.
 */
static void
writecommand(PrivateData *p, int value, int chip)
{
    if (p->interface == 68) {
	port_out(p->port + 2, 0 ^ OUTMASK);
	port_out(p->port, value);
	/* cycle E */
	port_out(p->port + 2, ((chip & CS1) + (chip & CS2)) ^ OUTMASK);
	if (p->delayMult)
	    uPause(p->delayMult);
	port_out(p->port + 2, 0 ^ OUTMASK);
    }
    else {
	port_out(p->port, value);
	if (p->haveInverter) {
	    /*
	     * lower WR, rise A0 and CS1 and/or CS2 taking bit inversion of
	     * parallel port into account. External inverter required!
	     */
	    port_out(p->port + 2, WR + CS1 - (chip & CS1) + (chip & CS2));
	    /* rise WR */
	    port_out(p->port + 2, CS1 - (chip & CS1) + (chip & CS2));
	    if (p->delayMult)
		uPause(p->delayMult);
	    /* lower WR again */
	    port_out(p->port + 2, WR + CS1 - (chip & CS1) + (chip & CS2));
	    if (p->delayMult)
		uPause(p->delayMult);
	}
	else {			/* No inverter connected */
	    /* Note: CS?-(chip&CS?) drive the pin low if controller is set */
	    port_out(p->port + 2, (WR + CS1 - (chip & CS1) + CS2 - (chip & CS2)) ^ OUTMASK);
	    port_out(p->port + 2, (CS1 - (chip & CS1) + CS2 - (chip & CS2)) ^ OUTMASK);
	    if (p->delayMult)
		uPause(p->delayMult);
	    port_out(p->port + 2, (WR + CS1 - (chip & CS1) + CS2 - (chip & CS2)) ^ OUTMASK);
	    if (p->delayMult)
		uPause(p->delayMult);
	}
    }
}

/**
 * Writes data value to one or both SED1520 selected by chip.
 * \param p      Pointer to private data structure
 * \param value  Data byte to write
 * \param chip   Bitmap of controllers to send value to
 */
static void
writedata(PrivateData *p, int value, int chip)
{
    if (p->interface == 68) {
	port_out(p->port + 2, (A0) ^ OUTMASK);
	port_out(p->port, value);
	/* cycle E */
	port_out(p->port + 2, (A0 + (chip & CS1) + (chip & CS2)) ^ OUTMASK);
	if (p->delayMult)
	    uPause(p->delayMult);
	port_out(p->port + 2, (A0) ^ OUTMASK);
    }
    else {
	port_out(p->port, value);
	if (p->haveInverter) {
	    /* lower WR and A0, rise CS1 and/or CS2. See also writecommand. */
	    port_out(p->port + 2, A0 + WR + CS1 - (chip & CS1) + (chip & CS2));
	    port_out(p->port + 2, A0 + CS1 - (chip & CS1) + (chip & CS2));
	    if (p->delayMult)
		uPause(p->delayMult);
	    port_out(p->port + 2, A0 + WR + CS1 - (chip & CS1) + (chip & CS2));
	    if (p->delayMult)
		uPause(p->delayMult);
	}
	else {
	    port_out(p->port + 2, (A0 + WR + CS1 - (chip & CS1) + CS2 - (chip & CS2)) ^ OUTMASK);
	    port_out(p->port + 2, (A0 + CS1 - (chip & CS1) + CS2 - (chip & CS2)) ^ OUTMASK);
	    if (p->delayMult)
		uPause(p->delayMult);
	    port_out(p->port + 2, (A0 + WR + CS1 - (chip & CS1) + CS2 - (chip & CS2)) ^ OUTMASK);
	    if (p->delayMult)
		uPause(p->delayMult);
	}
    }
}

/**
 * Selects a page (=row) on both SED1520.
 * \param p     Pointer to private data structure
 * \param page  Page (=row) number (0-3)
 */
static void
selectpage(PrivateData *p, int page)
{
    writecommand(p, PAGE_ADR + (page & 0x03), CS1 + CS2);
}

/**
 * Selects a column on the SED1520 specified by chip.
 * \param p       Pointer to private data structure
 * \param column  Select column (segment) in controller (0-60)
 * \param chip    Bitmap of controllers to send value to
 */
static void
selectcolumn(PrivateData *p, int column, int chip)
{
    writecommand(p, COLUMN_ADR + (column & 0x7F), chip);
}

/*-
 * Display memory layout information:
 *
 * The SED1520 does not use a linear display data memory but a paged memory
 * with four pages (0-3) corresponding to rows 0-7, 8-15, 16-23, and 24-31.
 * Within each page, one bytes represents 8 pixels of one column (from 0-60)
 * with bit 0 (LSB) being the top and bit 7 (MSB) the bottom pixel within
 * that page.
 *
 * To write data to display memory one selects the page and column and then
 * writes bytes of pixel data to the display. The column counter is increased
 * by each write so the display fills from left to right (normal mode) or
 * right to left (inverted mapping).
 */

/**
 * Draws character z from fontmap to the framebuffer at position x,y.
 * The fontmap is stored in rows while the framebuffer is stored in columns,
 * so we need a little conversion.
 *
 * \param framebuf  Pointer to framebuffer
 * \param x         Character column (zero-based)
 * \param y         Line (zero-based)
 * \param z         Character index in fontmap
 */
static void
drawchar2fb(unsigned char *framebuf, int x, int y, unsigned char z)
{
    int i, j;

    if ((x < 0) || (x >= WIDTH) || (y < 0) || (y >= HEIGHT))
	return;

    /* for each raster column */
    for (i = CELLWIDTH; i > 0; i--) {
	int k = 0;

	/* gather the bits from the fontmap for each raster row */
	for (j = 0; j < CELLHEIGHT; j++)
	    k |= ((glcd_iso8859_1[(int)z][j] >> (i - 1)) & 0x01) << j;

	/* And store it in framebuffer pixel column */
	framebuf[(y * PIXELWIDTH) + (x * CELLWIDTH) + (CELLWIDTH - i)] = k;
    }
}

/**
 * API: Initialize the driver.
 */
MODULE_EXPORT int
sed1520_init(Driver * drvthis)
{
    PrivateData *p;
    char inverted;

    /* Allocate and store private data */
    p = (PrivateData *) calloc(1, sizeof(PrivateData));
    if (p == NULL)
	return -1;
    if (drvthis->store_private_ptr(drvthis, p))
	return -1;

    /* What port to use */
    p->port = drvthis->config_get_int(drvthis->name, "Port", 0, DEFAULT_PORT);

    /* Initialize timing */
    if (timing_init() == -1) {
	report(RPT_ERR, "%s: timing_init() failed (%s)", drvthis->name, strerror(errno));
	return -1;
    }
    p->delayMult = drvthis->config_get_int(drvthis->name, "delaymult", 0, 1);
    if (p->delayMult < 0 || p->delayMult > 1000) {
	report(RPT_WARNING, "%s: DelayMult value invalid, using default (1)", drvthis->name);
	p->delayMult = 1;
    }
    if (p->delayMult == 0)
	report(RPT_INFO, "%s: Delay is disabled", drvthis->name);

    /* Allocate our framebuffer */
    p->framebuf = (unsigned char *) calloc(PIXELWIDTH * HEIGHT, sizeof(unsigned char));
    if (p->framebuf == NULL) {
	report(RPT_ERR, "%s: unable to allocate framebuffer", drvthis->name);
	return -1;
    }

    /* Clear screen */
    memset(p->framebuf, '\0', PIXELWIDTH * HEIGHT);

    /* Open port */
    if (port_access_multiple(p->port, 3)) {
	report(RPT_ERR, "%s: unable to access port 0x%03X", drvthis->name, p->port);
	return -1;
    }

    /* Get interface style (68-family or 80-family) */
    p->interface = drvthis->config_get_int(drvthis->name, "InterfaceType", 0, 80);
    if (p->interface != 68 && p->interface != 80) {
	report(RPT_WARNING, "%s: Invalid interface configured, using type 80", drvthis->name);
	p->interface = 80;
    }

    /*
     * The original wiring used an inverter to drive the control lines. As
     * someone may still be using this, the following setting in ON by default.
     */
    p->haveInverter = drvthis->config_get_bool(drvthis->name, "HaveInverter", 0, 1);

    /*
     * Inverted Mapping: Segments are addressed from right to left and start
     * at column 19 (address 0x13) up to column 79 (address 0x4F).
     */
    inverted = drvthis->config_get_bool(drvthis->name, "InvertedMapping", 0, 0);
    if (inverted)
	p->colStartAdd = 0x13;
    else
	p->colStartAdd = 0;

    /* Initialize display */
    if (drvthis->config_get_bool(drvthis->name, "UseHardReset", 0, 0) == 1) {
	writedata(p, 0xFF, CS1 + CS2);
	writedata(p, 0xFF, CS1 + CS2);
	writedata(p, 0xFF, CS1 + CS2);
    }
    writecommand(p, SOFT_RESET, CS1 + CS2);
    writecommand(p, (inverted) ? ADC_INV : ADC_NORM, CS1 + CS2);
    writecommand(p, DISP_ON, CS1 + CS2);
    writecommand(p, DISP_START_LINE, CS1 + CS2);
    selectpage(p, 3);

    report(RPT_DEBUG, "%s: init() done", drvthis->name);
    return 0;
}

/**
 * API: Frees the framebuffer and exits the driver.
 */
MODULE_EXPORT void
sed1520_close(Driver * drvthis)
{
    PrivateData *p = drvthis->private_data;

    if (p != NULL) {
	if (p->framebuf != NULL)
	    free(p->framebuf);

	free(p);
    }
    drvthis->store_private_ptr(drvthis, NULL);
}

/**
 * API: Returns the display width
 */
MODULE_EXPORT int
sed1520_width(Driver * drvthis)
{
    return WIDTH;
}

/**
 * API: Returns the display height
 */
MODULE_EXPORT int
sed1520_height(Driver * drvthis)
{
    return HEIGHT;
}

/**
 * API: Return the width of a character in pixels.
 */
MODULE_EXPORT int
sed1520_cellwidth(Driver * drvthis)
{
    return CELLWIDTH;
}

/**
 * API: Return the height of a character in pixels.
 */
MODULE_EXPORT int
sed1520_cellheight(Driver * drvthis)
{
    return CELLHEIGHT;
}

/**
 * API: Clears the LCD screen
 */
MODULE_EXPORT void
sed1520_clear(Driver * drvthis)
{
    PrivateData *p = drvthis->private_data;

    memset(p->framebuf, '\0', PIXELWIDTH * HEIGHT);
}

/**
 * API: Flushes all output to the lcd. No conversion needed here as
 * framebuffer is prepared by \c drawchar2fb.
 */
MODULE_EXPORT void
sed1520_flush(Driver * drvthis)
{
    PrivateData *p = drvthis->private_data;
    int i, j;

    for (i = 0; i < HEIGHT; i++) {
	/* select line/page */
	selectpage(p, i);

	/* update left half of display */
	selectcolumn(p, p->colStartAdd, CS1);
	for (j = 0; j < PIXELWIDTH / 2; j++)
	    writedata(p, p->framebuf[j + (i * PIXELWIDTH)], CS1);

	/* update right half of display */
	selectcolumn(p, p->colStartAdd, CS2);
	for (j = PIXELWIDTH / 2; j < PIXELWIDTH; j++)
	    writedata(p, p->framebuf[j + (i * PIXELWIDTH)], CS2);
    }
}

/**
 * API: Prints a string on the lc display, at position (x,y). The
 * upper-left is (1,1), and the lower right should be (20,4).
 */
MODULE_EXPORT void
sed1520_string(Driver * drvthis, int x, int y, const char string[])
{
    PrivateData *p = drvthis->private_data;
    int i;

    x--;			/* Convert 1-based coords to 0-based */
    y--;

    for (i = 0; string[i] != '\0'; i++)
	drawchar2fb(p->framebuf, x + i, y, string[i]);
}

/**
 * API: Writes  char c at position x,y into the framebuffer.
 * x and y are 1-based textmode coordinates.
 */
MODULE_EXPORT void
sed1520_chr(Driver * drvthis, int x, int y, char c)
{
    PrivateData *p = drvthis->private_data;

    y--;
    x--;
    drawchar2fb(p->framebuf, x, y, c);
}

/**
 * API: This function draws a number num into the last 3 rows of the
 * framebuffer at 1-based position x. It should draw a 4-row font, but me
 * thinks this would look a little stretched. When num=10 a colon is drawn.
 */
MODULE_EXPORT void
sed1520_num(Driver * drvthis, int x, int num)
{
    PrivateData *p = drvthis->private_data;
    int z, c;

    x--;

    /* return on illegal char or illegal position */
    if ((x >= WIDTH) || (num < 0) || (num > 10))
	return;

    /*
     * For each page (= row of 8 dots) starting in page 1 (page 0 is left
     * empty - thus the numbers are aligned at the bottom of the display),
     * copy <width> number of bytes into the framebuffer.
     */
    for (z = 0; z < 3; z++) {
	for (c = 0; c < widtbl_NUM[num]; c++) {
	    if ((x * CELLWIDTH + c >= 0) && (x * CELLWIDTH + c < PIXELWIDTH)) {
		p->framebuf[((z + 1) * PIXELWIDTH) + (x * CELLWIDTH) + c] =
		    chrtbl_NUM[num][c * 3 + z];
	    }
	}
    }
}

/**
 * API: Changes the font of character n to a pattern given by *dat.
 * HD44780 controllers only posses 8 programmable chars. But we store the
 * fontmap completely in RAM, so every character can be altered.
 *
 * Important: Characters have to be redrawn by drawchar2fb() to show their
 * new shape. Because we use a non-standard 6x8 font a *dat not calculated
 * from width and height will fail.
 */
MODULE_EXPORT void
sed1520_set_char(Driver * drvthis, int n, char *dat)
{
    int row;
    unsigned char mask = (1 << CELLWIDTH) - 1;

    if (n < 0 || n > 255)
	return;
    if (!dat)
	return;

    for (row = 0; row < CELLHEIGHT; row++)
	glcd_iso8859_1[n][row] = dat[row] & mask;
}

/**
 * API: Draws a vertical from the bottom up at 1-based position x.
 */
MODULE_EXPORT void
sed1520_vbar(Driver *drvthis, int x, int y, int len, int promille, int options)
{
    PrivateData *p = drvthis->private_data;
    int i, j, k;
    int pixels;

    x--;

    if (x < 0 || y < 1 || x >= WIDTH || y > HEIGHT || len > HEIGHT)
	return;

    pixels = len * CELLHEIGHT * promille / 1000;

    for (j = 0; j < 3; j++) {
	k = 0;
	/* Set height of block. Bottom is leftmost bit */
	for (i = 0; i < CELLHEIGHT; i++) {
	    if (pixels > i)
		k |= (1 << (CELLHEIGHT - 1 - i));
	}

	/* Draw directly into framebuffer starting from bottom */
	p->framebuf[((3 - j) * PIXELWIDTH) + (x * CELLWIDTH) + 0] = 0;
	p->framebuf[((3 - j) * PIXELWIDTH) + (x * CELLWIDTH) + 1] = k;
	p->framebuf[((3 - j) * PIXELWIDTH) + (x * CELLWIDTH) + 2] = k;
	p->framebuf[((3 - j) * PIXELWIDTH) + (x * CELLWIDTH) + 3] = k;
	p->framebuf[((3 - j) * PIXELWIDTH) + (x * CELLWIDTH) + 4] = k;
	p->framebuf[((3 - j) * PIXELWIDTH) + (x * CELLWIDTH) + 5] = 0;

	pixels -= CELLHEIGHT;
    }
}

/**
 * API: Draws a horizontal bar from left to right at 1-based position x,y into
 * the framebuffer.
 */
MODULE_EXPORT void
sed1520_hbar(Driver *drvthis, int x, int y, int len, int promille, int options)
{
    PrivateData *p = drvthis->private_data;
    int i;
    int pixels;

    x--;
    y--;

    if ((y < 0) || (y >= HEIGHT) || (x < 0) || (len < 0) || (x + len > WIDTH))
	return;

    pixels = len * CELLWIDTH * promille / 1000;

    /*
     * Draw directly into framebuffer. Drawing is easy, as one byte is one
     * column in a page. Use 5 dots in the middle (0x3E).
     */
    for (i = 0; i < pixels; i++)
	p->framebuf[(y * PIXELWIDTH) + (x * CELLWIDTH) + i] = 0x7C;
}

/**
 * API: Place an icon on the screen.
 */
MODULE_EXPORT int
sed1520_icon(Driver * drvthis, int x, int y, int icon)
{
    int icon_char;

    if ((icon_char = glcd_icon5x8(icon)) != -1) {
        sed1520_chr(drvthis, x, y, icon_char);
        return 0;
    }
    return -1;
}
