/*
 * Copyright (C) 2012-2021 Robin Haberkorn
 *
 * 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/>.
 */

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

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

#include <glib.h>

#include "sciteco.h"
#include "string-utils.h"
#include "error.h"
#include "parser.h"
#include "core-commands.h"
#include "undo.h"
#include "expressions.h"
#include "interface.h"
#include "symbols.h"

teco_symbol_list_t teco_symbol_list_scintilla = {NULL, 0};
teco_symbol_list_t teco_symbol_list_scilexer = {NULL, 0};

/*
 * FIXME: Could be static.
 */
TECO_DEFINE_UNDO_OBJECT_OWN(scintilla_message, teco_machine_scintilla_t, /* don't delete */);

/** @memberof teco_symbol_list_t */
void
teco_symbol_list_init(teco_symbol_list_t *ctx, const teco_symbol_entry_t *entries, gint size,
                      gboolean case_sensitive)
{
	memset(ctx, 0, sizeof(*ctx));
	ctx->entries = entries;
	ctx->size = size;
	ctx->cmp_fnc = case_sensitive ? strncmp : g_ascii_strncasecmp;
}

/**
 * @note Since symbol lists are presorted constant arrays we can do a simple
 * binary search.
 * This does not use bsearch() since we'd have to prepend `prefix` in front of
 * the name.
 *
 * @memberof teco_symbol_list_t
 */
gint
teco_symbol_list_lookup(teco_symbol_list_t *ctx, const gchar *name, const gchar *prefix)
{
	gsize name_len = strlen(name);

	gsize prefix_skip = strlen(prefix);
	if (!ctx->cmp_fnc(name, prefix, prefix_skip))
		prefix_skip = 0;

	gint left = 0;
	gint right = ctx->size - 1;

	while (left <= right) {
		gint cur = left + (right-left)/2;
		gint cmp = ctx->cmp_fnc(ctx->entries[cur].name + prefix_skip,
				        name, name_len + 1);

		if (!cmp)
			return ctx->entries[cur].value;

		if (cmp > 0)
			right = cur-1;
		else /* cmp < 0 */
			left = cur+1;
	}

	return -1;
}

/**
 * Auto-complete a Scintilla symbol.
 *
 * @param ctx The symbol list.
 * @param symbol The symbol to auto-complete or NULL.
 * @param insert String to initialize with the completion.
 * @return TRUE in case of an unambiguous completion.
 *
 * @memberof teco_symbol_list_t
 */
gboolean
teco_symbol_list_auto_complete(teco_symbol_list_t *ctx, const gchar *symbol, teco_string_t *insert)
{
	memset(insert, 0, sizeof(*insert));

	if (!symbol)
		symbol = "";
	gsize symbol_len = strlen(symbol);

	if (G_UNLIKELY(!ctx->list))
		for (gint i = ctx->size; i; i--)
			ctx->list = g_list_prepend(ctx->list, (gchar *)ctx->entries[i-1].name);

	/* NOTE: element data must not be freed */
	g_autoptr(GList) glist = g_list_copy(ctx->list);
	guint glist_len = 0;

	gsize prefix_len = 0;

	for (GList *entry = g_list_first(glist), *next = g_list_next(entry);
	     entry != NULL;
	     entry = next, next = entry ? g_list_next(entry) : NULL) {
		if (g_ascii_strncasecmp(entry->data, symbol, symbol_len) != 0) {
			glist = g_list_delete_link(glist, entry);
			continue;
		}

		teco_string_t glist_str;
		glist_str.data = (gchar *)glist->data + symbol_len;
		glist_str.len = strlen(glist_str.data);

		gsize len = teco_string_casediff(&glist_str, (gchar *)entry->data + symbol_len,
		                                 strlen(entry->data) - symbol_len);
		if (!prefix_len || len < prefix_len)
			prefix_len = len;

		glist_len++;
	}

	if (prefix_len > 0) {
		teco_string_init(insert, (gchar *)glist->data + symbol_len, prefix_len);
	} else if (glist_len > 1) {
		for (GList *entry = g_list_first(glist);
		     entry != NULL;
		     entry = g_list_next(entry)) {
			teco_interface_popup_add(TECO_POPUP_PLAIN, entry->data,
			                         strlen(entry->data), FALSE);
		}

		teco_interface_popup_show();
	}

	return glist_len == 1;
}

/*
 * Command states
 */

/*
 * FIXME: This state could be static.
 */
TECO_DECLARE_STATE(teco_state_scintilla_lparam);

