#include "selection.h"
#include <buffer/buffer.h>
#include <bundles/bundles.h>
#include <regexp/find.h>
#include <regexp/regexp.h>
#include <text/classification.h>
#include <text/utf8.h>
#include <text/ctype.h>
#include <text/types.h>
#include <text/tokenize.h>
#include <crash/info.h>

namespace ng
{
	static size_t count_columns (buffer_api_t const& buffer, index_t caret)
	{
		size_t const tabSize = buffer.indent().tab_size();
		std::string const str = buffer.substr(buffer.begin(buffer.convert(caret.index).line), caret.index);
		size_t len = 0;
		citerate(ch, diacritics::make_range(str.data(), str.data() + str.size()))
			len += *ch == '\t' ? tabSize - (len % tabSize) : (text::is_east_asian_width(*ch) ? 2 : 1);
		return len + caret.carry;
	}

	static index_t at_column (buffer_api_t const& buffer, size_t line, size_t column)
	{
		size_t const tabSize = buffer.indent().tab_size();
		size_t caret = buffer.begin(line), len = 0;
		std::string const str = buffer.substr(caret, buffer.eol(line));
		citerate(ch, diacritics::make_range(str.data(), str.data() + str.size()))
		{
			if(len == column)
				return caret + (&ch - str.data());

			size_t chWidth = *ch == '\t' ? tabSize - (len % tabSize) : (text::is_east_asian_width(*ch) ? 2 : 1);
			if(len + chWidth > column || *ch == '\n')
				return index_t(caret + (&ch - str.data()), column - len);

			len += chWidth;
		}
		return index_t(caret + str.size(), column - len);
	}

	std::string const kCharacterClassWord    = "word";
	std::string const kCharacterClassSpace   = "space";
	std::string const kCharacterClassOther   = "other";
	std::string const kCharacterClassUnknown = "unknown";

	std::string character_class (buffer_api_t const& buffer, size_t index)
	{
		crash_reporter_info_t crashInfo(text::format("find word at %zu in buffer of size %zu", index, buffer.size()));

		bundles::item_ptr match;
		plist::any_t value = bundles::value_for_setting("characterClass", buffer.scope(index), &match);
		if(match)
			return boost::get<std::string>(value);
		else if(text::is_word_char(buffer[index]))
			return kCharacterClassWord;
		else
		{
			value = bundles::value_for_setting("wordCharacters", buffer.scope(index), &match);
			if(match && boost::get<std::string>(value).find(buffer[index]) != std::string::npos)
				return kCharacterClassWord;
			else if(text::is_whitespace(buffer[index]))
				return kCharacterClassSpace;
		}
		return kCharacterClassOther;
	}

	static bool is_part_of_word (buffer_api_t const& buffer, size_t index)
	{
		return character_class(buffer, index) != kCharacterClassSpace && character_class(buffer, index) != kCharacterClassOther;
	}

	static size_t extend_scope_left (buffer_api_t const& buffer, size_t caret, scope::scope_t const& scope)
	{
		while(caret && buffer.scope(caret).left.has_prefix(scope))
			caret -= buffer[caret-1].size();
		return caret;
	}

	static size_t extend_scope_right (buffer_api_t const& buffer, size_t caret, scope::scope_t const& scope)
	{
		while(caret < buffer.size() && buffer.scope(caret).right.has_prefix(scope))
			caret += buffer[caret].size();
		return caret;
	}

	ranges_t sanitize (buffer_api_t const& buffer, ranges_t const& selection)
	{
		/* This function will transform the selection so that
		   all indexes are on proper multi-byte boundaries and
		   ensure that no ranges overlap. The latter should
		   probably not be done here, but instead prior to
		   edits (and drawing should take overlap into account).

		   The main reason is that the user may temporarily
		   create overlapping ranges, and if we fix them,
		   successive selection adjustment commands will not
		   work as expected.

		   It should also be noted that for column selections
		   we are not able to ensure that there is no overlap.
		*/

		struct indexed_range_t
		{
			range_t range;
			size_t index;

			indexed_range_t (range_t const& range, size_t const& index) : range(range), index(index) { }

			bool operator< (indexed_range_t const& rhs) const
			{
				return as_tuple() < rhs.as_tuple();
			}

		private:
			std::tuple<size_t, size_t, size_t, size_t, size_t> as_tuple () const
			{
				size_t col = range.columnar ? 1 : 0;
				size_t min = range.min().index;
				size_t off = range.freehanded ? range.min().carry : 0;
				size_t max = SIZE_T_MAX - range.max().index;
				size_t mOff = range.freehanded ? range.max().carry : 0;
				return std::make_tuple(col, min, off, max, mOff);
			}
		};

		size_t index = 0;
		std::set<indexed_range_t> set;
		for(auto range : selection)
			set.emplace(range_t(index_t(buffer.sanitize_index(range.first.index), range.first.carry), index_t(buffer.sanitize_index(range.last.index), range.last.carry), range.columnar, range.freehanded, range.unanchored, range.color), index++);

		index_t last;
		std::map<size_t, range_t> map;
		for(auto record : set)
		{
			range_t range = record.range;
			auto max = range.normalized().max();

			if(!last || range.columnar)
			{
				map.emplace(record.index, range);
				last = max;
			}
			else if(last < max || last == max && range.empty())
			{
				auto min = range.normalized().min();
				if(min < last)
					range.min() = last;
				map.emplace(record.index, range);
				last = max;
			}
		}

		ranges_t res;
		for(auto pair : map)
			res.push_back(pair.second);

		return res;
	}

	static index_t cap (buffer_api_t const& buffer, text::pos_t const& pos)
	{
		size_t line = oak::cap<size_t>(0, pos.line,   buffer.lines()-1);
		size_t col  = oak::cap<size_t>(0, pos.column, buffer.eol(line) - buffer.begin(line));
		index_t res = buffer.sanitize_index(buffer.convert(text::pos_t(line, col)));
		if(pos.offset && res.index < buffer.size() && buffer[res.index] == "\n")
			res.carry = pos.offset;
		return res;
	}

	static range_t cap (buffer_api_t const& buffer, text::range_t const& range)
	{
		return range_t(cap(buffer, range.from), cap(buffer, range.to), range.columnar, false, true);
	}

	ranges_t convert (buffer_api_t const& buffer, text::selection_t const& sel)
	{
		ranges_t res;
		for(auto const& range : sel)
			res.push_back(cap(buffer, range));
		return sanitize(buffer, res);
	}

	// =======================
	// = Typing Pair Support =
	// =======================

	namespace
	{
		struct pattern_t
		{
			pattern_t () { }

			pattern_t (std::string const& plain) : plain(plain)
			{
				if(plain.size() > 2 && plain.front() == '/' && plain.back() == '/')
				{
					left_anchored_regexp = "\\G" + plain.substr(1, plain.size()-2);
					right_anchored_regexp = plain.substr(1, plain.size()-2) + "\\G";
					is_regexp = true;
				}
			}

			std::string plain;
			regexp::pattern_t left_anchored_regexp;
			regexp::pattern_t right_anchored_regexp;
			bool is_regexp = false;
		};

		struct match_t
		{
			match_t (std::string const& match = NULL_STR, pattern_t const& counterpart_ptrn = pattern_t(), bool matched_opener = false) : match(match), counterpart_ptrn(counterpart_ptrn), matched_opener(matched_opener) { }
			operator bool () const { return match != NULL_STR; }
			std::string match;
			pattern_t counterpart_ptrn;
			bool matched_opener;
		};

		struct enclosed_range_t
		{
			enclosed_range_t () { }
			enclosed_range_t (std::pair<pattern_t, pattern_t> const& pair) : opener_ptrn(pair.first), closer_ptrn(pair.second) { }

			operator bool () const { return open_index != SIZE_T_MAX && close_index != 0; }

