/*
 *      autoclose.c
 *
 *      Copyright 2013 Pavel Roschin <rpg89(at)post(dot)ru>
 *
 *      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
 *      (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, write to the Free Software
 *      Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *      MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
	#include "config.h" /* for the gettext domain */
#endif

#include <string.h>
#ifdef HAVE_LOCALE_H
	#include <locale.h>
#endif

#include <gdk/gdkkeysyms.h>

#include <geanyplugin.h>
#include <geany.h>

#include "Scintilla.h"
#include "SciLexer.h"

#define AC_STOP_ACTION TRUE
#define AC_CONTINUE_ACTION FALSE
#define SSM(s, m, w, l) scintilla_send_message(s, m, w, l)

GeanyPlugin	*geany_plugin;
GeanyData	*geany_data;


typedef struct {
	/* close chars */
	gboolean parenthesis;
	gboolean abracket;
	gboolean abracket_htmlonly;
	gboolean cbracket;
	gboolean sbracket;
	gboolean dquote;
	gboolean squote;
	gboolean backquote;
	gboolean backquote_bashonly;
	/* settings */
	gboolean delete_pairing_brace;
	gboolean suppress_doubling;
	gboolean enclose_selections;
	gboolean comments_ac_enable;
	gboolean comments_enclose;
	gboolean keep_selection;
	gboolean make_indent_for_cbracket;
	gboolean move_cursor_to_beginning;
	gboolean improved_cbracket_indent;
	gboolean whitesmiths_style;
	gboolean close_functions;
	gboolean bcksp_remove_pair;
	gboolean jump_on_tab;
	/* others */
	gchar *config_file;
} AutocloseInfo;

static AutocloseInfo *ac_info = NULL;

typedef struct {
	/* used to place the caret after autoclosed items on tab (similar to eclipse) */
	gint jump_on_tab;
	/* used to reset jump_on_tab when needed */
	gint last_caret;
	/* used to reset jump_on_tab when needed */
	gint last_line;
	struct GeanyDocument *doc;
} AutocloseUserData;

static gint
get_indent(ScintillaObject *sci, gint line)
{
	return (gint) SSM(sci, SCI_GETLINEINDENTPOSITION, (uptr_t) line, 0);
}

static gchar
char_at(ScintillaObject *sci, gint pos)
{
	return sci_get_char_at(sci, pos);
}

static const gchar *
get_char_range(ScintillaObject *sci, gint start, gint length)
{
	return (const gchar *) SSM(sci, SCI_GETRANGEPOINTER, start, length);
}

static gboolean
blank_line(ScintillaObject *sci, gint line)
{
	return get_indent(sci, line) ==
		sci_get_line_end_position(sci, line);
}

static void
unindent_line(ScintillaObject *sci, gint line, gint indent_width)
{
	gint indent = sci_get_line_indentation(sci, line);
	sci_set_line_indentation(sci, line, indent > 0 ? indent - indent_width : 0);
}

static void
delete_line(ScintillaObject *sci, gint line)
{
	gint start = sci_get_position_from_line(sci, line);
	gint len = sci_get_line_length(sci, line);
	SSM(sci, SCI_DELETERANGE, start, len);
}

static gint
get_lines_selected(ScintillaObject *sci)
{
	gint start = (gint) SSM(sci, SCI_GETSELECTIONSTART, 0, 0);
	gint end = (gint) SSM(sci, SCI_GETSELECTIONEND, 0, 0);
	gint line_start;
	gint line_end;

	if (start == end)
		return 0; /* no selection */

	line_start = (gint) SSM(sci, SCI_LINEFROMPOSITION, (uptr_t) start, 0);
	line_end = (gint) SSM(sci, SCI_LINEFROMPOSITION, (uptr_t) end, 0);

	return line_end - line_start + 1;
}

static void
insert_text(ScintillaObject *sci, gint pos, const gchar *text)
{
	SSM(sci, SCI_INSERTTEXT, pos, (sptr_t) text);
}

static gint
get_selections(ScintillaObject *sci)
{
	return (gint) SSM(sci, SCI_GETSELECTIONS, 0, 0);
}

static gint
get_caret_pos(ScintillaObject *sci, gint selection)
{
	return (gint) SSM(sci, SCI_GETSELECTIONNCARET, selection, 0);
}

static gint
get_ancor_pos(ScintillaObject *sci, gint selection)
{
	return (gint) SSM(sci, SCI_GETSELECTIONNANCHOR, selection, 0);
}

static gboolean
char_is_quote(gchar ch)
{
	return '\'' == ch || '"' == ch;
}

static gboolean
char_is_curly_bracket(gchar ch)
{
	return '{' == ch || '}' == ch;
}

static gboolean
isspace_no_newline(gchar ch)
{
	return g_ascii_isspace(ch) && ch != '\n' && ch != '\r';
}

/**
 * This function is based on Geany's source but has different meaning: check
 * ability to enclose selection. Calls only for selected text so using
 * sci_get_selection_start/end is ok here.
 * */
static gboolean
lexer_has_braces(ScintillaObject *sci, gint lexer)
{
	gint sel_start;
	switch (lexer)
	{
		case SCLEX_CPP:
		case SCLEX_D:
		case SCLEX_PASCAL:
		case SCLEX_TCL:
		case SCLEX_CSS:
			return TRUE;
		case SCLEX_HTML:	/* for PHP & JS */
		case SCLEX_PERL:
		case SCLEX_BASH:
			/* PHP, Perl, bash has vars like ${var} */
			if (get_lines_selected(sci) > 1)
				return TRUE;
			sel_start = sci_get_selection_start(sci);
			if ('$' == char_at(sci, sel_start - 1))
				return FALSE;
			return TRUE;
		default:
			return FALSE;
	}
}

