/*
 * ***** BEGIN GPL LICENSE BLOCK *****
 *
 * 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.
 *
 * The Original Code is Copyright (C) 2001-2002 by NaN Holding BV.
 * All rights reserved.
 *
 * The Original Code is: all of this file.
 *
 * Contributor(s): none yet.
 *
 * ***** END GPL LICENSE BLOCK *****
 */

/** \file blender/editors/space_text/text_ops.c
 *  \ingroup sptext
 */


#include <string.h>
#include <errno.h>

#include "MEM_guardedalloc.h"

#include "DNA_text_types.h"

#include "BLI_blenlib.h"

#include "BLF_translation.h"

#include "PIL_time.h"

#include "BKE_context.h"
#include "BKE_global.h"
#include "BKE_library.h"
#include "BKE_main.h"
#include "BKE_report.h"
#include "BKE_text.h"

#include "WM_api.h"
#include "WM_types.h"

#include "ED_text.h"
#include "ED_curve.h"
#include "ED_screen.h"
#include "UI_interface.h"
#include "UI_resources.h"

#include "RNA_access.h"
#include "RNA_define.h"

#ifdef WITH_PYTHON
#include "BPY_extern.h"
#endif

#include "text_intern.h"
#include "text_format.h"

static void txt_screen_clamp(SpaceText *st, ARegion *ar);

/************************ poll ***************************/


BLI_INLINE int text_pixel_x_to_column(SpaceText *st, const int x)
{
	/* add half the char width so mouse cursor selection is inbetween letters */
	return (x + (st->cwidth / 2)) / st->cwidth;
}

static int text_new_poll(bContext *UNUSED(C))
{
	return 1;
}

static int text_edit_poll(bContext *C)
{
	Text *text = CTX_data_edit_text(C);

	if (!text)
		return 0;

	if (text->id.lib) {
		// BKE_report(op->reports, RPT_ERROR, "Cannot edit external libdata");
		return 0;
	}

	return 1;
}

int text_space_edit_poll(bContext *C)
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);

	if (!st || !text)
		return 0;

	if (text->id.lib) {
		// BKE_report(op->reports, RPT_ERROR, "Cannot edit external libdata");
		return 0;
	}

	return 1;
}

static int text_region_edit_poll(bContext *C)
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	ARegion *ar = CTX_wm_region(C);

	if (!st || !text)
		return 0;
	
	if (!ar || ar->regiontype != RGN_TYPE_WINDOW)
		return 0;

	if (text->id.lib) {
		// BKE_report(op->reports, RPT_ERROR, "Cannot edit external libdata");
		return 0;
	}

	return 1;
}

/********************** updates *********************/

void text_update_line_edited(TextLine *line)
{
	if (!line)
		return;

	/* we just free format here, and let it rebuild during draw */
	if (line->format) {
		MEM_freeN(line->format);
		line->format = NULL;
	}
}

void text_update_edited(Text *text)
{
	TextLine *line;

	for (line = text->lines.first; line; line = line->next)
		text_update_line_edited(line);
}

/******************* new operator *********************/

static int text_new_exec(bContext *C, wmOperator *UNUSED(op))
{
	SpaceText *st = CTX_wm_space_text(C);
	Main *bmain = CTX_data_main(C);
	Text *text;
	PointerRNA ptr, idptr;
	PropertyRNA *prop;

	text = BKE_text_add(bmain, "Text");

	/* hook into UI */
	UI_context_active_but_prop_get_templateID(C, &ptr, &prop);

	if (prop) {
		RNA_id_pointer_create(&text->id, &idptr);
		RNA_property_pointer_set(&ptr, prop, idptr);
		RNA_property_update(C, &ptr, prop);
	}
	else if (st) {
		st->text = text;
		st->left = 0;
		st->top = 0;
		st->scroll_accum[0] = 0.0f;
		st->scroll_accum[1] = 0.0f;
		text_drawcache_tag_update(st, 1);
	}

	WM_event_add_notifier(C, NC_TEXT | NA_ADDED, text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_new(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Create Text Block";
	ot->idname = "TEXT_OT_new";
	ot->description = "Create a new text data block";
	
	/* api callbacks */
	ot->exec = text_new_exec;
	ot->poll = text_new_poll;
	
	/* flags */
	ot->flag = OPTYPE_UNDO;
}

/******************* open operator *********************/

static void text_open_init(bContext *C, wmOperator *op)
{
	PropertyPointerRNA *pprop;

	op->customdata = pprop = MEM_callocN(sizeof(PropertyPointerRNA), "OpenPropertyPointerRNA");
	UI_context_active_but_prop_get_templateID(C, &pprop->ptr, &pprop->prop);
}

static void text_open_cancel(bContext *UNUSED(C), wmOperator *op)
{
	MEM_freeN(op->customdata);
}

static int text_open_exec(bContext *C, wmOperator *op)
{
	SpaceText *st = CTX_wm_space_text(C);
	Main *bmain = CTX_data_main(C);
	Text *text;
	PropertyPointerRNA *pprop;
	PointerRNA idptr;
	char str[FILE_MAX];
	const bool internal = RNA_boolean_get(op->ptr, "internal");

	RNA_string_get(op->ptr, "filepath", str);

	text = BKE_text_load_ex(bmain, str, G.main->name, internal);

	if (!text) {
		if (op->customdata) MEM_freeN(op->customdata);
		return OPERATOR_CANCELLED;
	}

	if (!op->customdata)
		text_open_init(C, op);

	/* hook into UI */
	pprop = op->customdata;

	if (pprop->prop) {
		RNA_id_pointer_create(&text->id, &idptr);
		RNA_property_pointer_set(&pprop->ptr, pprop->prop, idptr);
		RNA_property_update(C, &pprop->ptr, pprop->prop);
	}
	else if (st) {
		st->text = text;
		st->left = 0;
		st->top = 0;
		st->scroll_accum[0] = 0.0f;
		st->scroll_accum[1] = 0.0f;
	}

	text_drawcache_tag_update(st, 1);
	WM_event_add_notifier(C, NC_TEXT | NA_ADDED, text);

	MEM_freeN(op->customdata);

	return OPERATOR_FINISHED;
}

static int text_open_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event))
{
	Text *text = CTX_data_edit_text(C);
	const char *path = (text && text->name) ? text->name : G.main->name;

	if (RNA_struct_property_is_set(op->ptr, "filepath"))
		return text_open_exec(C, op);
	
	text_open_init(C, op);
	RNA_string_set(op->ptr, "filepath", path);
	WM_event_add_fileselect(C, op); 

	return OPERATOR_RUNNING_MODAL;
}

void TEXT_OT_open(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Open Text Block";
	ot->idname = "TEXT_OT_open";
	ot->description = "Open a new text data block";

	/* api callbacks */
	ot->exec = text_open_exec;
	ot->invoke = text_open_invoke;
	ot->cancel = text_open_cancel;
	ot->poll = text_new_poll;

	/* flags */
	ot->flag = OPTYPE_UNDO;
	
	/* properties */
	WM_operator_properties_filesel(ot, FOLDERFILE | TEXTFILE | PYSCRIPTFILE, FILE_SPECIAL, FILE_OPENFILE,
	                               WM_FILESEL_FILEPATH, FILE_DEFAULTDISPLAY);  //XXX TODO, relative_path
	RNA_def_boolean(ot->srna, "internal", 0, "Make internal", "Make text file internal after loading");
}

/******************* reload operator *********************/

static int text_reload_exec(bContext *C, wmOperator *op)
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	ARegion *ar = CTX_wm_region(C);

	/* store view & cursor state */
	const int orig_top = st->top;
	const int orig_curl = BLI_findindex(&text->lines, text->curl);
	const int orig_curc = text->curc;

	if (!BKE_text_reload(text)) {
		BKE_report(op->reports, RPT_ERROR, "Could not reopen file");
		return OPERATOR_CANCELLED;
	}

#ifdef WITH_PYTHON
	if (text->compiled)
		BPY_text_free_code(text);
#endif

	text_update_edited(text);
	text_update_cursor_moved(C);
	text_drawcache_tag_update(CTX_wm_space_text(C), 1);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	/* return to scroll position */
	st->top = orig_top;
	txt_screen_clamp(st, ar);
	/* return cursor */
	txt_move_to(text, orig_curl, orig_curc, false);

	return OPERATOR_FINISHED;
}

void TEXT_OT_reload(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Reload";
	ot->idname = "TEXT_OT_reload";
	ot->description = "Reload active text data block from its file";
	
	/* api callbacks */
	ot->exec = text_reload_exec;
	ot->invoke = WM_operator_confirm;
	ot->poll = text_edit_poll;
}

/******************* delete operator *********************/

static int text_unlink_poll(bContext *C)
{
	/* it should be possible to unlink texts if they're lib-linked in... */
	return CTX_data_edit_text(C) != NULL;
}

static int text_unlink_exec(bContext *C, wmOperator *UNUSED(op))
{
	Main *bmain = CTX_data_main(C);
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);

	/* make the previous text active, if its not there make the next text active */
	if (st) {
		if (text->id.prev) {
			st->text = text->id.prev;
			text_update_cursor_moved(C);
			WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);
		}
		else if (text->id.next) {
			st->text = text->id.next;
			text_update_cursor_moved(C);
			WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);
		}
	}

	BKE_text_unlink(bmain, text);
	BKE_libblock_free(bmain, text);

	text_drawcache_tag_update(st, 1);
	WM_event_add_notifier(C, NC_TEXT | NA_REMOVED, NULL);

	return OPERATOR_FINISHED;
}

void TEXT_OT_unlink(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Unlink";
	ot->idname = "TEXT_OT_unlink";
	ot->description = "Unlink active text data block";
	
	/* api callbacks */
	ot->exec = text_unlink_exec;
	ot->invoke = WM_operator_confirm;
	ot->poll = text_unlink_poll;
	
	/* flags */
	ot->flag = OPTYPE_UNDO;
}

/******************* make internal operator *********************/

static int text_make_internal_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);

	text->flags |= TXT_ISMEM | TXT_ISDIRTY;

	if (text->name) {
		MEM_freeN(text->name);
		text->name = NULL;
	}

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_make_internal(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Make Internal";
	ot->idname = "TEXT_OT_make_internal";
	ot->description = "Make active text file internal";

	/* api callbacks */
	ot->exec = text_make_internal_exec;
	ot->poll = text_edit_poll;
	
	/* flags */
	ot->flag = OPTYPE_UNDO;
}

/******************* save operator *********************/

static int text_save_poll(bContext *C)
{
	Text *text = CTX_data_edit_text(C);

	if (!text_edit_poll(C))
		return 0;
	
	return (text->name != NULL && !(text->flags & TXT_ISMEM));
}

