#include "CodeEditor.h"

namespace Upp {

#define LLOG(x)    // DLOG(x)
#define LTIMING(x) // RTIMING(x)

#define IMAGEVECTOR Vector
#define IMAGECLASS  CodeEditorImg
#define IMAGEFILE   <CodeEditor/CodeEditor.iml>
#include <Draw/iml_source.h>

#define  TFILE <CodeEditor/CodeEditor.t>
#include <Core/t.h>

void RegisterSyntaxModules();

INITBLOCK {
	RegisterSyntaxModules();
}

One<EditorSyntax> CodeEditor::GetSyntax(int line)
{
	LTIMING("GetSyntax");
	One<EditorSyntax> syntax = EditorSyntax::Create(highlight);
	syntax->SpellCheckComments(spellcheck_comments);
	int ln = 0;
	for(int i = 0; i < __countof(syntax_cache); i++)
		if(line >= syntax_cache[i].line && syntax_cache[i].line > 0) {
			syntax->Set(syntax_cache[i].data);
			ln = syntax_cache[i].line;
			LLOG("Found " << line << " " << syntax_cache[i].line);
			break;
		}
	line = min(line, GetLineCount());
	if(line - ln > 10000) { // optimization hack for very huge files
		syntax = EditorSyntax::Create(highlight);
		ln = line - 10000; // we just pray it gets synced
	}
	while(ln < line) {
		WString l = GetWLine(ln);
		CTIMING("ScanSyntax3");
		syntax->ScanSyntax(l, l.End(), ln, GetTabSize());
		ln++;
		static int d[] = { 0, 100, 1000, 3000, 7000 };
		for(int i = 0; i < __countof(d); i++)
			if(ln == cline - d[i]) {
				syntax_cache[i + 1].data = syntax->Get();
				syntax_cache[i + 1].line = ln;
			}
	}
	syntax_cache[0].data = syntax->Get();
	syntax_cache[0].line = ln;
	return pick(syntax);
}

void CodeEditor::Highlight(const String& h)
{
	highlight = h;
	SetColor(LineEdit::INK_NORMAL, hl_style[HighlightSetup::INK_NORMAL].color);
	SetColor(LineEdit::INK_DISABLED, hl_style[HighlightSetup::INK_DISABLED].color);
	SetColor(LineEdit::INK_SELECTED, hl_style[HighlightSetup::INK_SELECTED].color);
	SetColor(LineEdit::PAPER_NORMAL, hl_style[HighlightSetup::PAPER_NORMAL].color);
	SetColor(LineEdit::PAPER_READONLY, hl_style[HighlightSetup::PAPER_READONLY].color);
	SetColor(LineEdit::PAPER_SELECTED, hl_style[HighlightSetup::PAPER_SELECTED].color);
	SetColor(LineEdit::WHITESPACE, hl_style[HighlightSetup::WHITESPACE].color);
	SetColor(LineEdit::WARN_WHITESPACE, hl_style[HighlightSetup::WARN_WHITESPACE].color);
	Refresh();
	EditorBarLayout();
}

void CodeEditor::HighlightLine(int line, Vector<LineEdit::Highlight>& hl, int64 pos)
{
	CTIMING("HighlightLine");
	HighlightOutput hls(hl);
	WString l = GetWLine(line);
	GetSyntax(line)->Highlight(l.Begin(), l.End(), hls, this, line, pos);
	if(illuminated.GetCount()) {
		int q = 0;
		while(q < l.GetCount() && q < hl.GetCount()) {
			q = l.Find(illuminated, q);
			if(q < 0)
				break;
			int n = illuminated.GetCount();
			if(n > 1 || !iscid(illuminated[0]) ||
			   (q == 0 || !iscid(l[q - 1])) && (q + n >= l.GetCount() || !iscid(l[q + n])))
				while(n-- && q < hl.GetCount()) {
					const HlStyle& st = hl_style[PAPER_SELWORD];
					hl[q].paper = st.color;
					if(st.bold)
						hl[q].font.Bold();
					q++;
				}
			else
				q++;
		}
	}
	for(Point p : errors)
		if(p.y == line && p.x < hl.GetCount()) {
			hl[p.x].paper = hl_style[PAPER_ERROR_FILE].color;
			hl[p.x].flags |= LineEdit::NOENDFILL;
		}
}

void CodeEditor::Paint(Draw& w)
{
	LineEdit::Paint(w);

	if(!blk0_header)
		return;

	int sline = GetColumnLine(GetCursor()).y - GetScrollPos().y;
	if(sline >= 0 && sline < 2)
		return;
	Point start, end; // paint zero level block header
	if(!GetSyntax(GetScrollPos().y + 1)->GetBlockHeader(start, end))
		return;
	if(IsNull(start) || (!IsNull(end) && end.y > start.y))
		return;

	int line0 = -1;
	for(int i = IsNull(end) ? 0 : end.y + 1; i <= start.y; i++) {
		String l = GetUtf8Line(i);
		bool hdr = false;
		bool empty = true;
		try {
			CParser p(l);
			while(!p.IsEof()) {
				if(p.Char('(') || p.Id("class") || p.Id("struct") || p.Id("enum")) {
					hdr = true;
					break;
				}
				p.Skip();
			}
		}
		catch(CParser::Error) {}
		if(hdr) {
			line0 = i;
			break;
		}
		if(!empty && line0 < 0)
			line0 = i;
	}
	
	if(line0 < 0 || GetScrollPos().y <= line0)
		return;

	int l = GetPos(line0, 0);
	int h = GetPos(start.y, start.x) - 1;
	if(l < 0 || l >= h)
		return;

	Size sz = GetSize();

	bool dark = IsDark(HighlightSetup::GetHlStyle(HighlightSetup::PAPER_NORMAL).color);
	Color paper = GrayColor(dark ? 40 : 245);

	w.DrawRect(0, 0, GetSize().cx, GetFontSize().cy, paper);
	
	Size hsz = CodeEditorImg::HdrSep0().GetSize();
	for(int x = 0; x < sz.cx; x += hsz.cx) {
		w.DrawImage(x, GetFontSize().cy, CodeEditorImg::HdrSep0(), paper);
		w.DrawImage(x, GetFontSize().cy, CodeEditorImg::HdrSep(), dark ? White() : Black());
	}
	
	String ls = TrimBoth(Get(l, h - l));
	WString r;
	for(const char *s = ls; *s;) {
		if(IsSpace(*s)) {
			while(IsSpace(*s))
				s++;
			r.Cat(' ');
		}
		else
			r.Cat(*s++);
	}
	r << " {";
	
	Vector<LineEdit::Highlight> hln;
	hln.SetCount(r.GetCount() + 1);
	for(int i = 0; i < hln.GetCount(); i++) {
		LineEdit::Highlight& h = hln[i];
		h.paper = paper;
		h.ink = SColorText();
		h.chr = r[i];
		h.font = GetFont();
	}
	
	HighlightOutput hls(hln);
	GetSyntax(0)->Highlight(r.Begin(), r.End(), hls, this, 0, 0);

	int ii = 0;
	int x = 0;
	while(ii < hln.GetCount() - 1) {
		int n = 1;
		while(ii + n < hln.GetCount() - 1 && hln[ii + n].ink == hln[ii].ink && hln[ii + n].font == hln[ii].font)
			n++;
		w.DrawText(x, 0, ~r + ii, hln[ii].font, hln[ii].ink, n);
		x += GetTextSize(~r + ii, hln[ii].font, n).cx;
		ii += n;
	}
}

void CodeEditor::RefreshBlkHeader()
{
	if(blk0_header)
		Refresh(0, 0, GetSize().cx, GetFontSize().cy + CodeEditorImg::HdrSep().GetSize().cy);
}

void CodeEditor::NewScrollPos() {
	bar.Scroll();
	RefreshBlkHeader();
}

void CodeEditor::DirtyFrom(int line) {
	for(int i = 0; i < __countof(syntax_cache); i++)
		if(syntax_cache[i].line >= line)
			syntax_cache[i].Clear();

	if(check_edited)
		bar.Refresh();
}

inline bool IsComment(int a, int b) {
	return a == '/' && b == '*' || a == '*' && b == '/' || a == '/' && b == '/';
}

inline bool RBR(int c) {
	return isbrkt(c);
}

String CodeEditor::GetRefreshInfo(int pos)
{
	int ii = GetLine(pos) + 1;
	return ii < GetLineCount() ? GetSyntax(ii)->Get() : String();
}

void CodeEditor::CheckSyntaxRefresh(int64 pos, const WString& text)
{
	GetSyntax(GetLine(pos))->CheckSyntaxRefresh(*this, pos, text);
}

void CodeEditor::PreInsert(int pos, const WString& text)
{
	refresh_info = GetRefreshInfo(pos);
}

void CodeEditor::PostInsert(int pos, const WString& text) {
	if(check_edited)
		bar.SetEdited(GetLine(pos));
	if(!IsFullRefresh()) {
		if(text.GetCount() > 200 || GetRefreshInfo(pos) != refresh_info || text.Find('\n') >= 0)
			Refresh();
		else
		if(!console_mode)
			CheckSyntaxRefresh(pos, text);
	}
	WhenUpdate();
	EditorBarLayout();
}

void CodeEditor::PreRemove(int pos, int size) {
	if(IsFullRefresh()) return;
	if(size > 200)
		Refresh();
	else {
		WString text = GetW(pos, size);
		if(text.Find('\n') >= 0)
			Refresh();
		else {
			CheckSyntaxRefresh(pos, text);
			refresh_info = GetRefreshInfo(pos);
		}
	}
	sbi.Refresh();
}

void CodeEditor::PostRemove(int pos, int size) {
	if(check_edited)
		bar.SetEdited(GetLine(pos));
	WhenUpdate();
	EditorBarLayout();
	if(GetRefreshInfo(pos) != refresh_info)
		Refresh();
	sbi.Refresh();
}

void CodeEditor::ClearLines() {
	bar.ClearLines();
	sbi.Refresh();
}

void CodeEditor::InsertLines(int line, int count) {
	if(IsView())
		return;
	bar.InsertLines(line, count);
	if(line <= line2.GetCount())
		line2.Insert(line, GetLine2(line), count);
	EditorBarLayout();
}

void CodeEditor::RemoveLines(int line, int count) {
	bar.RemoveLines(line, count);
	if(line + count <= line2.GetCount())
		line2.Remove(line, count);
	EditorBarLayout();
}

void CodeEditor::Renumber2()
{
	if(IsView())
		return;
	line2.SetCount(GetLineCount());
	for(int i = 0; i < GetLineCount(); i++)
		line2[i] = i;
}

int CodeEditor::GetLine2(int i) const
{
	return line2.GetCount() ? line2[min(line2.GetCount() - 1, i)] : 0;
}

String CodeEditor::GetPasteText()
{
	String h;
	WhenPaste(h);
	return h;
}

bool CodeEditor::IsCursorBracket(int64 pos) const
{
	return pos == highlight_bracket_pos0 && hilite_bracket;
}

bool CodeEditor::IsMatchingBracket(int64 pos) const
{
	return pos == highlight_bracket_pos && (hilite_bracket == 1 || hilite_bracket == 2 && bracket_flash);
}

void CodeEditor::CheckBrackets()
{
	CancelBracketHighlight(highlight_bracket_pos0);
	CancelBracketHighlight(highlight_bracket_pos);
	if(!IsSelection()) {
		if(!console_mode && GetSyntax(GetCursorLine())->CheckBrackets(*this, highlight_bracket_pos0, highlight_bracket_pos)) {
			RefreshLine(GetLine(highlight_bracket_pos0));
			RefreshLine(GetLine(highlight_bracket_pos));
			bracket_start = GetTimeClick();
		}
	}
	WhenSelection();
}

void CodeEditor::CopyWord() {
	int64 p = GetCursor64();
	if(iscidl(GetChar(p)) || (p > 0 && iscidl(GetChar(--p)))) {
		int64 e = GetLength64();
		int64 f = p;
		while(--p >= 0 && iscidl(GetChar(p))) {}
		++p;
		while(++f < e && iscidl(GetChar(f)));
		WString txt = GetW(p, LimitSize(f - p));
		WriteClipboardUnicodeText(txt);
		AppendClipboardText(txt.ToString());
	}
}

void CodeEditor::DuplicateLine()
{
	if(IsReadOnly()) return;
	int i = GetLine(cursor);
	int pos = GetPos32(i);
	int len = GetLineLength(i);
	Insert(pos + len, "\n" + GetW(pos, len));
}

void CodeEditor::SwapChars() {
	if(IsReadOnly()) return;
	int i = GetLine(cursor);
	int j = GetPos32(i);
	if (j < cursor && cursor - j < GetLineLength(i)) {
		int p = (int)cursor;
		WString txt(GetChar(p-1),1);
		Remove(p-1,1);
		Insert(p, txt, true);
		PlaceCaret(p);
	}
}

void CodeEditor::Put(int chr)
{
	Insert((int)cursor++, WString(chr, 1), true);
}

void CodeEditor::FinishPut()
{
	PlaceCaret(cursor);
	Action();
}

void CodeEditor::ReformatComment()
{
	if(IsReadOnly()) return;
	NextUndo();
	GetSyntax(GetLine(cursor))->ReformatComment(*this);
}

void CodeEditor::CancelBracketHighlight(int64& pos)
{
	if(pos >= 0) {
		RefreshLine(GetLine(pos));
		pos = -1;
	}
}

void CodeEditor::Periodic()
{
	bool b = (((GetTimeClick() - bracket_start) >> 8) & 1) == 0;
	if(b != bracket_flash && EditorSyntax::hilite_bracket == 2) {
		bracket_flash = b;
		if(highlight_bracket_pos0 >= 0)
			RefreshLine(GetLine(highlight_bracket_pos0));
		if(highlight_bracket_pos >= 0)
			RefreshLine(GetLine(highlight_bracket_pos));
	}
}

void CodeEditor::SelectionChanged()
{
	int64 l, h;
	WString nilluminated;
	bool sel = GetSelection(l, h);
	bool ill = false;
	if(sel && h - l < 128) {
		for(int64 i = l; i < h; i++) {
			int c = GetChar(i);
			if(c == '\n') {
				nilluminated.Clear();
				break;
			}
			if(!IsSpace(c))
				ill = true;
			nilluminated.Cat(c);
		}
	}
	if(!ill)
		nilluminated.Clear();
	if(illuminated != nilluminated) {
		illuminated = nilluminated;
		Refresh();
	}
	if(!foundsel) {
		if(!persistent_find_replace)
			CloseFindReplace();
		found = false;
		notfoundfw = notfoundbk = false;
		findreplace.amend.Disable();
	}
	CheckBrackets();
	bar.Refresh();
	RefreshBlkHeader();
}

void CodeEditor::Illuminate(const WString& text)
{
	if(illuminated != text) {
		illuminated = text;
		Refresh();
	}
}

bool CodeEditor::InsertRS(int chr, int count) {
	if(IsReadOnly()) return true;
	if(IsSelection()) {
		InsertChar(chr, count);
		return true;
	}
	return false;
}

void CodeEditor::IndentInsert(int chr, int count) {
	if(InsertRS(chr)) return;
	One<EditorSyntax> s = GetSyntax(GetCursorLine());
	if(s)
		s->IndentInsert(*this, chr, count);
	else
		InsertChar(chr, count, true);
}

void CodeEditor::Make(Event<String&> op)
{
	if(IsReadOnly()) return;
	Point cursor = GetColumnLine(GetCursor32());
	Point scroll = GetScrollPos();
	int l, h;
	bool is_sel = GetSelection32(l, h);
	if(!is_sel) { l = 0; h = GetLength32(); }
	if(h <= l)
	{
		BeepExclamation();
		return;
	}
	l = GetPos32(GetLine(l));
	h = GetPos32(GetLine(h - 1) + 1);
	String substring = Get(l, h - l);
	String out = substring;
	op(out);
	if(out == substring)
	{
		BeepInformation();
		return;
	}
	Remove(l, h - l);
	Insert(l, out.ToWString());
	if(is_sel)
		SetSelection(l, l + out.GetLength());
	SetCursor(GetGPos(cursor.y, cursor.x));
	SetScrollPos(scroll);
}

void CodeEditor::TabsOrSpaces(String& out, bool maketabs)
{
	String substring = out;
	out.Clear();
	int tab = GetTabSize();
	if(tab <= 0) tab = 8;
	for(const char *p = substring.Begin(), *e = substring.End(); p < e;)
	{
		int pos = 0;
		for(; p < e; p++)
			if(*p == '\t')
				pos = (pos / tab + 1) * tab;
			else if(*p == ' ')
				pos++;
			else
				break;
		if(maketabs)
		{
			out.Cat('\t', pos / tab);
			const char *b = p;
			while(p < e && *p++ != '\n')
				;
			out.Cat(b, int(p - b));
		}
		else
		{
			out.Cat(' ', pos);
			for(; p < e && *p != '\n'; p++)
				if(*p == '\t')
				{
					int npos = (pos / tab + 1) * tab;
					out.Cat(' ', npos - pos);
					pos = npos;
				}
				else
				{
					out.Cat(*p);
					pos++;
				}
			if(p < e)
				out.Cat(*p++);
		}
	}
}

void CodeEditor::MakeTabsOrSpaces(bool maketabs)
{
	Make(THISBACK1(TabsOrSpaces, maketabs));
}

void CodeEditor::LineEnds(String& out)
{
	String substring = out;
	out.Clear();
	const char *q = ~substring;
	const char *b = q;
	for(const char *p = b, *e = substring.End(); p < e; p++)
	{
		if(*p == '\n') {
			out.Cat(b, q);
			out.Cat("\r\n");
			b = q = p + 1;
		}
		else
		if(*p != '\t' && *p != ' ' && *p != '\r')
			q = p + 1;
	}
	out.Cat(b, substring.End());
}

void CodeEditor::MakeLineEnds()
{
	Make(THISBACK(LineEnds));
}

void CodeEditor::MoveNextWord(bool sel) {
	int64 p = GetCursor64();
	int64 e = GetLength64();
	if(iscidl(GetChar(p)))
		while(p < e && iscidl(GetChar(p))) p++;
	else
		while(p < e && !iscidl(GetChar(p))) p++;
	PlaceCaret(p, sel);
}

void CodeEditor::MovePrevWord(bool sel) {
	int64 p = GetCursor64();
	if(p == 0) return;
	if(iscidl(GetChar(p - 1)))
		while(p > 0 && iscidl(GetChar(p - 1))) p--;
	else
		while(p > 0 && !iscidl(GetChar(p - 1))) p--;
	PlaceCaret(p, sel);
}

void CodeEditor::MoveNextBrk(bool sel) {
	int64 p = GetCursor64();
	int64 e = GetLength64();
	if(!islbrkt(GetChar(p)))
		while(p < e && !islbrkt(GetChar(p))) p++;
	else {
		int lvl = 1;
		p++;
		for(;;) {
			if(p >= e) break;
			int c = GetChar(p++);
			if(islbrkt(c)) lvl++;
			if(isrbrkt(c) && --lvl == 0) break;
		}
	}
	PlaceCaret(p, sel);
}

void CodeEditor::MovePrevBrk(bool sel) {
	int64 p = GetCursor64();
	if(p < 2) return;
	if(!isrbrkt(GetChar(p - 1))) {
		if(p < GetLength64() - 1 && isrbrkt(GetChar(p)))
			p++;
		else {
			while(p > 0 && !isrbrkt(GetChar(p - 1))) p--;
			PlaceCaret(p, sel);
			return;
		}
	}
	int lvl = 1;
	p -= 2;
	for(;;) {
		if(p <= 1) break;
		int c = GetChar(p);
		if(isrbrkt(c)) lvl++;
		if(islbrkt(c) && --lvl == 0) break;
		p--;
	}
	PlaceCaret(p, sel);
}

bool isspctab(int c) {
	return c == ' ' || c == '\t';
}

void CodeEditor::DeleteWord() {
	if(IsReadOnly() || RemoveSelection()) return;
	int p = GetCursor32();
	int e = GetLength32();
	int c = GetChar(p);
	if(iscidl(c))
		while(p < e && iscidl(GetChar(p))) p++;
	else
	if(isspctab(c))
		while(p < e && isspctab(GetChar(p))) p++;
	else {
		DeleteChar();
		return;
	}
	Remove(GetCursor32(), p - GetCursor32());
}

void CodeEditor::DeleteWordBack() {
	if(IsReadOnly() || RemoveSelection()) return;
	int p = GetCursor32();
	if(p < 1) return;
	int c = GetChar(p - 1);
	if(iscidl(GetChar(p - 1)))
		while(p > 1 && iscidl(GetChar(p - 1))) p--;
	else
	if(isspctab(c))
		while(p > 1 && isspctab(GetChar(p - 1))) p--;
	else {
		Backspace();
		return;
	}
	Remove(p, GetCursor32() - p);
	PlaceCaret(p);
}

void CodeEditor::SetLineSelection(int l, int h) {
	SetSelection(GetPos64(l), GetPos64(h));
}

bool CodeEditor::GetLineSelection(int& l, int& h) {
	int64 ll, hh;
	if(!GetSelection(ll, hh)) return false;
	l = GetLine(ll);
	h = GetLinePos64(hh);
	if(hh && h < GetLineCount()) h++;
	SetLineSelection(l, h);
	return true;
}

void CodeEditor::SwapUpDown(bool up)
{
	int l, h;
	if(GetSelection(l, h)) {
		l = GetLine(l);
		int hh = h;
		h = GetLinePos(hh);
		if(hh && h < GetLineCount()) h++;
	}
	else {
		l = GetLine(cursor);
		h = l + 1;
	}
	if(up) {
		if(l == 0)
			return;
		Insert(GetPos(h), GetWLine(l - 1) + "\n");
		Remove(GetPos(l - 1), GetLineLength(l - 1) + 1);
		SetSelection(GetPos(l - 1), GetPos(h - 1));
	}
	else {
		if(h >= GetLineCount() - 1)
			return;
		WString line = GetWLine(h) + "\n";
		Remove(GetPos(h), GetLineLength(h) + 1);
		Insert(GetPos(l), line);
		SetSelection(GetPos(l + 1), GetPos(h + 1));
	}
}

void CodeEditor::TabRight() {
	if(IsReadOnly()) return;
	int l, h;
	if(!GetLineSelection(l, h)) return;
	int ll = l;
	String tab(indent_spaces ? ' ' : '\t', indent_spaces ? GetTabSize() : 1);
	while(l < h)
		Insert(GetPos32(l++), tab);
	SetLineSelection(ll, h);
}

void CodeEditor::TabLeft() {
	if(IsReadOnly()) return;
	int l, h;
	if(!GetLineSelection(l, h)) return;
	int ll = l;
	while(l < h) {
		WString ln = GetWLine(l);
		int spc = 0;
		while(spc < tabsize && ln[spc] == ' ') spc++;
		if(spc < tabsize && ln[spc] == '\t') spc++;
		Remove(GetPos32(l++), spc);
	}
	SetLineSelection(ll, h);
}

bool CodeEditor::GetWordPos(int64 pos, int64& l, int64& h) {
	l = h = pos;
	if(!iscidl(GetChar(pos))) return false;
	while(l > 0 && iscidl(GetChar(l - 1))) l--;
	while(iscidl(GetChar(h))) h++;
	return true;
}

String CodeEditor::GetWord(int64 pos)
{
	int64 l, h;
	GetWordPos(pos, l, h);
	return Get(l, LimitSize(h - l));
}

String CodeEditor::GetWord()
{
	return GetWord(cursor);
}

void CodeEditor::LeftDouble(Point p, dword keyflags) {
	int64 l, h;
	int64 pos = GetMousePos(p);
	if(GetWordPos(pos, l, h))
		SetSelection(l, h);
	else
		SetSelection(pos, pos + 1);
	selkind = SEL_WORDS;
	SetFocus();
	SetCapture();
}

void CodeEditor::LeftTriple(Point p, dword keyflags)
{
	LineEdit::LeftTriple(p, keyflags);
	selkind = SEL_LINES;
	SetFocus();
	SetCapture();
}

void CodeEditor::LeftDown(Point p, dword keyflags) {
	if((keyflags & K_CTRL) && WhenCtrlClick) {
		WhenCtrlClick(GetMousePos(p));
		return;
	}
	LineEdit::LeftDown(p, keyflags);
	WhenLeftDown();
	CloseFindReplace();
	selkind = SEL_CHARS;
}

void CodeEditor::Tip::Paint(Draw& w)
{
	Rect r = GetSize();
	w.DrawRect(r, background);
	r.left++;
	if(d)
		d->Paint(w, r, v, SColorText(), background, 0);
}

CodeEditor::Tip::Tip()
{
	SetFrame(BlackFrame());
	BackPaint();
}

bool CodeEditor::SyncCloseTip()
{
	if(HasMouse() && IsVisible() && GetTopCtrl()->HasFocusDeep())
		return false;
	CloseTip();
	return true;
}

void CodeEditor::SyncTip()
{
	if(SyncCloseTip())
		return;
	Rect wa = GetWorkArea();
	Point p = Upp::GetMousePos();
	MouseTip mt;
	mt.background = Blend(SWhite(), SLtYellow());
	mt.pos = tippos;
	mt.sz.cx = min(DPI(1000), 2 * wa.GetWidth() / 3);
	if(tippos >= 0 &&
	   (WhenTip(mt) || delayed_tip && DelayedTip(mt) && p == delayed_pos)) {
		mt.sz.cy = min(wa.GetHeight() / 2 - DPI(20), mt.sz.cy);
		tip.d = mt.display;
		tip.v = mt.value;
		tip.background = mt.background;
		Size sz = tip.AddFrameSize(mt.sz);
		int y = p.y + DPI(24);
		if(y + sz.cy > wa.bottom)
			y = max(0, p.y - sz.cy - DPI(4));
		int x = p.x;
		if(x + sz.cx > wa.right)
			x = max(0, wa.right - sz.cx);
		tip.SetRect(RectC(x, y, sz.cx, sz.cy) & wa);
		if(!tip.IsOpen())
			tip.PopUp(this, false, false, true);
		tip.Refresh();
	}
	else
		CloseTip();
}

void CodeEditor::CloseTip()
{
	if(tip.IsOpen())
		tip.Close();
	tip.d = NULL;
}

bool CodeEditor::MouseSelSpecial(Point p, dword flags) {
	if((flags & K_MOUSELEFT) && HasFocus() && HasCapture() && !(flags & K_ALT) && selkind != SEL_CHARS) {
		int64 c = GetMousePos(p);
		int64 l, h;
		
		if(selkind == SEL_LINES) {
			l = c;
			int i = GetLinePos64(l);
			l = c - l;
			h = min(l + GetLineLength(i) + 1, GetLength64() - 1);
			c = c < anchor ? l : h;
		}
		else
			c = iscidl(GetChar(c - 1)) && GetWordPos(c, l, h) ? c < anchor ? l : h : c;
		PlaceCaret(c, mpos != c);
		return true;
	}
	return false;
}

void CodeEditor::LeftRepeat(Point p, dword flags)
{
	if(!MouseSelSpecial(p, flags))
		LineEdit::LeftRepeat(p, flags);
}

void CodeEditor::MouseMove(Point p, dword flags) {
	if(!MouseSelSpecial(p, flags))
		LineEdit::MouseMove(p, flags);

	tippos = Null;

	if(IsSelection()) return;

	if(p.x > 0) { // ignore calls from EditorBar::MouseMove
		Size fsz = GetFontSize();
		p = (p + fsz * (Size)sb.Get()) / fsz;
		int64 h = GetGPos(p.y, p.x);
		tippos = h < INT_MAX && GetColumnLine(h) == p ? (int)h : -1;
	}
	
	SyncTip();
	delayed_tip = false;
	delayed_pos = Upp::GetMousePos();
	delayed.KillSet(1000, [=] {
		delayed_tip = true;
		SyncTip();
	});
}

bool CodeEditor::DelayedTip(MouseTip& tip) { return false; }

Image CodeEditor::CursorImage(Point p, dword keyflags)
{
	if(WhenCtrlClick && (keyflags & K_CTRL))
		return Image::Hand();
	if(tip.IsOpen())
		return Image::Arrow();
	return LineEdit::CursorImage(p, keyflags);
}

void CodeEditor::MouseLeave()
{
	delayed_tip = false;
	delayed_pos = Null;
	tippos = -1;
	LineEdit::MouseLeave();
	CloseTip();
	delayed.Kill();
}

WString CodeEditor::GetI()
{
	int64 l, h;
	WString ft;
	if((GetSelection(l, h) || GetWordPos(GetCursor64(), l, h)) && h - l < 60)
		while(l < h) {
			int c = GetChar(l++);
			if(c == '\n')
				break;
			ft.Cat(c);
		}
	return ft;
}

void CodeEditor::SetI(Ctrl *edit)
{
	*edit <<= GetI();
}

void CodeEditor::Goto() {
	String line = AsString(GetCursorLine());
	if(EditText(line, t_("Go to"), t_("Line:")))
		SetCursor(GetPos64(atoi(line) - 1));
}

bool CodeEditor::ToggleSimpleComment(int &start_line, int &end_line, bool usestars)
{
	if(IsReadOnly()) return true;

	int l, h;
	if(!GetSelection32(l, h))
		return true;

	int pos = h;
	start_line = GetLine(l);
	end_line = GetLinePos32(pos);

	if(usestars && start_line == end_line) {
		Enclose("/*", "*/", l, h);
		return true;
	}

	if(pos && end_line < GetLineCount()) end_line++;
	SetLineSelection(start_line, end_line);

	return false;
}

void CodeEditor::ToggleLineComments(bool usestars)
{
	if(IsReadOnly()) return;

	int start_line, end_line;
	if(ToggleSimpleComment(start_line, end_line))
		return;

	int us = static_cast<int>(usestars);

	bool is_commented = true;

	if(usestars) {
		is_commented &= GetChar(GetPos64(start_line) + 0) == '/' &&
						GetChar(GetPos64(start_line) + 1) == '*';

		is_commented &= GetChar(GetPos64(end_line - 1) + 1) == '*' &&
						GetChar(GetPos64(end_line - 1) + 2) == '/';
	}

	for(int line = start_line + us; is_commented && (line < end_line - us * 2); ++line)
		is_commented &= GetChar(GetPos64(line)) == (usestars ? ' ' : '/') &&
						GetChar(GetPos64(line)+1) == (usestars ? '*' : '/');

	if(!is_commented) {

		if(usestars) {
			Insert(GetPos32(end_line)," */\n");
			Insert(GetPos32(start_line),"/*\n");
		}

		for(int line = start_line + us; line < end_line + us; ++line)
			Insert(GetPos32(line), usestars ? " * " : "//");
	}
	else
	{
		int line = start_line;
		if(usestars)
			Remove(GetPos32(start_line), 3);
		for(; line < end_line - us * 2; ++line)
			Remove(GetPos32(line), 2 + us);
		if(usestars)
			Remove(GetPos32(line), 4);
	}

	if(usestars)
		SetLineSelection(start_line, end_line + (is_commented ? -2 : 2));
	else
		SetLineSelection(start_line, end_line);
}

void CodeEditor::ToggleStarComments()
{
	if(IsReadOnly()) return;

	int start_line, end_line;
	if(ToggleSimpleComment(start_line, end_line))
		return;

	bool is_commented =
		GetChar(GetPos64(start_line)) == '/' &&
		GetChar(GetPos64(start_line)+1) == '*' &&
		GetChar(GetPos64(start_line)+2) == '\n' &&
		GetChar(GetPos64(end_line-1)) == '*' &&
		GetChar(GetPos64(end_line-1)+1) == '/' &&
		GetChar(GetPos64(end_line-1)+2) == '\n';

	if(!is_commented) {
		// Backwards because inserting changes the end line #
		Insert(GetPos32(end_line),"*/\n");
		Insert(GetPos32(start_line),"/*\n");
		SetLineSelection(start_line, end_line+2);
	} else {
		// Backwards because inserting changes the end line #
		Remove(GetPos32(end_line-1),3);
		Remove(GetPos32(start_line),3);
		SetLineSelection(start_line, end_line-2);
	}
}

void CodeEditor::Enclose(const char *c1, const char *c2, int l, int h)
{
	if(IsReadOnly()) return;

	if((l < 0 || h < 0) && !GetSelection32(l, h))
		return;
	Insert(l, WString(c1));
	Insert(h + (int)strlen(c1), WString(c2));
	ClearSelection();
	SetCursor(h + (int)strlen(c1) + (int)strlen(c2));
}

bool CodeEditor::Key(dword code, int count) {
	Time key_time = GetSysTime();
	double diff;
	if(!IsNull(last_key_time) && (diff = int(key_time - last_key_time)) <= 3)
		stat_edit_time += diff;
	last_key_time = key_time;

	NextUndo();
	if(code == replace_key) {
		Replace();
		return true;
	}
	if(IsRectSelection())
		return LineEdit::Key(code, count);
	switch(code) {
	case K_SHIFT_CTRL_UP:
		SwapUpDown(true);
		return true;
	case K_SHIFT_CTRL_DOWN:
		SwapUpDown(false);
		return true;
	case K_CTRL_DELETE:
		DeleteWord();
		return true;
	case K_CTRL_BACKSPACE:
		DeleteWordBack();
		return true;
	case K_BACKSPACE:
		if(!IsReadOnly() && !IsAnySelection() && indent_spaces) {
			int c = GetCursor32();
			Point ixln = GetIndexLine(c);
			WString ln = GetWLine(ixln.y);
			bool white = true;
			int startindex = -1, pos = 0, tabsz = GetTabSize();
			for(int i = 0; i < ixln.x; i++) {
				if(ln[i] == '\t' || ln[i] == ' ') {
					if(pos == 0)
						startindex = i;
					if(ln[i] == '\t' || ++pos >= tabsz)
						pos = 0;
				}
				else {
					white = false;
					break;
				}
			}
			if(white && startindex >= 0) {
				int count = ixln.x - startindex;
				PlaceCaret(c - count);
				Remove(c - count, count);
				Action();
				return true;
			}
		}
		break;
	case K_SHIFT_CTRL_TAB:
		return LineEdit::Key(K_TAB, count);
	case K_ENTER:
		if(!IsRectSelection())
			IndentInsert('\n', count);
		return true;
	}
	bool sel = code & K_SHIFT;
	switch(code & ~K_SHIFT) {
	case K_CTRL_F:
		if(withfindreplace) {
			FindReplace(sel, true, false);
			return true;
		}
		break;
	case K_CTRL_H:
		if(withfindreplace) {
			FindReplace(sel, true, true);
			return true;
		}
		break;
	case K_F3:
		if(sel)
			FindPrev();
		else
			FindNext();
		return true;
//	case K_CTRL_F3:
//		FindWord(sel);
//		return true;
	case K_CTRL_RIGHT:
		MoveNextWord(sel);
		return true;
	case K_CTRL_LEFT:
		MovePrevWord(sel);
		return true;
	case K_CTRL_RBRACKET:
		MoveNextBrk(sel);
		return true;
	case K_CTRL_LBRACKET:
		MovePrevBrk(sel);
		return true;
	case K_CTRL_ADD:
		Zoom(1);
		return true;
	case K_CTRL_SUBTRACT:
		Zoom(-1);
		return true;
	case K_TAB:
		if(!IsReadOnly()) {
			if(IsSelection()) {
				if(sel)
					TabLeft();
				else
					TabRight();
				return true;
			}
			if(!sel && indent_spaces) {
				int x = GetColumnLine(GetCursor64()).x;
				int add = GetTabSize() - x % GetTabSize();
				InsertChar(' ', add, false);
				return true;
			}
		}
	default:
		if(IsSelection() && auto_enclose) {
			if(code == '(') {
				Enclose("(", ")");
				return true;
			}
			if(code == '{') {
				Enclose("{", "}");
				return true;
			}
			if(code == '\"') {
				Enclose("\"", "\"");
				return true;
			}
			if(code == '[') {
				Enclose("[", "]");
				return true;
			}
			if(code == '/') {
				//Enclose("/*", "*/");
				ToggleLineComments(false);
				return true;
			}
			if(code == K_CTRL_SLASH)
			{
				ToggleLineComments(true);
				return true;
			}
			if(code == '*') {
				//Enclose("/*", "*/");
				ToggleStarComments();
				return true;
			}
		}
		if(wordwrap && code > 0 && code < 65535) {
			int limit = GetBorderColumn();
			int pos = GetCursor32();
			int lp = pos;
			int l = GetLinePos32(lp);
			if(limit > 10 && GetColumnLine(pos).x >= limit && lp == GetLineLength(l)) {
				int lp0 = GetPos32(l);
				WString ln = GetWLine(l);
				int wl = (int)GetGPos(l, limit) - lp0;
				while(wl > 0 && ln[wl - 1] != ' ')
					wl--;
				int sl = wl - 1;
				while(sl > 0 && ln[wl - 1] != '\n' && ln[sl - 1] == ' ')
					sl--;
				wordwrap = false;
				Remove(lp0 + sl, pos - (lp0 + sl));
				SetCursor(lp0 + sl);
				Put('\n');
				for(int i = 0; i < wl && findarg(ln[i], ' ', '\t') >= 0; i++)
					Put(ln[i]);
				for(int i = wl; i < ln.GetCount(); i++)
					Put(ln[i]);
				while(count--)
					Put(code);
				FinishPut();
				wordwrap = true;
				return true;
			}
	
		}
		if(code >= 32 && code < 128 && count == 1) {
			IndentInsert(code, 1);
			return true;
		}
		if(code == 9 && IsSelection())
			return true;
	}
	if(GetCharset() != CHARSET_UTF8)
		if(code >= 128 && code < 65536 && FromUnicode((wchar)code, GetCharset()) == DEFAULTCHAR)
			return true;
	return LineEdit::Key(code, count);
}

void CodeEditor::ForwardWhenBreakpoint(int i) {
	WhenBreakpoint(i);
}

void CodeEditor::GotoBarLine(int line)
{
	SetCursor(GetPos64(GetLineNo(line)));
}

void CodeEditor::Serialize(Stream& s) {
	int version = 0;
	s / version;
	SerializeFind(s);
}

void CodeEditor::SetLineInfo(const LineInfo& lf)
{
	bar.SetLineInfo(lf, GetLineCount());
}

void CodeEditor::PutI(WithDropChoice<EditString>& edit)
{
	edit.AddButton().SetMonoImage(CodeEditorImg::I()).Tip(t_("Set word/selection (Ctrl+I)"))
	    <<= THISBACK1(SetI, &edit);
}

void CodeEditor::Zoom(int d)
{
	Font f = GetFont();
	int h = f.GetCy();
	int q = f.GetHeight();
	while(f.GetCy() == h && (d < 0 ? f.GetCy() > 5 : f.GetCy() < 80))
		f.Height(q += d);
	SetFont(f);
	EditorBarLayout();
}

void CodeEditor::MouseWheel(Point p, int zdelta, dword keyFlags) {
	if(keyFlags & K_CTRL)
		Zoom(sgn(zdelta));
	else
		LineEdit::MouseWheel(p, zdelta, keyFlags);
}

void CodeEditor::Clear()
{
	for(SyntaxPos& p : syntax_cache)
		p.Clear();
	LineEdit::Clear();
	found = notfoundfw = notfoundbk = false;
}

void CodeEditor::ScrollBarItems::Paint(Draw& w)
{
	Rect sr = sb.GetSliderRect();
	for(const Tuple<int, Image, Color>& x : pos) {
		int y = sb.GetSliderPos(x.Get<int>());
		if(!IsNull(y)) {
			Image m = x.Get<Image>();
			Size isz = m.GetSize();
			if(y + isz.cy >= sr.top && y < sr.bottom)
				w.DrawImage(sr.CenterPoint().x - isz.cx / 2, sr.top + y - isz.cy / 2, m, x.Get<Color>());
		}
	}
	Color bg = IsDarkTheme() ? GrayColor(70) : SColorLtFace();

	if(editor.bar.li.GetCount() > 200000) // otherwise it gets too slow
		return;

	int py = -1; // accelerate long files
	int h = max(sr.GetHeight() / max(sb.GetTotal(), 1) + DPI(1), DPI(4));
	for(int i = 0; i < editor.bar.li.GetCount(); i++) {
		int edit = editor.bar.li[i].edited;
		if(edit) {
			int y = sb.GetSliderPos(i);
			if(y > sr.bottom)
				break;
			if(!IsNull(y) && y + h >= sr.top && y != py) {
				int age = (int)(log((double)(editor.GetUndoCount() + 1 - edit)) * 30);
				w.DrawRect(sr.left + DPI(2), sr.top + y, DPI(2), h,
				           Blend(SLtBlue(), bg, min(220, age)));
				py = y;
			}
		}
	}
}

CodeEditor::ScrollBarItems::ScrollBarItems(ScrollBar& sb, CodeEditor& e)
:	sb(sb), editor(e) {
	sb.Add(SizePos());
	Transparent();
	IgnoreMouse();
}

void CodeEditor::Errors(Vector<Point>&& errs)
{
	errors = pick(errs);
	Refresh();
	sbi.pos.Clear();
	for(Point p : errors)
		sbi.pos.Add({ p.y, CodeEditorImg::dot(), LtRed() });
	sbi.Refresh();
}

void CodeEditor::Layout()
{
	sbi.Refresh();
	LineEdit::Layout();
}

CodeEditor::CodeEditor()
:	sbi(sb.y, *this) {
	bracket_flash = false;
	highlight_bracket_pos0 = 0;
	bracket_start = 0;
	stat_edit_time = 0;
	last_key_time = Null;
	SetFont(CourierZ(12));
	AddFrame(bar);
	bar.SetEditor(this);
	UndoSteps(10000);
	InitFindReplace();
	bar.WhenBreakpoint = THISBACK(ForwardWhenBreakpoint);
	bar.WhenAnnotationMove = WhenAnnotationMove.Proxy();
	bar.WhenAnnotationClick = WhenAnnotationClick.Proxy();
	bar.WhenAnnotationRightClick = WhenAnnotationRightClick.Proxy();
	barline = true;
	sb.WithSizeGrip();
	HighlightSetup::InitOnce();
	Highlight(Null);
	sb.y.NoAutoHide();
	sb.y.AddFrame(topsbbutton);
	sb.y.AddFrame(topsbbutton1);
	topsbbutton.Hide();
	topsbbutton1.Hide();
	highlight_bracket_pos = 10;
	SetTimeCallback(-20, THISBACK(Periodic), TIMEID_PERIODIC);
	auto_enclose = false;
	mark_lines = true;
	check_edited = false;
	tippos = -1;
	selkind = SEL_CHARS;
	withfindreplace = true;
	wordwrap = false;
	blk0_header = false;
	closetip.Set(-200, [=] { SyncCloseTip(); });
}

CodeEditor::~CodeEditor() {}

}