static gboolean
lexer_cpp_like(gint lexer, gint style)
{
	if (lexer == SCLEX_CPP && style == SCE_C_IDENTIFIER)
		return TRUE;
	return FALSE;
}

static gboolean
filetype_c_or_cpp(gint type)
{
	return type == GEANY_FILETYPES_C || type == GEANY_FILETYPES_CPP;
}

static gboolean
filetype_cpp(gint type)
{
	return type == GEANY_FILETYPES_CPP;
}

static gint
get_end_pos(ScintillaObject *sci, gint line)
{
	gint end;
	gchar ch;
	end = sci_get_line_end_position(sci, line);
	ch = char_at(sci, end - 1);
	/* ignore spaces and "}" */
	while (isspace_no_newline(ch) || '}' == ch)
	{
		end--;
		ch = char_at(sci, end - 1);
	}
	return end;
}

static gboolean
check_chars(
	ScintillaObject *sci,
	gint             ch,
	gchar           *chars_left,
	gchar           *chars_right)
{
	switch (ch)
	{
		case '(':
		case ')':
			if (!ac_info->parenthesis)
				return FALSE;
			*chars_left = '(';
			*chars_right = ')';
			break;
		case ';':
			if (!ac_info->close_functions)
				return FALSE;
			break;
		case '{':
		case '}':
			if (!ac_info->cbracket)
				return FALSE;
			*chars_left = '{';
			*chars_right = '}';
			break;
		case '[':
		case ']':
			if (!ac_info->sbracket)
				return FALSE;
			*chars_left = '[';
			*chars_right = ']';
			break;
		case '<':
		case '>':
			if (!ac_info->abracket)
				return FALSE;
			if (ac_info->abracket_htmlonly &&
					sci_get_lexer(sci) != SCLEX_HTML)
				return FALSE;
			*chars_left = '<';
			*chars_right = '>';
			break;
		case '\'':
			if (!ac_info->squote)
				return FALSE;
			*chars_left = *chars_right = ch;
			break;
		case '"':
			if (!ac_info->dquote)
				return FALSE;
			*chars_left = *chars_right = ch;
			break;
		case '`':
			if (!ac_info->backquote)
				return FALSE;
			if (ac_info->backquote_bashonly &&
					sci_get_lexer(sci) != SCLEX_BASH)
				return FALSE;
			*chars_left = *chars_right = ch;
			break;
		default:
			return FALSE;
	}
	return TRUE;
}

static gboolean
improve_indent(
	ScintillaObject *sci,
	GeanyEditor     *editor,
	gint             pos)
{
	gint ch, ch_next;
	gint line;
	gint indent, indent_width;
	gint end_pos;
	if (!ac_info->improved_cbracket_indent)
		return AC_CONTINUE_ACTION;
	ch = char_at(sci, pos - 1);
	if (ch != '{')
		return AC_CONTINUE_ACTION;
	/* if curly bracket completion is enabled - just make indents
	 * but ensure that second "}" exists. If disabled - make indent
	 * and complete second curly bracket */
	ch_next = char_at(sci, pos);
	if (ac_info->cbracket && ch_next != '}')
		return AC_CONTINUE_ACTION;
	line = sci_get_line_from_position(sci, pos);
	indent = sci_get_line_indentation(sci, line);
	indent_width = editor_get_indent_prefs(editor)->width;
	sci_start_undo_action(sci);
	if (ac_info->cbracket)
		SSM(sci, SCI_ADDTEXT, 2, (sptr_t)"\n\n");
	else
		SSM(sci, SCI_ADDTEXT, 3, (sptr_t)"\n\n}");
	if (ac_info->whitesmiths_style)
	{
		sci_set_line_indentation(sci, line,     indent);
		sci_set_line_indentation(sci, line + 1, indent);
		sci_set_line_indentation(sci, line + 2, indent);
	}
	else
	{
		sci_set_line_indentation(sci, line + 1, indent + indent_width);
		sci_set_line_indentation(sci, line + 2, indent);
	}
	/* move to the end of added line */
	end_pos = sci_get_line_end_position(sci, line + 1);
	sci_set_current_position(sci, end_pos, TRUE);
	sci_end_undo_action(sci);
	/* do not alow internal auto-indenter to do the work */
	return AC_STOP_ACTION;
}


/** Find previous string/last occurence from a position
 *
 * The function finds the last occurence of @a text before the given
 * position @a pos. It will only search the range of @a max before
 * @a pos. @a length gives the length of the string at @a text. @a text
 * does not need to be NULL terminated.
 *
 * @param sci	 The ScintillaObject
 * @param pos	 The position to search backwards from (excluding pos)
 * @param max	 Maximum number of chars to search through (the function
 *				 will search the range from pos to pos-max)
 * @param length Length of the search text
 * @param text	 Text to search for
 * @returns The position where the text was found or -1 if not found
 *
 **/
static gint ao_sci_find_prev_string(ScintillaObject *sci, gint pos, gint max, gint length, gchar *text)
{
	gint start = 0;
	gint found;

	if (pos == 0)
	{
		return -1;
	}
	if (pos >= max)
	{
		start = pos - max;
	}
	scintilla_send_message(sci, SCI_SETTARGETRANGE, pos-1, start);
	found = scintilla_send_message(sci, SCI_SEARCHINTARGET, length, (sptr_t)text);

	return found;
}