static void txt_write_file(Text *text, ReportList *reports) 
{
	FILE *fp;
	TextLine *tmp;
	BLI_stat_t st;
	char filepath[FILE_MAX];
	
	BLI_strncpy(filepath, text->name, FILE_MAX);
	BLI_path_abs(filepath, G.main->name);
	
	fp = BLI_fopen(filepath, "w");
	if (fp == NULL) {
		BKE_reportf(reports, RPT_ERROR, "Unable to save '%s': %s",
		            filepath, errno ? strerror(errno) : TIP_("unknown error writing file"));
		return;
	}

	for (tmp = text->lines.first; tmp; tmp = tmp->next) {
		fputs(tmp->line, fp);
		if (tmp->next) {
			fputc('\n', fp);
		}
	}
	
	fclose(fp);

	if (BLI_stat(filepath, &st) == 0) {
		text->mtime = st.st_mtime;

		/* report since this can be called from key-shortcuts */
		BKE_reportf(reports, RPT_INFO, "Saved Text '%s'", filepath);
	}
	else {
		text->mtime = 0;
		BKE_reportf(reports, RPT_WARNING, "Unable to stat '%s': %s",
		            filepath, errno ? strerror(errno) : TIP_("unknown error stating file"));
	}
	
	text->flags &= ~TXT_ISDIRTY;
}

static int text_save_exec(bContext *C, wmOperator *op)
{
	Text *text = CTX_data_edit_text(C);

	txt_write_file(text, op->reports);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_save(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Save";
	ot->idname = "TEXT_OT_save";
	ot->description = "Save active text data block";

	/* api callbacks */
	ot->exec = text_save_exec;
	ot->poll = text_save_poll;
}

/******************* save as operator *********************/

static int text_save_as_exec(bContext *C, wmOperator *op)
{
	Text *text = CTX_data_edit_text(C);
	char str[FILE_MAX];

	if (!text)
		return OPERATOR_CANCELLED;

	RNA_string_get(op->ptr, "filepath", str);

	if (text->name) MEM_freeN(text->name);
	text->name = BLI_strdup(str);
	text->flags &= ~TXT_ISMEM;

	txt_write_file(text, op->reports);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_FINISHED;
}

static int text_save_as_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event))
{
	Text *text = CTX_data_edit_text(C);
	const char *str;

	if (RNA_struct_property_is_set(op->ptr, "filepath"))
		return text_save_as_exec(C, op);

	if (text->name)
		str = text->name;
	else if (text->flags & TXT_ISMEM)
		str = text->id.name + 2;
	else
		str = G.main->name;
	
	RNA_string_set(op->ptr, "filepath", str);
	WM_event_add_fileselect(C, op); 

	return OPERATOR_RUNNING_MODAL;
}

void TEXT_OT_save_as(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Save As";
	ot->idname = "TEXT_OT_save_as";
	ot->description = "Save active text file with options";
	
	/* api callbacks */
	ot->exec = text_save_as_exec;
	ot->invoke = text_save_as_invoke;
	ot->poll = text_edit_poll;

	/* properties */
	WM_operator_properties_filesel(ot, FOLDERFILE | TEXTFILE | PYSCRIPTFILE, FILE_SPECIAL, FILE_SAVE,
	                               WM_FILESEL_FILEPATH, FILE_DEFAULTDISPLAY);  //XXX TODO, relative_path
}

/******************* run script operator *********************/

static int text_run_script_poll(bContext *C)
{
	return (CTX_data_edit_text(C) != NULL);
}

static int text_run_script(bContext *C, ReportList *reports)
{
#ifdef WITH_PYTHON
	Text *text = CTX_data_edit_text(C);
	const bool is_live = (reports == NULL);

	/* only for comparison */
	void *curl_prev = text->curl;
	int curc_prev = text->curc;

	if (BPY_text_exec(C, text, reports, !is_live)) {
		if (is_live) {
			/* for nice live updates */
			WM_event_add_notifier(C, NC_WINDOW | NA_EDITED, NULL);
		}
		return OPERATOR_FINISHED;
	}

	/* Don't report error messages while live editing */
	if (!is_live) {
		/* text may have freed its self */
		if (CTX_data_edit_text(C) == text) {
			if (text->curl != curl_prev || curc_prev != text->curc) {
				text_update_cursor_moved(C);
				WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
			}
		}

		BKE_report(reports, RPT_ERROR, "Python script fail, look in the console for now...");

		return OPERATOR_FINISHED;
	}
#else
	(void)C;
	(void)reports;
#endif /* !WITH_PYTHON */
	return OPERATOR_CANCELLED;
}

static int text_run_script_exec(bContext *C, wmOperator *op)
{
#ifndef WITH_PYTHON
	(void)C; /* unused */

	BKE_report(op->reports, RPT_ERROR, "Python disabled in this build");

	return OPERATOR_CANCELLED;
#else
	return text_run_script(C, op->reports);
#endif
}

void TEXT_OT_run_script(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Run Script";
	ot->idname = "TEXT_OT_run_script";
	ot->description = "Run active script";
	
	/* api callbacks */
	ot->poll = text_run_script_poll;
	ot->exec = text_run_script_exec;

	/* flags */
	ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}

/******************* refresh pyconstraints operator *********************/

static int text_refresh_pyconstraints_exec(bContext *UNUSED(C), wmOperator *UNUSED(op))
{
#ifdef WITH_PYTHON
#if 0
	Text *text = CTX_data_edit_text(C);
	Object *ob;
	bConstraint *con;
	short update;
	
	/* check all pyconstraints */
	for (ob = CTX_data_main(C)->object.first; ob; ob = ob->id.next) {
		update = 0;
		if (ob->type == OB_ARMATURE && ob->pose) {
			bPoseChannel *pchan;
			for (pchan = ob->pose->chanbase.first; pchan; pchan = pchan->next) {
				for (con = pchan->constraints.first; con; con = con->next) {
					if (con->type == CONSTRAINT_TYPE_PYTHON) {
						bPythonConstraint *data = con->data;
						if (data->text == text) BPY_pyconstraint_update(ob, con);
						update = 1;
						
					}
				}
			}
		}
		for (con = ob->constraints.first; con; con = con->next) {
			if (con->type == CONSTRAINT_TYPE_PYTHON) {
				bPythonConstraint *data = con->data;
				if (data->text == text) BPY_pyconstraint_update(ob, con);
				update = 1;
			}
		}
		
		if (update) {
			DAG_id_tag_update(&ob->id, OB_RECALC_DATA);
		}
	}
#endif
#endif

	return OPERATOR_FINISHED;
}

void TEXT_OT_refresh_pyconstraints(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Refresh PyConstraints";
	ot->idname = "TEXT_OT_refresh_pyconstraints";
	ot->description = "Refresh all pyconstraints";
	
	/* api callbacks */
	ot->exec = text_refresh_pyconstraints_exec;
	ot->poll = text_edit_poll;
}

/******************* paste operator *********************/

static char *txt_copy_selected(Text *text)
{
	TextLine *tmp, *linef, *linel;
	char *buf = NULL;
	int charf, charl, length = 0;
	
	if (!text) return NULL;
	if (!text->curl) return NULL;
	if (!text->sell) return NULL;

	if (!txt_has_sel(text)) return NULL;

	if (text->curl == text->sell) {
		linef = linel = text->curl;
		
		if (text->curc < text->selc) {
			charf = text->curc;
			charl = text->selc;
		}
		else {
			charf = text->selc;
			charl = text->curc;
		}
	}
	else if (txt_get_span(text->curl, text->sell) < 0) {
		linef = text->sell;
		linel = text->curl;

		charf = text->selc;
		charl = text->curc;
	}
	else {
		linef = text->curl;
		linel = text->sell;
		
		charf = text->curc;
		charl = text->selc;
	}

	if (linef == linel) {
		length = charl - charf;

		buf = MEM_callocN(length + 1, "cut buffera");
		
		BLI_strncpy(buf, linef->line + charf, length + 1);
	}
	else {
		length += linef->len - charf;
		length += charl;
		length++; /* For the '\n' */
		
		tmp = linef->next;
		while (tmp && tmp != linel) {
			length += tmp->len + 1;
			tmp = tmp->next;
		}
		
		buf = MEM_callocN(length + 1, "cut bufferb");
		
		strncpy(buf, linef->line + charf, linef->len - charf);
		length = linef->len - charf;
		
		buf[length++] = '\n';
		
		tmp = linef->next;
		while (tmp && tmp != linel) {
			strncpy(buf + length, tmp->line, tmp->len);
			length += tmp->len;
			
			buf[length++] = '\n';
			
			tmp = tmp->next;
		}
		strncpy(buf + length, linel->line, charl);
		length += charl;
		
		buf[length] = 0;
	}

	return buf;
}

static int text_paste_exec(bContext *C, wmOperator *op)
{
	const bool selection = RNA_boolean_get(op->ptr, "selection");
	Text *text = CTX_data_edit_text(C);
	char *buf;
	int buf_len;

	buf = WM_clipboard_text_get(selection, &buf_len);

	if (!buf)
		return OPERATOR_CANCELLED;

	text_drawcache_tag_update(CTX_wm_space_text(C), 0);

	txt_insert_buf(text, buf);
	text_update_edited(text);

	MEM_freeN(buf);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	/* run the script while editing, evil but useful */
	if (CTX_wm_space_text(C)->live_edit)
		text_run_script(C, NULL);

	return OPERATOR_FINISHED;
}

void TEXT_OT_paste(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Paste";
	ot->idname = "TEXT_OT_paste";
	ot->description = "Paste text from clipboard";
	
	/* api callbacks */
	ot->exec = text_paste_exec;
	ot->poll = text_edit_poll;
	
	/* properties */
	RNA_def_boolean(ot->srna, "selection", 0, "Selection", "Paste text selected elsewhere rather than copied (X11 only)");
}

/**************** duplicate operator *******************/

static int text_duplicate_line_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);
	
	txt_duplicate_line(text);
	
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	/* run the script while editing, evil but useful */
	if (CTX_wm_space_text(C)->live_edit) {
		text_run_script(C, NULL);
	}

	return OPERATOR_FINISHED;
}

void TEXT_OT_duplicate_line(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Duplicate Line";
	ot->idname = "TEXT_OT_duplicate_line";
	ot->description = "Duplicate the current line";
	
	/* api callbacks */
	ot->exec = text_duplicate_line_exec;
	ot->poll = text_edit_poll;
}

/******************* copy operator *********************/

static void txt_copy_clipboard(Text *text)
{
	char *buf;

	buf = txt_copy_selected(text);

	if (buf) {
		WM_clipboard_text_set(buf, 0);
		MEM_freeN(buf);
	}
}

static int text_copy_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);

	txt_copy_clipboard(text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_copy(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Copy";
	ot->idname = "TEXT_OT_copy";
	ot->description = "Copy selected text to clipboard";

	/* api callbacks */
	ot->exec = text_copy_exec;
	ot->poll = text_edit_poll;
}

/******************* cut operator *********************/

static int text_cut_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);

	text_drawcache_tag_update(CTX_wm_space_text(C), 0);

	txt_copy_clipboard(text);
	txt_delete_selected(text);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	/* run the script while editing, evil but useful */
	if (CTX_wm_space_text(C)->live_edit)
		text_run_script(C, NULL);

	return OPERATOR_FINISHED;
}