static gboolean
teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, const teco_string_t *str, GError **error)
{
	if (teco_string_contains(str, '\0')) {
		g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
		                    "Scintilla symbol names must not contain null-byte");
		return FALSE;
	}

	g_auto(GStrv) symbols = g_strsplit(str->data, ",", -1);

	if (!symbols[0])
		return TRUE;
	if (*symbols[0]) {
		gint v = teco_symbol_list_lookup(&teco_symbol_list_scintilla, symbols[0], "SCI_");
		if (v < 0) {
			g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
			            "Unknown Scintilla message symbol \"%s\"",
				    symbols[0]);
			return FALSE;
		}
		scintilla->iMessage = v;
	}

	if (!symbols[1])
		return TRUE;
	if (*symbols[1]) {
		gint v = teco_symbol_list_lookup(&teco_symbol_list_scilexer, symbols[1], "");
		if (v < 0) {
			g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
			            "Unknown Scintilla Lexer symbol \"%s\"",
				    symbols[1]);
			return FALSE;
		}
		scintilla->wParam = v;
	}

	if (!symbols[2])
		return TRUE;
	if (*symbols[2]) {
		gint v = teco_symbol_list_lookup(&teco_symbol_list_scilexer, symbols[2], "");
		if (v < 0) {
			g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED,
			            "Unknown Scintilla Lexer symbol \"%s\"",
				    symbols[2]);
			return FALSE;
		}
		scintilla->lParam = v;
	}

	return TRUE;
}

static teco_state_t *
teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
{
	if (ctx->mode > TECO_MODE_NORMAL)
		return &teco_state_scintilla_lparam;

	/*
	 * NOTE: This is more memory efficient than pushing the individual
	 * members of teco_machine_scintilla_t and we won't need to define
	 * undo methods for the Scintilla types.
	 */
	if (ctx->parent.must_undo)
		teco_undo_object_scintilla_message_push(&ctx->scintilla);
	memset(&ctx->scintilla, 0, sizeof(ctx->scintilla));

	if ((str->len > 0 && !teco_scintilla_parse_symbols(&ctx->scintilla, str, error)) ||
	    !teco_expressions_eval(FALSE, error))
		return NULL;

	teco_int_t value;

	if (!ctx->scintilla.iMessage) {
		if (!teco_expressions_args()) {
			g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
			                    "<ES> command requires at least a message code");
			return NULL;
		}

		if (!teco_expressions_pop_num_calc(&value, 0, error))
			return NULL;
		ctx->scintilla.iMessage = value;
	}
	if (!ctx->scintilla.wParam) {
		if (!teco_expressions_pop_num_calc(&value, 0, error))
			return NULL;
		ctx->scintilla.wParam = value;
	}

	return &teco_state_scintilla_lparam;
}

/* in cmdline.c */
gboolean teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar key, GError **error);

/*$ ES scintilla message
 * -- Send Scintilla message
 * [lParam[,wParam]]ESmessage[,wParam[,lParam]]$[lParam]$ -> result
 *
 * Send Scintilla message with code specified by symbolic
 * name <message>, <wParam> and <lParam>.
 * <wParam> may be symbolic when specified as part of the
 * first string argument.
 * If not it is popped from the stack.
 * <lParam> may be specified as a constant string whose
 * pointer is passed to Scintilla if specified as the second
 * string argument.
 * If the second string argument is empty, <lParam> is popped
 * from the stack instead.
 * Parameters popped from the stack may be omitted, in which
 * case 0 is implied.
 * The message's return value is pushed onto the stack.
 *
 * All messages defined by Scintilla (as C macros) can be
 * used by passing their name as a string to ES
 * (e.g. ESSCI_LINESONSCREEN...).
 * The \(lqSCI_\(rq prefix may be omitted and message symbols
 * are case-insensitive.
 * Only the Scintilla lexer symbols (SCLEX_..., SCE_...)
 * may be used symbolically with the ES command as <wParam>,
 * other values must be passed as integers on the stack.
 * In interactive mode, symbols may be auto-completed by
 * pressing Tab.
 * String-building characters are by default interpreted
 * in the string arguments.
 *
 * .BR Warning :
 * Almost all Scintilla messages may be dispatched using
 * this command.
 * \*(ST does not keep track of the editor state changes
 * performed by these commands and cannot undo them.
 * You should never use it to change the editor state
 * (position changes, deletions, etc.) or otherwise
 * rub out will result in an inconsistent editor state.
 * There are however exceptions:
 *   - In the editor profile and batch mode in general,
 *     the ES command may be used freely.
 *   - In the ED hook macro (register \(lqED\(rq),
 *     when a file is added to the ring, most destructive
 *     operations can be performed since rubbing out the
 *     EB command responsible for the hook execution also
 *     removes the buffer from the ring again.
 *   - As part of function key macros that immediately
 *     terminate the command line.
 */
TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_symbols,
	.process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_scintilla_symbols_process_edit_cmd,
	.expectstring.last = FALSE
);

static teco_state_t *
teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error)
{
	if (ctx->mode > TECO_MODE_NORMAL)
		return &teco_state_start;

	if (teco_string_contains(str, '\0')) {
		g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED,
		                    "Scintilla lParam string must not contain null-byte.");
		return NULL;
	}

	if (!ctx->scintilla.lParam) {
		if (str->len > 0) {
			ctx->scintilla.lParam = (sptr_t)str->data;
		} else {
			teco_int_t v;
			if (!teco_expressions_pop_num_calc(&v, 0, error))
				return NULL;
			ctx->scintilla.lParam = v;
		}
	}

	teco_expressions_push(teco_interface_ssm(ctx->scintilla.iMessage,
	                                         ctx->scintilla.wParam,
	                                         ctx->scintilla.lParam));

	return &teco_state_start;
}

TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_lparam);