			pattern_t opener_ptrn, closer_ptrn;
			std::string opener_match, closer_match;
			size_t open_index = SIZE_T_MAX, close_index = 0;
			ssize_t open_count = 0, close_count = 0;
		};
	}

	static std::vector<std::pair<pattern_t, pattern_t>> character_pairs (scope::context_t const& scope, std::string const& key)
	{
		std::vector<std::pair<pattern_t, pattern_t>> res;
		plist::any_t value = bundles::value_for_setting(key, scope);
		if(plist::array_t const* array = boost::get<plist::array_t>(&value))
		{
			for(auto const& pair : *array)
			{
				if(plist::array_t const* value = boost::get<plist::array_t>(&pair))
				{
					std::string const* a1 = value->size() == 2 ? boost::get<std::string>(&(*value)[0]) : NULL;
					std::string const* a2 = value->size() == 2 ? boost::get<std::string>(&(*value)[1]) : NULL;
					if(a1 && a2 && *a1 != *a2)
						res.emplace_back(*a1, *a2);
				}
			}
		}
		else
		{
			res.emplace_back(pattern_t("("), pattern_t(")"));
			res.emplace_back(pattern_t("{"), pattern_t("}"));
			res.emplace_back(pattern_t("["), pattern_t("]"));
		}
		return res;
	}

	static bool does_match (regexp::pattern_t const& ptrn, buffer_api_t const& buffer, size_t from, size_t to, std::string* didMatch)
	{
		if(ptrn)
		{
			size_t bol = std::min(from, to), eol = std::max(from, to);
			std::string line = buffer.substr(bol, eol);
			if(auto m = regexp::search(ptrn, line.data(), line.data() + line.size(), line.data() + (from - bol), line.data() + (to - bol), ONIG_OPTION_FIND_NOT_EMPTY))
			{
				*didMatch = line.substr(m.begin(), m.end() - m.begin());
				return true;
			}
		}
		return false;
	}

	static bool does_match_left (pattern_t const& ptrn, buffer_api_t const& buffer, size_t index, std::string* didMatch)
	{
		if(!ptrn.is_regexp && ptrn.plain.size() <= index && ptrn.plain == buffer.substr(index - ptrn.plain.size(), index))
		{
			*didMatch = ptrn.plain;
			return true;
		}
		return ptrn.is_regexp && does_match(ptrn.right_anchored_regexp, buffer, index, buffer.begin(buffer.convert(index).line), didMatch);
	}

	static bool does_match_right (pattern_t const& ptrn, buffer_api_t const& buffer, size_t index, std::string* didMatch)
	{
		if(!ptrn.is_regexp && index + ptrn.plain.size() <= buffer.size() && ptrn.plain == buffer.substr(index, index + ptrn.plain.size()))
		{
			*didMatch = ptrn.plain;
			return true;
		}
		return ptrn.is_regexp && does_match(ptrn.left_anchored_regexp, buffer, index, buffer.eol(buffer.convert(index).line), didMatch);
	}

	static enclosed_range_t find_enclosed_range (buffer_api_t const& buffer, size_t index, std::vector<std::pair<pattern_t, pattern_t>> const& pairs)
	{
		std::vector<enclosed_range_t> records(pairs.begin(), pairs.end());

		size_t left = index, right = index;
		while(0 < left || right < buffer.size())
		{
			for(auto& r : records)
			{
				std::string match;
				if(0 < left && r.open_index == SIZE_T_MAX)
				{
					if(does_match_left(r.opener_ptrn, buffer, left, &match) && ++r.open_count == 1)
					{
						r.open_index = left;
						r.opener_match = match;
					}
					else if(does_match_left(r.closer_ptrn, buffer, left, &match))
					{
						--r.open_count;
					}
				}

				if(right < buffer.size() && r.close_index == 0)
				{
					if(does_match_right(r.closer_ptrn, buffer, right, &match) && ++r.close_count == 1)
					{
						r.close_index = right;
						r.closer_match = match;
					}
					else if(does_match_right(r.opener_ptrn, buffer, right, &match))
					{
						--r.close_count;
					}
				}

				if(r)
					return r;
			}

			if(0 < left)
				left -= buffer[left-1].size();
			if(right < buffer.size())
				right += buffer[right].size();
		}
		return enclosed_range_t();
	}

	static match_t first_match (buffer_api_t const& buffer, size_t index, std::vector<std::pair<pattern_t, pattern_t>> const& pairs, bool(*matcher)(pattern_t const&, buffer_api_t const&, size_t, std::string*))
	{
		std::string didMatch;
		for(auto const& pair : pairs)
		{
			if(matcher(pair.first, buffer, index, &didMatch))
				return match_t(didMatch, pair.second, true);
			else if(matcher(pair.second, buffer, index, &didMatch))
				return match_t(didMatch, pair.first, false);
		}
		return match_t();
	}

	static size_t begin_of_typing_pair (buffer_api_t const& buffer, size_t caret, bool moveToBefore)
	{
		size_t orgCaret = caret;
		auto pairs = character_pairs(buffer.scope(caret), "highlightPairs");

		pattern_t openerPtrn;
		if(!moveToBefore)
		{
			if(auto m = first_match(buffer, caret, pairs, &does_match_left))
			{
				openerPtrn = m.matched_opener ? pattern_t() : m.counterpart_ptrn;
				caret -= m.match.size();
			}
		}

		if(auto range = find_enclosed_range(buffer, caret, pairs))
		{
			caret = range.open_index;
			if(moveToBefore || does_match_left(openerPtrn, buffer, caret, &range.opener_match))
				caret -= range.opener_match.size();
			return caret;
		}

		return orgCaret;
	}

	static size_t end_of_typing_pair (buffer_api_t const& buffer, size_t caret, bool moveToAfter)
	{
		size_t orgCaret = caret;
		auto pairs = character_pairs(buffer.scope(caret), "highlightPairs");

		pattern_t closerPtrn;
		if(!moveToAfter)
		{
			if(auto m = first_match(buffer, caret, pairs, &does_match_right))
			{
				closerPtrn = m.matched_opener ? m.counterpart_ptrn : pattern_t();
				caret += m.match.size();
			}
		}

		if(auto range = find_enclosed_range(buffer, caret, pairs))
		{
			caret = range.close_index;
			if(moveToAfter || does_match_right(closerPtrn, buffer, caret, &range.closer_match))
				caret += range.closer_match.size();
			return caret;
		}

		return orgCaret;
	}

	// ====================
	// = Columnar Support =
	// ====================

	bool not_empty (buffer_api_t const& buffer, ranges_t const& selection)
	{
		bool isEmpty = true;
		for(auto const& range : selection)
			isEmpty = isEmpty && (range.empty() || (range.columnar && count_columns(buffer, range.first) == count_columns(buffer, range.last)));
		return !isEmpty;
	}

	bool multiline (buffer_api_t const& buffer, ranges_t const& selection)
	{
		bool isMultiline = false;
		for(auto const& range : selection)
			isMultiline = isMultiline || buffer.convert(range.first.index).line != buffer.convert(range.last.index).line;
		return isMultiline;
	}

	ranges_t dissect_columnar (buffer_api_t const& buffer, ranges_t const& selection)
	{
		ranges_t res;
		for(auto const& range : selection)
		{
			if(range.columnar && !range.empty())
			{
				size_t colA = count_columns(buffer, range.first);
				size_t colB = count_columns(buffer, range.last);

				size_t lineA = buffer.convert(range.first.index).line;
				size_t lineB = buffer.convert(range.last.index).line;

				size_t fromLine = std::min(lineA, lineB), fromCol = std::min(colA, colB);
				size_t toLine   = std::max(lineA, lineB), toCol   = std::max(colA, colB);

				for(size_t n = fromLine; n <= toLine; ++n)
				{
					index_t from = at_column(buffer, n, fromCol), to = at_column(buffer, n, toCol);
					res.push_back(range_t(from, to, false, from.carry || to.carry, range.unanchored, range.color));
				}
			}
			else
			{
				res.push_back(range_t(range.min(), range.max(), range.columnar, range.freehanded, range.unanchored, range.color));
			}
		}
		return res;
	}