void TEXT_OT_cut(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Cut";
	ot->idname = "TEXT_OT_cut";
	ot->description = "Cut selected text to clipboard";
	
	/* api callbacks */
	ot->exec = text_cut_exec;
	ot->poll = text_edit_poll;
}

/******************* indent operator *********************/

static int text_indent_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);

	text_drawcache_tag_update(CTX_wm_space_text(C), 0);

	if (txt_has_sel(text)) {
		txt_order_cursors(text, false);
		txt_indent(text);
	}
	else
		txt_add_char(text, '\t');

	text_update_edited(text);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_indent(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Indent";
	ot->idname = "TEXT_OT_indent";
	ot->description = "Indent selected text";
	
	/* api callbacks */
	ot->exec = text_indent_exec;
	ot->poll = text_edit_poll;
}

/******************* unindent operator *********************/

static int text_unindent_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);

	text_drawcache_tag_update(CTX_wm_space_text(C), 0);

	txt_order_cursors(text, false);
	txt_unindent(text);

	text_update_edited(text);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_unindent(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Unindent";
	ot->idname = "TEXT_OT_unindent";
	ot->description = "Unindent selected text";
	
	/* api callbacks */
	ot->exec = text_unindent_exec;
	ot->poll = text_edit_poll;
}

/******************* line break operator *********************/

static int text_line_break_exec(bContext *C, wmOperator *UNUSED(op))
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	int a, curts;
	int space = (text->flags & TXT_TABSTOSPACES) ? st->tabnumber : 1;

	text_drawcache_tag_update(st, 0);

	// double check tabs/spaces before splitting the line
	curts = txt_setcurr_tab_spaces(text, space);
	txt_split_curline(text);

	for (a = 0; a < curts; a++) {
		if (text->flags & TXT_TABSTOSPACES) {
			txt_add_char(text, ' ');
		}
		else {
			txt_add_char(text, '\t');
		}
	}

	if (text->curl) {
		if (text->curl->prev)
			text_update_line_edited(text->curl->prev);
		text_update_line_edited(text->curl);
	}

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_CANCELLED;
}

void TEXT_OT_line_break(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Line Break";
	ot->idname = "TEXT_OT_line_break";
	ot->description = "Insert line break at cursor position";
	
	/* api callbacks */
	ot->exec = text_line_break_exec;
	ot->poll = text_edit_poll;
}

/******************* comment operator *********************/

static int text_comment_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);

	if (txt_has_sel(text)) {
		text_drawcache_tag_update(CTX_wm_space_text(C), 0);

		txt_order_cursors(text, false);
		txt_comment(text);
		text_update_edited(text);

		text_update_cursor_moved(C);
		WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
		return OPERATOR_FINISHED;
	}

	return OPERATOR_CANCELLED;
}

void TEXT_OT_comment(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Comment";
	ot->idname = "TEXT_OT_comment";
	ot->description = "Convert selected text to comment";
	
	/* api callbacks */
	ot->exec = text_comment_exec;
	ot->poll = text_edit_poll;
}

/******************* uncomment operator *********************/

static int text_uncomment_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);

	if (txt_has_sel(text)) {
		text_drawcache_tag_update(CTX_wm_space_text(C), 0);

		txt_order_cursors(text, false);
		txt_uncomment(text);
		text_update_edited(text);

		text_update_cursor_moved(C);
		WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

		return OPERATOR_FINISHED;
	}

	return OPERATOR_CANCELLED;
}

void TEXT_OT_uncomment(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Uncomment";
	ot->idname = "TEXT_OT_uncomment";
	ot->description = "Convert selected comment to text";
	
	/* api callbacks */
	ot->exec = text_uncomment_exec;
	ot->poll = text_edit_poll;
}

/******************* convert whitespace operator *********************/

enum { TO_SPACES, TO_TABS };
static EnumPropertyItem whitespace_type_items[] = {
	{TO_SPACES, "SPACES", 0, "To Spaces", NULL},
	{TO_TABS, "TABS", 0, "To Tabs", NULL},
	{0, NULL, 0, NULL, NULL}};

static int text_convert_whitespace_exec(bContext *C, wmOperator *op)
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	TextLine *tmp;
	FlattenString fs;
	size_t a, j;
	char *new_line;
	int extra, number; //unknown for now
	int type = RNA_enum_get(op->ptr, "type");

	/* first convert to all space, this make it a lot easier to convert to tabs
	 * because there is no mixtures of ' ' && '\t' */
	for (tmp = text->lines.first; tmp; tmp = tmp->next) {
		const char *text_check_line     = tmp->line;
		const int   text_check_line_len = tmp->len;
		number = flatten_string(st, &fs, text_check_line) + 1;
		flatten_string_free(&fs);
		new_line = MEM_callocN(number, "Converted_Line");
		j = 0;
		for (a = 0; a < text_check_line_len; a++) { //foreach char in line
			if (text_check_line[a] == '\t') { //checking for tabs
				//get the number of spaces this tabs is showing
				//i don't like doing it this way but will look into it later
				new_line[j] = '\0';
				number = flatten_string(st, &fs, new_line);
				flatten_string_free(&fs);
				new_line[j] = '\t';
				new_line[j + 1] = '\0';
				number = flatten_string(st, &fs, new_line) - number;
				flatten_string_free(&fs);

				for (extra = 0; extra < number; extra++) {
					new_line[j] = ' ';
					j++;
				}
			}
			else {
				new_line[j] = text_check_line[a];
				j++;
			}
		}
		new_line[j] = '\0';
		// put new_line in the tmp->line spot still need to try and set the curc correctly
		if (tmp->line) MEM_freeN(tmp->line);
		if (tmp->format) MEM_freeN(tmp->format);
		
		tmp->line = new_line;
		tmp->len = strlen(new_line);
		tmp->format = NULL;
	}
	
	if (type == TO_TABS) { // Converting to tabs
		//start over from the beginning
		
		for (tmp = text->lines.first; tmp; tmp = tmp->next) {
			const char *text_check_line     = tmp->line;
			const int   text_check_line_len = tmp->len;
			extra = 0;
			for (a = 0; a < text_check_line_len; a++) {
				number = 0;
				for (j = 0; j < (size_t)st->tabnumber; j++) {
					if ((a + j) <= text_check_line_len) { //check to make sure we are not pass the end of the line
						if (text_check_line[a + j] != ' ') {
							number = 1;
						}
					}
				}
				if (!number) { //found all number of space to equal a tab
					a = a + (st->tabnumber - 1);
					extra = extra + 1;
				}
			}
			
			if (extra > 0) {   //got tabs make malloc and do what you have to do
				new_line = MEM_callocN(text_check_line_len - (((st->tabnumber * extra) - extra) - 1), "Converted_Line");
				extra = 0; //reuse vars
				for (a = 0; a < text_check_line_len; a++) {
					number = 0;
					for (j = 0; j < (size_t)st->tabnumber; j++) {
						if ((a + j) <= text_check_line_len) { //check to make sure we are not pass the end of the line
							if (text_check_line[a + j] != ' ') {
								number = 1;
							}
						}
					}

					if (!number) { //found all number of space to equal a tab
						new_line[extra] = '\t';
						a = a + (st->tabnumber - 1);
						extra++;
						
					}
					else { //not adding a tab
						new_line[extra] = text_check_line[a];
						extra++;
					}
				}
				new_line[extra] = '\0';
				// put new_line in the tmp->line spot still need to try and set the curc correctly
				if (tmp->line) MEM_freeN(tmp->line);
				if (tmp->format) MEM_freeN(tmp->format);
				
				tmp->line = new_line;
				tmp->len = strlen(new_line);
				tmp->format = NULL;
			}
		}
	}

	text_update_edited(text);
	text_update_cursor_moved(C);
	text_drawcache_tag_update(st, 1);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_convert_whitespace(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Convert Whitespace";
	ot->idname = "TEXT_OT_convert_whitespace";
	ot->description = "Convert whitespaces by type";
	
	/* api callbacks */
	ot->exec = text_convert_whitespace_exec;
	ot->poll = text_edit_poll;

	/* properties */
	RNA_def_enum(ot->srna, "type", whitespace_type_items, TO_SPACES, "Type", "Type of whitespace to convert to");
}

/******************* select all operator *********************/

static int text_select_all_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);

	txt_sel_all(text);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_select_all(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Select All";
	ot->idname = "TEXT_OT_select_all";
	ot->description = "Select all text";
	
	/* api callbacks */
	ot->exec = text_select_all_exec;
	ot->poll = text_edit_poll;
}

/******************* select line operator *********************/

static int text_select_line_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);

	txt_sel_line(text);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_select_line(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Select Line";
	ot->idname = "TEXT_OT_select_line";
	ot->description = "Select text by line";
	
	/* api callbacks */
	ot->exec = text_select_line_exec;
	ot->poll = text_edit_poll;
}

/******************* select word operator *********************/

