/* 
 *  gretl -- Gnu Regression, Econometrics and Time-series Library
 *  Copyright (C) 2001 Allin Cottrell and Riccardo "Jack" Lucchetti
 * 
 *  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 3 of the License, or
 *  (at your option) 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, see <http://www.gnu.org/licenses/>.
 * 
 */

/* console.c for gretl */

#include "gretl.h"
#include "console.h"
#include "menustate.h"

#ifdef G_OS_WIN32
# include "gretlwin32.h"
#endif

#include "libset.h"
#include "monte_carlo.h"
#include "gretl_func.h"
#include "cmd_private.h"

static GtkWidget *console_view;
static PRN *console_prn;
static gchar *cbuf;
static ExecState cstate;

#define DEFAULT_HLINES 32

static char **cmd_history;
static int hl, hlmax, hlines;

static gint console_key_handler (GtkWidget *w, GdkEventKey *key, gpointer d);
static gint console_mouse_handler (GtkWidget *w, GdkEventButton *event,
				   gpointer p);
static void console_paste_handler (GtkTextView *w, gpointer p);
static gint console_click_handler (GtkWidget *w, GdkEventButton *event,
				   gpointer p);

static int gretl_console_init (void)
{
    char *hstr;
    int i;

    hlines = 0;

    hstr = getenv("GRETL_HISTORY_LINES");
    if (hstr != NULL) {
	hlines = atoi(hstr);
    }

    if (hlines <= 2 || hlines > 128) {
	hlines = DEFAULT_HLINES;
    }

    cmd_history = mymalloc(hlines * sizeof *cmd_history);
    if (cmd_history == NULL) {
	return E_ALLOC;
    }

    for (i=0; i<hlines; i++) {
	cmd_history[i] = NULL;
    }

    hlmax = 0;
    hl = -1;

    cbuf = NULL;

    set_gretl_echo(1);

    gretl_exec_state_init(&cstate, CONSOLE_EXEC, NULL, 
			  get_lib_cmd(), models, NULL);

    return 0;
}

static void gretl_console_free (GtkWidget *w, gpointer p)
{
    int i;

    if (cmd_history != NULL) {
	for (i=0; i<hlines; i++) {
	    free(cmd_history[i]);
	}
	free(cmd_history);
	cmd_history = NULL;
    }

    hlines = hlmax = 0;
    hl = -1;

    if (console_prn != NULL) {
	infobox(_("Closing redirected output file"));
	gretl_print_destroy(console_prn);
	console_prn = NULL;
    }

    free(cbuf);
    cbuf = NULL;
}

static int push_history_line (const char *line)
{
    int i;

    /* drop last entry */
    free(cmd_history[hlines-1]);

    /* push the other lines down one place */
    for (i=hlines-1; i>0; i--) {
	cmd_history[i] = cmd_history[i-1];
    }

    /* add the new line */
    cmd_history[0] = g_strdup(line);
    
    if (hlmax < hlines) {
	hlmax++;
    }

    hl = -1;

    return 0;
}

static void beep (void)
{
#ifdef G_OS_WIN32
    MessageBeep(MB_ICONEXCLAMATION);
#else
    putchar('\a');
    fflush(stdout);
#endif
}

static char *pop_history_line (int keyval)
{
    static int beeptime;
    char *ret = NULL;

    if (keyval == GDK_Up) {
	if (hl < hlmax) hl++;
	if (hl == hlmax) {
	    hl--;
	    beep();
	} else {
	    ret = cmd_history[hl];
	}
    } else if (keyval == GDK_Down) {
	if (hl >= 0) hl--;
	if (hl < 0) {
	    if (beeptime) {
		beep();
	    }
	    beeptime = 1;
	} else {
	    beeptime = 0;
	    ret = cmd_history[hl];
	}
    }

    return ret;
}

static void console_scroll_to_end (GtkTextBuffer *buf, 
				   GtkTextIter *start)
{
    GtkTextMark *mark;

    gtk_text_buffer_place_cursor(buf, start);
    mark = gtk_text_buffer_create_mark(buf, NULL, start, FALSE);
    gtk_text_view_scroll_mark_onscreen(GTK_TEXT_VIEW(console_view),
				       mark);
}

enum {
    SAMPLE_RECORD,
    SAMPLE_CHECK
};