	ranges_t toggle_columnar (ranges_t const& selection)
	{
		ranges_t res = selection;
		if(res.size() != 0)
		{
			range_t& last = res.last();
			last = range_t(last.first, last.last, !last.empty() && !last.columnar, last.freehanded);
		}
		return res;
	}

	ranges_t deselect_last (ranges_t const& selection)
	{
		if(selection.empty())
			return selection;

		if(selection.size() == 1)
		{
			range_t r = selection.first();
			r.freehanded = false;
			return r;
		}

		bool singleCaret = true;
		for(auto const& range : selection)
			singleCaret = singleCaret && range.empty();

		if(singleCaret)
			return selection.first();

		ranges_t res;
		auto last = --selection.end();
		for(auto range = selection.begin(); range != last; ++range)
			res.push_back(*range);

		return res;
	}

	// =================
	// = Move Caret(s) =
	// =================

	static bool all_spaces (buffer_api_t const& buffer, size_t from, size_t to)
	{
		bool allSpaces = true;
		while(from < to && allSpaces)
			allSpaces = allSpaces && buffer[from++] == " ";
		return allSpaces;
	}

	static bool all_whitespace (buffer_api_t const& buffer, size_t from, size_t to)
	{
		bool res = true;
		static std::string const whitespaceChars[] = { " ", "\t", "\n" };
		while(from < to && res)
			res = res && oak::contains(std::begin(whitespaceChars), std::end(whitespaceChars), buffer[from++]);
		return res;
	}

	static size_t end_of_leading_indent (buffer_api_t const& buffer, size_t line)
	{
		size_t bol = buffer.begin(line);
		size_t eol = buffer.eol(line);
		while(bol < eol && isspace(utf8::to_ch(buffer[bol])))
			++bol;
		return bol;
	}

	static index_t move (buffer_api_t const& buffer, index_t const& index, move_unit_type unit, layout_movement_t const* layout)
	{
		size_t const caret = index.index;
		size_t const line  = buffer.convert(caret).line;

		if((unit == kSelectionMoveLeft || unit == kSelectionMoveRight) && buffer.indent().soft_tabs())
		{
			size_t const bol = buffer.begin(line);
			if((caret - bol) % buffer.indent().indent_size() == 0)
			{
				if(all_spaces(buffer, bol, caret))
				{
					size_t const tabSize = buffer.indent().tab_size();
					if(unit == kSelectionMoveLeft && caret != bol)
						return caret - tabSize;
					else if(unit == kSelectionMoveRight && caret + tabSize <= buffer.eol(line) && all_spaces(buffer, caret, caret + tabSize))
						return caret + tabSize;
				}
			}
		}

		switch(unit)
		{
			case kSelectionMoveToBeginOfDocument:     return 0;
			case kSelectionMoveToEndOfDocument:       return buffer.size();
			case kSelectionMoveToBeginOfParagraph:    return buffer.begin(line);
			case kSelectionMoveToEndOfParagraph:      return buffer.eol(line);
			case kSelectionMoveToBeginOfLine:         return buffer.begin(line);
			case kSelectionMoveToEndOfLine:           return buffer.eol(line);
			case kSelectionMoveToBeginOfSoftLine:     return layout ? layout->index_at_bol_for(caret) : buffer.begin(line);
			case kSelectionMoveToEndOfSoftLine:       return layout ? layout->index_at_eol_for(caret) : buffer.eol(line);
			case kSelectionMoveUp:                    return layout ? layout->index_above(index) : (line ? at_column(buffer, line-1, count_columns(buffer, index)) : 0);
			case kSelectionMoveDown:                  return layout ? layout->index_below(index) : (line+1 < buffer.lines() ? at_column(buffer, line+1, count_columns(buffer, index)) : buffer.size());
			case kSelectionMoveLeft:                  return layout ? layout->index_left_of(caret)  : (caret ? caret - buffer[caret-1].size() : caret);
			case kSelectionMoveRight:                 return layout ? layout->index_right_of(caret) : (caret < buffer.size() ? caret + buffer[caret].size() : caret);
			case kSelectionMoveToBeginOfTypingPair:   return begin_of_typing_pair(buffer, caret, false);
			case kSelectionMoveToEndOfTypingPair:     return end_of_typing_pair(buffer, caret, false);
			case kSelectionMovePageUp:                return layout ? layout->page_up_for(index)   : index;
			case kSelectionMovePageDown:              return layout ? layout->page_down_for(index) : index;

			case kSelectionMoveToBeginOfIndentedLine:
			{
				size_t bol = buffer.begin(line);
				size_t eoi = end_of_leading_indent(buffer, line);
				return eoi < caret ? eoi : bol;
			}
			break;

			case kSelectionMoveToEndOfIndentedLine:
			{
				size_t eoi = end_of_leading_indent(buffer, line);
				size_t eol = buffer.eol(line);
				return caret < eoi ? eoi : eol;
			}
			break;

			case kSelectionMoveToBeginOfHardParagraph:
			{
				if(line == 0)
					return 0;

				for(size_t n = line-1; n > 0; --n)
				{
					size_t eol = buffer.eol(n);
					if(plist::is_true(bundles::value_for_setting("excludeFromParagraphSelection", buffer.scope(eol))))
						return buffer.begin(n+1);
					std::string const& line = buffer.substr(buffer.begin(n), eol);
					if(text::is_blank(line.data(), line.data() + line.size()))
						return buffer.begin(n+1);
				}
				return 0;
			}
			break;

			case kSelectionMoveToEndOfHardParagraph:
			{
				for(size_t n = line+1; n < buffer.lines(); ++n)
				{
					size_t eol = buffer.eol(n);
					if(plist::is_true(bundles::value_for_setting("excludeFromParagraphSelection", buffer.scope(eol))))
						return buffer.begin(n);
					std::string const& line = buffer.substr(buffer.begin(n), eol);
					if(text::is_blank(line.data(), line.data() + line.size()))
						return buffer.begin(n);
				}
				return buffer.size();
			}
			break;

			case kSelectionMoveFreehandedLeft:
			{
				if(index.carry != 0)
					return index_t(caret, index.carry - 1);
				else if(caret != 0 && buffer[caret-1] == "\t")
					return at_column(buffer, line, count_columns(buffer, caret) - 1);
				else
					return move(buffer, index, kSelectionMoveLeft, layout); // FIXME With soft tabs this might move a full tab stop.
			}
			break;

			case kSelectionMoveFreehandedRight:
			{
				if(caret == buffer.size() || buffer[caret] == "\n")
					return index_t(caret, index.carry + 1);
				else if(buffer[caret] == "\t")
					return at_column(buffer, line, count_columns(buffer, caret) + index.carry + 1);
				else
					return move(buffer, index, kSelectionMoveRight, layout); // FIXME With soft tabs this might move a full tab stop.
			}
			break;

			case kSelectionMoveToBeginOfWord:
			{
				size_t i = caret;
				size_t bol = buffer.begin(line);
				bol = i == bol && line != 0 ? buffer.begin(line-1) : bol;
				if(i == bol)
					return bol;

				std::string charType = character_class(buffer, i-1);
				while(bol < i && character_class(buffer, i-1) == charType)
					i -= buffer[i-1].size();

				if((charType == kCharacterClassSpace || charType == kCharacterClassOther) && bol < i && i + buffer[i].size() == caret)
				{
					std::string charType = character_class(buffer, i-1);
					while(bol < i && character_class(buffer, i-1) == charType)
						i -= buffer[i-1].size();
				}

				return i;
			}
			break;

			case kSelectionMoveToEndOfWord:
			{
				size_t i = caret;
				size_t eol = buffer.eol(line);
				eol = caret == eol && line+1 < buffer.lines() ? buffer.eol(line+1) : eol;
				if(caret == eol)
					return eol;

				std::string charType = character_class(buffer, i);
				while(i < eol && character_class(buffer, i) == charType)
					i += buffer[i].size();

				if((charType == kCharacterClassSpace || charType == kCharacterClassOther) && i < eol && i == caret + buffer[caret].size())
				{
					charType = character_class(buffer, i);
					while(i < eol && character_class(buffer, i) == charType)
						i += buffer[i].size();
				}

				return i;
			}
			break;

			case kSelectionMoveToBeginOfSubWord:
			{
				static regexp::pattern_t ptrn(
					    "(\\p{Upper}\\p{Lower}+"
					    "|\\p{Upper}+"
					    "|\\p{Lower}+"
					    ")"
					    "[^\\p{Upper}\\p{Lower}]?$"
					"|" "[^\\p{Alnum}]+$"
					"|" "[\\p{Digit}]+$"
					"|" "[^\\p{Digit}\\p{Upper}\\p{Lower}]+$"
				);

				size_t n = line && caret == buffer.begin(line) ? line-1 : line;
				std::string const& line = buffer.substr(buffer.begin(n), caret);
				if(regexp::match_t const& m = search(ptrn, line.data(), line.data() + line.size()))
					return buffer.begin(n) + m.begin();
			}
			break;

			case kSelectionMoveToEndOfSubWord:
			{
				static regexp::pattern_t ptrn("\\G([^\\p{Upper}\\p{Lower}]?("
					 "\\p{Upper}\\p{Upper}+?(?=\\p{Upper}\\p{Lower})" // NS‸String
					"|\\p{Upper}\\p{Upper}+"                          // NDEBUG‸
					"|\\p{Upper}\\p{Lower}*"                          // Camel‸Case || Camel‸_case
					"|\\p{Lower}+"                                    // camel‸Case || camel‸_case
					")"
					"|[^\\p{Alnum}]+"
					"|[\\p{Digit}]+"
					"|[^\\p{Digit}\\p{Upper}\\p{Lower}]+"
					")");

				size_t n = line+1 < buffer.lines() && caret == buffer.eol(line) ? line+1 : line;
				std::string const& line = buffer.substr(caret, buffer.eol(n));
				if(regexp::match_t const& m = search(ptrn, line.data(), line.data() + line.size()))
					return caret + m.end();
			}
			break;

			case kSelectionMoveToBeginOfColumn:
			{
				size_t index = caret;
				if(index == buffer.size() || !is_part_of_word(buffer, index))
				{
					while(buffer.convert(index).column && !is_part_of_word(buffer, index-1))
						index -= buffer[index-1].size();
				}
				while(buffer.convert(index).column && is_part_of_word(buffer, index-1))
					index -= buffer[index-1].size();

				size_t orgCol = count_columns(buffer, caret);
				size_t col    = count_columns(buffer, index);
				size_t n      = line;
				for(; n != 0; --n)
				{
					index_t newIndex = at_column(buffer, n-1, col);
					if(newIndex.carry || (col && is_part_of_word(buffer, newIndex.index-1)) || !is_part_of_word(buffer, newIndex.index))
						break;
				}

				if(n && n == line)
				{
					while(--n != 0)
					{
						index_t newIndex = at_column(buffer, n, orgCol);
						if(!newIndex.carry && ((orgCol && is_part_of_word(buffer, newIndex.index-1)) || is_part_of_word(buffer, newIndex.index)))
							break;
					}
				}

				return at_column(buffer, n, orgCol);
			}
			break;

			case kSelectionMoveToEndOfColumn:
			{
				size_t index = caret;
				if(index == buffer.size() || !is_part_of_word(buffer, index))
				{
					while(buffer.convert(index).column && !is_part_of_word(buffer, index-1))
						index -= buffer[index-1].size();
				}
				while(buffer.convert(index).column && is_part_of_word(buffer, index-1))
					index -= buffer[index-1].size();

				size_t orgCol = count_columns(buffer, caret);
				size_t col    = count_columns(buffer, index);
				size_t n      = line;
				for(; n+1 != buffer.lines(); ++n)
				{
					index_t newIndex = at_column(buffer, n+1, col);
					if(newIndex.carry || newIndex.index == buffer.size() || (col && is_part_of_word(buffer, newIndex.index-1)) || !is_part_of_word(buffer, newIndex.index))
						break;
				}

				if(n+1 != buffer.lines() && n == line)
				{
					while(++n != buffer.lines()-1)
					{
						index_t newIndex = at_column(buffer, n, orgCol);
						if(!newIndex.carry && newIndex.index != buffer.size() && ((orgCol && is_part_of_word(buffer, newIndex.index-1)) || is_part_of_word(buffer, newIndex.index)))
							break;
					}
				}

				return at_column(buffer, n, orgCol);
			}
			break;
		}
		return index;
	}