static int text_select_word_exec(bContext *C, wmOperator *UNUSED(op))
{
	Text *text = CTX_data_edit_text(C);
	/* don't advance cursor before stepping */
	const bool use_init_step = false;

	txt_jump_left(text, false, use_init_step);
	txt_jump_right(text, true, use_init_step);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_select_word(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Select Word";
	ot->idname = "TEXT_OT_select_word";
	ot->description = "Select word under cursor";

	/* api callbacks */
	ot->exec = text_select_word_exec;
	ot->poll = text_edit_poll;
}

/********************* move lines operators ***********************/

static int move_lines_exec(bContext *C, wmOperator *op)
{
	Text *text = CTX_data_edit_text(C);
	const int direction = RNA_enum_get(op->ptr, "direction");
	
	txt_move_lines(text, direction);
	
	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	/* run the script while editing, evil but useful */
	if (CTX_wm_space_text(C)->live_edit)
		text_run_script(C, NULL);
	
	return OPERATOR_FINISHED;
}

void TEXT_OT_move_lines(wmOperatorType *ot)
{
	static EnumPropertyItem direction_items[] = {
		{TXT_MOVE_LINE_UP, "UP", 0, "Up", ""},
		{TXT_MOVE_LINE_DOWN, "DOWN", 0, "Down", ""},
		{0, NULL, 0, NULL, NULL}
	};

	/* identifiers */
	ot->name = "Move Lines";
	ot->idname = "TEXT_OT_move_lines";
	ot->description = "Move the currently selected line(s) up/down";
	
	/* api callbacks */
	ot->exec = move_lines_exec;
	ot->poll = text_edit_poll;

	/* properties */
	RNA_def_enum(ot->srna, "direction", direction_items, 1, "Direction", "");
}

/************************ move operator ************************/

static EnumPropertyItem move_type_items[] = {
	{LINE_BEGIN, "LINE_BEGIN", 0, "Line Begin", ""},
	{LINE_END, "LINE_END", 0, "Line End", ""},
	{FILE_TOP, "FILE_TOP", 0, "File Top", ""},
	{FILE_BOTTOM, "FILE_BOTTOM", 0, "File Bottom", ""},
	{PREV_CHAR, "PREVIOUS_CHARACTER", 0, "Previous Character", ""},
	{NEXT_CHAR, "NEXT_CHARACTER", 0, "Next Character", ""},
	{PREV_WORD, "PREVIOUS_WORD", 0, "Previous Word", ""},
	{NEXT_WORD, "NEXT_WORD", 0, "Next Word", ""},
	{PREV_LINE, "PREVIOUS_LINE", 0, "Previous Line", ""},
	{NEXT_LINE, "NEXT_LINE", 0, "Next Line", ""},
	{PREV_PAGE, "PREVIOUS_PAGE", 0, "Previous Page", ""},
	{NEXT_PAGE, "NEXT_PAGE", 0, "Next Page", ""},
	{0, NULL, 0, NULL, NULL}};

/* get cursor position in line by relative wrapped line and column positions */
static int text_get_cursor_rel(SpaceText *st, ARegion *ar, TextLine *linein, int rell, int relc)
{
	int i, j, start, end, max, chop, curs, loop, endj, found, selc;
	char ch;

	max = wrap_width(st, ar);

	selc = start = endj = curs = found = 0;
	end = max;
	chop = loop = 1;

	for (i = 0, j = 0; loop; j += BLI_str_utf8_size_safe(linein->line + j)) {
		int chars;
		int columns = BLI_str_utf8_char_width_safe(linein->line + j); /* = 1 for tab */

		/* Mimic replacement of tabs */
		ch = linein->line[j];
		if (ch == '\t') {
			chars = st->tabnumber - i % st->tabnumber;
			ch = ' ';
		}
		else {
			chars = 1;
		}

		while (chars--) {
			if (rell == 0 && i - start <= relc && i + columns - start > relc) {
				/* current position could be wrapped to next line */
				/* this should be checked when end of current line would be reached */
				selc = j;
				found = 1;
			}
			else if (i - end <= relc && i + columns - end > relc) {
				curs = j;
			}
			if (i + columns - start > max) {
				end = MIN2(end, i);

				if (found) {
					/* exact cursor position was found, check if it's */
					/* still on needed line (hasn't been wrapped) */
					if (selc > endj && !chop) selc = endj;
					loop = 0;
					break;
				}

				if (chop) endj = j;

				start = end;
				end += max;
				chop = 1;
				rell--;

				if (rell == 0 && i + columns - start > relc) {
					selc = curs;
					loop = 0;
					break;
				}
			}
			else if (ch == '\0') {
				if (!found) selc = linein->len;
				loop = 0;
				break;
			}
			else if (ch == ' ' || ch == '-') {
				if (found) {
					loop = 0;
					break;
				}

				if (rell == 0 && i + columns - start > relc) {
					selc = curs;
					loop = 0;
					break;
				}
				end = i + 1;
				endj = j;
				chop = 0;
			}
			i += columns;
		}
	}

	return selc;
}

static int cursor_skip_find_line(SpaceText *st, ARegion *ar,
                                 int lines, TextLine **linep, int *charp, int *rell, int *relc)
{
	int offl, offc, visible_lines;

	wrap_offset_in_line(st, ar, *linep, *charp, &offl, &offc);
	*relc = text_get_char_pos(st, (*linep)->line, *charp) + offc;
	*rell = lines;

	/* handle current line */
	if (lines > 0) {
		visible_lines = text_get_visible_lines(st, ar, (*linep)->line);

		if (*rell - visible_lines + offl >= 0) {
			if (!(*linep)->next) {
				if (offl < visible_lines - 1) {
					*rell = visible_lines - 1;
					return 1;
				}

				*charp = (*linep)->len;
				return 0;
			}

			*rell -= visible_lines - offl;
			*linep = (*linep)->next;
		}
		else {
			*rell += offl;
			return 1;
		}
	}
	else {
		if (*rell + offl <= 0) {
			if (!(*linep)->prev) {
				if (offl) {
					*rell = 0;
					return 1;
				}

				*charp = 0;
				return 0;
			}

			*rell += offl;
			*linep = (*linep)->prev;
		}
		else {
			*rell += offl;
			return 1;
		}
	}

	/* skip lines and find destination line and offsets */
	while (*linep) {
		visible_lines = text_get_visible_lines(st, ar, (*linep)->line);

		if (lines < 0) { /* moving top */
			if (*rell + visible_lines >= 0) {
				*rell += visible_lines;
				break;
			}

			if (!(*linep)->prev) {
				*rell = 0;
				break;
			}

			*rell += visible_lines;
			*linep = (*linep)->prev;
		}
		else { /* moving bottom */
			if (*rell - visible_lines < 0) break;

			if (!(*linep)->next) {
				*rell = visible_lines - 1;
				break;
			}

			*rell -= visible_lines;
			*linep = (*linep)->next;
		}
	}

	return 1;
}

static void txt_wrap_move_bol(SpaceText *st, ARegion *ar, const bool sel)
{
	Text *text = st->text;
	TextLine **linep;
	int *charp;
	int oldc, i, j, max, start, end, endj, chop, loop;
	char ch;

	text_update_character_width(st);

	if (sel) { linep = &text->sell; charp = &text->selc; }
	else     { linep = &text->curl; charp = &text->curc; }

	oldc = *charp;

	max = wrap_width(st, ar);

	start = endj = 0;
	end = max;
	chop = loop = 1;
	*charp = 0;

	for (i = 0, j = 0; loop; j += BLI_str_utf8_size_safe((*linep)->line + j)) {
		int chars;
		int columns = BLI_str_utf8_char_width_safe((*linep)->line + j); /* = 1 for tab */

		/* Mimic replacement of tabs */
		ch = (*linep)->line[j];
		if (ch == '\t') {
			chars = st->tabnumber - i % st->tabnumber;
			ch = ' ';
		}
		else {
			chars = 1;
		}

		while (chars--) {
			if (i + columns - start > max) {
				end = MIN2(end, i);

				*charp = endj;

				if (j >= oldc) {
					if (ch == '\0') *charp = txt_utf8_column_to_offset((*linep)->line, start);
					loop = 0;
					break;
				}

				if (chop) endj = j;

				start = end;
				end += max;
				chop = 1;
			}
			else if (ch == ' ' || ch == '-' || ch == '\0') {
				if (j >= oldc) {
					*charp = txt_utf8_column_to_offset((*linep)->line, start);
					loop = 0;
					break;
				}

				end = i + 1;
				endj = j + 1;
				chop = 0;
			}
			i += columns;
		}
	}

	if (!sel) txt_pop_sel(text);
}

static void txt_wrap_move_eol(SpaceText *st, ARegion *ar, const bool sel)
{
	Text *text = st->text;
	TextLine **linep;
	int *charp;
	int oldc, i, j, max, start, end, endj, chop, loop;
	char ch;

	text_update_character_width(st);

	if (sel) { linep = &text->sell; charp = &text->selc; }
	else     { linep = &text->curl; charp = &text->curc; }

	oldc = *charp;

	max = wrap_width(st, ar);

	start = endj = 0;
	end = max;
	chop = loop = 1;
	*charp = 0;

	for (i = 0, j = 0; loop; j += BLI_str_utf8_size_safe((*linep)->line + j)) {
		int chars;
		int columns = BLI_str_utf8_char_width_safe((*linep)->line + j); /* = 1 for tab */

		/* Mimic replacement of tabs */
		ch = (*linep)->line[j];
		if (ch == '\t') {
			chars = st->tabnumber - i % st->tabnumber;
			ch = ' ';
		}
		else {
			chars = 1;
		}

		while (chars--) {
			if (i + columns - start > max) {
				end = MIN2(end, i);

				if (chop) endj = BLI_str_prev_char_utf8((*linep)->line + j) - (*linep)->line;

				if (endj >= oldc) {
					if (ch == '\0') *charp = (*linep)->len;
					else *charp = endj;
					loop = 0;
					break;
				}

				start = end;
				end += max;
				chop = 1;
			}
			else if (ch == '\0') {
				*charp = (*linep)->len;
				loop = 0;
				break;
			}
			else if (ch == ' ' || ch == '-') {
				end = i + 1;
				endj = j;
				chop = 0;
			}
			i += columns;
		}
	}

	if (!sel) txt_pop_sel(text);
}

static void txt_wrap_move_up(SpaceText *st, ARegion *ar, const bool sel)
{
	Text *text = st->text;
	TextLine **linep;
	int *charp;
	int offl, offc, col;

	text_update_character_width(st);

	if (sel) { linep = &text->sell; charp = &text->selc; }
	else     { linep = &text->curl; charp = &text->curc; }

	wrap_offset_in_line(st, ar, *linep, *charp, &offl, &offc);
	col = text_get_char_pos(st, (*linep)->line, *charp) + offc;
	if (offl) {
		*charp = text_get_cursor_rel(st, ar, *linep, offl - 1, col);
	}
	else {
		if ((*linep)->prev) {
			int visible_lines;

			*linep = (*linep)->prev;
			visible_lines = text_get_visible_lines(st, ar, (*linep)->line);
			*charp = text_get_cursor_rel(st, ar, *linep, visible_lines - 1, col);
		}
		else {
			*charp = 0;
		}
	}

	if (!sel) txt_pop_sel(text);
}

static void txt_wrap_move_down(SpaceText *st, ARegion *ar, const bool sel)
{
	Text *text = st->text;
	TextLine **linep;
	int *charp;
	int offl, offc, col, visible_lines;

	text_update_character_width(st);

	if (sel) { linep = &text->sell; charp = &text->selc; }
	else     { linep = &text->curl; charp = &text->curc; }

	wrap_offset_in_line(st, ar, *linep, *charp, &offl, &offc);
	col = text_get_char_pos(st, (*linep)->line, *charp) + offc;
	visible_lines = text_get_visible_lines(st, ar, (*linep)->line);
	if (offl < visible_lines - 1) {
		*charp = text_get_cursor_rel(st, ar, *linep, offl + 1, col);
	}
	else {
		if ((*linep)->next) {
			*linep = (*linep)->next;
			*charp = text_get_cursor_rel(st, ar, *linep, 0, col);
		}
		else {
			*charp = (*linep)->len;
		}
	}

	if (!sel) txt_pop_sel(text);
}

/* Moves the cursor vertically by the specified number of lines.
 * If the destination line is shorter than the current cursor position, the
 * cursor will be positioned at the end of this line.
 *
 * This is to replace screen_skip for PageUp/Down operations.
 */
static void cursor_skip(SpaceText *st, ARegion *ar, Text *text, int lines, const bool sel)
{
	TextLine **linep;
	int *charp;
	
	if (sel) { linep = &text->sell; charp = &text->selc; }
	else     { linep = &text->curl; charp = &text->curc; }

	if (st && ar && st->wordwrap) {
		int rell, relc;

		/* find line and offsets inside it needed to set cursor position */
		if (cursor_skip_find_line(st, ar, lines, linep, charp, &rell, &relc))
			*charp = text_get_cursor_rel(st, ar, *linep, rell, relc);
	}
	else {
		while (lines > 0 && (*linep)->next) {
			*linep = (*linep)->next;
			lines--;
		}
		while (lines < 0 && (*linep)->prev) {
			*linep = (*linep)->prev;
			lines++;
		}
	}

	if (*charp > (*linep)->len) *charp = (*linep)->len;

	if (!sel) txt_pop_sel(text);
}

static int text_move_cursor(bContext *C, int type, bool select)
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	ARegion *ar = CTX_wm_region(C);

	/* ensure we have the right region, it's optional */
	if (ar && ar->regiontype != RGN_TYPE_WINDOW)
		ar = NULL;

	switch (type) {
		case LINE_BEGIN:
			if (!select) {
				txt_sel_clear(text);
			}
			if (st && st->wordwrap && ar) txt_wrap_move_bol(st, ar, select);
			else txt_move_bol(text, select);
			break;
			
		case LINE_END:
			if (!select) {
				txt_sel_clear(text);
			}
			if (st && st->wordwrap && ar) txt_wrap_move_eol(st, ar, select);
			else txt_move_eol(text, select);
			break;

		case FILE_TOP:
			txt_move_bof(text, select);
			break;
			
		case FILE_BOTTOM:
			txt_move_eof(text, select);
			break;

		case PREV_WORD:
			if (txt_cursor_is_line_start(text)) {
				txt_move_left(text, select);
			}
			txt_jump_left(text, select, true);
			break;

		case NEXT_WORD:
			if (txt_cursor_is_line_end(text)) {
				txt_move_right(text, select);
			}
			txt_jump_right(text, select, true);
			break;

		case PREV_CHAR:
			if (txt_has_sel(text) && !select) {
				txt_order_cursors(text, false);
				txt_pop_sel(text);
			}
			else {
				txt_move_left(text, select);
			}
			break;

		case NEXT_CHAR:
			if (txt_has_sel(text) && !select) {
				txt_order_cursors(text, true);
				txt_pop_sel(text);
			}
			else {
				txt_move_right(text, select);
			}
			break;

		case PREV_LINE:
			if (st && st->wordwrap && ar) txt_wrap_move_up(st, ar, select);
			else txt_move_up(text, select);
			break;
			
		case NEXT_LINE:
			if (st && st->wordwrap && ar) txt_wrap_move_down(st, ar, select);
			else txt_move_down(text, select);
			break;

		case PREV_PAGE:
			if (st) cursor_skip(st, ar, st->text, -st->viewlines, select);
			else cursor_skip(NULL, NULL, text, -10, select);
			break;

		case NEXT_PAGE:
			if (st) cursor_skip(st, ar, st->text, st->viewlines, select);
			else cursor_skip(NULL, NULL, text, 10, select);
			break;
	}

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);

	return OPERATOR_FINISHED;
}

