/* $Id$
 * $URL$
 *
 * plugin kvv (karlsruher verkehrsverbund)
 *
 * Copyright (C) 2006 Till Harbaum <till@harbaum.org>
 * Copyright (C) 2006 The LCD4Linux Team <lcd4linux-devel@users.sourceforge.net>
 *
 * This file is part of LCD4Linux.
 *
 * LCD4Linux 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, or (at your option)
 * any later version.
 *
 * LCD4Linux 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 program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

/* 
 * exported functions:
 *
 * int plugin_init_kvv (void)
 *  adds various functions
 * void plugin_exit_kvv (void)
 *
 */

/* define the include files you need */
#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>

/* network specific includes */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>

/* these should always be included */
#include "debug.h"
#include "plugin.h"
#include "cfg.h"
#include "thread.h"

/* these can't be configured as it doesn't make sense to change them */
#define HTTP_SERVER "www.init-ka.de"
#define HTTP_REQUEST "/webfgi/StopInfoInplace.aspx?ID=%s"
#define USER_AGENT   "lcd4linux - KVV plugin (http://ssl.bulix.org/projects/lcd4linux/wiki/plugin_kvv)"

#define DEFAULT_STATION_ID    "89"	/* Hauptbahnhof */

/* example ids: 
 * 89     = Hauptbahnhof
 * 12_701 = Berufsakademie
 */

/* total max values to calculate shm size */
#define MAX_LINES           4
#define MAX_LINE_LENGTH     8
#define MAX_STATION_LENGTH 40

typedef struct {
    char line[MAX_LINE_LENGTH + 1];
    char station[MAX_STATION_LENGTH + 1];
    int time;
} kvv_entry_t;

typedef struct {
    int entries, error;
    kvv_entry_t entry[MAX_LINES];
} kvv_shm_t;

static char *station_id = NULL;
static char *proxy_name = NULL;
static int port = 80;
static pid_t pid = -1;
static int refresh = 60;
static int abbreviate = 0;

static int initialized = 0;
static int mutex = 0;
static int shmid = -1;
static kvv_shm_t *shm = NULL;

#define SECTION   "Plugin:KVV"

#define TIMEOUT_SHORT 1		/* wait this long for additional data */
#define TIMEOUT_LONG  10	/* wait this long for initial data */

/* search an element in the result string */
static int get_element(char *input, char *name, char **data)
{
    int skip = 0;
    int len = 0;
    int state = 0;		/* nothing found yet */

    /* search entire string */
    while (*input) {

	if (skip == 0) {
	    switch (state) {
	    case 0:
		if (*input == '<')
		    state = 1;
		else
		    state = 0;
		break;

	    case 1:
		/* ignore white spaces */
		if (*input != ' ') {
		    if (strncasecmp(input, name, strlen(name)) == 0) {
			state = 2;
			skip = strlen(name) - 1;
		    } else
			state = 0;
		}
		break;

	    case 2:
		if (*input == ' ') {
		    *data = ++input;
		    while (*input && (*input++ != '>'))
			len++;

		    return len;
		} else
		    state = 0;
		break;
	    }
	} else if (skip)
	    skip--;

	input++;
    }

    return -1;
}

/* serach an attribute within an element */
static int get_attrib(char *input, char *name, char **data)
{
    int skip = 0;
    int len = 0;
    int state = 0;		/* nothing found */

    /* search in this element */
    while (*input != '>') {
	/* ignore white spaces */
	if (((*input != ' ') && (*input != '\t')) && (skip == 0)) {
	    switch (state) {
	    case 0:
		if (strncasecmp(input, name, strlen(name)) == 0) {
		    state = 1;
		    skip = strlen(name) - 1;
		}
		break;

	    case 1:
		if (*input == '=')
		    state = 2;
		else
		    state = 0;
		break;

	    case 2:
		if (*input == '\"') {
		    *data = ++input;
		    while (*input++ != '\"')
			len++;

		    return len;
		} else
		    state = 0;

		break;
	    }
	} else if (skip)
	    skip--;

	input++;
    }
    return -1;
}