static gboolean
handle_backspace(
	AutocloseUserData *data,
	ScintillaObject   *sci,
	gchar              ch,
	gchar             *ch_left,
	gchar             *ch_right,
	GdkEventKey       *event,
	gint               indent_width)
{
	gint pos = sci_get_current_position(sci);
	gint end_pos;
	gint line_start, line_end, line;
	gint i;
	if (!ac_info->delete_pairing_brace)
		return AC_CONTINUE_ACTION;
	ch = char_at(sci, pos - 1);

	if (!check_chars(sci, ch, ch_left, ch_right))
		return AC_CONTINUE_ACTION;

	if (event->state & GDK_SHIFT_MASK)
	{
		if ((ch_left[0] == ch || ch_right[0] == ch) &&
				ac_info->bcksp_remove_pair)
		{
			if (ch_left[0] != ch_right[0])
			{
				end_pos = sci_find_matching_brace(sci, pos - 1);
			}
			else
			{
				end_pos = ao_sci_find_prev_string(sci, pos, pos, 1, ch_left);
			}
			if (-1 == end_pos)
				return AC_CONTINUE_ACTION;
			sci_start_undo_action(sci);
			line_start = sci_get_line_from_position(sci, pos);
			line_end = sci_get_line_from_position(sci, end_pos);
			SSM(sci, SCI_DELETERANGE, end_pos, 1);
			if (end_pos < pos)
				pos--;
			SSM(sci, SCI_DELETERANGE, pos - 1, 1);
			/* remove indentation magick */
			if (char_is_curly_bracket(ch))
			{
				if (line_start == line_end)
					goto final;
				if (line_start > line_end)
				{
					line = line_end;
					line_end = line_start;
					line_start = line;
				}
				if (blank_line(sci, line_start))
				{
					delete_line(sci, line_start);
					line_end--;
				}
				else
					line_start++;
				if (blank_line(sci, line_end))
					delete_line(sci, line_end);
				line_end--;
				/* unindent */
				for (i = line_start; i <= line_end; i++)
				{
					unindent_line(sci, i, indent_width);
				}
			}
final:
			sci_end_undo_action(sci);
			return AC_STOP_ACTION;
		}
	}

	/* handle \'|' situation */
	if (char_is_quote(ch) && char_at(sci, pos - 2) == '\\')
		return AC_CONTINUE_ACTION;

	if (ch_left[0] == ch && ch_right[0] == char_at(sci, pos))
	{
		SSM(sci, SCI_DELETERANGE, pos, 1);
		data->jump_on_tab = 0;
		return AC_CONTINUE_ACTION;
	}
	return AC_CONTINUE_ACTION;
}


static gboolean
enclose_selection(
	AutocloseUserData *data,
	ScintillaObject   *sci,
	gchar              ch,
	gint               lexer,
	gint               style,
	gchar             *chars_left,
	gchar             *chars_right,
	GeanyEditor       *editor)
{
	gint       i;
	gint       start, end;
	gboolean   in_comment;
	gint       start_line, start_pos, end_line, text_end_pos;
	gint       start_indent, indent_width, current_indent;

	start = sci_get_selection_start(sci);
	end = sci_get_selection_end(sci);

	/* case if selection covers mixed style */
	if (highlighting_is_code_style(lexer, sci_get_style_at(sci, start)) !=
		 highlighting_is_code_style(lexer, sci_get_style_at(sci, end)))
		in_comment = FALSE;
	else
		in_comment = !highlighting_is_code_style(lexer, style);
	if (!ac_info->comments_enclose && in_comment)
		return AC_CONTINUE_ACTION;

	sci_start_undo_action(sci);


	/* Insert {} block - special case: make indents, move cursor to beginning */
	if (char_is_curly_bracket(ch) && lexer_has_braces(sci, lexer) &&
			ac_info->make_indent_for_cbracket && !in_comment)
	{
		start_line = sci_get_line_from_position(sci, start);
		start_pos = SSM(sci, SCI_GETLINEINDENTPOSITION, (uptr_t)start_line, 0);
		insert_text(sci, start_pos, "{\n");

		end_line = sci_get_line_from_position(sci, end);
		start_indent = sci_get_line_indentation(sci, start_line);
		indent_width = editor_get_indent_prefs(editor)->width;
		sci_set_line_indentation(sci, start_line, start_indent);
		if (!ac_info->whitesmiths_style)
			sci_set_line_indentation(sci, start_line + 1, start_indent + indent_width);
		else
			sci_set_line_indentation(sci, start_line + 1, start_indent);
		for (i = start_line + 2; i <= end_line; i++)
		{
			current_indent = sci_get_line_indentation(sci, i);
			if (!ac_info->whitesmiths_style)
				sci_set_line_indentation(sci, i, current_indent + indent_width);
			else
				sci_set_line_indentation(sci, i, current_indent);
		}
		text_end_pos = sci_get_line_end_position(sci, i - 1);
		sci_set_current_position(sci, text_end_pos, FALSE);
		SSM(sci, SCI_ADDTEXT, 2, (sptr_t)"\n}");
		sci_set_line_indentation(sci, i, start_indent);
		if (ac_info->move_cursor_to_beginning)
			sci_set_current_position(sci, start_pos, TRUE);
	}
	else
	{
		gint selections = get_selections(sci);
		/* specially handle rectangular selection */
		if (selections > 1)
		{
			gint *sels_left = g_malloc(selections * sizeof(gint));
			gint *sels_right = g_malloc(selections * sizeof(gint));
			gint caret = get_caret_pos(sci, 0);
			gint anchor = get_ancor_pos(sci, 0);
			gboolean caret_is_left = caret < anchor;
			gint pos_first = get_caret_pos(sci, 0);
			gint pos_second = get_caret_pos(sci, 1);
			gboolean selection_is_up_down = pos_first < pos_second;
			gint line;
			/* looks like a forward loop but actually lines processed in reverse order */
			for (i = 0; i < selections; i++)
			{
				if (selection_is_up_down)
					line = selections - i - 1;
				else
					line = i;
				if (caret_is_left)
				{
					sels_left[i]  = get_caret_pos(sci, line);
					sels_right[i] = get_ancor_pos(sci, line) + 1;
				}
				else
				{
					sels_right[i] = get_caret_pos(sci, line) + 1;
					sels_left[i]  = get_ancor_pos(sci, line);
				}
			}
			for (i = 0; i < selections; i++)
			{
				insert_text(sci, sels_left[i],  chars_left);
				insert_text(sci, sels_right[i], chars_right);
				sels_left[i]  += (selections - i - 1) * 2 + 1;
				sels_right[i] += (selections - i - 1) * 2 + 1;
			}
			if (ac_info->keep_selection)
			{
				i = 0;
				SSM(sci, SCI_SETSELECTION, sels_left[i], sels_right[i] - 1);
				for (i = 1; i < selections; i++)
					SSM(sci, SCI_ADDSELECTION, sels_left[i], sels_right[i] - 1);
			}
			g_free(sels_left);
			g_free(sels_right);
		}
		else /* normal selection */
		{
			insert_text(sci, start, chars_left);
			insert_text(sci, end + 1, chars_right);
			sci_set_current_position(sci, end + 1, TRUE);
			data->jump_on_tab += strlen(chars_right);
			data->last_caret = end + 1;
			data->last_line = sci_get_current_line(sci);
			if (ac_info->keep_selection)
			{
				sci_set_selection_start(sci, start + 1);
				sci_set_selection_end(sci, end + 1);
			}
		}
	}
	sci_end_undo_action(sci);
	return AC_STOP_ACTION;
}