	ranges_t move (buffer_api_t const& buffer, ranges_t const& selection, move_unit_type const orgUnit, layout_movement_t const* layout)
	{
		static std::set<move_unit_type> const leftward  = { kSelectionMoveLeft,  kSelectionMoveFreehandedLeft, kSelectionMoveToBeginOfLine, kSelectionMoveToBeginOfParagraph, kSelectionMoveToBeginOfTypingPair, kSelectionMoveToBeginOfSoftLine, kSelectionMoveToBeginOfSubWord, kSelectionMoveToBeginOfWord };
		static std::set<move_unit_type> const rightward = { kSelectionMoveRight, kSelectionMoveFreehandedRight, kSelectionMoveToEndOfLine, kSelectionMoveToEndOfParagraph, kSelectionMoveToEndOfTypingPair, kSelectionMoveToEndOfSoftLine, kSelectionMoveToEndOfSubWord, kSelectionMoveToEndOfWord            };

		bool isLeftward  = leftward.find(orgUnit)  != leftward.end();
		bool isRightward = rightward.find(orgUnit) != rightward.end();

		ranges_t res;
		for(auto const& range : isLeftward || isRightward ? dissect_columnar(buffer, selection) : selection)
		{
			move_unit_type unit = orgUnit;
			index_t index       = range.last;
			bool freehanded     = range.freehanded;

			if(!range.empty())
			{
				if(isLeftward || isRightward)
				{
					index = isLeftward ? range.min() : range.max();

					static std::set<move_unit_type> const left_right = { kSelectionMoveLeft,  kSelectionMoveRight, kSelectionMoveFreehandedRight, kSelectionMoveFreehandedLeft                                                                                                           };
					if(left_right.find(unit) != left_right.end())
						unit = kSelectionMoveNowhere;
				}
				else if(!range.columnar && (unit == kSelectionMoveUp || unit == kSelectionMoveDown) && buffer.convert(range.first.index).line != buffer.convert(range.last.index).line)
				{
					index = unit == kSelectionMoveUp ? range.min() : range.max();
					if(unit == kSelectionMoveDown && buffer.begin(buffer.convert(index.index).line) == index.index)
						unit = kSelectionMoveNowhere;
				}
			}

			if(freehanded)
			{
				if(unit == kSelectionMoveLeft)
					unit = kSelectionMoveFreehandedLeft;
				else if(unit == kSelectionMoveRight)
					unit = kSelectionMoveFreehandedRight;
			}

			switch(unit)
			{
				case kSelectionMoveToBeginOfSelection: index = range.min();                      break;
				case kSelectionMoveToEndOfSelection:   index = range.max();                      break;
				default:                               index = move(buffer, index, unit, layout); break;
			}

			if(freehanded && orgUnit == kSelectionMoveLeft && range.empty() && buffer.convert(index.index).line + 1 == buffer.convert(range.last.index).line)
				freehanded = false;

			res.push_back(range_t(index, index, false, freehanded));
		}

		if(res.size() > 1 && (orgUnit == kSelectionMoveUp || orgUnit == kSelectionMoveDown))
		{
			std::set<size_t> lines;
			for(auto const& range : res)
				lines.insert(buffer.convert(range.first.index).line);

			if(lines.size() > 1)
			{
				index_t min(SIZE_T_MAX), max(0);
				for(auto const& range : res)
				{
					min = std::min(min, range.min());
					max = std::max(max, range.max());
				}
				res = orgUnit == kSelectionMoveUp ? min : max;
			}
		}

		return sanitize(buffer, res);
	}