static int http_open(char *name)
{
    struct sockaddr_in server;
    struct hostent *host_info;
    unsigned long addr;
    int sock;

    /* create socket */
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
	perror("failed to create socket");
	return -1;
    }

    /* Erzeuge die Socketadresse des Servers 
     * Sie besteht aus Typ, IP-Adresse und Portnummer */
    memset(&server, 0, sizeof(server));
    if ((addr = inet_addr(name)) != INADDR_NONE) {
	memcpy((char *) &server.sin_addr, &addr, sizeof(addr));
    } else {
	/* Wandle den Servernamen in eine IP-Adresse um */
	host_info = gethostbyname(name);
	if (NULL == host_info) {
	    error("[KVV] Unknown server: %s", name);
	    return -1;
	}
	memcpy((char *) &server.sin_addr, host_info->h_addr, host_info->h_length);
    }

    server.sin_family = AF_INET;
    server.sin_port = htons(port);

    /* Baue die Verbindung zum Server auf */
    if (connect(sock, (struct sockaddr *) &server, sizeof(server)) < 0) {
	perror("can't connect to server");
	return -1;
    }

    return sock;
}

static void get_text(char *input, char *end, char *dest, int dlen)
{
    int state = 0;		/* nothing yet, outside any element */
    int cnt = 0;

    while (*input) {
	switch (state) {
	case 0:
	    if (*input == '<')
		state = 1;
	    else {
		if (cnt < (dlen - 1))
		    dest[cnt++] = *input;
	    }
	    break;

	case 1:
	    if (*input == '/')
		state = 2;
	    else if (*input == '>')
		state = 0;
	    break;

	case 2:
	    if (strncasecmp(input, end, strlen(end)) == 0) {
		dest[cnt++] = 0;
		return;
	    }
	    break;
	}

	input++;
    }

}

static void process_station_string(char *str)
{
    char *p, *q;
    int last, i;

    /* some strings to replace */
    char *repl[] = {
	"Hauptbahnhof", "Hbf.",
	"Bahnhof", "Bhf.",
	"Karlsruhe", "KA",
	"Schienenersatzverkehr", "Ersatzv.",
	"Marktplatz", "Marktpl.",
    };

    /* decode utf8 */
    p = q = str;
    last = 0;
    while (*p) {
	if (last) {
	    *q++ = (last << 6) | (*p & 0x3f);
	    last = 0;
	} else if ((*p & 0xe0) == 0xc0) {
	    last = *p & 3;
	} else
	    *q++ = *p;

	p++;
    }
    *q++ = 0;

    /* erase multiple spaces and replace umlauts */
    p = q = str;
    last = 1;			/* no leading spaces */
    while (*p) {
	if ((!last) || (*p != ' ')) {

	    /* translate from latin1 to hd44780 */
	    if (*p == (char) 228)	/* lower a umlaut */
		*q++ = (char) 0xe1;
	    else if (*p == (char) 223)	/* sz ligature */
		*q++ = (char) 0xe2;
	    else if (*p == (char) 246)	/* lower o umlaut */
		*q++ = (char) 0xef;
	    else if (*p == (char) 252)	/* lower u umlaut */
		*q++ = (char) 0xf5;
	    else
		*q++ = *p;
	}

	last = (*p == ' ');
	p++;
    }
    *q++ = 0;

    /* replace certain (long) words with e.g. abbreviations if enabled */
    if (abbreviate) {

	for (i = 0; i < (int) (sizeof(repl) / (2 * sizeof(char *))); i++) {
	    if ((p = strstr(str, repl[2 * i])) != NULL) {

		/* move new string */
		memcpy(p, repl[2 * i + 1], strlen(repl[2 * i + 1]));
		/* move rest of string down */
		memmove(p + strlen(repl[2 * i + 1]),
			p + strlen(repl[2 * i]), strlen(str) - (p - str) - strlen(repl[2 * i]) + 1);
	    }
	}
    }
}

