/*
 * python-indentation.c
 *
 * Copyright (C) 2011 - Johannes Schmid
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

#include <ctype.h>
#include <stdlib.h>
#include <ctype.h>
#include <libanjuta/anjuta-shell.h>
#include <libanjuta/anjuta-debug.h>
#include <libanjuta/anjuta-launcher.h>
#include <libanjuta/anjuta-preferences.h>
#include <libanjuta/anjuta-modeline.h>
#include <libanjuta/interfaces/ianjuta-iterable.h>
#include <libanjuta/interfaces/ianjuta-document.h>
#include <libanjuta/interfaces/ianjuta-document-manager.h>
#include <libanjuta/interfaces/ianjuta-editor.h>
#include <libanjuta/interfaces/ianjuta-file.h>
#include <libanjuta/interfaces/ianjuta-editor-cell.h>
#include <libanjuta/interfaces/ianjuta-editor-language.h>
#include <libanjuta/interfaces/ianjuta-editor-selection.h>
#include <libanjuta/interfaces/ianjuta-editor-assist.h>
#include <libanjuta/interfaces/ianjuta-preferences.h>
#include <libanjuta/interfaces/ianjuta-symbol.h>
#include <libanjuta/interfaces/ianjuta-language.h>

#include "python-indentation.h"

#define PREF_INDENT_AUTOMATIC "indent-automatic"
#define PREF_INDENT_ADAPTIVE "indent-adaptive"
#define PREF_INDENT_TAB_INDENTS "indent-tab-indents"
#define PREF_INDENT_BRACE_SIZE "indent-brace-size"

#define TAB_SIZE (ianjuta_editor_get_tabsize (editor, NULL))

#define USE_SPACES_FOR_INDENTATION (ianjuta_editor_get_use_spaces (editor, NULL))

#define INDENT_SIZE (ianjuta_editor_get_indentsize (editor, NULL))

#define BRACE_INDENT \
(plugin->param_brace_indentation >= 0? \
plugin->param_brace_indentation : \
g_settings_get_int (plugin->settings, PREF_INDENT_BRACE_SIZE))

#define CASE_INDENT (INDENT_SIZE)
#define LABEL_INDENT (INDENT_SIZE)

static gboolean
iter_is_newline (IAnjutaIterable *iter, gchar ch)
{
	if (ch == '\n' || ch == '\r')
		return TRUE;
	return FALSE;
}

/* Returns TRUE if iter was moved */
static gboolean
skip_iter_to_newline_head (IAnjutaIterable *iter, gchar ch)
{
	gboolean ret_val = FALSE;

	if (ch == '\n')
	{
		/* Possibly at tail */
		if (ianjuta_iterable_previous (iter, NULL))
		{
			ch = ianjuta_editor_cell_get_char (IANJUTA_EDITOR_CELL (iter),
			                                   0, NULL);
			if (ch != '\r')
				/* Already at head, undo iter */
				ianjuta_iterable_next (iter, NULL);
			else
				/* Correctly at head */
				ret_val = TRUE;
		}
	}
	return ret_val;
}

/* Returns TRUE if iter was moved */
static gboolean
skip_iter_to_newline_tail (IAnjutaIterable *iter, gchar ch)
{
	gboolean ret_val = FALSE;

	if (ch == '\r')
	{
		/* Possibly at head */
		if (ianjuta_iterable_previous (iter, NULL))
		{
			ch = ianjuta_editor_cell_get_char (IANJUTA_EDITOR_CELL (iter),
			                                   0, NULL);
			if (ch != '\n')
				/* Already at tail, undo iter */
				ianjuta_iterable_next (iter, NULL);
			else
				/* Correctly at tail */
				ret_val = TRUE;
		}
	}
	return ret_val;
}

/* Jumps to the reverse matching brace of the given brace character */