static int text_move_exec(bContext *C, wmOperator *op)
{
	int type = RNA_enum_get(op->ptr, "type");

	return text_move_cursor(C, type, 0);
}

void TEXT_OT_move(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Move Cursor";
	ot->idname = "TEXT_OT_move";
	ot->description = "Move cursor to position type";
	
	/* api callbacks */
	ot->exec = text_move_exec;
	ot->poll = text_edit_poll;

	/* properties */
	RNA_def_enum(ot->srna, "type", move_type_items, LINE_BEGIN, "Type", "Where to move cursor to");
}

/******************* move select operator ********************/

static int text_move_select_exec(bContext *C, wmOperator *op)
{
	int type = RNA_enum_get(op->ptr, "type");

	return text_move_cursor(C, type, 1);
}

void TEXT_OT_move_select(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Move Select";
	ot->idname = "TEXT_OT_move_select";
	ot->description = "Move the cursor while selecting";
	
	/* api callbacks */
	ot->exec = text_move_select_exec;
	ot->poll = text_space_edit_poll;

	/* properties */
	RNA_def_enum(ot->srna, "type", move_type_items, LINE_BEGIN, "Type", "Where to move cursor to, to make a selection");
}

/******************* jump operator *********************/

static int text_jump_exec(bContext *C, wmOperator *op)
{
	Text *text = CTX_data_edit_text(C);
	int line = RNA_int_get(op->ptr, "line");
	short nlines = txt_get_span(text->lines.first, text->lines.last) + 1;

	if (line < 1)
		txt_move_toline(text, 1, 0);
	else if (line > nlines)
		txt_move_toline(text, nlines - 1, 0);
	else
		txt_move_toline(text, line - 1, 0);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);

	return OPERATOR_FINISHED;
}

static int text_jump_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event))
{
	return WM_operator_props_dialog_popup(C, op, 10 * UI_UNIT_X, 5 * UI_UNIT_Y);

}

void TEXT_OT_jump(wmOperatorType *ot)
{
	PropertyRNA *prop;

	/* identifiers */
	ot->name = "Jump";
	ot->idname = "TEXT_OT_jump";
	ot->description = "Jump cursor to line";
	
	/* api callbacks */
	ot->invoke = text_jump_invoke;
	ot->exec = text_jump_exec;
	ot->poll = text_edit_poll;

	/* properties */
	prop = RNA_def_int(ot->srna, "line", 1, 1, INT_MAX, "Line", "Line number to jump to", 1, 10000);
	RNA_def_property_translation_context(prop, BLF_I18NCONTEXT_ID_TEXT);
}

/******************* delete operator **********************/

static EnumPropertyItem delete_type_items[] = {
	{DEL_NEXT_CHAR, "NEXT_CHARACTER", 0, "Next Character", ""},
	{DEL_PREV_CHAR, "PREVIOUS_CHARACTER", 0, "Previous Character", ""},
	{DEL_NEXT_WORD, "NEXT_WORD", 0, "Next Word", ""},
	{DEL_PREV_WORD, "PREVIOUS_WORD", 0, "Previous Word", ""},
	{0, NULL, 0, NULL, NULL}};

static int text_delete_exec(bContext *C, wmOperator *op)
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	int type = RNA_enum_get(op->ptr, "type");

	text_drawcache_tag_update(st, 0);

	/* behavior could be changed here,
	 * but for now just don't jump words when we have a selection */
	if (txt_has_sel(text)) {
		if      (type == DEL_PREV_WORD) type = DEL_PREV_CHAR;
		else if (type == DEL_NEXT_WORD) type = DEL_NEXT_CHAR;
	}

	if (type == DEL_PREV_WORD) {
		if (txt_cursor_is_line_start(text)) {
			txt_backspace_char(text);
		}
		txt_backspace_word(text);
	}
	else if (type == DEL_PREV_CHAR) {
		txt_backspace_char(text);
	}
	else if (type == DEL_NEXT_WORD) {
		if (txt_cursor_is_line_end(text)) {
			txt_delete_char(text);
		}
		txt_delete_word(text);
	}
	else if (type == DEL_NEXT_CHAR) {
		txt_delete_char(text);
	}

	text_update_line_edited(text->curl);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	/* run the script while editing, evil but useful */
	if (st->live_edit)
		text_run_script(C, NULL);
	
	return OPERATOR_FINISHED;
}

void TEXT_OT_delete(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Delete";
	ot->idname = "TEXT_OT_delete";
	ot->description = "Delete text by cursor position";
	
	/* api callbacks */
	ot->exec = text_delete_exec;
	ot->poll = text_edit_poll;

	/* properties */
	RNA_def_enum(ot->srna, "type", delete_type_items, DEL_NEXT_CHAR, "Type", "Which part of the text to delete");
}

/******************* toggle overwrite operator **********************/

