#include "CtrlLib.h"

namespace Upp {

#define LLOG(x) //  LOG(x)
#define LTIMING(x) // DTIMING(x)

LineEdit::LineEdit() {
	isdrag = false;
	nohbar = false;
	showtabs = false;
	tabsize = 4;
	font = CourierZ(12);
	SyncFont();
	SetFrame(ViewFrame());
	sb.NoBox();
	AddFrame(sb);
	sb.WhenScroll = THISBACK(Scroll);
	cutline = true;
	bordercolumn = -1;
	bordercolor = Null;
	overwrite = false;
	filter = NULL;
	showspaces = false;
	block_caret = false;
	showlines = false;
	showreadonly = true;
	dorectsel = false;
	hline = vline = Null;
	vlinex = 0;
	warnwhitespace = false;
}

LineEdit::~LineEdit() {}

Font LineEdit::GetPreeditFont()
{
	return font;
}

void LineEdit::MouseWheel(Point, int zdelta, dword keyflags) {
	if(keyflags & K_SHIFT)
		sb.WheelX(zdelta);
	else
		sb.WheelY(zdelta);
}

void LineEdit::HorzMouseWheel(Point, int zdelta, dword)
{
	sb.WheelX(zdelta);
}

void   LineEdit::Clear() {
	gcolumn = 0;
	TextCtrl::Clear();
	sb.SetTotal(0, 0);
	sb.Set(0, 0);
	NewScrollPos();
	PlaceCaret(0, false);
}

LineEdit& LineEdit::TabSize(int n) {
	tabsize = n;
	PlaceCaret0();
	Refresh();
	return *this;
}

LineEdit& LineEdit::BorderColumn(int col, Color c)
{
	bordercolumn = col;
	bordercolor = c;
	Refresh();
	return *this;
}

LineEdit& LineEdit::SetFont(Font f) {
	font = f;
	Layout();
	TabSize(tabsize);
	SetSb();
	return *this;
}

void LineEdit::SyncFont()
{
	fsz = GetFontSize();
}

Size LineEdit::GetFontSize() const {
	return Size(font.GetMonoWidth(), font.GetCy());
}

bool LineEdit::IsDoubleChar(int ch) const
{
	return ch >= 2048 && (IsDoubleWidth(ch) || font.GetWidth(ch) > fsz.cx);
}

void LineEdit::SetRectSelection(int64 anchor, int64 cursor)
{
	dorectsel = true;
	SetSelection(anchor, cursor);
	dorectsel = false;
}

void LineEdit::SetRectSelection(const Rect& rect)
{
	SetRectSelection(GetGPos(rect.top, rect.left), GetGPos(rect.bottom, rect.right));
}

Rect LineEdit::GetRectSelection() const
{
	if(IsRectSelection()) {
		int64 sell, selh;
		GetSelection(sell, selh);
		Rect r(GetColumnLine(sell), GetColumnLine(selh));
		if(r.left > r.right)
			Swap(r.left, r.right);
		return r;
	}
	return Null;
}

bool LineEdit::GetRectSelection(const Rect& rect, int line, int64& l, int64& h)
{
	if(line >= rect.top && line <= rect.bottom) {
		l = GetGPos(line, rect.left);
		h = GetGPos(line, rect.right);
		return true;
	}
	l = h = 0;
	return false;
}


int LineEdit::RectSelectionOp(Event<int, Rect, int64, int64, WString&> op, Event<Rect&> changesel)
{
	Rect rect = GetRectSelection();
	WString txt;
	for(int i = rect.top; i <= rect.bottom; i++) {
		int64 l, h;
		CacheLinePos(i);
		GetRectSelection(rect, i, l, h);
		WString s = GetWLine(i);
		op(i, rect, l, h, s);
		txt.Cat(s);
		txt.Cat('\n');
	}
	int l = GetPos32(rect.top);
	int h = GetPos32(rect.bottom) + GetLineLength(rect.bottom);
	if(h < GetLength32())
		h++;
	Remove((int)l, int(h - l));
	Insert((int)l, txt);
	changesel(rect);
	anchor = (int)GetGPos(rect.top, rect.left);
	cursor = (int)GetGPos(rect.bottom, rect.left);
	PlaceCaret0();
	return (int)cursor;
}

void LineEdit::RectSelectionChar(int c)
{
	RectSelectionText(WString(c, 1));
}

void LineEdit::RectSelectionText(const WString& text)
{
	if(GetRectSelection().GetWidth())
		RemoveRectSelection();
	int64 p = -1; // position after insertion, because of '\t' and double size chars cannot do just left+text.GetCount()
	RectSelectionOp(
		[&](int i, Rect rect, int64 l, int64 h, WString& s) {
			int x = GetColumnLine(l).x;
			if(x < rect.left) {
				s.Cat(' ', rect.left - x);
				s.Cat(text);
				if(p < 0)
					p = GetPos(i) + s.GetCount();
			}
			else {
				s.Insert(int(l - GetPos64(i)), text);
				if(p < 0) // first time the position is unchanged after whole text is replaced
					p = l + text.GetCount();
			}
			
		},
		[&](Rect& r) { r.left = GetColumnLine(p).x; }
	);
	column_typing = true;
}

void LineEdit::RectSelectionBackspace()
{
	if(GetRectSelection().GetWidth())
		RectSelectionDelete();
	else {
		int64 a = anchor;
		RectSelectionLeftRight(-1, false);
		if(a != anchor)
			RectSelectionDelete();
	}
}

void LineEdit::RectSelectionDelete()
{
	Rect r = GetRectSelection();
	if(r.GetWidth())
		RemoveRectSelection();
	else
		RectSelectionOp(
			[&](int i, Rect rect, int64 l, int64 h, WString& s) {
				int p = int(l - GetPos64(i));
				if(GetColumnLine(l).x == rect.left && p < s.GetCount())
					s.Remove(p, 1);
			},
			[&](Rect& r) {}
		);
}

void LineEdit::RectSelectionLeftRight(int dir, bool homeend)
{
	Rect rect = GetRectSelection();
	
	if(rect.left != rect.right) {
		if(dir > 0)
			rect.left = rect.right;
		anchor = (int)GetGPos(rect.top, rect.left);
		cursor = (int)GetGPos(rect.bottom, rect.left);
		PlaceCaret0();
		Refresh();
		return;
	}

	auto Try = [&](int64& anchor, int64& cursor) {
		Point a0 = GetColumnLine(anchor);
		int64 a = clamp(anchor + dir, (int64)0, GetLength64());
		Point a1 = GetColumnLine(a);
		
		if(a1.y != a0.y)
			return false;
		int c = (int)GetGPos(GetColumnLine(cursor).y, a1.x);
		if(GetColumnLine(c).x == a1.x) {
			anchor = a;
			cursor = c;
			PlaceCaret0();
			return true;
		}
		return false;
	};
	
	int n = homeend ? 2000 : 1;
	
	while(n > 0 && (Try(anchor, cursor) || Try(cursor, anchor)))
		n--;
}

int LineEdit::RemoveRectSelection()
{
	return RectSelectionOp(
		[&](int i, Rect, int64 l, int64 h, WString& s) { s.Remove(int(l - GetPos64(i)), int(h - l)); }
	);
}

WString LineEdit::CopyRectSelection()
{
	WString txt;
	Rect rect = GetRectSelection();
	for(int i = rect.top; i <= rect.bottom && txt.GetCount() < max_total; i++) {
		int64 l, h;
		CacheLinePos(i);
		int64 pos = GetPos64(i);
		GetRectSelection(rect, i, l, h);
		txt.Cat(GetWLine(i).Mid(int(l - pos), int(h - l)));
#ifdef PLATFORM_WIN32
		txt.Cat('\r');
#endif
		txt.Cat('\n');
	}
	return txt;
}

int LineEdit::PasteRectSelection(const WString& s)
{
	Vector<WString> cl = Split(s, '\n', false);
	int64 cursor0 = cursor;
	if(cl.GetCount() == 1)
		RectSelectionText(cl[0]);
	else {
		Rect rect = GetRectSelection();
		int64 pos = cursor;
		for(int i = 0; i < cl.GetCount() && rect.top + i <= rect.bottom; i++) {
			int64 l, h;
			CacheLinePos(i);
			GetRectSelection(rect, i + rect.top, l, h);
			Remove((int)l, int(h - l));
			int nn = Insert((int)l, cl[i]);
			pos = l + nn;
		}
		PlaceCaret(pos);
	}
	return int(cursor - cursor0);
}

void LineEdit::PasteColumn(const WString& text)
{
	Vector<WString> cl = Split(text, '\n', false);
	if(cl.GetCount() && cl.Top().IsEmpty())
		cl.Drop();
	if(cl.GetCount() == 0)
		return;
	int pos;
	if(IsRectSelection()) {
		Rect t = GetRectSelection();
		RemoveSelection();
		Point p = t.TopLeft();
		pos = (int)cursor;
		for(int i = 0; i < t.bottom - t.top + 1; i++) {
			CacheLinePos(i + p.y);
			int l = (int)GetGPos(i + p.y, p.x);
			pos = l + Insert(l, cl[i % cl.GetCount()]);
		}
	}
	else {
		RemoveSelection();
		Point p = GetColumnLine(cursor);
		pos = (int)cursor;
		for(int i = 0; i < cl.GetCount(); i++) {
			CacheLinePos(i + p.y);
			int li = p.y + i;
			if(li < GetLineCount()) {
				int l = (int)GetGPos(i + p.y, p.x);
				pos = l + Insert(l, WString(' ', max(p.x - GetColumnLine(l).x, 0)) + cl[i]);
			}
			else {
				Insert(GetLength32(), "\n" + WString(' ', p.x) + cl[i]);
				pos = GetLength32();
			}
		}
	}
	PlaceCaret(pos);
}

void LineEdit::PasteColumn()
{
	WString w = ReadClipboardUnicodeText();
	if(w.IsEmpty())
		w = ReadClipboardText().ToWString();
	PasteColumn(w);
	Action();
}

bool sSortLineOrder(const WString& l1, const WString& l2)
{
	return ToUpper(l1) < ToUpper(l2);
}

void LineEdit::Sort()
{
	if(!IsRectSelection())
		return;
	CopyRectSelection();
	Rect rect = GetRectSelection();
	Vector<WString> key;
	Vector<WString> ln;
	for(int i = rect.top; i <= rect.bottom; i++) {
		int64 l, h;
		GetRectSelection(rect, i, l, h);
		key.Add(GetW((int)l, int(h - l)));
		ln.Add(GetWLine(i));
	}
	int sell = GetPos32(rect.top);
	int selh = rect.bottom + 1 < GetLineCount() ? GetPos32(rect.bottom + 1) : GetLength32();
	IndexSort(key, ln, sSortLineOrder);
	Remove(sell, selh - sell);
	Insert(sell, Join(ln, "\n"));
}

class sOptimizedRectRenderer {
	Draw& w;
	Rect  cr;
	Color color;

public:
	void DrawRect(const Rect& r, Color color);
	void DrawRect(int x, int y, int cx, int cy, Color color) { DrawRect(RectC(x, y, cx, cy), color); }
	void Flush();

