/*
 * Copyright (C) 2006 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * 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 any later version.
 *
 * This program 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., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 * You can also choose to distribute this program under the terms of
 * the Unmodified Binary Distribution Licence (as given in the file
 * COPYING.UBDL), provided that you have satisfied its requirements.
 */

FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <ipxe/console.h>
#include <ipxe/keys.h>
#include <ipxe/editstring.h>
#include <readline/readline.h>

/** @file
 *
 * Minimal readline
 *
 */

#define READLINE_MAX 1024

/**
 * Synchronise console with edited string
 *
 * @v string		Editable string
 */
static void sync_console ( struct edit_string *string ) {
	unsigned int mod_start = string->mod_start;
	unsigned int mod_end = string->mod_end;
	unsigned int cursor = string->last_cursor;
	size_t len = strlen ( string->buf );

	/* Expand region back to old cursor position if applicable */
	if ( mod_start > string->last_cursor )
		mod_start = string->last_cursor;

	/* Expand region forward to new cursor position if applicable */
	if ( mod_end < string->cursor )
		mod_end = string->cursor;

	/* Backspace to start of region */
	while ( cursor > mod_start ) {
		putchar ( '\b' );
		cursor--;
	}

	/* Print modified region */
	while ( cursor < mod_end ) {
		putchar ( ( cursor >= len ) ? ' ' : string->buf[cursor] );
		cursor++;
	}

	/* Backspace to new cursor position */
	while ( cursor > string->cursor ) {
		putchar ( '\b' );
		cursor--;
	}
}

/**
 * Locate history entry
 *
 * @v history		History buffer
 * @v depth		Depth within history buffer
 * @ret entry		History entry
 */
static struct readline_history_entry *
history_entry ( struct readline_history *history, unsigned int depth ) {
	unsigned int offset;

	offset = ( ( history->next - depth ) %
		   ( sizeof ( history->entries ) /
		     sizeof ( history->entries[0] ) ) );
	return &history->entries[offset];
}

/**
 * Read string from history buffer
 *
 * @v history		History buffer
 * @v depth		Depth within history buffer
 * @ret string		String
 */
static const char * history_fetch ( struct readline_history *history,
				    unsigned int depth ) {
	struct readline_history_entry *entry;

	/* Return the temporary copy if it exists, otherwise return
	 * the persistent copy.
	 */
	entry = history_entry ( history, depth );
	return ( entry->temp ? entry->temp : entry->string );
}

/**
 * Write temporary string copy to history buffer
 *
 * @v history		History buffer
 * @v depth		Depth within history buffer
 * @v string		String
 */
static void history_store ( struct readline_history *history,
			    unsigned int depth, const char *string ) {
	struct readline_history_entry *entry;
	char *temp;

	/* Create temporary copy of string */
	temp = strdup ( string );
	if ( ! temp ) {
		/* Just discard the string; there's nothing we can do */
		DBGC ( history, "READLINE %p could not store string\n",
		       history );
		return;
	}

	/* Store temporary copy */
	entry = history_entry ( history, depth );
	free ( entry->temp );
	entry->temp = temp;
}

/**
 * Move to new history depth
 *
 * @v history		History buffer
 * @v offset		Offset by which to change depth
 * @v old_string	String (possibly modified) at current depth
 * @ret new_string	String at new depth, or NULL for no movement
 */
static const char * history_move ( struct readline_history *history,
				   int offset, const char *old_string ) {
	unsigned int new_depth = ( history->depth + offset );
	const char * new_string = history_fetch ( history, new_depth );

	/* Depth checks */
	if ( new_depth > READLINE_HISTORY_MAX_DEPTH )
		return NULL;
	if ( ! new_string )
		return NULL;

	/* Store temporary copy of old string at current depth */
	history_store ( history, history->depth, old_string );

	/* Update depth */
	history->depth = new_depth;

	/* Return new string */
	return new_string;
}

/**
 * Append new history entry
 *
 * @v history		History buffer
 * @v string		String
 */
static void history_append ( struct readline_history *history,
			     const char *string ) {
	struct readline_history_entry *entry;

	/* Store new entry */
	entry = history_entry ( history, 0 );
	assert ( entry->string == NULL );
	entry->string = strdup ( string );
	if ( ! entry->string ) {
		/* Just discard the string; there's nothing we can do */
		DBGC ( history, "READLINE %p could not append string\n",
		       history );
		return;
	}

	/* Increment history position */
	history->next++;

	/* Prepare empty "next" slot */
	entry = history_entry ( history, 0 );
	free ( entry->string );
	entry->string = NULL;
}