	// ====================
	// = Extend Selection =
	// ====================

	static range_t extend (buffer_api_t const& buffer, range_t const& range, select_unit_type unit, layout_movement_t const* layout)
	{
		index_t first      = range.first;
		index_t last       = range.last;
		index_t const& min = range.min();
		index_t const& max = range.max();

		if(first == last && !range.freehanded)
			first.carry = last.carry = 0;

		if(range.unanchored)
		{
			static std::set<select_unit_type> towardBegin = { kSelectionExtendLeft, kSelectionExtendFreehandedLeft, kSelectionExtendUp, kSelectionExtendToBeginOfWord, kSelectionExtendToBeginOfSubWord, kSelectionExtendToBeginOfSoftLine, kSelectionExtendToBeginOfIndentedLine, kSelectionExtendToBeginOfLine, kSelectionExtendToBeginOfParagraph, kSelectionExtendToBeginOfColumn, kSelectionExtendToBeginOfDocument, kSelectionExtendPageUp };
			static std::set<select_unit_type> towardEnd   = { kSelectionExtendRight, kSelectionExtendFreehandedRight, kSelectionExtendDown, kSelectionExtendToEndOfWord, kSelectionExtendToEndOfSubWord, kSelectionExtendToEndOfSoftLine, kSelectionExtendToEndOfIndentedLine, kSelectionExtendToEndOfLine, kSelectionExtendToEndOfParagraph, kSelectionExtendToEndOfColumn, kSelectionExtendToEndOfDocument, kSelectionExtendPageDown };

			if(first < last && towardBegin.find(unit) != towardBegin.end())
				std::swap(first, last);
			else if(last < first && towardEnd.find(unit) != towardEnd.end())
				std::swap(first, last);
		}

		if(range.freehanded || range.columnar)
		{
			if(unit == kSelectionExtendLeft)
				unit = kSelectionExtendFreehandedLeft;
			else if(unit == kSelectionExtendRight)
				unit = kSelectionExtendFreehandedRight;
		}

		switch(unit)
		{
			case kSelectionExtendLeft:                 return range_t(first, move(buffer, last, kSelectionMoveLeft,               layout), range.columnar, range.freehanded);
			case kSelectionExtendRight:                return range_t(first, move(buffer, last, kSelectionMoveRight,              layout), range.columnar, range.freehanded);
			case kSelectionExtendFreehandedLeft:       return range_t(first, move(buffer, last, kSelectionMoveFreehandedLeft,     layout), range.columnar, range.freehanded);
			case kSelectionExtendFreehandedRight:      return range_t(first, move(buffer, last, kSelectionMoveFreehandedRight,    layout), range.columnar, range.freehanded);
			case kSelectionExtendUp:                   return range_t(first, move(buffer, last, kSelectionMoveUp,                 layout), range.columnar, range.freehanded);
			case kSelectionExtendDown:                 return range_t(first, move(buffer, last, kSelectionMoveDown,               layout), range.columnar, range.freehanded);
			case kSelectionExtendToBeginOfWord:        return range_t(first, move(buffer, last, kSelectionMoveToBeginOfWord,      layout), range.columnar, range.freehanded);
			case kSelectionExtendToEndOfWord:          return range_t(first, move(buffer, last, kSelectionMoveToEndOfWord,        layout), range.columnar, range.freehanded);
			case kSelectionExtendToBeginOfSubWord:     return range_t(first, move(buffer, last, kSelectionMoveToBeginOfSubWord,   layout), range.columnar, range.freehanded);
			case kSelectionExtendToEndOfSubWord:       return range_t(first, move(buffer, last, kSelectionMoveToEndOfSubWord,     layout), range.columnar, range.freehanded);
			case kSelectionExtendToBeginOfIndentedLine: return range_t(first, move(buffer, last, kSelectionMoveToBeginOfIndentedLine, layout), range.columnar, range.freehanded);
			case kSelectionExtendToEndOfIndentedLine:  return range_t(first, move(buffer, last, kSelectionMoveToEndOfIndentedLine, layout), range.columnar, range.freehanded);
			case kSelectionExtendToBeginOfSoftLine:    return range_t(first, move(buffer, last, kSelectionMoveToBeginOfSoftLine,  layout), range.columnar, range.freehanded);
			case kSelectionExtendToEndOfSoftLine:      return range_t(first, move(buffer, last, kSelectionMoveToEndOfSoftLine,    layout), range.columnar, range.freehanded);
			case kSelectionExtendToBeginOfLine:        return range_t(first, move(buffer, last, kSelectionMoveToBeginOfLine,      layout), range.columnar, range.freehanded);
			case kSelectionExtendToEndOfLine:          return range_t(first, move(buffer, last, kSelectionMoveToEndOfLine,        layout), range.columnar, range.freehanded);
			case kSelectionExtendToBeginOfParagraph:   return range_t(first, move(buffer, last, kSelectionMoveToBeginOfParagraph, layout), range.columnar, range.freehanded);
			case kSelectionExtendToEndOfParagraph:     return range_t(first, move(buffer, last, kSelectionMoveToEndOfParagraph,   layout), range.columnar, range.freehanded);
			case kSelectionExtendToBeginOfTypingPair:  return range_t(first, move(buffer, last, kSelectionMoveToBeginOfTypingPair, layout), range.columnar, range.freehanded);
			case kSelectionExtendToEndOfTypingPair:    return range_t(first, move(buffer, last, kSelectionMoveToEndOfTypingPair,  layout), range.columnar, range.freehanded);
			case kSelectionExtendToBeginOfColumn:      return range_t(first, move(buffer, last, kSelectionMoveToBeginOfColumn,    layout), range.columnar, range.freehanded);
			case kSelectionExtendToEndOfColumn:        return range_t(first, move(buffer, last, kSelectionMoveToEndOfColumn,      layout), range.columnar, range.freehanded);
			case kSelectionExtendToBeginOfDocument:    return range_t(first, move(buffer, last, kSelectionMoveToBeginOfDocument,  layout), range.columnar, range.freehanded);
			case kSelectionExtendToEndOfDocument:      return range_t(first, move(buffer, last, kSelectionMoveToEndOfDocument,    layout), range.columnar, range.freehanded);
			case kSelectionExtendPageUp:               return range_t(first, move(buffer, last, kSelectionMovePageUp,             layout), range.columnar, range.freehanded);
			case kSelectionExtendPageDown:             return range_t(first, move(buffer, last, kSelectionMovePageDown,           layout), range.columnar, range.freehanded);

			case kSelectionExtendToSoftLine:           return range_t(move(buffer, min, kSelectionMoveToBeginOfSoftLine,          layout), min.index != max.index && max.index == move(buffer, max, kSelectionMoveToBeginOfSoftLine, layout).index ? max : move(buffer, move(buffer, max, kSelectionMoveToEndOfSoftLine, layout), kSelectionMoveRight, layout), false, false, true);
			case kSelectionExtendToLine:               return range_t(move(buffer, min, kSelectionMoveToBeginOfLine,              layout), min.index != max.index && buffer.convert(max.index).column == 0 ? max : move(buffer, move(buffer, max, kSelectionMoveToEndOfLine, layout), kSelectionMoveRight, layout), false, false, true);
			case kSelectionExtendToLineExclLF:         return range_t(move(buffer, min, kSelectionMoveToBeginOfLine,              layout), min.index != max.index && buffer.convert(max.index).column == 0 ? max : move(buffer, max, kSelectionMoveToEndOfLine, layout), false, false, true);
			case kSelectionExtendToParagraph:          return range_t(move(buffer, min, kSelectionMoveToBeginOfHardParagraph,     layout), move(buffer, max, kSelectionMoveToEndOfHardParagraph, layout), false, false, true);
			case kSelectionExtendToAll:                return range_t(move(buffer, min, kSelectionMoveToBeginOfDocument,          layout), move(buffer, max, kSelectionMoveToEndOfDocument,  layout), false, false, true);

			case kSelectionExtendToWord:
			case kSelectionExtendToWordOrTypingPair:
			{
				size_t from = min.index, to = max.index;
				size_t bol = buffer.begin(buffer.convert(from).line);
				size_t eol = buffer.eol(buffer.convert(to).line);

				std::string outerLeftType  = from == bol ? kCharacterClassUnknown : character_class(buffer, from-1);
				std::string innerLeftType  = from == eol ? kCharacterClassUnknown : character_class(buffer, from);
				std::string innerRightType = to == bol   ? kCharacterClassUnknown : character_class(buffer, to-1);
				std::string outerRightType = to == eol   ? kCharacterClassUnknown : character_class(buffer, to);

				bool extendLeft = false, extendRight = false;

				if(unit == kSelectionExtendToWordOrTypingPair && from == to)
				{
					std::string didMatch;
					auto pairs = character_pairs(buffer.scope(from), "highlightPairs");
					if(auto m = first_match(buffer, from, pairs, &does_match_right))
					{
						if(m.matched_opener)
						{
							size_t endOfTypingPair = end_of_typing_pair(buffer, from + m.match.size(), true);
							if(endOfTypingPair != from + m.match.size() && does_match_left(m.counterpart_ptrn, buffer, endOfTypingPair, &didMatch))
								return range_t(from, endOfTypingPair, false, false, false);
						}
						else
						{
							size_t beginOfTypingPair = begin_of_typing_pair(buffer, to, true);
							if(beginOfTypingPair != to)
								return range_t(beginOfTypingPair, to + m.match.size(), false, false, false);
						}
					}
					else if(auto m = first_match(buffer, from, pairs, &does_match_left))
					{
						if(m.matched_opener)
						{
							size_t endOfTypingPair = end_of_typing_pair(buffer, from, true);
							if(endOfTypingPair != from && does_match_left(m.counterpart_ptrn, buffer, endOfTypingPair, &didMatch))
								return range_t(from - m.match.size(), endOfTypingPair, false, false, false);
						}
						else
						{
							size_t beginOfTypingPair = begin_of_typing_pair(buffer, to - m.match.size(), true);
							if(beginOfTypingPair != to - m.match.size())
								return range_t(beginOfTypingPair, to, false, false, false);
						}
					}
				}

				if(from == to) // no existing selection
				{
					if(outerLeftType == outerRightType)
						extendLeft = extendRight = true;
					else if(outerRightType != kCharacterClassUnknown && (outerRightType == kCharacterClassWord || outerLeftType == kCharacterClassSpace || outerLeftType == kCharacterClassUnknown || (outerLeftType == kCharacterClassOther && outerRightType != kCharacterClassSpace)))
						extendRight = true;
					else
						extendLeft = true;
				}
				else
				{
					if(outerLeftType == innerLeftType && innerRightType != outerRightType)
						extendLeft = true;
					else if(outerLeftType != innerLeftType && outerRightType == innerRightType)
						extendRight = true;
					else if(outerLeftType == innerLeftType && outerRightType == innerRightType)
						extendLeft = extendRight = true;
				}

				if(extendLeft)
				{
					while(bol < from && character_class(buffer, from-1) == outerLeftType)
						from -= buffer[from-1].size();
				}

				if(extendRight)
				{
					while(to < eol && character_class(buffer, to) == outerRightType)
						to += buffer[to].size();
				}

				return range_t(from, to, range.columnar, false, true);
			}
			break;

			case kSelectionExtendToTypingPair:
			{
				size_t from = begin_of_typing_pair(buffer, min.index, true);
				size_t to   = end_of_typing_pair(buffer, max.index, true);

				if(from != min.index && to != max.index)
				{
					size_t firstLine = buffer.convert(from).line, lastLine = buffer.convert(to).line;
					size_t bol = buffer.begin(firstLine), eol = buffer.end(lastLine);
					return firstLine != lastLine && all_whitespace(buffer, bol, from) && all_whitespace(buffer, to, eol) ? range_t(bol, eol, false, false, true) : range_t(from, to, false, false, true);
				}
			}
			break;

			case kSelectionExtendToScope:
			{
				size_t from = min.index, to = max.index;

				scope::scope_t leftScope  = buffer.scope(from, false).left;
				scope::scope_t rightScope = buffer.scope(to, false).right;
				if(leftScope == rightScope)
				{
					// D(DBF_TextView_Internal, bug("select both sides: %s\n", to_s(leftScope).c_str()););
					from = extend_scope_left(buffer, from, leftScope);
					to   = extend_scope_right(buffer, to, rightScope);
				}
				else if(from != to)
				{
					scope::scope_t innerLeftScope  = buffer.scope(from, false).right;
					scope::scope_t innerRightScope = buffer.scope(to, false).left;

					// D(DBF_TextView_Internal, bug("%s\n", to_s(leftScope).c_str()););
					// D(DBF_TextView_Internal, bug("%s\n", to_s(innerLeftScope).c_str()););
					// D(DBF_TextView_Internal, bug("%s\n", to_s(innerRightScope).c_str()););
					// D(DBF_TextView_Internal, bug("%s\n", to_s(rightScope).c_str()););

					scope::scope_t scope = shared_prefix(shared_prefix(leftScope, innerLeftScope), shared_prefix(rightScope, innerRightScope));
					// D(DBF_TextView_Internal, bug("→ %s\n", to_s(scope).c_str()););

					from = extend_scope_left(buffer, from, scope);
					to   = extend_scope_right(buffer, to, scope);
				}
				else if(leftScope.has_prefix(rightScope))
				{
					scope::scope_t scope = leftScope;
					for(leftScope.pop_scope(); leftScope != rightScope; leftScope.pop_scope())
						scope = leftScope;
					// D(DBF_TextView_Internal, bug("select left side: %s\n", to_s(scope).c_str()););
					from = extend_scope_left(buffer, from, scope);
				}
				else if(rightScope.has_prefix(leftScope))
				{
					scope::scope_t scope = rightScope;
					for(rightScope.pop_scope(); rightScope != leftScope; rightScope.pop_scope())
						scope = rightScope;
					// D(DBF_TextView_Internal, bug("select right side: %s\n", to_s(scope).c_str()););
					to = extend_scope_right(buffer, to, scope);
				}
				else if(from == to && buffer.convert(to).column == 0)
				{
					to = extend_scope_right(buffer, to, rightScope);
				}
				else
				{
					// D(DBF_TextView_Internal, bug("intersection: %s\n           != %s\n", to_s(leftScope).c_str(), to_s(rightScope).c_str()););
					scope::scope_t const& scope = shared_prefix(leftScope, rightScope);
					// D(DBF_TextView_Internal, bug("→ %s\n", to_s(scope).c_str()););
					from = extend_scope_left(buffer, from, scope);
					to   = extend_scope_right(buffer, to, scope);
				}
				return range_t(from, to, false, false, true);
			}
			break;
		}
		return range;
	}