static int console_sample_handler (const DATAINFO *pdinfo, int code)
{
    static int pd, t1, t2, ts;
    static double sd0;
    int ret = 0;

    if (code == SAMPLE_RECORD) {
	pd = pdinfo->pd;
	t1 = pdinfo->t1;
	t2 = pdinfo->t2;
	ts = pdinfo->structure;
	sd0 = pdinfo->sd0;
    } else if (code == SAMPLE_CHECK && pdinfo->v > 0) {
	if (pdinfo->pd != pd ||
	    pdinfo->t1 != t1 ||
	    pdinfo->t2 != t2 ||
	    pdinfo->structure != ts ||
	    pdinfo->sd0 != sd0) {
	    ret = 1;
	}
    }

    return ret;
}

void console_record_sample (const DATAINFO *pdinfo)
{
    console_sample_handler(pdinfo, SAMPLE_RECORD);
}

int console_sample_changed (const DATAINFO *pdinfo)
{
    return console_sample_handler(pdinfo, SAMPLE_CHECK);
}

/* callback from Enter key in gretl console */

static void console_exec (void)
{
    GtkTextBuffer *buf;
    GtkTextIter start, end;
    char execline[MAXLINE];
    int coding = 0;
    int err = 0;

    buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(console_view));
    gtk_text_buffer_get_end_iter(buf, &end);
    start = end;
    gtk_text_iter_set_line_offset(&start, 2);

    top_n_tail(cbuf, 0, NULL);

    if (console_prn == NULL && bufopen(&console_prn)) {
	g_free(cbuf);
	cbuf = NULL;
	return;
    }

    *execline = 0;
    strncat(execline, cbuf, MAXLINE - 1);
    g_free(cbuf);
    cbuf = NULL;

    console_record_sample(datainfo);

    push_history_line(execline);

    cstate.line = execline;
    cstate.flags = CONSOLE_EXEC;
    cstate.prn = console_prn;

    /* actually execute the command line */
    err = gui_exec_line(&cstate, &Z, datainfo);

    while (!err && gretl_execute_loop()) {
	err = gretl_loop_exec(&cstate, &Z, datainfo);
    }

    gtk_text_buffer_get_end_iter(buf, &start);
    gtk_text_buffer_insert(buf, &start, "\n", 1);

    if (printing_is_redirected(console_prn)) {
	gretl_print_reset_buffer(console_prn);
    } else {
	/* put results into console window */
	const char *getbuf = gretl_print_get_buffer(console_prn);

#ifdef ENABLE_NLS
	if (!g_utf8_validate(getbuf, -1, NULL)) {
	    gchar *trbuf = my_locale_to_utf8(getbuf);

	    fprintf(stderr, "console text did not validate as utf8\n");
	    if (trbuf != NULL) {
		gtk_text_buffer_insert(buf, &start, trbuf, strlen(trbuf));
		g_free(trbuf);
	    }
	} else {
	    gtk_text_buffer_insert(buf, &start, getbuf, strlen(getbuf));
	}
#else
	gtk_text_buffer_insert(buf, &start, getbuf, strlen(getbuf));
#endif
	gretl_print_destroy(console_prn);
	console_prn = NULL;
    }

    if (cstate.cmd->ci == QUIT) {
	gtk_widget_destroy(gtk_widget_get_toplevel(console_view));
	return;
    }

    coding = gretl_compiling_loop() || gretl_compiling_function();

    gtk_text_buffer_insert_with_tags_by_name(buf, &start, 
					     (coding)? "> " : "? ", 
					     2, "redtext", NULL);

    /* scroll to end of buffer */
    console_scroll_to_end(buf, &start);

    /* update variable listing in main window if needed */
    if (check_dataset_is_changed()) {
	mark_dataset_as_modified();
	populate_varlist();
    }

    /* update sample info and options if needed */
    if (console_sample_changed(datainfo)) {
	set_sample_label(datainfo);
    }
}