static gboolean
check_struct(
	ScintillaObject *sci,
	gint             pos,
	const gchar     *str)
{
	gchar ch;
	gint line, len;
	ch = char_at(sci, pos - 1);
	while (g_ascii_isspace(ch))
	{
		pos--;
		ch = char_at(sci, pos - 1);
	}
	line = sci_get_line_from_position(sci, pos);
	len = strlen(str);
	const gchar *sci_buf = get_char_range(sci, get_indent(sci, line), len);
	g_return_val_if_fail(sci_buf, FALSE);
	if (strncmp(sci_buf, str, len) == 0)
		return TRUE;
	return FALSE;
}

static void
struct_semicolon(
	ScintillaObject *sci,
	gint             pos,
	gchar           *chars_right,
	gint             filetype)
{
	if (filetype_c_or_cpp(filetype) &&
	   (check_struct(sci, pos, "struct") || check_struct(sci, pos, "typedef struct")))
	{
		chars_right[1] = ';';
		return;
	}
	if (filetype_cpp(filetype) && check_struct(sci, pos, "class"))
	{
		chars_right[1] = ';';
		return;
	}
}

static gboolean
check_define(
	ScintillaObject *sci,
	gint             line)
{
	const gchar* sci_buf = get_char_range(sci, get_indent(sci, line), 7);
	g_return_val_if_fail(sci_buf, FALSE);
	if (strncmp(sci_buf, "#define", 7) == 0)
		return TRUE;
	return FALSE;
}