	ranges_t extend (buffer_api_t const& buffer, ranges_t const& selection, select_unit_type const unit, layout_movement_t const* layout)
	{
		static std::set<select_unit_type> const splittingUnits = { kSelectionExtendToWord, kSelectionExtendToTypingPair, kSelectionExtendToScope, kSelectionExtendToEndOfSoftLine, kSelectionExtendToEndOfIndentedLine, kSelectionExtendToEndOfLine, kSelectionExtendToEndOfParagraph, kSelectionExtendToBeginOfTypingPair, kSelectionExtendToEndOfTypingPair };
		bool isColumnar    = selection.size() == 1 && selection.last().columnar;
		bool shouldDissect = isColumnar && splittingUnits.find(unit) != splittingUnits.end();

		ranges_t res;
		for(auto const& range : shouldDissect ? dissect_columnar(buffer, selection) : selection)
			res.push_back(extend(buffer, range, unit, layout));
		return sanitize(buffer, res);
	}

	ranges_t extend_if_empty (buffer_api_t const& buffer, ranges_t const& selection, select_unit_type const unit, layout_movement_t const* layout)
	{
		ranges_t res;
		for(auto const& range : selection)
		{
			if(not_empty(buffer, range))
			{
				res.push_back(range);
				res.last().color = true;
			}
			else
			{
				for(auto const& r : dissect_columnar(buffer, range))
				{
					range_t range = extend(buffer, r, unit, layout);
					if(unit == kSelectionExtendToEndOfParagraph && range.empty())
						range = extend(buffer, range, kSelectionExtendRight, layout);
					res.push_back(range);
					res.last().color = false;
				}
			}
		}
		return sanitize(buffer, res);
	}