	sOptimizedRectRenderer(Draw& w) : w(w) { cr = Null; color = Null; }
	~sOptimizedRectRenderer()              { Flush(); }
};

void sOptimizedRectRenderer::Flush()
{
	LTIMING("RectFlush");
	if(!IsNull(cr)) {
		w.DrawRect(cr, color);
		cr = Null;
	}
}

void sOptimizedRectRenderer::DrawRect(const Rect& r, Color c)
{
	LTIMING("DrawRect");
	if(cr.top == r.top && cr.bottom == r.bottom && cr.right == r.left && c == color) {
		cr.right = r.right;
		return;
	}
	Flush();
	cr = r;
	color = c;
}

#if 1 // This is a more ambitious approach combining non-continual chunks of text, it is a bit faster...
class sOptimizedTextRenderer {
	Draw&       w;
	int         y;
	struct Chrs : Moveable<Chrs> {
		Vector<int> x;
		Vector<int> width;
		WString     text;
	};
	VectorMap< Tuple2<Font, Color>, Chrs > cache;

public:
	void DrawChar(int x, int y, int chr, int width, Font afont, Color acolor);
	void Flush();

	sOptimizedTextRenderer(Draw& w) : w(w) { y = Null; }
	~sOptimizedTextRenderer()              { Flush(); }
};

void sOptimizedTextRenderer::Flush()
{
	if(cache.GetCount() == 0)
		return;
	LTIMING("Flush");
	for(int i = 0; i < cache.GetCount(); i++) {
		Chrs& c = cache[i];
		if(c.x.GetCount()) {
			Tuple2<Font, Color> fc = cache.GetKey(i);
			int x = c.x[0];
			for(int i = 0; i < c.x.GetCount() - 1; i++)
				c.x[i] = c.x[i + 1] - c.x[i];
			c.x.Top() = c.width.Top();
			w.DrawText(x, y, c.text, fc.a, fc.b, c.x);
		}
	}
	cache.Clear();
}

void sOptimizedTextRenderer::DrawChar(int x, int _y, int chr, int width, Font font, Color color)
{
	LTIMING("DrawChar");
	if(y != _y) {
		Flush();
		y = _y;
	}
	Chrs *c;
	{
		LTIMING("Map");
		c = &cache.GetAdd(MakeTuple(font, color));
	}
	if(c->x.GetCount() && c->x.Top() > x || font.IsUnderline() && cache.GetCount() > 1) {
		Flush();
		c = &cache.GetAdd(MakeTuple(font, color));
	}
	c->text.Cat(chr);
	c->width.Add(width);
	c->x.Add(x);
}
#else
class sOptimizedTextRenderer {
	Draw&       w;
	int         x, y;
	int         xpos;
	Vector<int> dx;
	WString     text;
	Font        font;
	Color       color;

public:
	void DrawChar(int x, int y, int chr, int width, Font afont, Color acolor);
	void Flush();