static gboolean
auto_close_chars(
	AutocloseUserData *data,
	GdkEventKey       *event)
{
	ScintillaObject *sci;
	GeanyEditor     *editor;
	GeanyDocument   *doc;
	gint             ch, ch_next, ch_buf;
	gchar            chars_left[2]  = {0, 0};
	gchar            chars_right[3] = {0, 0, 0};
	gint             lexer, style;
	gint             pos, line, lex_offset;
	gboolean         has_sel;
	gint             filetype = 0;

	g_return_val_if_fail(data, AC_CONTINUE_ACTION);
	doc = data->doc;
	g_return_val_if_fail(DOC_VALID(doc), AC_CONTINUE_ACTION);
	editor = doc->editor;
	g_return_val_if_fail(editor, AC_CONTINUE_ACTION);
	sci = editor->sci;
	g_return_val_if_fail(sci, AC_CONTINUE_ACTION);

	if (doc->file_type)
		filetype = doc->file_type->id;

	pos = sci_get_current_position(sci);
	line = sci_get_current_line(sci);
	ch = event->keyval;

	if (ch == GDK_BackSpace)
	{
		return handle_backspace(data, sci, ch, chars_left, chars_right,
		                        event, editor_get_indent_prefs(editor)->width);
	}
	else if (ch == GDK_Return)
	{
		return improve_indent(sci, editor, pos);
	}
	else if (ch == GDK_Tab && ac_info->jump_on_tab)
	{
		/* jump behind inserted "); */
		if (data->jump_on_tab == 0)
			return AC_CONTINUE_ACTION;
		sci_set_current_position(sci, pos + data->jump_on_tab, FALSE);
		data->jump_on_tab = 0;
		return AC_STOP_ACTION;
	}

	/* set up completion chars */
	if (!check_chars(sci, ch, chars_left, chars_right))
		return AC_CONTINUE_ACTION;

	has_sel = sci_has_selection(sci);

	/* do not suppress/complete in case: '\|' */
	if (char_is_quote(ch) && char_at(sci, pos - 1) == '\\' && !has_sel)
		return AC_CONTINUE_ACTION;

	lexer = sci_get_lexer(sci);

	/* in C-like languages - complete functions with ; */
	lex_offset = -1;
	ch_buf = char_at(sci, pos + lex_offset);
	while (g_ascii_isspace(ch_buf))
	{
		--lex_offset;
		ch_buf = char_at(sci, pos + lex_offset);
	}

	style = sci_get_style_at(sci, pos + lex_offset);

	/* add ; after functions */
	if (
		!has_sel &&
		ac_info->close_functions &&
		chars_left[0] == '(' &&
		lexer_cpp_like(lexer, style) &&
		pos == get_end_pos(sci, line) &&
		sci_get_line_indentation(sci, line) != 0 &&
		!check_define(sci, line)
	)
		chars_right[1] = ';';

	style = sci_get_style_at(sci, pos);

	/* suppress double completion symbols */
	ch_next = char_at(sci, pos);
	if (ch == ch_next && !has_sel && ac_info->suppress_doubling &&
	  !(chars_left[0] != chars_right[0] && ch == chars_left[0]))
	{
		/* jump_on_data may be 2 (due to autoclosing ");"). Need to decrement if ")" is pressed */
		if (data->jump_on_tab > 0)
			data->jump_on_tab -= 1;
		if ((!ac_info->comments_ac_enable && !highlighting_is_code_style(lexer, style)) &&
		      ch != '"' && ch != '\'')
			return AC_CONTINUE_ACTION;

		/* suppress ; only at end of line */
		if (ch == ';' && pos + 1 != get_end_pos(sci, line))
			return AC_CONTINUE_ACTION;
		SSM(sci, SCI_DELETERANGE, pos, 1);
		return AC_CONTINUE_ACTION;
	}

	if (ch == ';')
		return AC_CONTINUE_ACTION;

	/* If we have selected text */
	if (has_sel && ac_info->enclose_selections)
		return enclose_selection(data, sci, ch, lexer, style, chars_left, chars_right, editor);

	/* disable autocompletion inside comments and strings */
	if (!ac_info->comments_ac_enable && !highlighting_is_code_style(lexer, style))
		return AC_CONTINUE_ACTION;

	if (ch == chars_right[0] && chars_left[0] != chars_right[0])
		return AC_CONTINUE_ACTION;

	/* add ; after struct */
	struct_semicolon(sci, pos, chars_right, filetype);

	/* just close char */
	SSM(sci, SCI_INSERTTEXT, pos, (sptr_t)chars_right);
	sci_set_current_position(sci, pos, TRUE);
	data->jump_on_tab += strlen(chars_right);
	data->last_caret = pos;
	data->last_line = sci_get_current_line(sci);
	return AC_CONTINUE_ACTION;
}

static gboolean
on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
{
	AutocloseUserData *data = user_data;
	g_return_val_if_fail(data && DOC_VALID(data->doc), AC_CONTINUE_ACTION);
	return auto_close_chars(data, event);
}

static void
on_sci_notify(ScintillaObject *sci, gint scn, SCNotification *nt, gpointer user_data)
{
	AutocloseUserData *data = user_data;

	if (!ac_info->jump_on_tab)
		return;
	g_return_if_fail(data);

	/* reset jump_on_tab state when user clicked away */
	gboolean updated_sel  = nt->updated & SC_UPDATE_SELECTION;
	gboolean updated_text = nt->updated & SC_UPDATE_CONTENT;
	gint new_caret = sci_get_current_position(sci);
	gint new_line  = sci_get_current_line(sci);
	if (updated_sel && !updated_text)
	{
		gint delta = data->last_caret -  new_caret;
		gint delta_l = data->last_line - new_line;
		if (delta_l == 0 && data->jump_on_tab)
			data->jump_on_tab += delta;
		else
			data->jump_on_tab = 0;
	}
	data->last_caret = new_caret;
	data->last_line = new_line;
}

static void
on_document_open(GObject *obj, GeanyDocument *doc, gpointer user_data)
{
	AutocloseUserData *data;
	ScintillaObject   *sci;
	g_return_if_fail(DOC_VALID(doc));

	sci = doc->editor->sci;
	data = g_new0(AutocloseUserData, 1);
	data->doc = doc;
	plugin_signal_connect(geany_plugin, G_OBJECT(sci), "sci-notify",
		    FALSE, G_CALLBACK(on_sci_notify), data);
	plugin_signal_connect(geany_plugin, G_OBJECT(sci), "key-press-event",
			FALSE, G_CALLBACK(on_key_press), data);
	/* This will free the data when the sci is destroyed */
	g_object_set_data_full(G_OBJECT(sci), "autoclose-userdata", data, g_free);
}

static PluginCallback plugin_autoclose_callbacks[] =
{
	{ "document-open",  (GCallback) &on_document_open, FALSE, NULL },
	{ "document-new",   (GCallback) &on_document_open, FALSE, NULL },
	{ NULL, NULL, FALSE, NULL }
};