	// ================
	// = Select Scope =
	// ================

	static size_t extend_scope_left (buffer_api_t const& buffer, size_t caret, scope::selector_t const& scopeSelector)
	{
		while(caret && scopeSelector.does_match(buffer.scope(caret).left))
			caret -= buffer[caret-1].size();
		return caret;
	}

	static size_t extend_scope_right (buffer_api_t const& buffer, size_t caret, scope::selector_t const& scopeSelector)
	{
		while(caret < buffer.size() && scopeSelector.does_match(buffer.scope(caret).right))
			caret += buffer[caret].size();
		return caret;
	}

	static range_t select_scope (buffer_api_t const& buffer, range_t const& range, scope::selector_t const& scopeSelector)
	{
		return range_t(extend_scope_left(buffer, range.min().index, scopeSelector), extend_scope_right(buffer, range.max().index, scopeSelector));
	}

	ranges_t select_scope (buffer_api_t const& buffer, ranges_t const& selection, scope::selector_t const& scopeSelector)
	{
		ranges_t res;
		for(auto const& range : selection)
			res.push_back(select_scope(buffer, range, scopeSelector));
		return sanitize(buffer, res);
	}

	// ================
	// = Obtain Scope =
	// ================

	scope::context_t scope (buffer_api_t const& buffer, ranges_t const& selection, std::string const& extraAttributes)
	{
		scope::context_t res;
		if(selection.size() == 1)
		{
			range_t const range = selection.last();
			if(range.empty())
					res = buffer.scope(range.last.index);
			else	res = shared_prefix(buffer.scope(range.min().index).right, buffer.scope(range.max().index).left);
		}
		else
		{
			scope::scope_t scope;
			for(auto const& range : selection)
			{
				scope::scope_t newScope = shared_prefix(buffer.scope(range.min().index).right, buffer.scope(range.max().index).left);
				scope = scope.empty() ? newScope : shared_prefix(scope, newScope);
			}
			res = scope;
		}

		if(!extraAttributes.empty() && extraAttributes != NULL_STR)
		{
			for(auto const& str : text::tokenize(extraAttributes.begin(), extraAttributes.end(), ' '))
			{
				res.left.push_scope(str);
				res.right.push_scope(str);
			}
		}

		if(selection.size() > 1)
		{
			res.left.push_scope("dyn.caret.mixed");
			res.right.push_scope("dyn.caret.mixed");
		}
		else if(selection.last().columnar)
		{
			res.left.push_scope("dyn.caret.mixed.columnar");
			res.right.push_scope("dyn.caret.mixed.columnar");
		}
		else
		{
			size_t const leftCaret  = selection.last().min().index;
			size_t const rightCaret = selection.last().max().index;

			if(leftCaret == 0)
				res.left.push_scope("dyn.caret.begin.document");
			else if(leftCaret == buffer.begin(buffer.convert(leftCaret).line))
				res.left.push_scope("dyn.caret.begin.line");

			if(rightCaret == buffer.size())
				res.right.push_scope("dyn.caret.end.document");
			else if(rightCaret == buffer.eol(buffer.convert(rightCaret).line))
				res.right.push_scope("dyn.caret.end.line");
		}

		if(not_empty(buffer, selection))
		{
			res.left.push_scope("dyn.selection");
			res.right.push_scope("dyn.selection");
		}

		return res;
	}

	// ====================
	// = Highlight Ranges =
	// ====================

	ranges_t highlight_ranges_for_movement (buffer_api_t const& buffer, ranges_t const& oldSelection, ranges_t const& newSelection)
	{
		ranges_t res;
		if(oldSelection.size() != newSelection.size())
			return res;

		for(auto oldRange = oldSelection.begin(), newRange = newSelection.begin(); oldRange != oldSelection.end(); ++oldRange, ++newRange)
		{
			if(*oldRange == *newRange || !oldRange->empty() || !newRange->empty())
				continue;

			size_t prevCaret = oldRange->last.index;
			size_t curCaret  = newRange->last.index;
			auto pairs       = character_pairs(buffer.scope(std::min(prevCaret, curCaret)), "highlightPairs");

			std::string didMatch;
			if(prevCaret < curCaret && prevCaret + buffer[prevCaret].size() == curCaret) // moved right, check end character
			{
				auto m = first_match(buffer, curCaret, pairs, &does_match_left);
				if(m && !m.matched_opener)
				{
					size_t from = begin_of_typing_pair(buffer, curCaret - m.match.size(), true);
					if(from != prevCaret && does_match_right(m.counterpart_ptrn, buffer, from, &didMatch))
						res.push_back(range_t(from, from + didMatch.size()));
				}
			}
			else if(curCaret < prevCaret && curCaret + buffer[curCaret].size() == prevCaret) // moved left, check begin character
			{
				auto m = first_match(buffer, curCaret, pairs, &does_match_right);
				if(m && m.matched_opener)
				{
					size_t to = end_of_typing_pair(buffer, curCaret + m.match.size(), true);
					if(to != prevCaret && does_match_left(m.counterpart_ptrn, buffer, to, &didMatch))
						res.push_back(range_t(to - didMatch.size(), to));
				}
			}
		}

		return res;
	}

	// ========
	// = Find =
	// ========

	static bool is_subset (range_t const& range, ranges_t const& ranges)
	{
		for(auto const& r : ranges)
		{
			if(r.min() <= range.min() && range.max() <= r.max())
				return true;
		}
		return ranges.empty();
	}

	std::map< range_t, std::map<std::string, std::string> > find_all (buffer_api_t const& buffer, std::string const& searchFor, find::options_t options, ranges_t const& searchRanges)
	{
		std::map< range_t, std::map<std::string, std::string> > res;
		if(searchFor == NULL_STR || searchFor == "")
			return res;

		ranges_t ranges = dissect_columnar(buffer, searchRanges);
		find::find_t f(searchFor, (find::options_t)(options & ~find::backwards));

		ssize_t total = 0;
		buffer.visit_data([&](char const* buf, size_t, size_t len, bool*){
			for(ssize_t offset = 0; offset < len; )
			{
				std::map<std::string, std::string> captures;
				std::pair<ssize_t, ssize_t> const& m = f.match(buf + offset, len - offset, &captures);
				if(m.first <= m.second)
				{
					range_t r(total + offset + m.first, total + offset + m.second, false, false, true);
					if(is_subset(r, ranges))
						res.emplace(r, captures);
				}
				ASSERT_NE(m.second, 0); ASSERT_LE(m.second, len - offset);
				offset += m.second;
			}
			total += len;
		});

		std::map<std::string, std::string> captures;
		std::pair<ssize_t, ssize_t> m = f.match(NULL, 0, &captures);
		while(m.first <= m.second)
		{
			range_t r(total + m.first, total + m.second, false, false, true);
			if(is_subset(r, ranges))
				res.emplace(r, captures);
			captures.clear();
			m = f.match(NULL, 0, &captures);
		}

		return res;
	}