static int text_toggle_overwrite_exec(bContext *C, wmOperator *UNUSED(op))
{
	SpaceText *st = CTX_wm_space_text(C);

	st->overwrite = !st->overwrite;

	WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_overwrite_toggle(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Toggle Overwrite";
	ot->idname = "TEXT_OT_overwrite_toggle";
	ot->description = "Toggle overwrite while typing";
	
	/* api callbacks */
	ot->exec = text_toggle_overwrite_exec;
	ot->poll = text_space_edit_poll;
}

/******************* scroll operator **********************/

static void txt_screen_clamp(SpaceText *st, ARegion *ar)
{
	if (st->top <= 0) {
		st->top = 0;
	}
	else {
		int last;
		last = text_get_total_lines(st, ar);
		last = last - (st->viewlines / 2);
		if (last > 0 && st->top > last) {
			st->top = last;
		}
	}
}

/* Moves the view vertically by the specified number of lines */
static void txt_screen_skip(SpaceText *st, ARegion *ar, int lines)
{
	st->top += lines;
	txt_screen_clamp(st, ar);
}

/* quick enum for tsc->zone (scroller handles) */
enum {
	SCROLLHANDLE_BAR,
	SCROLLHANDLE_MIN_OUTSIDE,
	SCROLLHANDLE_MAX_OUTSIDE
};

typedef struct TextScroll {
	int old[2];
	int delta[2];

	int first;
	int scrollbar;

	int zone;
} TextScroll;

static int text_scroll_poll(bContext *C)
{
	/* it should be possible to still scroll linked texts to read them, even if they can't be edited... */
	return CTX_data_edit_text(C) != NULL;
}

static int text_scroll_exec(bContext *C, wmOperator *op)
{
	SpaceText *st = CTX_wm_space_text(C);
	ARegion *ar = CTX_wm_region(C);

	int lines = RNA_int_get(op->ptr, "lines");

	if (lines == 0)
		return OPERATOR_CANCELLED;

	txt_screen_skip(st, ar, lines * U.wheellinescroll);

	ED_area_tag_redraw(CTX_wm_area(C));

	return OPERATOR_FINISHED;
}

static void text_scroll_apply(bContext *C, wmOperator *op, const wmEvent *event)
{
	SpaceText *st = CTX_wm_space_text(C);
	ARegion *ar = CTX_wm_region(C);
	TextScroll *tsc = op->customdata;
	int mval[2] = {event->x, event->y};
	int scroll_steps[2] = {0, 0};

	text_update_character_width(st);

	/* compute mouse move distance */
	if (tsc->first) {
		tsc->old[0] = mval[0];
		tsc->old[1] = mval[1];
		tsc->first = 0;
	}

	if (event->type != MOUSEPAN) {
		tsc->delta[0] = mval[0] - tsc->old[0];
		tsc->delta[1] = mval[1] - tsc->old[1];
	}

	/* accumulate scroll, in float values for events that give less than one
	 * line offset but taken together should still scroll */
	if (!tsc->scrollbar) {
		st->scroll_accum[0] += -tsc->delta[0] / (float)st->cwidth;
		st->scroll_accum[1] += tsc->delta[1] / (float)(st->lheight_dpi + TXT_LINE_SPACING);
	}
	else {
		st->scroll_accum[1] += -tsc->delta[1] * st->pix_per_line;
	}

	/* round to number of lines to scroll */
	scroll_steps[0] = (int)st->scroll_accum[0];
	scroll_steps[1] = (int)st->scroll_accum[1];

	st->scroll_accum[0] -= scroll_steps[0];
	st->scroll_accum[1] -= scroll_steps[1];

	/* perform vertical and/or horizontal scroll */
	if (scroll_steps[0] || scroll_steps[1]) {
		txt_screen_skip(st, ar, scroll_steps[1]);

		if (st->wordwrap) {
			st->left = 0;
		}
		else {
			st->left += scroll_steps[0];
			if (st->left < 0) st->left = 0;
		}

		ED_area_tag_redraw(CTX_wm_area(C));
	}

	tsc->old[0] = mval[0];
	tsc->old[1] = mval[1];
}

static void scroll_exit(bContext *C, wmOperator *op)
{
	SpaceText *st = CTX_wm_space_text(C);

	st->flags &= ~ST_SCROLL_SELECT;
	MEM_freeN(op->customdata);
}

static int text_scroll_modal(bContext *C, wmOperator *op, const wmEvent *event)
{
	TextScroll *tsc = op->customdata;
	SpaceText *st = CTX_wm_space_text(C);
	ARegion *ar = CTX_wm_region(C);

	switch (event->type) {
		case MOUSEMOVE:
			if (tsc->zone == SCROLLHANDLE_BAR)
				text_scroll_apply(C, op, event);
			break;
		case LEFTMOUSE:
		case RIGHTMOUSE:
		case MIDDLEMOUSE:
			if (ELEM(tsc->zone, SCROLLHANDLE_MIN_OUTSIDE, SCROLLHANDLE_MAX_OUTSIDE)) {
				txt_screen_skip(st, ar, st->viewlines * (tsc->zone == SCROLLHANDLE_MIN_OUTSIDE ? 1 : -1));

				ED_area_tag_redraw(CTX_wm_area(C));
			}
			scroll_exit(C, op);
			return OPERATOR_FINISHED;
	}

	return OPERATOR_RUNNING_MODAL;
}

static void text_scroll_cancel(bContext *C, wmOperator *op)
{
	scroll_exit(C, op);
}

static int text_scroll_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
	SpaceText *st = CTX_wm_space_text(C);
	TextScroll *tsc;
	
	if (RNA_struct_property_is_set(op->ptr, "lines"))
		return text_scroll_exec(C, op);
	
	tsc = MEM_callocN(sizeof(TextScroll), "TextScroll");
	tsc->first = 1;
	tsc->zone = SCROLLHANDLE_BAR;
	op->customdata = tsc;
	
	st->flags |= ST_SCROLL_SELECT;
	
	if (event->type == MOUSEPAN) {
		text_update_character_width(st);
		
		tsc->old[0] = event->x;
		tsc->old[1] = event->y;
		/* Sensitivity of scroll set to 4pix per line/char */
		tsc->delta[0] = (event->x - event->prevx) * st->cwidth / 4;
		tsc->delta[1] = (event->y - event->prevy) * st->lheight_dpi / 4;
		tsc->first = 0;
		tsc->scrollbar = 0;
		text_scroll_apply(C, op, event);
		scroll_exit(C, op);
		return OPERATOR_FINISHED;
	}

	WM_event_add_modal_handler(C, op);
	
	return OPERATOR_RUNNING_MODAL;
}

void TEXT_OT_scroll(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Scroll";
	/* don't really see the difference between this and
	 * scroll_bar. Both do basically the same thing (aside 
	 * from keymaps).*/
	ot->idname = "TEXT_OT_scroll";
	ot->description = "";
	
	/* api callbacks */
	ot->exec = text_scroll_exec;
	ot->invoke = text_scroll_invoke;
	ot->modal = text_scroll_modal;
	ot->cancel = text_scroll_cancel;
	ot->poll = text_scroll_poll;

	/* flags */
	ot->flag = OPTYPE_BLOCKING | OPTYPE_GRAB_POINTER | OPTYPE_INTERNAL;

	/* properties */
	RNA_def_int(ot->srna, "lines", 1, INT_MIN, INT_MAX, "Lines", "Number of lines to scroll", -100, 100);
}

/******************** scroll bar operator *******************/

static int text_region_scroll_poll(bContext *C)
{
	/* same as text_region_edit_poll except it works on libdata too */
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	ARegion *ar = CTX_wm_region(C);

	if (!st || !text)
		return 0;
	
	if (!ar || ar->regiontype != RGN_TYPE_WINDOW)
		return 0;
	
	return 1;
}

static int text_scroll_bar_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
	SpaceText *st = CTX_wm_space_text(C);
	ARegion *ar = CTX_wm_region(C);
	TextScroll *tsc;
	const int *mval = event->mval;
	int zone = -1;

	if (RNA_struct_property_is_set(op->ptr, "lines"))
		return text_scroll_exec(C, op);
	
	/* verify we are in the right zone */
	if (mval[0] > st->txtbar.xmin && mval[0] < st->txtbar.xmax) {
		if (mval[1] >= st->txtbar.ymin && mval[1] <= st->txtbar.ymax) {
			/* mouse inside scroll handle */
			zone = SCROLLHANDLE_BAR;
		}
		else if (mval[1] > TXT_SCROLL_SPACE && mval[1] < ar->winy - TXT_SCROLL_SPACE) {
			if (mval[1] < st->txtbar.ymin) zone = SCROLLHANDLE_MIN_OUTSIDE;
			else zone = SCROLLHANDLE_MAX_OUTSIDE;
		}
	}

	if (zone == -1) {
		/* we are outside slider - nothing to do */
		return OPERATOR_PASS_THROUGH;
	}

	tsc = MEM_callocN(sizeof(TextScroll), "TextScroll");
	tsc->first = 1;
	tsc->scrollbar = 1;
	tsc->zone = zone;
	op->customdata = tsc;
	st->flags |= ST_SCROLL_SELECT;

	/* jump scroll, works in v2d but needs to be added here too :S */
	if (event->type == MIDDLEMOUSE) {
		tsc->old[0] = ar->winrct.xmin + BLI_rcti_cent_x(&st->txtbar);
		tsc->old[1] = ar->winrct.ymin + BLI_rcti_cent_y(&st->txtbar);

		tsc->first = 0;
		tsc->zone = SCROLLHANDLE_BAR;
		text_scroll_apply(C, op, event);
	}

	WM_event_add_modal_handler(C, op);

	return OPERATOR_RUNNING_MODAL;
}

void TEXT_OT_scroll_bar(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Scrollbar";
	/* don't really see the difference between this and
	 * scroll. Both do basically the same thing (aside 
	 * from keymaps).*/
	ot->idname = "TEXT_OT_scroll_bar";
	ot->description = "";
	
	/* api callbacks */
	ot->invoke = text_scroll_bar_invoke;
	ot->modal = text_scroll_modal;
	ot->cancel = text_scroll_cancel;
	ot->poll = text_region_scroll_poll;

	/* flags */
	ot->flag = OPTYPE_BLOCKING | OPTYPE_INTERNAL;

	/* properties */
	RNA_def_int(ot->srna, "lines", 1, INT_MIN, INT_MAX, "Lines", "Number of lines to scroll", -100, 100);
}

/******************* set selection operator **********************/

typedef struct SetSelection {
	int selecting;
	int selc, sell;
	short old[2];
	wmTimer *timer;  /* needed for scrolling when mouse at region bounds */
} SetSelection;

static int flatten_width(SpaceText *st, const char *str)
{
	int i, total = 0;

	for (i = 0; str[i]; i += BLI_str_utf8_size_safe(str + i)) {
		if (str[i] == '\t') {
			total += st->tabnumber - total % st->tabnumber;
		}
		else {
			total += BLI_str_utf8_char_width_safe(str + i);
		}
	}
	
	return total;
}

static int flatten_column_to_offset(SpaceText *st, const char *str, int index)
{
	int i = 0, j = 0, col;

	while (*(str + j)) {
		if (str[j] == '\t')
			col = st->tabnumber - i % st->tabnumber;
		else
			col = BLI_str_utf8_char_width_safe(str + j);
		
		if (i + col > index)
			break;
		
		i += col;
		j += BLI_str_utf8_size_safe(str + j);
	}
	
	return j;
}

static TextLine *get_first_visible_line(SpaceText *st, ARegion *ar, int *y)
{
	TextLine *linep = st->text->lines.first;
	int i;
	for (i = st->top; i > 0 && linep; ) {
		int lines = text_get_visible_lines(st, ar, linep->line);
		
		if (i - lines < 0) {
			*y += i;
			break;
		}
		else {
			linep = linep->next;
			i -= lines;
		}
	}
	return linep;
}

static void text_cursor_set_to_pos_wrapped(SpaceText *st, ARegion *ar, int x, int y, const bool sel)
{
	Text *text = st->text;
	int max = wrap_width(st, ar); /* column */
	int charp = -1;               /* mem */
	int loop = 1, found = 0;      /* flags */
	char ch;
	
	/* Point to first visible line */
	TextLine *linep = get_first_visible_line(st, ar, &y);
	
	while (loop && linep) {
		int i = 0, start = 0, end = max; /* column */
		int j = 0, curs = 0, endj = 0;   /* mem */
		int chop = 1;                    /* flags */
		
		for (; loop; j += BLI_str_utf8_size_safe(linep->line + j)) {
			int chars;
			int columns = BLI_str_utf8_char_width_safe(linep->line + j); /* = 1 for tab */
			
			/* Mimic replacement of tabs */
			ch = linep->line[j];
			if (ch == '\t') {
				chars = st->tabnumber - i % st->tabnumber;
				ch = ' ';
			}
			else {
				chars = 1;
			}
			
			while (chars--) {
				/* Gone too far, go back to last wrap point */
				if (y < 0) {
					charp = endj;
					loop = 0;
					break;
					/* Exactly at the cursor */
				}
				else if (y == 0 && i - start <= x && i + columns - start > x) {
					/* current position could be wrapped to next line */
					/* this should be checked when end of current line would be reached */
					charp = curs = j;
					found = 1;
					/* Prepare curs for next wrap */
				}
				else if (i - end <= x && i + columns - end > x) {
					curs = j;
				}
				if (i + columns - start > max) {
					end = MIN2(end, i);
					
					if (found) {
						/* exact cursor position was found, check if it's */
						/* still on needed line (hasn't been wrapped) */
						if (charp > endj && !chop && ch != '\0') charp = endj;
						loop = 0;
						break;
					}
					
					if (chop) endj = j;
					start = end;
					end += max;
					
					if (j < linep->len)
						y--;
					
					chop = 1;
					if (y == 0 && i + columns - start > x) {
						charp = curs;
						loop = 0;
						break;
					}
				}
				else if (ch == ' ' || ch == '-' || ch == '\0') {
					if (found) {
						loop = 0;
						break;
					}
					
					if (y == 0 && i + columns - start > x) {
						charp = curs;
						loop = 0;
						break;
					}
					end = i + 1;
					endj = j;
					chop = 0;
				}
				i += columns;
			}
			
			if (ch == '\0') break;
		}
		
		if (!loop || found) break;
		
		if (!linep->next) {
			charp = linep->len;
			break;
		}
		
		/* On correct line but didn't meet cursor, must be at end */
		if (y == 0) {
			charp = linep->len;
			break;
		}
		linep = linep->next;
		
		y--;
	}

	if (linep && charp != -1) {
		if (sel) { text->sell = linep; text->selc = charp; }
		else     { text->curl = linep; text->curc = charp; }
	}
}