static gint
get_line_indentation (IAnjutaEditor *editor, gint line_num)
{
	IAnjutaIterable *line_begin, *line_end;
	gchar *line_string, *idx;
	gint line_indent = 0;

	line_begin = ianjuta_editor_get_line_begin_position (editor, line_num, NULL);
	line_end = ianjuta_editor_get_line_end_position (editor, line_num, NULL);
	/*
	 DEBUG_PRINT ("%s: line begin = %d, line end = %d", __FUNCTION__,
	              line_begin, line_end);
				  */
	if (ianjuta_iterable_compare (line_begin, line_end, NULL) == 0)
	{
		g_object_unref (line_begin);
		g_object_unref (line_end);
		return 0;
	}

	line_string = ianjuta_editor_get_text (editor, line_begin, line_end,
	                                       NULL);
	g_object_unref (line_begin);
	g_object_unref (line_end);

	/* DEBUG_PRINT ("line_string = '%s'", line_string); */

	if (!line_string)
		return 0;

	idx = line_string;

	/* Find first non-white space */
	while (*idx != '\0' && isspace (*idx))
	{
		if (*idx == '\t')
			line_indent += TAB_SIZE;
		else
			line_indent++;
		idx++; /* Since we are looking for first non-space char, simple
		 * increment of the utf8 chars would do */
	}
	g_free (line_string);
	return line_indent;
}

static gchar *
get_line_indentation_string (IAnjutaEditor *editor, gint spaces, gint line_indent_spaces)
{
	gint i;
	gchar *indent_string;

	if ((spaces + line_indent_spaces) <= 0)
		return NULL;

	if (USE_SPACES_FOR_INDENTATION)
	{
		indent_string = g_new0 (gchar, spaces + line_indent_spaces + 1);
		for (i = 0; i < (spaces + line_indent_spaces); i++)
			indent_string[i] = ' ';
	}
	else
	{
		gint num_tabs = spaces / TAB_SIZE;
		gint num_spaces = spaces % TAB_SIZE;
		indent_string = g_new0 (gchar, num_tabs + num_spaces + line_indent_spaces + 1);

		for (i = 0; i < num_tabs; i++)
			indent_string[i] = '\t';
		for (; i < num_tabs + (num_spaces + line_indent_spaces); i++)
			indent_string[i] = ' ';
	}
	return indent_string;
}


void
python_indent_init (IndentPythonPlugin* plugin)
{

	/* Initialize indentation parameters */
	plugin->param_brace_indentation = -1;
	plugin->param_case_indentation = -1;
	plugin->param_label_indentation = -1;

	anjuta_apply_modeline (IANJUTA_EDITOR (plugin->current_editor));
}