static void kvv_client( __attribute__ ((unused))
		       void *dummy)
{
    char ibuffer[8192];
    char obuffer[1024];
    int count, i, sock;

    char server_name[] = HTTP_SERVER;
    char *connect_to;

    /* connect to proxy if given, to server otherwise */
    if ((proxy_name != NULL) && (strlen(proxy_name) != 0))
	connect_to = proxy_name;
    else
	connect_to = server_name;

    info("[KVV] Connecting to %s", connect_to);

    while (1) {

	sock = http_open(connect_to);
	if (sock < 0) {
	    error("[KVV] Error accessing server/proxy: %s", strerror(errno));
	    return;
	}
	/* create and set get request */
	if (snprintf(obuffer, sizeof(obuffer),
		     "GET http://%s" HTTP_REQUEST " HTTP/1.1\n"
		     "Host: %s\n" "User-Agent: " USER_AGENT "\n\n", server_name, station_id,
		     server_name) >= (int) sizeof(obuffer)) {

	    info("[KVV] Warning, request has been truncated!");
	}

	info("[KVV] Sending first (GET) request ...");
	send(sock, obuffer, strlen(obuffer), 0);

	count = 0;
	do {
	    fd_set rfds;
	    struct timeval tv;

	    FD_ZERO(&rfds);
	    FD_SET(sock, &rfds);

	    tv.tv_sec = count ? TIMEOUT_SHORT : TIMEOUT_LONG;
	    tv.tv_usec = 0;

	    i = select(FD_SETSIZE, &rfds, NULL, NULL, &tv);
	    if (i < 0) {
		perror("select");
		exit(1);
	    }

	    if (i != 0) {
		i = recv(sock, ibuffer + count, sizeof(ibuffer) - count - 1, 0);
		count += i;
	    }
	}
	while (i > 0);

	ibuffer[count] = 0;	/* terminate string */
	close(sock);

	if (!count)
	    info("[KVV] empty/no reply");

	if (count > 0) {
	    char *input, *cookie, *name = NULL, *value = NULL;
	    int input_len, cookie_len, name_len, value_len;

	    /* buffer to html encode value */
	    char value_enc[512];
	    int value_enc_len;

	    /* find cookie */
	    cookie_len = 0;
	    cookie = strstr(ibuffer, "Set-Cookie:");
	    if (cookie) {
		cookie += strlen("Set-Cookie:");

		while (*cookie == ' ')
		    cookie++;

		while (cookie[cookie_len] != ';')
		    cookie_len++;
	    }
	    /* find input element */
	    input_len = get_element(ibuffer, "input", &input);


	    if (input_len > 0) {
		char *input_end = input;
		while (*input_end != '>')
		    input_end++;
		while (*input_end != '\"')
		    input_end--;
		*(input_end + 1) = 0;

		name_len = get_attrib(input, "name", &name);
		value_len = get_attrib(input, "value", &value);

		for (value_enc_len = 0, i = 0; i < value_len; i++) {
		    if (isalnum(value[i]))
			value_enc[value_enc_len++] = value[i];
		    else {
			sprintf(value_enc + value_enc_len, "%%%02X", 0xff & value[i]);
			value_enc_len += 3;
		    }
		}

		if (cookie_len >= 0)
		    cookie[cookie_len] = 0;
		if (name_len >= 0)
		    name[name_len] = 0;
		if (value_len >= 0)
		    value[value_len] = 0;
		if (value_enc_len >= 0)
		    value_enc[value_enc_len] = 0;

		sock = http_open(connect_to);

		/* send POST */
		if (snprintf(obuffer, sizeof(obuffer),
			     "POST http://%s" HTTP_REQUEST " HTTP/1.1\n"
			     "Host: %s\n"
			     "User-Agent: " USER_AGENT "\n"
			     "Cookie: %s\n"
			     "Content-Type: application/x-www-form-urlencoded\n"
			     "Content-Length: %d\n"
			     "\n%s=%s",
			     server_name, station_id, server_name, cookie, name_len + value_enc_len + 1, name,
			     value_enc) >= (int) sizeof(obuffer)) {

		    info("[KVV] Warning, request has been truncated!");
		}

		info("[KVV] Sending second (POST) request ...");
		send(sock, obuffer, strlen(obuffer), 0);

		count = 0;
		do {
		    fd_set rfds;
		    struct timeval tv;

		    FD_ZERO(&rfds);
		    FD_SET(sock, &rfds);

		    tv.tv_sec = count ? TIMEOUT_SHORT : TIMEOUT_LONG;
		    tv.tv_usec = 0;

		    i = select(FD_SETSIZE, &rfds, NULL, NULL, &tv);
		    if (i > 0) {
			i = recv(sock, ibuffer + count, sizeof(ibuffer) - count - 1, 0);
			count += i;
		    }
		}
		while (i > 0);	/* leave on select or read error */

		ibuffer[count] = 0;

		/* printf("Result (%d):\n%s\n", count, ibuffer); */

		/* close connection */
		close(sock);

		if (!count)
		    info("[KVV] empty/no reply");

		if (count > 0) {
		    int last_was_stop = 0;
		    char *td = ibuffer;
		    char str[32];
		    int td_len, i, overflow = 0;

		    /* lock shared memory */
		    mutex_lock(mutex);

		    /* free allocated memory */
		    shm->entries = 0;

		    if (strstr(ibuffer, "Die Daten konnten nicht abgefragt werden.") != NULL) {
			info("[KVV] Server returned error!");
			/* printf("%s\n", ibuffer); */
			shm->error = 1;
		    } else
			shm->error = 0;

		    /* scan through all <td> entries and search the line nums */
		    do {
			if ((td_len = get_element(td, "td", &td)) > 0) {
			    char *attr, *p;
			    int attr_len;

			    /* time does not have a class but comes immediately after stop :-( */
			    if (last_was_stop) {
				td += td_len + 1;
				get_text(td, "td", str, sizeof(str));

				/* time needs special treatment */
				if (strncasecmp(str, "sofort", strlen("sofort")) == 0)
				    i = 0;
				else {
				    /* skip everything that is not a number */
				    p = str;
				    while (!isdigit(*p))
					p++;

				    /* and convert remaining to number */
				    i = atoi(p);
				}

				/* save time */
				if (!overflow)
				    shm->entry[shm->entries - 1].time = i;

				last_was_stop = 0;
			    }

			    /* linenum and stopname fields have proper classes */
			    if ((attr_len = get_attrib(td, "class", &attr)) > 0) {

				if (strncasecmp(attr, "lineNum", strlen("lineNum")) == 0) {
				    td += td_len + 1;
				    get_text(td, "td", str, sizeof(str));

				    if (shm->entries < MAX_LINES) {
					/* allocate a new slot */
					shm->entries++;
					shm->entry[shm->entries - 1].time = -1;
					memset(shm->entry[shm->entries - 1].line, 0, MAX_LINE_LENGTH + 1);
					memset(shm->entry[shm->entries - 1].station, 0, MAX_STATION_LENGTH + 1);

					/* add new lines entry */
					strncpy(shm->entry[shm->entries - 1].line, str, MAX_LINE_LENGTH);
				    } else
					overflow = 1;	/* don't add further entries */
				}

				if (strncasecmp(attr, "stopname", strlen("stopname")) == 0) {
				    td += td_len + 1;
				    get_text(td, "td", str, sizeof(str));


				    /* stopname may need further tuning */
				    process_station_string(str);

				    if (!overflow)
					strncpy(shm->entry[shm->entries - 1].station, str, MAX_STATION_LENGTH);

				    last_was_stop = 1;
				}
			    }
			}
		    } while (td_len >= 0);

		    mutex_unlock(mutex);
		}
	    }
	}

	sleep(refresh);
    }
}