void show_gretl_console (void)
{
    PRN *prn;
    char fname[MAXLEN];
    windata_t *vwin;
    GtkTextBuffer *buf;
    GtkTextIter end;
    const char *intro = 
	N_("gretl console: type 'help' for a list of commands\n? ");

    if (console_view != NULL) {
	gdk_window_show(console_view->parent->window);
	gdk_window_raise(console_view->parent->window);
	return;
    }

    if (user_fopen("console_tmp", fname, &prn)) {
	return;
    }

    if (gretl_console_init()) {
	return;
    }

    pputs(prn, _(intro));
    gretl_print_destroy(prn);

    vwin = view_file(fname, 1, 1, 78, 400, CONSOLE);
    console_view = vwin->text;

    g_signal_connect(G_OBJECT(console_view), "paste-clipboard",
		     G_CALLBACK(console_paste_handler), NULL);
    g_signal_connect(G_OBJECT(console_view), "button-press-event",
		     G_CALLBACK(console_click_handler), NULL);
    g_signal_connect(G_OBJECT(console_view), "button-release-event",
		     G_CALLBACK(console_mouse_handler), NULL);
    g_signal_connect(G_OBJECT(console_view), "key-press-event",
		     G_CALLBACK(console_key_handler), NULL);
    g_signal_connect(G_OBJECT(console_view), "destroy",
		     G_CALLBACK(gretl_console_free), NULL);
    g_signal_connect(G_OBJECT(console_view), "destroy",
		     G_CALLBACK(gtk_widget_destroyed),
		     &console_view);

    /* go to end of last line of text */
    buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(console_view));
    gtk_text_buffer_get_end_iter(buf, &end);
    gtk_text_buffer_place_cursor(buf, &end);

    gtk_widget_grab_focus(console_view);
}

#define IS_BACKKEY(k) (k == GDK_BackSpace || k == GDK_Left)

static int bslash_cont (const gchar *line)
{
    int bslash = ends_with_backslash(line);

    if (cbuf == NULL) {
	cbuf = g_strdup(line);
    } else {
	cbuf = g_realloc(cbuf, strlen(cbuf) + 1 + strlen(line));
	if (cbuf != NULL) {
	    strcat(cbuf, line);
	}
    }

    if (cbuf == NULL) {
	return 0;
    }

    if (bslash) {
	char *p = strrchr(cbuf, '\\');

	if (p - cbuf > 0 && !isspace(*(p - 1))) {
	    *p = ' ';
	} else {
	    *p = 0;
	}
    }

    return bslash;
}

const char *console_varname_complete (const char *s)
{
    size_t n = strlen(s);
    int i;

    for (i=0; i<datainfo->v; i++) {
	if (!strncmp(s, datainfo->varname[i], n)) {
	    return datainfo->varname[i];
	}
    }

    return NULL;
}  