static gint
set_line_indentation (IAnjutaEditor *editor, gint line_num, gint indentation, gint line_indent_spaces)
{
	IAnjutaIterable *line_begin, *line_end, *indent_position;
	IAnjutaIterable *current_pos;
	gint carat_offset, nchars = 0;
	gchar *old_indent_string = NULL, *indent_string = NULL;

	/* DEBUG_PRINT ("In %s()", __FUNCTION__); */
	line_begin = ianjuta_editor_get_line_begin_position (editor, line_num, NULL);
	line_end = ianjuta_editor_get_line_end_position (editor, line_num, NULL);

	/*
	 DEBUG_PRINT ("line begin = %d, line end = %d, current_pos = %d",
	              line_begin, line_end, current_pos);
				  */
	indent_position = ianjuta_iterable_clone (line_begin, NULL);

	if (ianjuta_iterable_compare (line_end, line_begin, NULL) > 0)
	{
		gchar *idx;
		gchar *line_string = ianjuta_editor_get_text (editor, line_begin,
		                                              line_end, NULL);

		//DEBUG_PRINT ("line_string = '%s'", line_string);
		if (line_string)
		{
			idx = line_string;

			/* Find first non-white space */
			while (*idx != '\0' && isspace (*idx))
			{
				idx = g_utf8_find_next_char (idx, NULL);
				ianjuta_iterable_next (indent_position, NULL);
			}
			g_free (line_string);
		}
	}
	/* Indent iter defined at this point, Identify how much is current
	 * position is beyound this point. We need to restore it later after
	 * indentation
	 */
	current_pos = ianjuta_editor_get_position (editor, NULL);
	carat_offset = ianjuta_iterable_diff (indent_position, current_pos, NULL);
	//DEBUG_PRINT ("carat offset is = %d", carat_offset);

	/* Set new indentation */
	if ((indentation + line_indent_spaces) > 0)
	{
		indent_string = get_line_indentation_string (editor, indentation, line_indent_spaces);
		nchars = indent_string ? g_utf8_strlen (indent_string, -1) : 0;

		/* Only indent if there is something to indent with */
		if (indent_string)
		{
			/* Get existing indentation */
			if (ianjuta_iterable_compare (indent_position, line_begin, NULL) > 0)
			{
				old_indent_string =
					ianjuta_editor_get_text (editor, line_begin,
					                         indent_position, NULL);

				//DEBUG_PRINT ("old_indent_string = '%s'", old_indent_string);
			}

			/* Only indent if there was no indentation before or old
			 * indentation string was different from the new indent string
			 */
			if (old_indent_string == NULL ||
			    strcmp (old_indent_string, indent_string) != 0)
			{
				/* Remove the old indentation string, if there is any */
				if (old_indent_string)
					ianjuta_editor_erase (editor, line_begin,
					                      indent_position, NULL);

				/* Insert the new indentation string */
				ianjuta_editor_insert (editor, line_begin,
				                       indent_string, -1, NULL);
			}
		}
	}

	/* If indentation == 0, we really didn't enter the previous code block,
	 * but we may need to clear existing indentation.
	 */
	if ((indentation + line_indent_spaces) == 0)
	{
		/* Get existing indentation */
		if (ianjuta_iterable_compare (indent_position, line_begin, NULL) > 0)
		{
			old_indent_string =
				ianjuta_editor_get_text (editor, line_begin,
				                         indent_position, NULL);
		}
		if (old_indent_string)
			ianjuta_editor_erase (editor, line_begin, indent_position, NULL);
	}

	/* Restore current position */
	if (carat_offset >= 0)
	{
		/* If the cursor was not before the first non-space character in
		 * the line, restore it's position after indentation.
		 */
		gint i;
		IAnjutaIterable *pos = ianjuta_editor_get_line_begin_position (editor, line_num, NULL);
		for (i = 0; i < nchars + carat_offset; i++)
			ianjuta_iterable_next (pos, NULL);
		ianjuta_editor_goto_position (editor, pos, NULL);
		g_object_unref (pos);
	}
	else /* cursor_offset < 0 */
	{
		/* If the cursor was somewhere in the old indentation spaces,
		 * home the cursor to first non-space character in the line (or
		                                                             * end of line if there is no non-space characters in the line.
		 */
		gint i;
		IAnjutaIterable *pos = ianjuta_editor_get_line_begin_position (editor, line_num, NULL);
		for (i = 0; i < nchars; i++)
			ianjuta_iterable_next (pos, NULL);
		ianjuta_editor_goto_position (editor, pos, NULL);
		g_object_unref (pos);
	}

	g_object_unref (current_pos);
	g_object_unref (indent_position);
	g_object_unref (line_begin);
	g_object_unref (line_end);

	g_free (old_indent_string);
	g_free (indent_string);
	return nchars;
}

/*  incomplete_statement:
 *  1 == COMPLETE STATEMENT
 *  0 == INCOMPLETE STATEMENT
 * -1 == UNKNOWN
 */

static gchar*
get_current_statement (IAnjutaEditor *editor, gint line_num)
{
	gchar point_ch;
	IAnjutaIterable *iter = ianjuta_editor_get_line_begin_position (editor, line_num, NULL);
	GString* statement = g_string_new (NULL);

	do
	{
		point_ch = ianjuta_editor_cell_get_char (IANJUTA_EDITOR_CELL (iter), 0, NULL);

		if (!ianjuta_iterable_next (iter, NULL) )
			break;
	} 
	while (g_ascii_isspace (point_ch) && point_ch != '\n');

	if (!ianjuta_iterable_previous (iter, NULL))
	{
		g_object_unref (iter);
		g_string_free (statement, TRUE);
		return g_strdup("");
	}

	do
	{
		point_ch = ianjuta_editor_cell_get_char (IANJUTA_EDITOR_CELL (iter), 0, NULL);
		g_string_append_c (statement, point_ch);

		if (!ianjuta_iterable_next (iter, NULL) )
			break;
	} 
	while (g_ascii_isalpha(point_ch) || g_ascii_isdigit(point_ch));

	g_object_unref (iter);
	return g_string_free (statement, FALSE);
}

static gchar 
get_last_char (IAnjutaEditor *editor, gint line_num, gint *found_line_num)
{
	gchar point_ch;
	IAnjutaIterable *iter = ianjuta_editor_get_line_end_position (editor, line_num, NULL);

	do
	{
		if (ianjuta_iterable_previous (iter, NULL) )
		{
			point_ch = ianjuta_editor_cell_get_char (IANJUTA_EDITOR_CELL (iter), 0,
			                                         NULL); 
		}
		else
			break;
	} 
	while (point_ch == ' ' || point_ch == '\n' || point_ch == '\r' || point_ch == '\t'); // Whitespace

	*found_line_num = ianjuta_editor_get_line_from_position (editor, iter, NULL);
	g_object_unref (iter);
	return point_ch;
}