static void text_cursor_set_to_pos(SpaceText *st, ARegion *ar, int x, int y, const bool sel)
{
	Text *text = st->text;
	text_update_character_width(st);
	y = (ar->winy - 2 - y) / (st->lheight_dpi + TXT_LINE_SPACING);

	if (st->showlinenrs) x -= TXT_OFFSET + TEXTXLOC;
	else x -= TXT_OFFSET;

	if (x < 0) x = 0;
	x = text_pixel_x_to_column(st, x) + st->left;
	
	if (st->wordwrap) {
		text_cursor_set_to_pos_wrapped(st, ar, x, y, sel);
	}
	else {
		TextLine **linep;
		int *charp;
		int w;
		
		if (sel) { linep = &text->sell; charp = &text->selc; }
		else     { linep = &text->curl; charp = &text->curc; }
		
		y -= txt_get_span(text->lines.first, *linep) - st->top;
		
		if (y > 0) {
			while (y-- != 0) if ((*linep)->next) *linep = (*linep)->next;
		}
		else if (y < 0) {
			while (y++ != 0) if ((*linep)->prev) *linep = (*linep)->prev;
		}

		
		w = flatten_width(st, (*linep)->line);
		if (x < w) *charp = flatten_column_to_offset(st, (*linep)->line, x);
		else *charp = (*linep)->len;
	}
	if (!sel) txt_pop_sel(text);
}

static void text_cursor_timer_ensure(bContext *C, SetSelection *ssel)
{
	if (ssel->timer == NULL) {
		wmWindowManager *wm = CTX_wm_manager(C);
		wmWindow *win = CTX_wm_window(C);

		ssel->timer = WM_event_add_timer(wm, win, TIMER, 0.02f);
	}
}

static void text_cursor_timer_remove(bContext *C, SetSelection *ssel)
{
	if (ssel->timer) {
		wmWindowManager *wm = CTX_wm_manager(C);
		wmWindow *win = CTX_wm_window(C);

		WM_event_remove_timer(wm, win, ssel->timer);
	}
	ssel->timer = NULL;
}



static void text_cursor_set_apply(bContext *C, wmOperator *op, const wmEvent *event)
{
	SpaceText *st = CTX_wm_space_text(C);
	ARegion *ar = CTX_wm_region(C);
	SetSelection *ssel = op->customdata;

	if (event->mval[1] < 0 || event->mval[1] > ar->winy) {
		text_cursor_timer_ensure(C, ssel);

		if (event->type == TIMER) {
			text_cursor_set_to_pos(st, ar, event->mval[0], event->mval[1], 1);
			text_scroll_to_cursor(st, ar, false);
			WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);
		}
	}
	else if (!st->wordwrap && (event->mval[0] < 0 || event->mval[0] > ar->winx)) {
		text_cursor_timer_ensure(C, ssel);
		
		if (event->type == TIMER) {
			text_cursor_set_to_pos(st, ar, CLAMPIS(event->mval[0], 0, ar->winx), event->mval[1], 1);
			text_scroll_to_cursor(st, ar, false);
			WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);
		}
	}
	else {
		text_cursor_timer_remove(C, ssel);

		if (event->type != TIMER) {
			text_cursor_set_to_pos(st, ar, event->mval[0], event->mval[1], 1);
			text_scroll_to_cursor(st, ar, false);
			WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);

			ssel->old[0] = event->mval[0];
			ssel->old[1] = event->mval[1];
		}
	}
}

static void text_cursor_set_exit(bContext *C, wmOperator *op)
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = st->text;
	SetSelection *ssel = op->customdata;
	char *buffer;

	if (txt_has_sel(text)) {
		buffer = txt_sel_to_buf(text);
		WM_clipboard_text_set(buffer, 1);
		MEM_freeN(buffer);
	}

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);

	text_cursor_timer_remove(C, ssel);
	MEM_freeN(ssel);
}

static int text_set_selection_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
	SpaceText *st = CTX_wm_space_text(C);
	SetSelection *ssel;

	if (event->mval[0] >= st->txtbar.xmin)
		return OPERATOR_PASS_THROUGH;

	op->customdata = MEM_callocN(sizeof(SetSelection), "SetCursor");
	ssel = op->customdata;
	ssel->selecting = RNA_boolean_get(op->ptr, "select");

	ssel->old[0] = event->mval[0];
	ssel->old[1] = event->mval[1];

	ssel->sell = txt_get_span(st->text->lines.first, st->text->sell);
	ssel->selc = st->text->selc;

	WM_event_add_modal_handler(C, op);

	text_cursor_set_apply(C, op, event);

	return OPERATOR_RUNNING_MODAL;
}

static int text_set_selection_modal(bContext *C, wmOperator *op, const wmEvent *event)
{
	switch (event->type) {
		case LEFTMOUSE:
		case MIDDLEMOUSE:
		case RIGHTMOUSE:
			text_cursor_set_exit(C, op);
			return OPERATOR_FINISHED;
		case TIMER:
		case MOUSEMOVE:
			text_cursor_set_apply(C, op, event);
			break;
	}

	return OPERATOR_RUNNING_MODAL;
}

static void text_set_selection_cancel(bContext *C, wmOperator *op)
{
	text_cursor_set_exit(C, op);
}

void TEXT_OT_selection_set(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Set Selection";
	ot->idname = "TEXT_OT_selection_set";
	ot->description = "Set cursor selection";

	/* api callbacks */
	ot->invoke = text_set_selection_invoke;
	ot->modal = text_set_selection_modal;
	ot->cancel = text_set_selection_cancel;
	ot->poll = text_region_edit_poll;

	/* properties */
	RNA_def_boolean(ot->srna, "select", 0, "Select", "Set selection end rather than cursor");
}

/******************* set cursor operator **********************/

static int text_cursor_set_exec(bContext *C, wmOperator *op)
{
	SpaceText *st = CTX_wm_space_text(C);
	ARegion *ar = CTX_wm_region(C);
	int x = RNA_int_get(op->ptr, "x");
	int y = RNA_int_get(op->ptr, "y");

	text_cursor_set_to_pos(st, ar, x, y, 0);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, st->text);

	return OPERATOR_PASS_THROUGH;
}

static int text_cursor_set_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
	SpaceText *st = CTX_wm_space_text(C);

	if (event->mval[0] >= st->txtbar.xmin)
		return OPERATOR_PASS_THROUGH;

	RNA_int_set(op->ptr, "x", event->mval[0]);
	RNA_int_set(op->ptr, "y", event->mval[1]);

	return text_cursor_set_exec(C, op);
}

void TEXT_OT_cursor_set(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Set Cursor";
	ot->idname = "TEXT_OT_cursor_set";
	ot->description = "Set cursor position";

	/* api callbacks */
	ot->invoke = text_cursor_set_invoke;
	ot->exec = text_cursor_set_exec;
	ot->poll = text_region_edit_poll;

	/* properties */
	RNA_def_int(ot->srna, "x", 0, INT_MIN, INT_MAX, "X", "", INT_MIN, INT_MAX);
	RNA_def_int(ot->srna, "y", 0, INT_MIN, INT_MAX, "Y", "", INT_MIN, INT_MAX);
}

/******************* line number operator **********************/

static int text_line_number_invoke(bContext *C, wmOperator *UNUSED(op), const wmEvent *event)
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	ARegion *ar = CTX_wm_region(C);
	const int *mval = event->mval;
	double time;
	static int jump_to = 0;
	static double last_jump = 0;

	text_update_character_width(st);

	if (!st->showlinenrs)
		return OPERATOR_PASS_THROUGH;

	if (!(mval[0] > 2 && mval[0] < (TXT_OFFSET + TEXTXLOC) && mval[1] > 2 && mval[1] < ar->winy - 2))
		return OPERATOR_PASS_THROUGH;

	if (!(event->ascii >= '0' && event->ascii <= '9'))
		return OPERATOR_PASS_THROUGH;

	time = PIL_check_seconds_timer();
	if (last_jump < time - 1)
		jump_to = 0;

	jump_to *= 10;
	jump_to += (int)(event->ascii - '0');

	txt_move_toline(text, jump_to - 1, 0);
	last_jump = time;

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);

	return OPERATOR_FINISHED;
}

void TEXT_OT_line_number(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Line Number";
	ot->idname = "TEXT_OT_line_number";
	ot->description = "The current line number";
	
	/* api callbacks */
	ot->invoke = text_line_number_invoke;
	ot->poll = text_region_edit_poll;
}

/******************* insert operator **********************/

static int text_insert_exec(bContext *C, wmOperator *op)
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	char *str;
	bool done = false;
	size_t i = 0;
	unsigned int code;

	text_drawcache_tag_update(st, 0);

	str = RNA_string_get_alloc(op->ptr, "text", NULL, 0);

	if (st && st->overwrite) {
		while (str[i]) {
			code = BLI_str_utf8_as_unicode_step(str, &i);
			done |= txt_replace_char(text, code);
		}
	}
	else {
		while (str[i]) {
			code = BLI_str_utf8_as_unicode_step(str, &i);
			done |= txt_add_char(text, code);
		}
	}

	MEM_freeN(str);
	
	if (!done)
		return OPERATOR_CANCELLED;

	text_update_line_edited(text->curl);

	text_update_cursor_moved(C);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);

	return OPERATOR_FINISHED;
}

static int text_insert_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
	int ret;

	// if (!RNA_struct_property_is_set(op->ptr, "text")) { /* always set from keymap XXX */
	if (!RNA_string_length(op->ptr, "text")) {
		/* if alt/ctrl/super are pressed pass through except for utf8 character event
		 * (when input method are used for utf8 inputs, the user may assign key event
		 * including alt/ctrl/super like ctrl+m to commit utf8 string.  in such case,
		 * the modifiers in the utf8 character event make no sense.) */
		if ((event->ctrl || event->oskey) && !event->utf8_buf[0]) {
			return OPERATOR_PASS_THROUGH;
		}
		else {
			char str[BLI_UTF8_MAX + 1];
			size_t len;
			
			if (event->utf8_buf[0]) {
				len = BLI_str_utf8_size_safe(event->utf8_buf);
				memcpy(str, event->utf8_buf, len);
			}
			else {
				/* in theory, ghost can set value to extended ascii here */
				len = BLI_str_utf8_from_unicode(event->ascii, str);
			}
			str[len] = '\0';
			RNA_string_set(op->ptr, "text", str);
		}
	}

	ret = text_insert_exec(C, op);
	
	/* run the script while editing, evil but useful */
	if (ret == OPERATOR_FINISHED && CTX_wm_space_text(C)->live_edit)
		text_run_script(C, NULL);

	return ret;
}