static int kvv_fork(void)
{
    if (initialized)
	return 0;

    info("[KVV] creating client thread");

    /* set this here to prevent continous retries if init fails */
    initialized = 1;

    /* create communication buffer */
    shmid = shm_create((void **) &shm, sizeof(kvv_shm_t));

    /* catch error */
    if (shmid < 0) {
	error("[KVV] Shared memory allocation failed!");
	return -1;
    }

    /* attach client thread */
    mutex = mutex_create();
    pid = thread_create("plugin_kvv", kvv_client, NULL);

    if (pid < 0) {
	error("[KVV] Unable to fork client: %s", strerror(errno));
	return -1;
    }

    info("[KVV] forked client with pid %d", pid);
    return 0;
}

static void kvv_line(RESULT * result, RESULT * arg1)
{
    int index = (int) R2N(arg1);

    if (kvv_fork() != 0) {
	SetResult(&result, R_STRING, "");
	return;
    }

    mutex_lock(mutex);

    if (index < shm->entries) {
	SetResult(&result, R_STRING, shm->entry[index].line);
    } else
	SetResult(&result, R_STRING, "");

    mutex_unlock(mutex);
}

static void kvv_station(RESULT * result, RESULT * arg1)
{
    int index = (int) R2N(arg1);

    if (kvv_fork() != 0) {
	SetResult(&result, R_STRING, "");
	return;
    }

    mutex_lock(mutex);

    if (shm->error && index == 0)
	SetResult(&result, R_STRING, "Server Err");
    else {
	if (index < shm->entries)
	    SetResult(&result, R_STRING, shm->entry[index].station);
	else
	    SetResult(&result, R_STRING, "");
    }

    mutex_unlock(mutex);
}