static gboolean
spaces_only (IAnjutaEditor* editor, IAnjutaIterable* begin, IAnjutaIterable* end);

static gboolean
is_spaces_only (IAnjutaEditor *editor, gint line_num)
{
	IAnjutaIterable* begin = ianjuta_editor_get_line_begin_position (editor, line_num, NULL);
	IAnjutaIterable* end = ianjuta_editor_get_line_end_position (editor, line_num , NULL);

	if (spaces_only (editor, begin, end))
	{
		return TRUE;
	}

	return FALSE;
}

static gint
get_line_indentation_base (IndentPythonPlugin *plugin,
                           IAnjutaEditor *editor,
                           gint line_num,
                           gint *incomplete_statement,
                           gint *line_indent_spaces,
                           gboolean *colon_indent)
{
	gchar point_ch;
	gint line_indent = 0;
	gint currentline = line_num - 1;
	gchar *statement;
	gchar *current_statement;

	*incomplete_statement = 0;
	*line_indent_spaces = 0;

	if (currentline <= 1)
		return 0;

	point_ch = get_last_char (editor, currentline, &currentline);
	statement = get_current_statement (editor, currentline);
	current_statement = get_current_statement (editor, line_num);

	if (g_str_equal(statement, "return") || 
	    g_str_equal(statement, "break") ||
	    g_str_equal(statement, "pass") || 
	    g_str_equal(statement, "raise") || 	    
	    g_str_equal(statement, "continue"))
	{
		if (get_line_indentation (editor, currentline)>= INDENT_SIZE)
			line_indent = get_line_indentation (editor, currentline) - INDENT_SIZE;

	}
	else if ((g_str_has_prefix(current_statement, "def") && point_ch != ':') ||
	         g_str_has_prefix(current_statement, "else") ||
	         g_str_has_prefix(current_statement, "elif") ||
	         g_str_has_prefix(current_statement, "except") ||
	         g_str_has_prefix(current_statement, "finally"))
	{
		if (get_line_indentation (editor, currentline)>= INDENT_SIZE)
			line_indent = get_line_indentation (editor, currentline) - INDENT_SIZE;
	}
	else if (point_ch == ':')
	{
		line_indent = get_line_indentation (editor, currentline) + INDENT_SIZE;
	}
	else
	{
		gint line = currentline;
		while (is_spaces_only(editor, line) && line >= 0)
			line--;
		line_indent = get_line_indentation (editor, line);
	}

	g_free (statement);
	g_free (current_statement);

	return line_indent;
}

static gboolean
spaces_only (IAnjutaEditor* editor, IAnjutaIterable* begin, IAnjutaIterable* end)
{
	gboolean empty = TRUE;
	gchar* idx;
	gchar* text = ianjuta_editor_get_text (editor, begin, end, NULL);

	if (text == NULL)
		return TRUE;


	for (idx = text; *idx != '\0'; idx++)
	{
		if (!isspace(*idx))
		{
			empty = FALSE;
			break;
		}
	}
	g_free(text);
	return empty;
}

static gint
get_line_auto_indentation (IndentPythonPlugin *plugin, IAnjutaEditor *editor,
                           gint line, gint *line_indent_spaces)
{
	IAnjutaIterable *iter;
	gint line_indent = 0;
	gint incomplete_statement = -1;
	gboolean colon_indent = FALSE;

	g_return_val_if_fail (line > 0, 0);

	if (line == 1) /* First line */
	{
		return 0;
	}
	else
	{
		IAnjutaIterable* begin = ianjuta_editor_get_line_begin_position (editor, line -1 , NULL);
		IAnjutaIterable* end = ianjuta_editor_get_line_end_position (editor, line -1 , NULL);

		if (spaces_only (editor, begin, end))
		{
			set_line_indentation (editor, line -1, 0, 0);
		}
		g_object_unref (begin);
		g_object_unref (end);
	}

	iter = ianjuta_editor_get_line_begin_position (editor, line, NULL);

	line_indent = get_line_indentation_base (plugin, editor, line,
	                                         &incomplete_statement, 
	                                         line_indent_spaces,
	                                         &colon_indent);

	/* Determine what the first non-white char in the line is */
	do
	{
		gchar ch;
		/* Check if we are *inside* comment or string. Begining of comment
		 * or string does not count as inside. If inside, just align with
		 * previous indentation.
		 */
		ch = ianjuta_editor_cell_get_char (IANJUTA_EDITOR_CELL (iter),
		                                   0, NULL);
		if (iter_is_newline (iter, ch))
		{
			skip_iter_to_newline_tail (iter, ch);

			/* First levels are excused from incomplete statement indent */
			if (incomplete_statement == 1 && line_indent > 0)
				line_indent += INDENT_SIZE;
			break;
		}

		else if (!isspace (ch))
		{
			/* First levels are excused from incomplete statement indent */
			if (incomplete_statement == 1 && line_indent > 0)
				line_indent += INDENT_SIZE;
			break;
		}

	}
	while (ianjuta_iterable_next (iter, NULL));
	g_object_unref (iter);

	return line_indent;
}

