#include <clocale>
#define _XOPEN_SOURCE_EXTENDED 1
#include <cwchar>
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <cstdlib>
#include <ncurses.h>
#include <regex>
#include <string>

#include "TerminalRenderer.h"
#include "Buffer.h"
#include "Editor.h"
#include "Highlight.h"

// Version string expected to be provided by build system as KTE_VERSION_STR
#ifndef KTE_VERSION_STR
#  define KTE_VERSION_STR "dev"
#endif


TerminalRenderer::TerminalRenderer() = default;

TerminalRenderer::~TerminalRenderer() = default;


void
TerminalRenderer::Draw(Editor &ed)
{
	// Determine terminal size and keep editor dimensions in sync
	int rows, cols;
	getmaxyx(stdscr, rows, cols);

	// Clear screen
	erase();

	const Buffer *buf = ed.CurrentBuffer();
	int content_rows  = rows - 1; // last line is status
	if (content_rows < 1)
		content_rows = 1;

	int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area
	if (buf) {
		const auto &lines   = buf->Rows();
		std::size_t rowoffs = buf->Rowoffs();
		std::size_t coloffs = buf->Coloffs();

		const int tabw = 8;
		// Phase 3: prefetch visible viewport highlights (current terminal area)
		if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
			int fr = static_cast<int>(rowoffs);
			int rc = std::max(0, content_rows);
			buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
		}

		for (int r = 0; r < content_rows; ++r) {
			move(r, 0);
			std::size_t li         = rowoffs + static_cast<std::size_t>(r);
			std::size_t render_col = 0;
			std::size_t src_i      = 0;
			// Compute matches for this line if search highlighting is active
			bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
			std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
			if (search_mode && li < lines.size()) {
				std::string sline = static_cast<std::string>(lines[li]);
				// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
				if (ed.PromptActive() && (
					    ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
					    CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
					try {
						std::regex rx(ed.SearchQuery());
						for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
						     it != std::sregex_iterator(); ++it) {
							const auto &m  = *it;
							std::size_t sx = static_cast<std::size_t>(m.position());
							std::size_t ex = sx + static_cast<std::size_t>(m.length());
							ranges.emplace_back(sx, ex);
						}
					} catch (const std::regex_error &) {
						// ignore invalid patterns here; status shows error
					}
				} else {
					const std::string &q = ed.SearchQuery();
					std::size_t pos      = 0;
					while (!q.empty() && (pos = sline.find(q, pos)) != std::string::npos) {
						ranges.emplace_back(pos, pos + q.size());
						pos += q.size();
					}
				}
			}
			auto is_src_in_hl = [&](std::size_t si) -> bool {
				if (ranges.empty())
					return false;
				// ranges are non-overlapping and ordered by construction
				// linear scan is fine for now
				for (const auto &rg: ranges) {
					if (si < rg.first)
						break;
					if (si >= rg.first && si < rg.second)
						return true;
				}
				return false;
			};
			// Track current-match to optionally emphasize
			const bool has_current     = ed.SearchActive() && ed.SearchMatchLen() > 0;
			const std::size_t cur_mx   = has_current ? ed.SearchMatchX() : 0;
			const std::size_t cur_my   = has_current ? ed.SearchMatchY() : 0;
			const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;

			// Mark selection (mark -> cursor), in source coordinates
			bool sel_active    = false;
			std::size_t sel_sy = 0, sel_sx = 0, sel_ey = 0, sel_ex = 0;
			if (buf->MarkSet()) {
				sel_sy = buf->MarkCury();
				sel_sx = buf->MarkCurx();
				sel_ey = buf->Cury();
				sel_ex = buf->Curx();
				if (sel_sy > sel_ey || (sel_sy == sel_ey && sel_sx > sel_ex)) {
					std::swap(sel_sy, sel_ey);
					std::swap(sel_sx, sel_ex);
				}
				sel_active = !(sel_sy == sel_ey && sel_sx == sel_ex);
			}
			// Visual-line selection: full-line selection range
			const bool vsel_active    = buf->VisualLineActive();
			const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
			const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
			auto is_src_in_mark_sel   = [&](std::size_t y, std::size_t sx) -> bool {
				if (!sel_active)
					return false;
				if (y < sel_sy || y > sel_ey)
					return false;
				if (sel_sy == sel_ey)
					return sx >= sel_sx && sx < sel_ex;
				if (y == sel_sy)
					return sx >= sel_sx;
				if (y == sel_ey)
					return sx < sel_ex;
				return true;
			};
			int written = 0;
			if (li < lines.size()) {
				std::string line                = static_cast<std::string>(lines[li]);
				const bool vsel_on_line         = vsel_active && li >= vsel_sy && li <= vsel_ey;
				const std::size_t vsel_spot_src = vsel_on_line
					                                  ? std::min(buf->Curx(), line.size())
					                                  : 0;
				const bool vsel_spot_is_eol = vsel_on_line && vsel_spot_src == line.size();
				std::size_t vsel_line_rx    = 0;
				if (vsel_spot_is_eol) {
					// Compute the rendered (column) width of the line so we can highlight a
					// single cell at EOL when the spot falls beyond the last character.
					std::size_t rc = 0;
					std::size_t si = 0;
					while (si < line.size()) {
						wchar_t wch = L' ';
						int wch_len = 1;
						std::mbstate_t state = std::mbstate_t();
						size_t res = std::mbrtowc(&wch, &line[si], line.size() - si, &state);
						if (res == (size_t) -1 || res == (size_t) -2) {
							wch     = static_cast<unsigned char>(line[si]);
							wch_len = 1;
						} else if (res == 0) {
							wch     = L'\0';
							wch_len = 1;
						} else {
							wch_len = static_cast<int>(res);
						}
						if (wch == L'\t') {
							constexpr std::size_t tab_width = 8;
							const std::size_t next_tab      = tab_width - (rc % tab_width);
							rc                              += next_tab;
						} else {
							int w = wcwidth(wch);
							if (w < 0)
								w = 1;
							rc += static_cast<std::size_t>(w);
						}
						si += static_cast<std::size_t>(wch_len);
					}
					vsel_line_rx = rc;
				}
				src_i      = 0;
				render_col = 0;
				// Syntax highlighting: fetch per-line spans (sanitized copy)
				std::vector<kte::HighlightSpan> sane_spans;
				if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
				    HasHighlighter()) {
					kte::LineHighlight lh_val = buf->Highlighter()->GetLine(
						*buf, static_cast<int>(li), buf->Version());
					// Sanitize defensively: clamp to [0, line.size()], ensure end>=start, drop empties
					const std::size_t line_len = line.size();
					sane_spans.reserve(lh_val.spans.size());
					for (const auto &sp: lh_val.spans) {
						int s_raw = sp.col_start;
						int e_raw = sp.col_end;
						if (e_raw < s_raw)
							std::swap(e_raw, s_raw);
						std::size_t s = static_cast<std::size_t>(std::max(
							0, std::min(s_raw, static_cast<int>(line_len))));
						std::size_t e = static_cast<std::size_t>(std::max(
							static_cast<int>(s),
							std::min(e_raw, static_cast<int>(line_len))));
						if (e <= s)
							continue;
						sane_spans.push_back(kte::HighlightSpan{
							static_cast<int>(s), static_cast<int>(e), sp.kind
						});
					}
					std::sort(sane_spans.begin(), sane_spans.end(),
					          [](const kte::HighlightSpan &a, const kte::HighlightSpan &b) {
						          return a.col_start < b.col_start;
					          });
				}
				auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
					if (sane_spans.empty())
						return kte::TokenKind::Default;
					int si = static_cast<int>(src_index);
					for (const auto &sp: sane_spans) {
						if (si < sp.col_start)
							break;
						if (si >= sp.col_start && si < sp.col_end)
							return sp.kind;
					}
					return kte::TokenKind::Default;
				};
				auto token_attr = [&](kte::TokenKind k) -> attr_t {
					switch (k) {
					case kte::TokenKind::Keyword:
					case kte::TokenKind::Type:
					case kte::TokenKind::Constant:
					case kte::TokenKind::Function:
						return A_BOLD;
					case kte::TokenKind::Comment:
						return A_DIM;
					case kte::TokenKind::String:
					case kte::TokenKind::Char:
					case kte::TokenKind::Number:
						return A_UNDERLINE;
					default:
						return 0;
					}
				};
				while (written < cols) {
					bool from_src = false;
					wchar_t wch   = L' ';
					int wch_len   = 1;
					int disp_w    = 1;

					if (src_i < line.size()) {
						// Decode UTF-8
						std::mbstate_t state = std::mbstate_t();
						size_t res           = std::mbrtowc(
							&wch, &line[src_i], line.size() - src_i, &state);
						if (res == (size_t) -1 || res == (size_t) -2) {
							// Invalid or incomplete; treat as single byte
							wch     = static_cast<unsigned char>(line[src_i]);
							wch_len = 1;
						} else if (res == 0) {
							wch     = L'\0';
							wch_len = 1;
						} else {
							wch_len = static_cast<int>(res);
						}

						if (wch == L'\t') {
							std::size_t next_tab = tabw - (render_col % tabw);
							if (render_col + next_tab <= coloffs) {
								render_col += next_tab;
								src_i      += wch_len;
								continue;
							}
							// Emit spaces for tab
							if (render_col < coloffs) {
								// skip to coloffs
								std::size_t to_skip = std::min<std::size_t>(
									next_tab, coloffs - render_col);
								render_col += to_skip;
								next_tab   -= to_skip;
							}
							// Now render visible spaces
							while (next_tab > 0 && written < cols) {
								bool in_mark = is_src_in_mark_sel(li, src_i);
								bool in_vsel =
									vsel_on_line && !vsel_spot_is_eol && src_i ==
									vsel_spot_src;
								bool in_sel = in_mark || in_vsel;
								bool in_hl  = search_mode && is_src_in_hl(src_i);
								bool in_cur =
									has_current && li == cur_my && src_i >= cur_mx
									&&
									src_i < cur_mend;
								attr_t a = A_NORMAL;
								a        |= token_attr(token_at(src_i));
								if (in_sel) {
									a |= A_REVERSE;
								} else {
									if (in_hl)
										a |= A_STANDOUT;
									if (in_cur)
										a |= A_BOLD;
								}
								attrset(a);
								addch(' ');
								++written;
								++render_col;
								--next_tab;
							}
							src_i += wch_len;
							continue;
						} else {
							// normal char
							disp_w = wcwidth(wch);
							if (disp_w < 0)
								disp_w = 1; // non-printable or similar

							if (render_col < coloffs) {
								render_col += disp_w;
								src_i      += wch_len;
								continue;
							}
							from_src = true;
						}
					} else {
						// beyond EOL, fill spaces
						wch      = L' ';
						wch_len  = 1;
						disp_w   = 1;
						from_src = false;
					}

					if (written + disp_w > cols) {
						// would overflow, just break
						break;
					}

					bool in_mark = from_src && is_src_in_mark_sel(li, src_i);
					bool in_vsel = false;
					if (vsel_on_line) {
						if (from_src) {
							in_vsel = !vsel_spot_is_eol && src_i == vsel_spot_src;
						} else {
							in_vsel = vsel_spot_is_eol && render_col == vsel_line_rx;
						}
					}
					bool in_sel = in_mark || in_vsel;
					bool in_hl  = search_mode && from_src && is_src_in_hl(src_i);
					bool in_cur = has_current && li == cur_my && from_src && src_i >= cur_mx &&
					              src_i < cur_mend;
					attr_t a = A_NORMAL;
					if (from_src)
						a |= token_attr(token_at(src_i));
					if (in_sel) {
						a |= A_REVERSE;
					} else {
						if (in_hl)
							a |= A_STANDOUT;
						if (in_cur)
							a |= A_BOLD;
					}
					attrset(a);

					if (from_src) {
						cchar_t cch;
						wchar_t warr[2] = {wch, L'\0'};
						setcchar(&cch, warr, 0, 0, nullptr);
						add_wch(&cch);
					} else {
						addch(' ');
					}

					written    += disp_w;
					render_col += disp_w;
					if (from_src)
						src_i += wch_len;
					if (src_i >= line.size() && written >= cols)
						break;
				}
			}
			attrset(A_NORMAL);
			clrtoeol();
		}

		// Place terminal cursor at logical position accounting for tabs and coloffs.
		// Recompute the rendered X using the same logic as the drawing loop to avoid
		// any drift between the command-layer computation and the terminal renderer.
		std::size_t cy            = buf->Cury();
		std::size_t cx            = buf->Curx();
		int cur_y                 = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
		std::size_t rx_recomputed = 0;
		if (cy < lines.size()) {
			const std::string line_for_cursor = static_cast<std::string>(lines[cy]);
			std::size_t src_i_cur             = 0;
			std::size_t render_col_cur        = 0;
			while (src_i_cur < line_for_cursor.size() && src_i_cur < cx) {
				std::mbstate_t state = std::mbstate_t();
				wchar_t wch;
				size_t res = std::mbrtowc(
					&wch, &line_for_cursor[src_i_cur], line_for_cursor.size() - src_i_cur,
					&state);

				if (res == (size_t) -1 || res == (size_t) -2) {
					render_col_cur += 1;
					src_i_cur      += 1;
				} else if (res == 0) {
					src_i_cur += 1;
				} else {
					if (wch == L'\t') {
						std::size_t next_tab = tabw - (render_col_cur % tabw);
						render_col_cur       += next_tab;
					} else {
						int dw         = wcwidth(wch);
						render_col_cur += (dw < 0) ? 1 : dw;
					}
					src_i_cur += res;
				}
			}
			rx_recomputed = render_col_cur;
		}
		int cur_x = static_cast<int>(rx_recomputed) - static_cast<int>(buf->Coloffs());
		if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) {
			// remember where to leave the terminal cursor after status is drawn
			saved_cur_y = cur_y;
			saved_cur_x = cur_x;
			move(cur_y, cur_x);
		}
	} else {
		mvaddstr(0, 0, "[no buffer]");
	}

	// Status line (inverse)
	move(rows - 1, 0);
	attron(A_REVERSE);

	// Fill the status line with spaces first
	for (int i = 0; i < cols; ++i)
		addch(' ');

	// If a prompt is active, replace the status bar with the full prompt text
	if (ed.PromptActive()) {
		// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
		std::string label = ed.PromptLabel();
		std::string ptext = ed.PromptText();
		auto kind         = ed.CurrentPromptKind();
		if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
		    kind == Editor::PromptKind::Chdir) {
			const char *home_c = std::getenv("HOME");
			if (home_c && *home_c) {
				std::string home(home_c);
				// Ensure we match only at the start
				if (ptext.rfind(home, 0) == 0) {
					std::string rest = ptext.substr(home.size());
					if (rest.empty())
						ptext = "~";
					else if (rest[0] == '/' || rest[0] == '\\')
						ptext = std::string("~") + rest;
				}
			}
		}
		// Prefer keeping the tail of the filename visible when it exceeds the window
		std::string msg;
		if (kind == Editor::PromptKind::Command) {
			msg = ": ";
		} else if (!label.empty()) {
			msg = label + ": ";
		}
		// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
		if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
		     Editor::PromptKind::Chdir) && cols > 0) {
			int avail = cols - static_cast<int>(msg.size());
			if (avail <= 0) {
				// No room for label; fall back to showing the rightmost portion of the whole string
				std::string whole = msg + ptext;
				if ((int) whole.size() > cols)
					whole = whole.substr(whole.size() - cols);
				msg = whole;
			} else {
				if ((int) ptext.size() > avail) {
					ptext = ptext.substr(ptext.size() - avail);
				}
				msg += ptext;
			}
		} else {
			// Non-file prompts: simple concatenation and clip by terminal
			msg += ptext;
		}

		// Draw left-aligned, clipped to width
		if (!msg.empty())
			mvaddnstr(rows - 1, 0, msg.c_str(), std::max(0, cols));

		// End status rendering for prompt mode
		attroff(A_REVERSE);
		// Restore logical cursor position in content area
		if (saved_cur_y >= 0 && saved_cur_x >= 0)
			move(saved_cur_y, saved_cur_x);
		return;
	}

	// Build left segment
	std::string left;
	{
		const char *app = "kte";
		left.reserve(256);
		left            += app;
		left            += " ";
		left            += KTE_VERSION_STR; // already includes leading 'v'
		const Buffer *b = buf;
		std::string fname;
		if (b) {
			try {
				fname = ed.DisplayNameFor(*b);
			} catch (...) {
				fname = b->Filename();
				try {
					fname = std::filesystem::path(fname).filename().string();
				} catch (...) {}
			}
		} else {
			fname = "[no name]";
		}
		left += "  ";
		// Insert buffer position prefix "[x/N] " before filename
		{
			std::size_t total = ed.BufferCount();
			if (total > 0) {
				std::size_t idx1 = ed.CurrentBufferIndex() + 1; // human-friendly 1-based
				left             += "[";
				left             += std::to_string(static_cast<unsigned long long>(idx1));
				left             += "/";
				left             += std::to_string(static_cast<unsigned long long>(total));
				left             += "] ";
			}
		}
		left += fname;
		if (b && b->Dirty())
			left += " *";
		// Append read-only indicator
		if (b && b->IsReadOnly())
			left += " [RO]";
		// Append total line count as "<n>L"
		if (b) {
			unsigned long lcount = static_cast<unsigned long>(b->Rows().size());
			left                 += " ";
			left                 += std::to_string(lcount);
			left                 += "L";
		}
	}

	// Build right segment (cursor and mark)
	std::string right;
	{
		int row1       = 0, col1  = 0;
		int mrow1      = 0, mcol1 = 0;
		bool have_mark = false;
		if (buf) {
			row1 = static_cast<int>(buf->Cury()) + 1;
			col1 = static_cast<int>(buf->Curx()) + 1;
			if (buf->MarkSet()) {
				have_mark = true;
				mrow1     = static_cast<int>(buf->MarkCury()) + 1;
				mcol1     = static_cast<int>(buf->MarkCurx()) + 1;
			}
		}
		char rbuf[128];
		if (have_mark)
			std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
		else
			std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
		right = rbuf;
	}

	// Compute placements with truncation rules: prioritize left and right; middle gets remaining
	int rlen = static_cast<int>(right.size());
	if (rlen > cols) {
		// Hard clip right if too long
		right = right.substr(static_cast<std::size_t>(rlen - cols), static_cast<std::size_t>(cols));
		rlen  = cols;
	}
	int left_max = std::max(0, cols - rlen - 1); // leave at least 1 space between left and right areas
	int llen     = static_cast<int>(left.size());
	if (llen > left_max)
		llen = left_max;

	// Draw left
	if (llen > 0)
		mvaddnstr(rows - 1, 0, left.c_str(), llen);

	// Draw right, flush to end
	int rstart = std::max(0, cols - rlen);
	if (rlen > 0)
		mvaddnstr(rows - 1, rstart, right.c_str(), rlen);

	// Middle message
	const std::string &msg = ed.Status();
	if (!msg.empty()) {
		int mid_start = llen + 1; // one space after left
		int mid_end   = rstart - 1; // one space before right
		if (mid_end >= mid_start) {
			int avail  = mid_end - mid_start + 1;
			int mlen   = static_cast<int>(msg.size());
			int mdraw  = std::min(avail, mlen);
			int mstart = mid_start + std::max(0, (avail - mdraw) / 2); // center within middle area
			mvaddnstr(rows - 1, mstart, msg.c_str(), mdraw);
		}
	}

	attroff(A_REVERSE);

	// Restore terminal cursor to the content position so a visible caret
	// remains in the editing area (not on the status line).
	if (saved_cur_y >= 0 && saved_cur_x >= 0) {
		move(saved_cur_y, saved_cur_x);
	}

	refresh();
}