	sOptimizedTextRenderer(Draw& w) : w(w) { y = Null; }
	~sOptimizedTextRenderer()              { Flush(); }
};

void sOptimizedTextRenderer::Flush()
{
	if(text.GetCount() == 0)
		return;
	LTIMING("Flush");
	w.DrawText(x, y, text, font, color, dx);
	y = Null;
	text.Clear();
	dx.Clear();
}

void sOptimizedTextRenderer::DrawChar(int _x, int _y, int chr, int width, Font _font, Color _color)
{
	LTIMING("DrawChar");
	if(y == _y && font == _font && color == _color && dx.GetCount() && _x >= xpos - dx.Top())
		dx.Top() += _x - xpos;
	else {
		LTIMING("DrawChar flush");
		Flush();
		x = _x;
		y = _y;
		font = _font;
		color = _color;
	}
	dx.Add(width);
	text.Cat(chr);
	xpos = _x + width;
}
#endif

void   LineEdit::Paint0(Draw& w)
{
	LTIMING("LineEdit::Paint0");
	GuiLock __;
	int64 sell, selh;
	GetSelection(sell, selh);
	if(!IsEnabled())
		sell = selh = 0;
	Rect rect(0, 0, 0, 0);
	bool rectsel = IsRectSelection();
	if(rectsel)
		rect = GetRectSelection();
	Size sz = GetSize();
	Point sc = sb;
	int ll = min(GetLineCount(), sz.cy / fsz.cy + sc.y + 1);
	int  y = 0;
	sc.y = minmax(sc.y, 0, GetLineCount() - 1);
	cpos = GetPos64(sc.y);
	cline = sc.y;
	sell -= cpos;
	selh -= cpos;
	int64 pos = cpos;
	int fascent = font.Info().GetAscent();
	int cursorline = GetLine(cursor);
	Highlight ih;
	ih.ink = color[IsShowEnabled() ? INK_NORMAL : INK_DISABLED];
	ih.paper = color[IsReadOnly() && showreadonly || !IsShowEnabled() ? PAPER_READONLY : PAPER_NORMAL];
	if(nobg)
		ih.paper = Null;
	ih.font = font;
	ih.chr = 0;
	for(int i = sc.y; i < ll; i++) {
		Color showcolor = color[WHITESPACE];
		WString tx = GetWLine(i);
		bool warn_whitespace = false;
		if(warnwhitespace && !IsSelection()) {
			int64 pos = GetCursor64();
			int linei = GetLinePos64(pos);
			if(linei != i || pos < tx.GetCount()) {
				int wkind = 0;
				bool empty = true;
				for(const wchar *s = tx; *s; s++) {
					if(*s == '\t') {
						if(wkind == ' ') {
							warn_whitespace = true;
							break;
						}
						wkind = '\t';
					}
					else
					if(*s == ' ')
						wkind = ' ';
					else
					if(*s > ' ') {
						empty = false;
						wkind = 0;
					}
				}
				if(wkind && !empty)
					warn_whitespace = true;
				if(warn_whitespace)
					showcolor = color[WARN_WHITESPACE];
			}
		}
		bool do_highlight = tx.GetCount() < 100000;
		int len = tx.GetLength();
		if(w.IsPainting(0, y, sz.cx, fsz.cy)) {
			LTIMING("PaintLine");
			Vector<Highlight> hl;
			int ln;
			if(do_highlight) {
				hl.SetCount(len + 1, ih);
				for(int q = 0; q < tx.GetCount(); q++)
					hl[q].chr = tx[q];
				LTIMING("HighlightLine");
				HighlightLine(i, hl, pos);
				ln = hl.GetCount() - 1;
			}
			else
				ln = tx.GetCount();
			int lgp = -1;
			for(int pass = 0; pass < 3; pass++) {
				int gp = 0;
				int scx = fsz.cx * sc.x;
				sOptimizedRectRenderer rw(w);
				if(ln >= 0) {
					int q = 0;
					int x = 0;
					int scx2 = scx - max(2, tabsize) * fsz.cx;
					while(q < ln && x < scx2) { // Skip part before left border
						wchar chr = do_highlight ? hl[q++].chr : tx[q++];
						if(chr == '\t') {
							gp = (gp + tabsize) / tabsize * tabsize;
							x = fsz.cx * gp;
						}
						else
						if(IsDoubleChar(chr)) {
							x += 2 * fsz.cx;
							gp += 2;
						}
						else {
							x += fsz.cx;
							gp++;
						}
					}
					sOptimizedTextRenderer tw(w);
					Highlight lastHighlight;
					while(q < ln) {
						if(q == tx.GetCount())
							lgp = gp;
						Highlight h;
						if(do_highlight)
							h = hl[q];
						else {
							h = ih;
							h.chr = tx[q];
						}
						int pos = min(q, len); // Highligting can add chars at the end of line
						if(rectsel ? i >= rect.top && i <= rect.bottom && gp >= rect.left && gp < rect.right
						           : pos >= sell && pos < selh) {
							h.paper = color[PAPER_SELECTED];
							h.ink = color[INK_SELECTED];
						}
						int x = gp * fsz.cx - scx;
						bool cjk = IsDoubleChar(h.chr);
						int xx = x + (gp + 1 + cjk) * fsz.cx;
						if(h.chr == '\t') {
							int ngp = (gp + tabsize) / tabsize * tabsize;
							int l = ngp - gp;
							LLOG("Highlight -> tab[" << q << "] paper = " << h.paper);
							if(x >= -fsz.cy * tabsize) {
								if(pass == 0) {
									rw.DrawRect(x, y, fsz.cx * l, fsz.cy, h.paper);
									if((showtabs || warn_whitespace) &&
									   h.paper != SColorHighlight && q < tx.GetLength()) {
										rw.DrawRect(x + 2, y + fsz.cy / 2, l * fsz.cx - 4, 1, showcolor);
										rw.DrawRect(ngp * fsz.cx - scx - 3, y + 3, 1, fsz.cy - 6, showcolor);
									}
									if(bordercolumn > 0 && bordercolumn >= gp && bordercolumn < gp + l)
										rw.DrawRect((bordercolumn - sc.x) * fsz.cx, y, 1, fsz.cy, bordercolor);
								}
								if(pass == 2) // resolve underlined tabs
									tw.DrawChar(x, y, ' ', fsz.cx * l, h.font, h.ink);
							}
							q++;
							gp = ngp;
						}
						else
						if(h.chr == ' ') {
							LLOG("Highlight -> space[" << q << "] paper = " << h.paper);
							if(x >= -fsz.cy) {
								if(pass == 0) {
									rw.DrawRect(x, y, fsz.cx, fsz.cy, h.paper);
									if((showspaces || warn_whitespace)
									   && h.paper != SColorHighlight && q < tx.GetLength()) {
										int n = fsz.cy / 10 + 1;
										rw.DrawRect(x + fsz.cx / 2, y + fsz.cy / 2, n, n, showcolor);
									}
									if(bordercolumn > 0 && bordercolumn >= gp && bordercolumn < gp + 1)
										rw.DrawRect((bordercolumn - sc.x) * fsz.cx, y, 1, fsz.cy, bordercolor);
								}
								if(pass == 2) // resolve underlined spaces
									tw.DrawChar(x, y, ' ', fsz.cx, h.font, h.ink);
							}
							q++;
							gp++;
						}
						else {
							LLOG("Highlight -> paper[" << q << "] = " << h.paper);
							if(max(x, 0) < min(xx, sz.cx) && fsz.cx >= -fsz.cy) {
								if(pass == 0) {
									rw.DrawRect(x, y, (cjk + 1) * fsz.cx, fsz.cy, h.paper);
									if(bordercolumn > 0 && bordercolumn >= gp && bordercolumn < gp + 1 + cjk)
										rw.DrawRect((bordercolumn - sc.x) * fsz.cx, y, 1, fsz.cy, bordercolor);
								}
								if(pass == 1 && (h.flags & SPELLERROR))
									rw.DrawRect(x, max(y, y + fsz.cy - Zy(1)), (cjk + 1) * fsz.cx, Zy(1), LtRed());
								if(pass == 2)
									tw.DrawChar(x + (h.flags & SHIFT_L ? -fsz.cx / 6 : h.flags & SHIFT_R ? fsz.cx / 6 : 0),
									            y + fascent - h.font.GetAscent(),
									            h.chr, (cjk + 1) * fsz.cx, h.font, h.ink);
							}
							q++;
							gp += 1 + cjk;
							if(x > sz.cx)
								break;
						}
					}
				}
				if(pass == 0) {
					int gpx = gp * fsz.cx - scx;
					if(!rectsel && sell <= len && len < selh)
						rw.DrawRect(gpx, y, sz.cx - gpx, fsz.cy, color[PAPER_SELECTED]);
					else {
						bool noendfill = hl.GetCount() && (hl.Top().flags & NOENDFILL);
						rw.DrawRect(gpx, y, noendfill ? fsz.cx : sz.cx - gpx, fsz.cy,
						            (do_highlight && hl.GetCount() ? hl.Top() : ih).paper);
						if(noendfill)
							rw.DrawRect(gpx + fsz.cx, y, sz.cx - gpx - fsz.cx, fsz.cy, ih.paper);
					}
					if(bordercolumn > 0 && bordercolumn >= gp)
						rw.DrawRect((bordercolumn - sc.x) * fsz.cx, y, 1, fsz.cy, bordercolor);
					if((showlines || warn_whitespace)) {
						int yy = 2 * fsz.cy / 3;
						int x = (lgp >= 0 ? lgp : gp) * fsz.cx - scx;
						rw.DrawRect(x, y + yy, fsz.cx / 2, 1, showcolor);
						if(fsz.cx > 2)
							rw.DrawRect(x + 1, y + yy - 1, 1, 3, showcolor);
						if(fsz.cx > 5)
							rw.DrawRect(x + 2, y + yy - 2, 1, 5, showcolor);
						rw.DrawRect(x + fsz.cx / 2, y + yy / 2, 1, yy - yy / 2, showcolor);
					}
					if(sell == selh) {
						if(!IsNull(hline) && i == cursorline) {
							rw.DrawRect(0, y, sz.cx, 1, hline);
							rw.DrawRect(0, y + fsz.cy - 1, sz.cx, 1, hline);
						}
						if(!IsNull(vline))
							rw.DrawRect(caretpos.x, y, 1, fsz.cy, vline);
					}
			//		if(rectsel && rect.left == rect.right && i >= rect.top && i <= rect.bottom)
			//			rw.DrawRect(rect.left * fsz.cx - scx, y, 2, fsz.cy, Blend(color[PAPER_SELECTED], color[PAPER_NORMAL]));
				}
			}
		}
		y += fsz.cy;
		sell -= len + 1;
		selh -= len + 1;
		pos += len + 1;
	}
	w.DrawRect(0, y, sz.cx, sz.cy - y, color[IsReadOnly() && showreadonly || !IsShowEnabled() ? PAPER_READONLY : PAPER_NORMAL]);
	DrawTiles(w, DropCaret(), CtrlImg::checkers());
	vlinex = caretpos.x;
}

void LineEdit::Paint(Draw& w)
{
	Paint0(w);
	scroller.Set(sb);
}

struct LineEdit::RefreshDraw : public NilDraw {
	Ctrl  *ctrl;
	bool (*chars)(int c);
	Size   fsz;
	virtual void DrawTextOp(int x, int y, int angle, const wchar *text, Font,
	                        Color, int n, const int *dx) {
		if(dx)
			while(n > 0) {
				if((*chars)(*text))
					ctrl->Refresh(x, y, fsz.cx, fsz.cy);
				text++;
				x += *dx++;
				n--;
			}
	}
	bool IsPaintingOp(const Rect& r) const {
		return true;
	}
};

void LineEdit::RefreshChars(bool (*chars)(int c))
{
	RefreshDraw rw;
	rw.ctrl = this;
	rw.fsz = GetFontSize();
	rw.chars = chars;
	Paint(rw);
}

void   LineEdit::Layout() {
	SyncFont();
	Size sz = sb.GetReducedViewSize();
	if(nohbar || isdrag)
		sz.cy = GetSize().cy;
	sb.SetPage(sz / GetFontSize());
	SetHBar();
}

int64  LineEdit::GetGPos(int ln, int cl) const {
	ln = minmax(ln, 0, GetLineCount() - 1);
	String h = GetUtf8Line(ln);
	const char *s = h.begin();
	const char *e = h.end();
	const char *b = s;
	int gl = 0;
	int wpos = 0;
	while(s < e) {
		if(*s == '\t')
			gl = (gl + tabsize) / tabsize * tabsize;
		else
		if((byte)*s < 128)
			gl++;
		else {
			WString txt = ToUtf32(s, int(e - s));
			const wchar *b = txt;
			const wchar *e = txt.End();
			const wchar *s = b;
			while(s < e) {
				if(*s == '\t')
					gl = (gl + tabsize) / tabsize * tabsize;
				else
					gl += 1 + IsDoubleChar(*s);
				if(cl < gl) break;
				s++;
			}
			wpos = int(s - b);
			break;
		}
		if(cl < gl) break;
		s++;
	}
	
	return GetPos64(ln, int(s - b) + wpos);
}

Point LineEdit::GetColumnLine(int64 pos) const {
	Point p;
	if(pos > GetLength64()) pos = GetLength64();
	p.y = GetLinePos64(pos);
	p.x = 0;
	WString txt = GetWLine(p.y);
	const wchar *s = txt;
	while(pos--) {
		if(*s == '\t')
			p.x = (p.x + tabsize) / tabsize * tabsize;
		else
			p.x += 1 + IsDoubleChar(*s);
		s++;
	}
	return p;
}

Point LineEdit::GetIndexLine(int64 pos) const
{
	Point p;
	if(pos > GetLength64()) pos = GetLength64();
	p.y = GetLinePos64(pos);
	p.x = minmax((int)pos, 0, GetLineLength(p.y));
	return p;
}

int64 LineEdit::GetIndexLinePos(Point pos) const
{
	if(pos.y < 0)
		return 0;
	if(pos.y >= GetLineCount())
		return GetLength64();
	return GetPos64(pos.y, minmax(pos.x, 0, GetLineLength(pos.y)));
}

void LineEdit::RefreshLine(int i) {
	Size sz = GetSize();
	int fcy = GetFontSize().cy;
	Refresh(0, (i - sb.Get().y) * fcy, sz.cx, fcy);
}

Rect LineEdit::GetLineScreenRect(int line) const {
	int fcy = GetFontSize().cy;
	Rect r = RectC(0, (line - sb.Get().y) * fcy, GetSize().cx, fcy);
	r.Offset(GetScreenView().TopLeft());
	return r;
}

void LineEdit::SetSb() {
	sb.SetTotalY(GetLineCount());
	SetHBar();
}

void LineEdit::NewScrollPos() {}
void LineEdit::HighlightLine(int line, Vector<Highlight>& h, int64 pos) {}

void LineEdit::AlignChar() {
	int c = GetCursor32();
	if(c == 0)
		return;
	Point pos = GetColumnLine(c);
	if(pos.x == 0)
		return;
	for(int d = 1; d <= pos.y && d < 100; d++) {
		int lny = pos.y - d;
		WString above = GetWLine(lny);
		int offset = int(GetGPos(lny, pos.x) - GetPos64(lny));
		int end = offset;
		char ch = GetChar(c - 1);
		if(ch == ' ')
		{
			offset++;
			while(end < above.GetLength() && above[end] != ' ')
				end++;
			while(end < above.GetLength() && above[end] == ' ')
				end++;
		}
		else
			while(end < above.GetLength() && above[end] != ch)
				end++;
		if(end < above.GetLength()) {
			int count = end - offset + 1;
			WString s(' ', count);
			Insert(c - 1, s, true);
			SetCursor(c + count);
			return;
		}
	}
}

Rect LineEdit::GetCaret() const
{
	if(overwrite && !IsRectSelection())
		return RectC(caretpos.x, caretpos.y + fsz.cy - 2, fsz.cx, 2);
	else
		return RectC(caretpos.x, caretpos.y, block_caret? fsz.cx : 2, fsz.cy * caretlines);
}

void LineEdit::PlaceCaret0()
{
	Point p = GetColumnLine(cursor);
	Size fsz = GetFontSize();
	p -= sb;
	caretlines = 1;
	if(IsRectSelection()) {
		Point ap = GetColumnLine(anchor) - sb;
		if(ap.y < p.y)
			Swap(ap.y, p.y);
		caretlines = ap.y - p.y + 1;
	}
	caretpos = Point(p.x * fsz.cx, p.y * fsz.cy);
}

int LineEdit::PlaceCaretNoG(int64 newcursor, bool sel) {
	if(newcursor > GetLength64()) newcursor = GetLength64();
	Point p = GetColumnLine(newcursor);
	if(sel) {
		if(anchor < 0) {
			anchor = cursor;
		}
		if(rectsel || rectsel != dorectsel)
			Refresh();
		else
			RefreshLines(p.y, GetLine(cursor));
		rectsel = dorectsel;
	}
	else {
		if(anchor >= 0) {
			if(rectsel || dorectsel)
				Refresh();
			else
				RefreshLines(GetLine(cursor), GetLine(anchor));
			anchor = -1;
		}
		rectsel = false;
	}
	RefreshLine(GetColumnLine(cursor).y);
	RefreshLine(p.y);
	cursor = newcursor;
	ScrollIntoCursor();
	PlaceCaret0();
	SelectionChanged();
	WhenSel();
	if(IsAnySelection())
		SetSelectionSource(ClipFmtsText());
	if(!IsNull(hline)) {
		Size sz = GetSize();
		Refresh(vlinex, 0, 1, sz.cy);
		Refresh(caretpos.x, 0, 1, sz.cy);
	}
	return p.x;
}

void LineEdit::PlaceCaret(int64 newcursor, bool sel) {
	gcolumn = PlaceCaretNoG(newcursor, sel);
}

void LineEdit::TopCursor(int lines)
{
	sb.SetY(max(0, GetLine(cursor) - lines));
}

void LineEdit::CenterCursor() {
	int cy = sb.GetPage().cy;
	if(cy > 4)
		sb.SetY(max(min(GetLine(cursor) - cy / 2, GetLineCount() - cy), 0));
}

void LineEdit::Scroll() {
	PlaceCaret0();
	scroller.Scroll(*this, GetSize(), sb.Get(), GetFontSize());
	SetHBar();
	NewScrollPos();
	WhenScroll();
}

int64 LineEdit::GetMousePos(Point p) const {
	Size fsz = GetFontSize();
	p = (p + fsz.cx / 2 + fsz * (Size)sb.Get()) / fsz;
	return GetGPos(p.y, p.x);
}

void LineEdit::LeftDown(Point p, dword flags) {
	mpos = GetMousePos(p);
	int64 l, h;
	if(GetSelection(l, h) && mpos >= l && mpos < h) {
		selclick = true;
		return;
	}
	dorectsel = flags & K_ALT;
	PlaceCaret(mpos, (flags & K_SHIFT) || dorectsel);
	dorectsel = false;
	SetFocus();
	SetCapture();
}

void LineEdit::LeftUp(Point p, dword flags)
{
	WhenLeftUp();
	if(!HasCapture() && selclick && !IsDragAndDropSource()) {
		mpos = GetMousePos(p);
		PlaceCaret(mpos, flags & K_SHIFT);
		SetFocus();
	}
	selclick = false;
	ReleaseCapture();
}

void LineEdit::RightDown(Point p, dword flags)
{
	mpos = GetMousePos(p);
	SetFocus();
	int64 l, h;
	GetSelection(l, h);
	if(!IsAnySelection() || !(mpos >= l && mpos < h))
		PlaceCaret(mpos, false);
	MenuBar::Execute(WhenBar);
}

void LineEdit::LeftDouble(Point, dword)
{
	int64 l, h;
	if(GetWordSelection(cursor, l, h))
		SetSelection(l, h);
}

void LineEdit::LeftTriple(Point, dword)
{
	int64 q = cursor;
	int i = GetLinePos64(q);
	q = cursor - q;
	SetSelection(q, q + GetLineLength(i) + 1);
}

void LineEdit::MouseMove(Point p, dword flags) {
	if((flags & K_MOUSELEFT) && HasFocus() && HasCapture()) {
		int64 c = GetMousePos(p);
		dorectsel = flags & K_ALT;
		PlaceCaret(c, mpos != c || HasCapture());
		dorectsel = false;
	}
}

void LineEdit::LeftRepeat(Point p, dword flags) {
	if(HasCapture()) {
		int64 c = GetMousePos(p);
		if(mpos != c) {
			dorectsel = flags & K_ALT;
			PlaceCaret(c, true);
			dorectsel = false;
		}
	}
}

Image LineEdit::CursorImage(Point, dword) {
	return Image::IBeam();
}

void LineEdit::MoveUpDown(int n, bool sel) {
	int64 cl = cursor;
	int ln = GetLinePos64(cl);
	if(ln + n >= GetLineCount())
		WaitView(ln + n);
	ln = minmax(ln + n, 0, GetLineCount() - 1);
	PlaceCaretNoG(GetGPos(ln, gcolumn), sel);
}

void LineEdit::MoveLeft(bool sel) {
	if(cursor)
		PlaceCaret(cursor - 1, sel);
}

void LineEdit::MoveRight(bool sel) {
	if(cursor < GetLength64())
		PlaceCaret(cursor + 1, sel);
}

void LineEdit::MoveUp(bool sel) {
	MoveUpDown(-1, sel);
}

void LineEdit::MoveDown(bool sel) {
	MoveUpDown(1, sel);
}

void LineEdit::MovePage(int dir, bool sel) {
	int n = dir * max(GetSize().cy / GetFontSize().cy - 2, 2);
	sb.SetY(Point(sb).y + n);
	MoveUpDown(n, sel);
}

void LineEdit::MovePageUp(bool sel) {
	MovePage(-1, sel);
}

void LineEdit::MovePageDown(bool sel) {
	MovePage(1, sel);
}

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

void LineEdit::MoveHome(bool sel) {
	int64 cl = cursor;
	int li = GetLinePos64(cl);
	int i = 0;
	WString l = GetWLine(li);
	while(sTabSpace(l[i]))
		i++;
	PlaceCaret(GetPos64(li, cl == i ? 0 : i), sel);
}

void LineEdit::MoveEnd(bool sel) {
	int i = GetLine(cursor);
	PlaceCaret(GetPos64(i, GetLineLength(i)), sel);
}

void LineEdit::MoveTextBegin(bool sel) {
	PlaceCaret(0, sel);
}

void LineEdit::MoveTextEnd(bool sel) {
	WaitView(INT_MAX, true);
	PlaceCaret(GetLength64(), sel);
}

bool LineEdit::InsertChar(dword key, int count, bool canow) {
	if(key == K_TAB && !processtab)
		return false;
	if(findarg(key, '\n', K_ENTER) >= 0 && IsRectSelection())
		return true;
	if(filter && key >= 32 && key < K_CHAR_LIM)
		key = (*filter)(key);
	if(!IsReadOnly() && (key >= 32 && key < K_CHAR_LIM || key == '\t' || key == '\n' ||
	   key == K_ENTER && processenter || key == K_SHIFT_SPACE)) {
		if(key >= 128 && key < K_CHAR_LIM && (charset != CHARSET_UTF8 && charset != CHARSET_UTF8_BOM)
		   && FromUnicode((wchar)key, charset) == DEFAULTCHAR)
			return true;
		if(IsRectSelection()) {
			RectSelectionChar(key);
			return true;
		}
		if(!RemoveSelection() && overwrite && key != '\n' && key != K_ENTER && canow) {
			int64 q = cursor;
			int i = GetLinePos64(q);
			if(q + count - 1 < GetLineLength(i))
				Remove((int)cursor, (int)count);
		}
		WString text(key == K_ENTER ? '\n' : key == K_SHIFT_SPACE ? ' ' : key, count);
		Insert((int)cursor, text, true);
		PlaceCaret(cursor + count);
		Action();
		return true;
	}
	return false;
}

void LineEdit::DeleteChar() {
	if(IsReadOnly() || RemoveSelection()) {
		Action();
		return;
	}
	if(cursor < GetLength32()) {
		Remove((int)cursor, 1);
		Action();
	}
}

void LineEdit::Backspace() {
	if(IsReadOnly() || RemoveSelection() || cursor == 0) return;
	MoveLeft();
	DeleteChar();
	Action();
}

void LineEdit::DeleteLine()
{
	int64 b, e;
	if(GetSelection(b, e) && GetLine(b) != GetLine(e)) {
		RemoveSelection();
		return;
	}
	int i = GetLine(cursor);
	int p = GetPos32(i);
	Remove(p, GetLineLength(i) + 1);
	PlaceCaret(p);
	Action();
}

void LineEdit::CutLine()
{
	if(IsReadOnly()) return;
	int64 b, e;
	if(GetSelection(b, e) && GetLine(b) != GetLine(e)) {
		Cut();
		return;
	}
	int i = GetLine(cursor);
	int p = GetPos32(i);
	WString txt = Get(p, GetLineLength(i) + 1).ToWString();
	WriteClipboardUnicodeText(txt);
	AppendClipboardText(txt.ToString());
	ClearSelection();
	DeleteLine();
}

LineEdit::EditPos LineEdit::GetEditPos() const {
	EditPos pos;
	pos.sby = sb.Get().y;
	pos.cursor = cursor;
	return pos;
}

void LineEdit::SetEditPos(const LineEdit::EditPos& pos) {
	SetEditPosSbOnly(pos);
	SetCursor(pos.cursor);
}

void LineEdit::SetEditPosSb(const LineEdit::EditPos& pos) {
	SetCursor(pos.cursor);
	SetEditPosSbOnly(pos);
}

void LineEdit::SetEditPosSbOnly(const LineEdit::EditPos& pos) {
	sb.SetY(minmax(pos.sby, 0, GetLineCount() - 1));
}

void LineEdit::SetHBar()
{
	int mpos = 0;
	if(!nohbar && !isdrag) {
		int m = min(sb.y + sb.GetPage().cy + 2, GetLineCount());
		for(int i = sb.y; i < m; i++) {
			int pos = 0;
			WString l = GetWLine(i);
			const wchar *s = l;
			const wchar *e = l.End();
			while(s < e) {
				if(*s == '\t')
					pos = (pos + tabsize) / tabsize * tabsize;
				else
					pos += 1 + IsDoubleChar(*s);
				s++;
			}
			mpos = max(mpos, pos);
		}
	}
	sb.SetTotalX(mpos + 1);
}

void LineEdit::ScrollIntoCursor()
{
	Point p = GetColumnLine(GetCursor64());
	sb.ScrollInto(p);
	SetHBar();
	sb.ScrollInto(p);
}

bool LineEdit::Key(dword key, int count) {
	NextUndo();
	switch(key) {
	case K_CTRL_UP:
		ScrollUp();
		return true;
	case K_CTRL_DOWN:
		ScrollDown();
		return true;
	case K_INSERT:
		OverWriteMode(!IsOverWriteMode());
		break;
	}
	bool sel = key & K_SHIFT;
	dorectsel = key & K_ALT;
	if(IsRectSelection() && sel)
		dorectsel = true;
	dword k = key & ~K_SHIFT;
	if((key & (K_SHIFT|K_ALT)) == (K_SHIFT|K_ALT))
		k &= ~K_ALT;
	switch(k) {
	case K_CTRL_LEFT:
		{
			PlaceCaret(GetPrevWord(cursor), sel);
			break;
		}
	case K_CTRL_RIGHT:
		{
			PlaceCaret(GetNextWord(cursor), sel);
			break;
		}
	case K_LEFT:
		if(IsRectSelection() && !dorectsel) {
			RectSelectionLeftRight(-1, false);
			break;
		}
		MoveLeft(sel);
		break;
	case K_RIGHT:
		if(IsRectSelection() && !dorectsel) {
			RectSelectionLeftRight(1, false);
			break;
		}
		MoveRight(sel);
		break;
	case K_HOME:
		if(IsRectSelection() && !dorectsel) {
			RectSelectionLeftRight(-1, true);
			break;
		}
		MoveHome(sel);
		break;
	case K_END:
		if(IsRectSelection() && !dorectsel) {
			RectSelectionLeftRight(1, true);
			break;
		}
		MoveEnd(sel);
		break;
	case K_UP:
		MoveUp(sel);
		break;
	case K_DOWN:
		MoveDown(sel);
		break;
	case K_PAGEUP:
		MovePageUp(sel);
		break;
	case K_PAGEDOWN:
		MovePageDown(sel);
		break;
	case K_CTRL_PAGEUP:
	case K_CTRL_HOME:
		MoveTextBegin(sel);
		break;
	case K_CTRL_PAGEDOWN:
	case K_CTRL_END:
		MoveTextEnd(sel);
		break;
	case K_CTRL_C:
	case K_CTRL_INSERT:
		Copy();
		break;
	case K_CTRL_A:
		SelectAll();
		break;
	case K_ALT_KEY|K_KEYUP:
		return IsRectSelection(); // prevent opening menu on Alt+Click
	default:
		dorectsel = false;
		if(IsReadOnly())
			return MenuBar::Scan(WhenBar, key);
		switch(key) {
		case K_DELETE:
			if(IsRectSelection() && !dorectsel) {
				RectSelectionDelete();
				break;
			}
			DeleteChar();
			break;
		case K_BACKSPACE:
			if(IsRectSelection() && !dorectsel) {
				RectSelectionBackspace();
				break;
			}
		case K_SHIFT|K_BACKSPACE:
			Backspace();
			break;
		case K_SHIFT_TAB:
			AlignChar();
			break;
		case K_CTRL_Y:
		case K_CTRL_L:
			if(cutline) {
				CutLine();
				break;
			}
		default:
			if(InsertChar(key, count, true))
				return true;
			return MenuBar::Scan(WhenBar, key);
		}
		return true;
	}
	dorectsel = false;
	Sync();
	return true;
}

void LineEdit::DragAndDrop(Point p, PasteClip& d)
{
	if(IsReadOnly()) return;
	int c = GetMousePos32(p);
	if(AcceptText(d)) {
		NextUndo();
		int a = sb.y;
		int sell, selh;
		WString text = GetWString(d);
		if(GetSelection32(sell, selh)) {
			if(c >= sell && c < selh) {
				if(!IsReadOnly())
					RemoveSelection();
				if(IsDragAndDropSource())
					d.SetAction(DND_COPY);
				c = sell;
			}
			else
			if(d.GetAction() == DND_MOVE && IsDragAndDropSource()) {
				if(c > sell)
					c -= selh - sell;
				if(!IsReadOnly())
					RemoveSelection();
				d.SetAction(DND_COPY);
			}
		}
		int count = Insert(c, text);
		sb.y = a;
		SetFocus();
		SetSelection(c, c + count);
		Action();
		return;
	}
	if(!d.IsAccepted()) return;
	if(!isdrag) {
		isdrag = true;
		ScrollIntoCursor();
	}
	Point dc = Null;
	if(c >= 0)
		dc = GetColumnLine(c);
	if(dc != dropcaret) {
		RefreshDropCaret();
		dropcaret = dc;
		RefreshDropCaret();
	}
}

Rect LineEdit::DropCaret()
{
	if(IsNull(dropcaret))
		return Rect(0, 0, 0, 0);
	Size fsz = GetFontSize();
	Point p = dropcaret - sb;
	p = Point(p.x * fsz.cx, p.y * fsz.cy);
	return RectC(p.x, p.y, 1, fsz.cy);
}

void LineEdit::RefreshDropCaret()
{
	Refresh(DropCaret());
}

void LineEdit::DragRepeat(Point p)
{
	sb.y = (int)sb.y + GetDragScroll(this, p, 1).y;
}

void LineEdit::DragLeave()
{
	RefreshDropCaret();
	dropcaret = Null;
	isdrag = false;
	Layout();
}

void LineEdit::LeftDrag(Point p, dword flags)
{
	int64 c = GetMousePos(p);
	int64 l, h;
	if(!HasCapture() && GetSelection(l, h) && c >= l && c < h) {
		WString sample = GetW(l, (int)min(h - l, (int64)3000));
		Size sz = StdSampleSize();
		ImageDraw iw(sz);
		iw.DrawRect(sz, Black());
		iw.Alpha().DrawRect(sz, Black());
		DrawTLText(iw.Alpha(), 0, 0, 9999, sample, CourierZ(10), White());
		NextUndo();
		if(DoDragAndDrop(ClipFmtsText(), iw) == DND_MOVE && !IsReadOnly()) {
			RemoveSelection();
			Action();
		}
	}
}

}