	static std::map< range_t, std::map<std::string, std::string> > regexp_find (buffer_api_t const& buffer, std::string const& searchFor, find::options_t options, ng::range_t const& range)
	{
		size_t first = (options & find::backwards) ? range.min().index : range.max().index;
		size_t last  = (options & find::backwards) ?                 0 :     buffer.size();

		std::map< range_t, std::map<std::string, std::string> > res;

		OnigOptionType ptrnOptions = ONIG_OPTION_NONE;
		if(options & find::ignore_case)
			ptrnOptions |= ONIG_OPTION_IGNORECASE;

		std::string str = buffer.substr(0, buffer.size());
		if(regexp::match_t m = search(regexp::pattern_t(searchFor, ptrnOptions), str.data(), str.data() + str.size(), str.data() + first, str.data() + last))
		{
			if(range.sorted() == ng::range_t(m.begin(), m.end()))
			{
				if(options & find::backwards)
				{
					if(0 < first)
						first -= buffer[first-1].size();
				}
				else
				{
					if(first < str.size())
						first += buffer[first].size();
				}
				m = search(regexp::pattern_t(searchFor, ptrnOptions), str.data(), str.data() + str.size(), str.data() + first, str.data() + last);
			}

			if(m && range.sorted() != ng::range_t(m.begin(), m.end()))
				res.emplace(ng::range_t(m.begin(), m.end()), m.captures());
		}

		return res;
	}

	std::map< range_t, std::map<std::string, std::string> > find (buffer_api_t const& buffer, ranges_t const& selection, std::string const& searchFor, find::options_t options, ranges_t const& searchRanges, bool* didWrap)
	{
		auto setDidWrap = [&didWrap](bool flag){ if(didWrap != nullptr) *didWrap = flag; };
		setDidWrap(false);

		std::map< range_t, std::map<std::string, std::string> > res;
		if(searchFor == NULL_STR || searchFor == "")
			return res;

		if(selection.size() == 1 && (options & (find::regular_expression|find::wrap_around|find::all_matches|find::extend_selection)) == find::regular_expression)
			return regexp_find(buffer, searchFor, options, selection.last());

		auto tmp = find_all(buffer, searchFor, options, searchRanges);
		if(options & find::all_matches)
			return tmp;

		if(options & find::extend_selection)
		{
			ng::index_t anchor(options & find::backwards ? buffer.size() : 0);
			for(auto const& range : selection)
			{
				anchor = options & find::backwards ? std::min(range.min(), anchor) : std::max(range.max(), anchor);
				res.emplace(range, std::map<std::string, std::string>());
			}

			if(options & find::backwards)
			{
				auto it = tmp.lower_bound(anchor);
				if(it != tmp.begin())
					res.insert(*--it);
			}
			else
			{
				auto it = tmp.upper_bound(anchor);
				if(it != tmp.end())
					res.insert(*it);
			}

			return res;
		}

		if(tmp.empty())
			return tmp;

		for(auto const& range : dissect_columnar(buffer, selection))
		{
			if(options & find::backwards)
			{
				auto it = std::lower_bound(tmp.begin(), tmp.end(), range, [](auto const& candidate, ng::range_t const& range){
					return candidate.first.empty() ? candidate.first.max() < range.min() : candidate.first.max() <= range.min();
				});

				if(it == tmp.begin())
				{
					if(options & find::wrap_around)
					{
						it = --tmp.end();
						if(range.sorted() == it->first.sorted())
							continue;
						else if(!(range.min() <= it->first.min() && it->first.max() <= range.max()))
							setDidWrap(true);
					}
					else
					{
						it = std::upper_bound(tmp.begin(), tmp.end(), range, [](ng::range_t const& range, decltype(tmp)::value_type const& candidate){
							return range.max() < candidate.first.max();
						});

						if(it == tmp.begin())
							continue;
						--it;
					}
				}
				else
				{
					--it;
				}

				if(range.sorted() != it->first.sorted())
					res.insert(*it);
			}
			else
			{
				auto it = std::upper_bound(tmp.begin(), tmp.end(), range, [](ng::range_t const& range, decltype(tmp)::value_type const& candidate){
					return candidate.first.empty() ? range.max() < candidate.first.min() : range.max() <= candidate.first.min();
				});

				if(it == tmp.end())
				{
					if(options & find::wrap_around)
					{
						it = tmp.begin();
						if(range.sorted() == it->first.sorted())
							continue;
						else if(!(range.min() <= it->first.min() && it->first.max() <= range.max()))
							setDidWrap(true);
					}
					else
					{
						it = std::upper_bound(tmp.begin(), tmp.end(), range, [](ng::range_t const& range, decltype(tmp)::value_type const& candidate){
							return range.min() <= candidate.first.min();
						});

						if(it == tmp.end())
							continue;
					}
				}

				if(range.sorted() != it->first.sorted())
					res.insert(*it);
			}
		}

		return res;
	}

	// =============
	// = All Words =
	// =============

	range_t word_at (buffer_api_t const& buffer, range_t const& range)
	{
		if(range.empty())
		{
			size_t i   = range.first.index;
			size_t bol = buffer.begin(buffer.convert(i).line);
			size_t eol = buffer.eol(buffer.convert(i).line);

			std::string leftType  = i == bol ? kCharacterClassSpace : character_class(buffer, i-1);
			std::string rightType = i == eol ? kCharacterClassSpace : character_class(buffer, i);

			if(leftType == kCharacterClassSpace && rightType == kCharacterClassSpace)
				return range;
		}
		return extend(buffer, range, kSelectionExtendToWord).last();
	}

	ranges_t all_words (buffer_api_t const& buffer)
	{
		ranges_t res;

		std::string charType = kCharacterClassOther;
		size_t from = 0;
		for(size_t i = 0; i < buffer.size(); )
		{
			std::string newCharType = character_class(buffer, i);
			if(charType != newCharType)
			{
				if(charType != kCharacterClassSpace && charType != kCharacterClassOther && from != i)
					res.push_back(range_t(from, i));
				charType = newCharType;
				from = i;
			}
			i += buffer[i].size();
		}

		if(charType != kCharacterClassSpace && charType != kCharacterClassOther && from != buffer.size())
			res.push_back(range_t(from, buffer.size()));

		return res;
	}

	// ==================
	// = to/from string =
	// ==================

	ranges_t from_string (buffer_api_t const& buffer, std::string const& str)
	{
		ranges_t res;
		for(auto const& range : text::selection_t(str))
			res.push_back(range_t(index_t(buffer.convert(range.from), range.from.offset), index_t(buffer.convert(range.to), range.to.offset), range.columnar, range.from.offset || range.to.offset));
		return res;
	}

	std::string to_s (buffer_api_t const& buffer, ranges_t const& ranges)
	{
		text::selection_t res;
		for(auto const& range : ranges)
		{
			text::pos_t from = buffer.convert(range.first.index), to = buffer.convert(range.last.index);
			from.offset = range.freehanded ? range.first.carry : 0;
			to.offset   = range.freehanded ? range.last.carry  : 0;
			res.push_back(text::range_t(from, to, range.columnar));
		}
		return res;
	}

} /* ng */