static void kvv_time(RESULT * result, RESULT * arg1)
{
    int index = (int) R2N(arg1);
    double value = -1.0;

    if (kvv_fork() != 0) {
	SetResult(&result, R_STRING, "");
	return;
    }

    mutex_lock(mutex);

    if (index < shm->entries)
	value = shm->entry[index].time;

    SetResult(&result, R_NUMBER, &value);

    mutex_unlock(mutex);
}

static void kvv_time_str(RESULT * result, RESULT * arg1)
{
    int index = (int) R2N(arg1);

    if (kvv_fork() != 0) {
	SetResult(&result, R_STRING, "");
	return;
    }

    mutex_lock(mutex);

    if (index < shm->entries) {
	char str[8];
	sprintf(str, "%d", shm->entry[index].time);
	SetResult(&result, R_STRING, str);
    } else
	SetResult(&result, R_STRING, "");

    mutex_unlock(mutex);
}

/* plugin initialization */
int plugin_init_kvv(void)
{
    int val;
    char *p;

    /* register all our cool functions */
    AddFunction("kvv::line", 1, kvv_line);
    AddFunction("kvv::station", 1, kvv_station);
    AddFunction("kvv::time", 1, kvv_time);
    AddFunction("kvv::time_str", 1, kvv_time_str);

    /* parse parameter */
    if ((p = cfg_get(SECTION, "StationID", DEFAULT_STATION_ID)) != NULL) {
	station_id = malloc(strlen(p) + 1);
	strcpy(station_id, p);
    }
    info("[KVV] Using station %s", station_id);

    if ((p = cfg_get(SECTION, "Proxy", NULL)) != NULL) {
	proxy_name = malloc(strlen(p) + 1);
	strcpy(proxy_name, p);
	info("[KVV] Using proxy \"%s\"", proxy_name);
    }

    if (cfg_number(SECTION, "Port", 0, 0, 65535, &val) > 0) {
	port = val;
	info("[KVV] Using port %d", port);
    } else {
	info("[KVV] Using default port %d", port);
    }

    if (cfg_number(SECTION, "Refresh", 0, 0, 65535, &val) > 0) {
	refresh = val;
	info("[KVV] Using %d seconds refresh interval", refresh);
    } else {
	info("[KVV] Using default refresh interval of %d seconds", refresh);
    }

    if (cfg_number(SECTION, "Abbreviate", 0, 0, 65535, &val) > 0) {
	abbreviate = val;
	info("[KVV] Abbreviation enabled: %s", abbreviate ? "on" : "off");
    } else {
	info("[KVV] Default abbreviation setting: %s", abbreviate ? "on" : "off");
    }

    return 0;
}

void plugin_exit_kvv(void)
{
    /* kill client thread if it's running */
    if (initialized) {
	/* kill client */
	if (pid != -1)
	    thread_destroy(pid);

	/* free shared mem and its mutex */
	if (shm) {
	    shm_destroy(shmid, shm);
	    mutex_destroy(mutex);
	}
    }

    if (station_id)
	free(station_id);
    if (proxy_name)
	free(proxy_name);
}