void
python_indent (IndentPythonPlugin *plugin,
               IAnjutaEditor *editor,
               IAnjutaIterable *insert_pos,
               gchar ch)
{
	IAnjutaIterable *iter;
	gboolean should_auto_indent = FALSE;

	iter = ianjuta_iterable_clone (insert_pos, NULL);

	/* If autoindent is enabled*/
	if (g_settings_get_boolean (plugin->settings, PREF_INDENT_AUTOMATIC))
	{
		if (iter_is_newline (iter, ch))
		{
			skip_iter_to_newline_head (iter, ch);
			/* All newline entries means enable indenting */
			should_auto_indent = TRUE;
		}

		if (should_auto_indent)
		{
			gint insert_line;
			gint line_indent;
			gint line_indent_spaces;

			ianjuta_document_begin_undo_action (IANJUTA_DOCUMENT(editor), NULL);
			python_indent_init (plugin);

			insert_line = ianjuta_editor_get_lineno (editor, NULL);
			line_indent = get_line_auto_indentation (plugin, editor, insert_line, &line_indent_spaces);
			set_line_indentation (editor, insert_line, line_indent, line_indent_spaces);
			ianjuta_document_end_undo_action (IANJUTA_DOCUMENT(editor), NULL);
		}
	}


	g_object_unref (iter);
}

void
python_indent_auto (IndentPythonPlugin* lang_plugin,
                    IAnjutaIterable* start,
                    IAnjutaIterable* end)
{
	gint line_start, line_end;
	gint insert_line;
	gint line_indent;
	gboolean has_selection;

	IAnjutaEditor *editor;
	editor = IANJUTA_EDITOR (lang_plugin->current_editor);

	if (!start || !end)
	{
		has_selection = ianjuta_editor_selection_has_selection
			(IANJUTA_EDITOR_SELECTION (editor), NULL);
		if (has_selection)
		{
			IAnjutaIterable *sel_start, *sel_end;
			sel_start = ianjuta_editor_selection_get_start (IANJUTA_EDITOR_SELECTION (editor),
				                                            NULL);
			sel_end = ianjuta_editor_selection_get_end (IANJUTA_EDITOR_SELECTION (editor),
				                                        NULL);
			line_start = ianjuta_editor_get_line_from_position (editor, sel_start, NULL);
			line_end = ianjuta_editor_get_line_from_position (editor, sel_end, NULL);
			g_object_unref (sel_start);
			g_object_unref (sel_end);
		}
		else
		{
			line_start = ianjuta_editor_get_lineno (IANJUTA_EDITOR(editor), NULL);
			line_end = line_start;
		}
	}
	else
	{
		line_start = ianjuta_editor_get_line_from_position (editor, start, NULL);
		line_end = ianjuta_editor_get_line_from_position (editor, end, NULL);
	}
	ianjuta_document_begin_undo_action (IANJUTA_DOCUMENT(editor), NULL);
	python_indent_init (lang_plugin);

	for (insert_line = line_start; insert_line <= line_end; insert_line++)
	{
		gint line_indent_spaces = 0;
		line_indent = get_line_auto_indentation (lang_plugin, editor,
		                                         insert_line,
		                                         &line_indent_spaces);
		set_line_indentation (editor, insert_line, line_indent, line_indent_spaces);
	}
	ianjuta_document_end_undo_action (IANJUTA_DOCUMENT(editor), NULL);
}