static void
configure_response_cb(GtkDialog *dialog, gint response, gpointer user_data)
{
	if (response != GTK_RESPONSE_OK && response != GTK_RESPONSE_APPLY)
		return;
	GKeyFile *config = g_key_file_new();
	gchar    *config_dir = g_path_get_dirname(ac_info->config_file);

	g_key_file_load_from_file(config, ac_info->config_file, G_KEY_FILE_NONE, NULL);

#define SAVE_CONF_BOOL(name) G_STMT_START {                                    \
    ac_info->name = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(            \
                        g_object_get_data(G_OBJECT(dialog), "check_" #name))); \
    g_key_file_set_boolean(config, "autoclose", #name, ac_info->name);         \
} G_STMT_END

	SAVE_CONF_BOOL(parenthesis);
	SAVE_CONF_BOOL(abracket);
	SAVE_CONF_BOOL(abracket_htmlonly);
	SAVE_CONF_BOOL(cbracket);
	SAVE_CONF_BOOL(sbracket);
	SAVE_CONF_BOOL(dquote);
	SAVE_CONF_BOOL(squote);
	SAVE_CONF_BOOL(backquote);
	SAVE_CONF_BOOL(backquote_bashonly);
	SAVE_CONF_BOOL(comments_ac_enable);
	SAVE_CONF_BOOL(delete_pairing_brace);
	SAVE_CONF_BOOL(suppress_doubling);
	SAVE_CONF_BOOL(enclose_selections);
	SAVE_CONF_BOOL(comments_enclose);
	SAVE_CONF_BOOL(keep_selection);
	SAVE_CONF_BOOL(make_indent_for_cbracket);
	SAVE_CONF_BOOL(move_cursor_to_beginning);
	SAVE_CONF_BOOL(improved_cbracket_indent);
	SAVE_CONF_BOOL(whitesmiths_style);
	SAVE_CONF_BOOL(close_functions);
	SAVE_CONF_BOOL(bcksp_remove_pair);
	SAVE_CONF_BOOL(jump_on_tab);

#undef SAVE_CONF_BOOL

	if (!g_file_test(config_dir, G_FILE_TEST_IS_DIR) && utils_mkdir(config_dir, TRUE) != 0)
	{
		dialogs_show_msgbox(GTK_MESSAGE_ERROR,
			_("Plugin configuration directory could not be created."));
	}
	else
	{
		/* write config to file */
		gchar *data;
		data = g_key_file_to_data(config, NULL, NULL);
		utils_write_file(ac_info->config_file, data);
		g_free(data);
	}
	g_free(config_dir);
	g_key_file_free(config);
}

/* Called by Geany to initialize the plugin */
static gboolean
plugin_autoclose_init(GeanyPlugin *plugin, G_GNUC_UNUSED gpointer pdata)
{
	guint i = 0;

	geany_plugin = plugin;
	geany_data = plugin->geany_data;

	foreach_document(i)
	{
		on_document_open(NULL, documents[i], NULL);
	}
	GKeyFile *config = g_key_file_new();

	ac_info = g_new0(AutocloseInfo, 1);

	ac_info->config_file = g_strconcat(geany->app->configdir, G_DIR_SEPARATOR_S,
		"plugins", G_DIR_SEPARATOR_S, "autoclose", G_DIR_SEPARATOR_S, "autoclose.conf", NULL);

	g_key_file_load_from_file(config, ac_info->config_file, G_KEY_FILE_NONE, NULL);

#define GET_CONF_BOOL(name, def) ac_info->name = utils_get_setting_boolean(config, "autoclose", #name, def)

	GET_CONF_BOOL(parenthesis, TRUE);
	/* Angular bracket conflicts with conditional statements, enable only for HTML by default */
	GET_CONF_BOOL(abracket, TRUE);
	GET_CONF_BOOL(abracket_htmlonly, TRUE);
	GET_CONF_BOOL(cbracket, TRUE);
	GET_CONF_BOOL(sbracket, TRUE);
	GET_CONF_BOOL(dquote, TRUE);
	GET_CONF_BOOL(squote, TRUE);
	GET_CONF_BOOL(backquote, TRUE);
	GET_CONF_BOOL(backquote_bashonly, TRUE);
	GET_CONF_BOOL(comments_ac_enable, FALSE);
	GET_CONF_BOOL(delete_pairing_brace, TRUE);
	GET_CONF_BOOL(suppress_doubling, TRUE);
	GET_CONF_BOOL(enclose_selections, TRUE);
	GET_CONF_BOOL(comments_enclose, FALSE);
	GET_CONF_BOOL(keep_selection, TRUE);
	GET_CONF_BOOL(make_indent_for_cbracket, TRUE);
	GET_CONF_BOOL(move_cursor_to_beginning, TRUE);
	GET_CONF_BOOL(improved_cbracket_indent, TRUE);
	GET_CONF_BOOL(whitesmiths_style, FALSE);
	GET_CONF_BOOL(close_functions, TRUE);
	GET_CONF_BOOL(bcksp_remove_pair, FALSE);
	GET_CONF_BOOL(jump_on_tab, TRUE);

#undef GET_CONF_BOOL

	g_key_file_free(config);
	return TRUE;
}

#define GET_CHECKBOX_ACTIVE(name) gboolean sens = gtk_toggle_button_get_active(\
           GTK_TOGGLE_BUTTON(g_object_get_data(G_OBJECT(data), "check_" #name)))

#define SET_SENS(name) gtk_widget_set_sensitive(                               \
                        g_object_get_data(G_OBJECT(data), "check_" #name), sens)

static void
ac_make_indent_for_cbracket_cb(GtkToggleButton *togglebutton, gpointer data)
{
	GET_CHECKBOX_ACTIVE(make_indent_for_cbracket);
	SET_SENS(move_cursor_to_beginning);
}

static void
ac_parenthesis_cb(GtkToggleButton *togglebutton, gpointer data)
{
	GET_CHECKBOX_ACTIVE(parenthesis);
	SET_SENS(close_functions);
}

static void
ac_cbracket_cb(GtkToggleButton *togglebutton, gpointer data)
{
	GET_CHECKBOX_ACTIVE(cbracket);
	SET_SENS(make_indent_for_cbracket);
	SET_SENS(move_cursor_to_beginning);
}

static void
ac_abracket_htmlonly_cb(GtkToggleButton *togglebutton, gpointer data)
{
	GET_CHECKBOX_ACTIVE(abracket);
	SET_SENS(abracket_htmlonly);
}

static void
ac_backquote_bashonly_cb(GtkToggleButton *togglebutton, gpointer data)
{
	GET_CHECKBOX_ACTIVE(backquote);
	SET_SENS(backquote_bashonly);
}

static void
ac_enclose_selections_cb(GtkToggleButton *togglebutton, gpointer data)
{
	GET_CHECKBOX_ACTIVE(enclose_selections);
	SET_SENS(keep_selection);
	SET_SENS(comments_enclose);
}

static void
ac_delete_pairing_brace_cb(GtkToggleButton *togglebutton, gpointer data)
{
	GET_CHECKBOX_ACTIVE(delete_pairing_brace);
	SET_SENS(bcksp_remove_pair);
}

static GtkWidget *
plugin_autoclose_configure(G_GNUC_UNUSED GeanyPlugin *plugin, GtkDialog *dialog, G_GNUC_UNUSED gpointer pdata)
{
	GtkWidget *widget, *vbox, *frame, *container, *scrollbox;
	vbox = gtk_vbox_new(FALSE, 0);
	scrollbox = gtk_scrolled_window_new(NULL, NULL);
	gtk_widget_set_size_request(GTK_WIDGET(scrollbox), -1, 400);
#if GTK_CHECK_VERSION(3, 8, 0)
	gtk_container_add(GTK_CONTAINER(scrollbox), vbox);
#else
	gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrollbox), vbox);
#endif
	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollbox),
		GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);

#define WIDGET_FRAME(description) G_STMT_START {                               \
    container = gtk_vbox_new(FALSE, 0);                                        \
    frame = gtk_frame_new(NULL);                                               \
    gtk_frame_set_label(GTK_FRAME(frame), description);                        \
    gtk_container_add(GTK_CONTAINER(frame), container);                        \
    gtk_box_pack_start(GTK_BOX(vbox), frame, FALSE, FALSE, 3);                 \
} G_STMT_END

#define WIDGET_CONF_BOOL(name, description, tooltip) G_STMT_START {            \
    widget = gtk_check_button_new_with_label(description);                     \
    if (tooltip) gtk_widget_set_tooltip_text(widget, tooltip);                 \
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), ac_info->name);    \
    gtk_box_pack_start(GTK_BOX(container), widget, FALSE, FALSE, 3);           \
    g_object_set_data(G_OBJECT(dialog), "check_" #name, widget);               \
} G_STMT_END

	WIDGET_FRAME(_("Auto-close quotes and brackets"));
	WIDGET_CONF_BOOL(parenthesis, _("Parenthesis ( )"),
		_("Auto-close parenthesis \"(\" -> \"(|)\""));
	g_signal_connect(widget, "toggled", G_CALLBACK(ac_parenthesis_cb), dialog);
	WIDGET_CONF_BOOL(cbracket, _("Curly brackets { }"),
		_("Auto-close curly brackets \"{\" -> \"{|}\""));
	g_signal_connect(widget, "toggled", G_CALLBACK(ac_cbracket_cb), dialog);
	WIDGET_CONF_BOOL(sbracket, _("Square brackets [ ]"),
		_("Auto-close square brackets \"[\" -> \"[|]\""));
	WIDGET_CONF_BOOL(abracket, _("Angular brackets < >"),
		_("Auto-close angular brackets \"<\" -> \"<|>\""));
	g_signal_connect(widget, "toggled", G_CALLBACK(ac_abracket_htmlonly_cb), dialog);
	WIDGET_CONF_BOOL(abracket_htmlonly, _("\tOnly for HTML"),
		_("Auto-close angular brackets only in HTML documents"));
	WIDGET_CONF_BOOL(dquote, _("Double quotes \" \""),
		_("Auto-close double quotes \" -> \"|\""));
	WIDGET_CONF_BOOL(squote, _("Single quotes \' \'"),
		_("Auto-close single quotes ' -> '|'"));
	WIDGET_CONF_BOOL(backquote, _("Backquote ` `"),
		_("Auto-close backquote ` -> `|`"));
	g_signal_connect(widget, "toggled", G_CALLBACK(ac_backquote_bashonly_cb), dialog);
	WIDGET_CONF_BOOL(backquote_bashonly, _("\tOnly for Shell-scripts (Bash)"),
		_("Auto-close backquote only in Shell-scripts like Bash"));

	WIDGET_FRAME(_("Improve curly brackets completion"));
	WIDGET_CONF_BOOL(make_indent_for_cbracket, _("Indent when enclosing"),
		_("If you select some text and press \"{\" or \"}\", plugin "
		"will auto-close selected lines and make new block with indent."
		"\nYou do not need to select block precisely - block enclosing "
		"takes into account only lines."));
	g_signal_connect(widget, "toggled", G_CALLBACK(ac_make_indent_for_cbracket_cb), dialog);
	WIDGET_CONF_BOOL(move_cursor_to_beginning, _("Move cursor to beginning"),
		_("If you checked \"Indent when enclosing\", moving cursor "
		"to beginning may be useful: usually you make new block "
		"and need to create new statement before this block."));
	WIDGET_CONF_BOOL(improved_cbracket_indent, _("Improved auto-indentation"),
		_("Improved auto-indent for curly brackets: type \"{\" "
		"and then press Enter - plugin will create full indented block. "
		"Works without \"auto-close { }\" checkbox."));
	WIDGET_CONF_BOOL(whitesmiths_style, _("\tWhitesmith's style"),
		_("This style puts the brace associated with a control statement on "
		"the next line, indented. Statements within the braces are indented "
		"to the same level as the braces."));

	container = vbox;
	WIDGET_CONF_BOOL(delete_pairing_brace, _("Delete pairing character while backspacing first"),
		_("Check if you want to delete pairing bracket by pressing BackSpace."));
	g_signal_connect(widget, "toggled", G_CALLBACK(ac_delete_pairing_brace_cb), dialog);
	WIDGET_CONF_BOOL(suppress_doubling, _("Suppress double-completion"),
		_("Check if you want to allow editor automatically fix mistypes "
		"with brackets: if you type \"{}\" you will get \"{}\", not \"{}}\"."));
	WIDGET_CONF_BOOL(enclose_selections, _("Enclose selections"),
		_("Automatically enclose selected text by pressing just one bracket key."));
	g_signal_connect(widget, "toggled", G_CALLBACK(ac_enclose_selections_cb), dialog);
	WIDGET_CONF_BOOL(keep_selection, _("Keep selection when enclosing"),
		_("Keep your previously selected text after enclosing."));

	WIDGET_FRAME(_("Behaviour inside comments and strings"));
	WIDGET_CONF_BOOL(comments_ac_enable, _("Allow auto-closing in strings and comments"),
		_("Check if you want to keep auto-closing inside strings and comments too."));
	WIDGET_CONF_BOOL(comments_enclose, _("Enclose selections in strings and comments"),
		_("Check if you want to enclose selections inside strings and comments too."));

	container = vbox;
	WIDGET_CONF_BOOL(close_functions, _("Auto-complete \";\" for functions"),
		_("Full function auto-closing (works only for C/C++): type \"sin(\" "
		"and you will get \"sin(|);\"."));
	WIDGET_CONF_BOOL(bcksp_remove_pair, _("Shift+BackSpace removes pairing brace too"),
		_("Remove left and right brace while pressing Shift+BackSpace.\nTip: "
		"to completely remove indented block just Shift+BackSpace first \"{\" "
		"or last \"}\"."));
	WIDGET_CONF_BOOL(jump_on_tab, _("Jump on Tab to enclosed char"),
		_("Jump behind autoclosed items on Tab press."));