static gint console_key_handler (GtkWidget *w, GdkEventKey *key, gpointer d)
{
    gint last_line, curr_line, line_pos;
    GtkTextBuffer *buf;
    GtkTextIter iter;
    gint ret = FALSE;

 start_again:

    /* where are we? */
    buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(console_view));
    gtk_text_buffer_get_iter_at_mark(buf, &iter, gtk_text_buffer_get_insert(buf));
    curr_line = gtk_text_iter_get_line(&iter);    
    line_pos = gtk_text_iter_get_line_index(&iter);
    last_line = gtk_text_buffer_get_line_count(buf) - 1;

    /* if at start of command line, backspacing does nothing */
    if (IS_BACKKEY(key->keyval) && line_pos < 3) {
	return TRUE;
    }

    /* if not on prompt line, return to (the end of) it */
    if (curr_line < last_line) {
	GtkTextIter end;

	gtk_text_buffer_get_end_iter(buf, &end);
	gtk_text_buffer_place_cursor(buf, &end);
	gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(console_view), TRUE);	
	goto start_again;
    }

    if (key->keyval == GDK_Return) {
	/* the Return key executes the command, unless backslash-
	   continuation is happening */
	GtkTextIter start, end;	
	gchar *bit;
	int cont = 0;

	start = end = iter;
	gtk_text_iter_set_line_index(&start, 2);
	gtk_text_iter_forward_to_line_end(&end);
	bit = gtk_text_buffer_get_text(buf, &start, &end, FALSE);

	if (bit != NULL) {
	    cont = bslash_cont(bit);
	    g_free(bit);
	}

	if (cont) {
	    gtk_text_buffer_insert_with_tags_by_name(buf, &end, 
						     "\n> ", 3,
						     "redtext", NULL);
	    console_scroll_to_end(buf, &end);
	} else {
	    console_exec();
#ifdef G_OS_WIN32
	    gdk_window_show(console_view->parent->window);
	    gdk_window_raise(console_view->parent->window);
	    gtk_widget_grab_focus(console_view);
#endif
	}

	key->keyval = GDK_End;
    } else if (key->keyval == GDK_Up || key->keyval == GDK_Down) {
	/* up/down arrows: navigate the command history */
	char *histline;

	histline = pop_history_line(key->keyval);

	if (histline != NULL || key->keyval == GDK_Down) {
	    GtkTextIter start, end;

	    start = end = iter;
	    gtk_text_iter_set_line_index(&start, 2);
	    gtk_text_iter_forward_to_line_end(&end);
	    gtk_text_buffer_delete(buf, &start, &end);
	    if (histline != NULL) {
		gtk_text_buffer_insert(buf, &start, histline, strlen(histline));
	    }
	}
	
	ret = TRUE;
    } else if (key->keyval == GDK_Tab) {
	/* tab completion for gretl commands, variable names */
	const char *complete = NULL;
	GtkTextIter start, end;	
	gchar *bit = NULL;
	int offset;

	start = end = iter;
	if (!gtk_text_iter_starts_word(&start)) {
	    gtk_text_iter_backward_word_start(&start);
	}
	offset = gtk_text_iter_get_line_offset(&start);
	if (!gtk_text_iter_ends_word(&end)) {
	    gtk_text_iter_forward_word_end(&end);
	}
	bit = gtk_text_buffer_get_text(buf, &start, &end, FALSE);

	if (bit != NULL && *bit != '\0') {
	    if (offset == 2) {
		complete = gretl_command_complete(bit);
	    } else {
		complete = console_varname_complete(bit);
	    }
	    if (complete != NULL) {
		gtk_text_buffer_delete(buf, &start, &end);
		gtk_text_buffer_insert(buf, &start, complete, 
				       strlen(complete));
	    }
	}
	g_free(bit);
	ret = TRUE;
    } else {
	GdkModifierType mods;

	gdk_window_get_pointer(console_view->window, NULL, NULL, &mods);

	if (mods & GDK_CONTROL_MASK && 
	    gdk_keyval_to_upper(key->keyval) == GDK_A) {
	    /* Ctrl-A: go to start of typing area */
	    GtkTextIter where = iter;

	    gtk_text_iter_set_line_index(&where, 2);	
	    gtk_text_buffer_place_cursor(buf, &where);
	    ret = TRUE;
	}
    }

    return ret;
}

static gint on_last_line (void)
{
    GtkTextBuffer *buf;
    GtkTextIter iter;
    
    buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(console_view));
    gtk_text_buffer_get_iter_at_mark(buf, &iter, gtk_text_buffer_get_insert(buf));

    return (gtk_text_iter_get_line(&iter) == 
	    gtk_text_buffer_get_line_count(buf) - 1);
}

static gint console_mouse_handler (GtkWidget *w, GdkEventButton *event,
				   gpointer p)
{
    int lline = on_last_line();

    gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(w), lline);

    return FALSE;
}

static gint console_paste_text (GtkTextView *w, GdkAtom atom)
{
    GtkClipboard *cb;
    gchar *ctext;

    cb = gtk_clipboard_get(atom);
    ctext = gtk_clipboard_wait_for_text(cb);

    if (ctext != NULL) {
	GtkTextBuffer *buf;
	GtkTextIter iter;
	char *p;

	p = strchr(ctext, '\n');
	if (p != NULL) {
	    *p = '\0';
	}

#if 0
	fprintf(stderr, "ctext: '%s'\n", ctext);
#endif

	buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(w));
	gtk_text_buffer_get_end_iter(buf, &iter);
	gtk_text_buffer_insert(buf, &iter, ctext, -1);

	g_free(ctext);
    }

    return TRUE;
}

/* paste from clipboard: only accept plain text, and 
   automatically paste onto the command line
*/

static void console_paste_handler (GtkTextView *w, gpointer p)
{
#if 0
    return console_paste_text(w, GDK_SELECTION_CLIPBOARD);
#endif
}

static gint console_click_handler (GtkWidget *w, 
				   GdkEventButton *event,
				   gpointer p)
{
    GdkModifierType mods;

    gdk_window_get_pointer(w->window, NULL, NULL, &mods);

    if (mods & GDK_BUTTON2_MASK) {
	return console_paste_text(GTK_TEXT_VIEW(w), 
				  GDK_SELECTION_PRIMARY);
    }

    return FALSE;
}