void TEXT_OT_insert(wmOperatorType *ot)
{
	PropertyRNA *prop;

	/* identifiers */
	ot->name = "Insert";
	ot->idname = "TEXT_OT_insert";
	ot->description = "Insert text at cursor position";
	
	/* api callbacks */
	ot->exec = text_insert_exec;
	ot->invoke = text_insert_invoke;
	ot->poll = text_edit_poll;

	/* properties */
	prop = RNA_def_string(ot->srna, "text", NULL, 0, "Text", "Text to insert at the cursor position");
	RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}

/******************* find operator *********************/

/* mode */
#define TEXT_FIND       0
#define TEXT_REPLACE    1

static int text_find_and_replace(bContext *C, wmOperator *op, short mode)
{
	Main *bmain = CTX_data_main(C);
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = st->text;
	int flags;
	int found = 0;
	char *tmp;

	if (!st->findstr[0])
		return OPERATOR_CANCELLED;

	flags = st->flags;
	if (flags & ST_FIND_ALL)
		flags &= ~ST_FIND_WRAP;

	/* Replace current */
	if (mode != TEXT_FIND && txt_has_sel(text)) {
		tmp = txt_sel_to_buf(text);

		if (flags & ST_MATCH_CASE) found = strcmp(st->findstr, tmp) == 0;
		else found = BLI_strcasecmp(st->findstr, tmp) == 0;

		if (found) {
			if (mode == TEXT_REPLACE) {
				txt_insert_buf(text, st->replacestr);
				if (text->curl && text->curl->format) {
					MEM_freeN(text->curl->format);
					text->curl->format = NULL;
				}
				text_update_cursor_moved(C);
				WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
				text_drawcache_tag_update(CTX_wm_space_text(C), 1);
			}
		}
		MEM_freeN(tmp);
		tmp = NULL;
	}

	/* Find next */
	if (txt_find_string(text, st->findstr, flags & ST_FIND_WRAP, flags & ST_MATCH_CASE)) {
		text_update_cursor_moved(C);
		WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);
	}
	else if (flags & ST_FIND_ALL) {
		if (text->id.next)
			text = st->text = text->id.next;
		else
			text = st->text = bmain->text.first;
		txt_move_toline(text, 0, 0);
		text_update_cursor_moved(C);
		WM_event_add_notifier(C, NC_TEXT | ND_CURSOR, text);
	}
	else {
		if (!found) BKE_reportf(op->reports, RPT_ERROR, "Text not found: %s", st->findstr);
	}

	return OPERATOR_FINISHED;
}

static int text_find_exec(bContext *C, wmOperator *op)
{
	return text_find_and_replace(C, op, TEXT_FIND);
}

void TEXT_OT_find(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Find Next";
	ot->idname = "TEXT_OT_find";
	ot->description = "Find specified text";
	
	/* api callbacks */
	ot->exec = text_find_exec;
	ot->poll = text_space_edit_poll;
}

/******************* replace operator *********************/

static int text_replace_exec(bContext *C, wmOperator *op)
{
	return text_find_and_replace(C, op, TEXT_REPLACE);
}

void TEXT_OT_replace(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Replace";
	ot->idname = "TEXT_OT_replace";
	ot->description = "Replace text with the specified text";

	/* api callbacks */
	ot->exec = text_replace_exec;
	ot->poll = text_space_edit_poll;
}

/******************* find set selected *********************/

static int text_find_set_selected_exec(bContext *C, wmOperator *op)
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	char *tmp;

	tmp = txt_sel_to_buf(text);
	BLI_strncpy(st->findstr, tmp, ST_MAX_FIND_STR);
	MEM_freeN(tmp);

	if (!st->findstr[0])
		return OPERATOR_FINISHED;

	return text_find_and_replace(C, op, TEXT_FIND);
}

void TEXT_OT_find_set_selected(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Find Set Selected";
	ot->idname = "TEXT_OT_find_set_selected";
	ot->description = "Find specified text and set as selected";
	
	/* api callbacks */
	ot->exec = text_find_set_selected_exec;
	ot->poll = text_space_edit_poll;
}

/******************* replace set selected *********************/

static int text_replace_set_selected_exec(bContext *C, wmOperator *UNUSED(op))
{
	SpaceText *st = CTX_wm_space_text(C);
	Text *text = CTX_data_edit_text(C);
	char *tmp;

	tmp = txt_sel_to_buf(text);
	BLI_strncpy(st->replacestr, tmp, ST_MAX_FIND_STR);
	MEM_freeN(tmp);

	return OPERATOR_FINISHED;
}

void TEXT_OT_replace_set_selected(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Replace Set Selected";
	ot->idname = "TEXT_OT_replace_set_selected";
	ot->description = "Replace text with specified text and set as selected";
	
	/* api callbacks */
	ot->exec = text_replace_set_selected_exec;
	ot->poll = text_space_edit_poll;
}

/****************** resolve conflict operator ******************/

enum { RESOLVE_IGNORE, RESOLVE_RELOAD, RESOLVE_SAVE, RESOLVE_MAKE_INTERNAL };
static EnumPropertyItem resolution_items[] = {
	{RESOLVE_IGNORE, "IGNORE", 0, "Ignore", ""},
	{RESOLVE_RELOAD, "RELOAD", 0, "Reload", ""},
	{RESOLVE_SAVE, "SAVE", 0, "Save", ""},
	{RESOLVE_MAKE_INTERNAL, "MAKE_INTERNAL", 0, "Make Internal", ""},
	{0, NULL, 0, NULL, NULL}
};

static int text_resolve_conflict_exec(bContext *C, wmOperator *op)
{
	Text *text = CTX_data_edit_text(C);
	int resolution = RNA_enum_get(op->ptr, "resolution");

	switch (resolution) {
		case RESOLVE_RELOAD:
			return text_reload_exec(C, op);
		case RESOLVE_SAVE:
			return text_save_exec(C, op);
		case RESOLVE_MAKE_INTERNAL:
			return text_make_internal_exec(C, op);
		case RESOLVE_IGNORE:
			BKE_text_file_modified_ignore(text);
			return OPERATOR_FINISHED;
	}

	return OPERATOR_CANCELLED;
}

static int text_resolve_conflict_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event))
{
	Text *text = CTX_data_edit_text(C);
	uiPopupMenu *pup;
	uiLayout *layout;

	switch (BKE_text_file_modified_check(text)) {
		case 1:
			if (text->flags & TXT_ISDIRTY) {
				/* modified locally and externally, ahhh. offer more possibilites. */
				pup = UI_popup_menu_begin(C, IFACE_("File Modified Outside and Inside Blender"), ICON_NONE);
				layout = UI_popup_menu_layout(pup);
				uiItemEnumO_ptr(layout, op->type, IFACE_("Reload from disk (ignore local changes)"),
				                0, "resolution", RESOLVE_RELOAD);
				uiItemEnumO_ptr(layout, op->type, IFACE_("Save to disk (ignore outside changes)"),
				                0, "resolution", RESOLVE_SAVE);
				uiItemEnumO_ptr(layout, op->type, IFACE_("Make text internal (separate copy)"),
				                0, "resolution", RESOLVE_MAKE_INTERNAL);
				UI_popup_menu_end(C, pup);
			}
			else {
				pup = UI_popup_menu_begin(C, IFACE_("File Modified Outside Blender"), ICON_NONE);
				layout = UI_popup_menu_layout(pup);
				uiItemEnumO_ptr(layout, op->type, IFACE_("Reload from disk"), 0, "resolution", RESOLVE_RELOAD);
				uiItemEnumO_ptr(layout, op->type, IFACE_("Make text internal (separate copy)"),
				                0, "resolution", RESOLVE_MAKE_INTERNAL);
				uiItemEnumO_ptr(layout, op->type, IFACE_("Ignore"), 0, "resolution", RESOLVE_IGNORE);
				UI_popup_menu_end(C, pup);
			}
			break;
		case 2:
			pup = UI_popup_menu_begin(C, IFACE_("File Deleted Outside Blender"), ICON_NONE);
			layout = UI_popup_menu_layout(pup);
			uiItemEnumO_ptr(layout, op->type, IFACE_("Make text internal"), 0, "resolution", RESOLVE_MAKE_INTERNAL);
			uiItemEnumO_ptr(layout, op->type, IFACE_("Recreate file"), 0, "resolution", RESOLVE_SAVE);
			UI_popup_menu_end(C, pup);
			break;
	}

	return OPERATOR_INTERFACE;
}

void TEXT_OT_resolve_conflict(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Resolve Conflict";
	ot->idname = "TEXT_OT_resolve_conflict";
	ot->description = "When external text is out of sync, resolve the conflict";

	/* api callbacks */
	ot->exec = text_resolve_conflict_exec;
	ot->invoke = text_resolve_conflict_invoke;
	ot->poll = text_save_poll;

	/* properties */
	RNA_def_enum(ot->srna, "resolution", resolution_items, RESOLVE_IGNORE, "Resolution", "How to solve conflict due to differences in internal and external text");
}

/********************** to 3d object operator *****************/

static int text_to_3d_object_exec(bContext *C, wmOperator *op)
{
	Text *text = CTX_data_edit_text(C);
	const bool split_lines = RNA_boolean_get(op->ptr, "split_lines");

	ED_text_to_object(C, text, split_lines);

	return OPERATOR_FINISHED;
}

void TEXT_OT_to_3d_object(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "To 3D Object";
	ot->idname = "TEXT_OT_to_3d_object";
	ot->description = "Create 3D text object from active text data block";
	
	/* api callbacks */
	ot->exec = text_to_3d_object_exec;
	ot->poll = text_edit_poll;
	
	/* flags */
	ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

	/* properties */
	RNA_def_boolean(ot->srna, "split_lines", 0, "Split Lines", "Create one object per line in the text");
}


/************************ undo ******************************/

void ED_text_undo_step(bContext *C, int step)
{
	Text *text = CTX_data_edit_text(C);

	if (!text)
		return;

	if (step == 1)
		txt_do_undo(text);
	else if (step == -1)
		txt_do_redo(text);

	text_update_edited(text);

	text_update_cursor_moved(C);
	text_drawcache_tag_update(CTX_wm_space_text(C), 1);
	WM_event_add_notifier(C, NC_TEXT | NA_EDITED, text);
}