#undef WIDGET_CONF_BOOL
#undef WIDGET_FRAME

	ac_make_indent_for_cbracket_cb(NULL, dialog);
	ac_cbracket_cb(NULL, dialog);
	ac_enclose_selections_cb(NULL, dialog);
	ac_parenthesis_cb(NULL, dialog);
	ac_abracket_htmlonly_cb(NULL, dialog);
	ac_delete_pairing_brace_cb(NULL, dialog);
	g_signal_connect(dialog, "response", G_CALLBACK(configure_response_cb), NULL);
	gtk_widget_show_all(scrollbox);
	return scrollbox;
}

static void
autoclose_cleanup(void)
{
	guint i = 0;

	foreach_document(i)
	{
		gpointer data;
		ScintillaObject *sci;

		sci = documents[i]->editor->sci;
		data = g_object_steal_data(G_OBJECT(sci), "autoclose-userdata");
		g_free(data);
	}
}

/* Called by Geany before unloading the plugin. */
static void
plugin_autoclose_cleanup(G_GNUC_UNUSED GeanyPlugin *plugin, G_GNUC_UNUSED gpointer pdata)
{
	autoclose_cleanup();
	g_free(ac_info->config_file);
	g_free(ac_info);
}

static void
plugin_autoclose_help(G_GNUC_UNUSED GeanyPlugin *plugin, G_GNUC_UNUSED gpointer pdata)
{
	utils_open_browser("http://plugins.geany.org/autoclose.html");
}


/* Load module */
G_MODULE_EXPORT
void geany_load_module(GeanyPlugin *plugin)
{
	/* Setup translation */
	main_locale_init(LOCALEDIR, GETTEXT_PACKAGE);

	/* Set metadata */
	plugin->info->name = _("Auto-close");
	plugin->info->description = _("Auto-close braces and brackets with lot of features");
	plugin->info->version = "0.3";
	plugin->info->author = "Pavel Roschin <rpg89(at)post(dot)ru>";

	/* Set functions */
	plugin->funcs->init = plugin_autoclose_init;
	plugin->funcs->cleanup = plugin_autoclose_cleanup;
	plugin->funcs->help = plugin_autoclose_help;
	plugin->funcs->callbacks = plugin_autoclose_callbacks;
	plugin->funcs->configure = plugin_autoclose_configure;

	/* Register! */
	GEANY_PLUGIN_REGISTER(plugin, 226);
}