/**
 * Clean up history after editing
 *
 * @v history		History buffer
 */
static void history_cleanup ( struct readline_history *history ) {
	struct readline_history_entry *entry;
	unsigned int i;

	/* Discard any temporary strings */
	for ( i = 0 ; i < ( sizeof ( history->entries ) /
			    sizeof ( history->entries[0] ) ) ; i++ ) {
		entry = &history->entries[i];
		free ( entry->temp );
		entry->temp = NULL;
	}

	/* Reset depth */
	history->depth = 0;

	/* Sanity check */
	entry = history_entry ( history, 0 );
	assert ( entry->string == NULL );
}

/**
 * Free history buffer
 *
 * @v history		History buffer
 */
void history_free ( struct readline_history *history ) {
	struct readline_history_entry *entry;
	unsigned int i;

	/* Discard any temporary strings */
	for ( i = 0 ; i < ( sizeof ( history->entries ) /
			    sizeof ( history->entries[0] ) ) ; i++ ) {
		entry = &history->entries[i];
		assert ( entry->temp == NULL );
		free ( entry->string );
	}
}

/**
 * Read line from console (with history)
 *
 * @v prompt		Prompt string
 * @v prefill		Prefill string, or NULL for no prefill
 * @v history		History buffer, or NULL for no history
 * @v timeout		Timeout period, in ticks (0=indefinite)
 * @ret line		Line read from console (excluding terminating newline)
 * @ret rc		Return status code
 *
 * The returned line is allocated with malloc(); the caller must
 * eventually call free() to release the storage.
 */
int readline_history ( const char *prompt, const char *prefill,
		       struct readline_history *history, unsigned long timeout,
		       char **line ) {
	struct edit_string string;
	char *buf;
	int key;
	int move_by;
	const char *new_string;
	int rc;

	/* Avoid returning uninitialised data on error */
	*line = NULL;

	/* Display prompt, if applicable */
	if ( prompt )
		printf ( "%s", prompt );

	/* Ensure cursor is visible */
	printf ( "\033[?25h" );

	/* Allocate buffer and initialise editable string */
	buf = zalloc ( READLINE_MAX );
	if ( ! buf ) {
		rc = -ENOMEM;
		goto done;
	}
	memset ( &string, 0, sizeof ( string ) );
	init_editstring ( &string, buf, READLINE_MAX );

	/* Prefill string, if applicable */
	if ( prefill ) {
		replace_string ( &string, prefill );
		sync_console ( &string );
	}

	while ( 1 ) {

		/* Get keypress */
		key = getkey ( timeout );
		if ( key < 0 ) {
			rc = -ETIMEDOUT;
			goto done;
		}
		timeout = 0;

		/* Handle keypress */
		key = edit_string ( &string, key );
		sync_console ( &string );
		move_by = 0;
		switch ( key ) {
		case CR:
		case LF:
			/* Shrink string (ignoring failures) */
			*line = realloc ( buf,
					  ( strlen ( buf ) + 1 /* NUL */ ) );
			if ( ! *line )
				*line = buf;
			buf = NULL;
			rc = 0;
			goto done;
		case CTRL_C:
			rc = -ECANCELED;
			goto done;
		case KEY_UP:
			move_by = 1;
			break;
		case KEY_DOWN:
			move_by = -1;
			break;
		default:
			/* Do nothing */
			break;
		}

		/* Handle history movement, if applicable */
		if ( move_by && history ) {
			new_string = history_move ( history, move_by, buf );
			if ( new_string ) {
				replace_string ( &string, new_string );
				sync_console ( &string );
			}
		}
	}

 done:
	putchar ( '\n' );
	free ( buf );
	if ( history ) {
		if ( *line && (*line)[0] )
			history_append ( history, *line );
		history_cleanup ( history );
	}
	assert ( ( rc == 0 ) ^ ( *line == NULL ) );
	return rc;
}

/**
 * Read line from console
 *
 * @v prompt		Prompt string
 * @ret line		Line read from console (excluding terminating newline)
 *
 * The returned line is allocated with malloc(); the caller must
 * eventually call free() to release the storage.
 */
char * readline ( const char *prompt ) {
	char *line;

	readline_history ( prompt, NULL, NULL, 0, &line );
	return line;
}
