#include <fstream>
#include <sstream>
#include <filesystem>
#include <cstdlib>
#include <limits>
#include <cerrno>
#include <cstring>
#include <string_view>

#include <vector>

#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "Buffer.h"
#include "SwapRecorder.h"
#include "UndoSystem.h"
#include "UndoTree.h"
#include "ErrorHandler.h"
#include "SyscallWrappers.h"
#include "ErrorRecovery.h"
// For reconstructing highlighter state on copies
#include "syntax/HighlighterRegistry.h"
#include "syntax/NullHighlighter.h"


Buffer::Buffer()
{
	// Initialize undo system per buffer
	undo_tree_ = std::make_unique<UndoTree>();
	undo_sys_  = std::make_unique<UndoSystem>(*this, *undo_tree_);
}


bool
Buffer::stat_identity(const std::string &path, FileIdentity &out)
{
	struct stat st{};
	if (::stat(path.c_str(), &st) != 0) {
		out.valid = false;
		return false;
	}
	out.valid = true;
	// Use nanosecond timestamp when available.
	std::uint64_t ns = 0;
#if defined(__APPLE__)
	ns = static_cast<std::uint64_t>(st.st_mtimespec.tv_sec) * 1000000000ull
	     + static_cast<std::uint64_t>(st.st_mtimespec.tv_nsec);
#else
	ns = static_cast<std::uint64_t>(st.st_mtim.tv_sec) * 1000000000ull
	     + static_cast<std::uint64_t>(st.st_mtim.tv_nsec);
#endif
	out.mtime_ns = ns;
	out.size     = static_cast<std::uint64_t>(st.st_size);
	out.dev      = static_cast<std::uint64_t>(st.st_dev);
	out.ino      = static_cast<std::uint64_t>(st.st_ino);
	return true;
}


bool
Buffer::current_disk_identity(FileIdentity &out) const
{
	if (!is_file_backed_ || filename_.empty()) {
		out.valid = false;
		return false;
	}
	return stat_identity(filename_, out);
}


bool
Buffer::ExternallyModifiedOnDisk() const
{
	if (!is_file_backed_ || filename_.empty())
		return false;
	FileIdentity now{};
	if (!current_disk_identity(now)) {
		// If the file vanished, treat as modified when we previously had an identity.
		return on_disk_identity_.valid;
	}
	if (!on_disk_identity_.valid)
		return false;
	return now.mtime_ns != on_disk_identity_.mtime_ns
	       || now.size != on_disk_identity_.size
	       || now.dev != on_disk_identity_.dev
	       || now.ino != on_disk_identity_.ino;
}


void
Buffer::RefreshOnDiskIdentity()
{
	FileIdentity id{};
	if (current_disk_identity(id))
		on_disk_identity_ = id;
}


static bool
write_all_fd(int fd, const char *data, std::size_t len, std::string &err)
{
	std::size_t off = 0;
	while (off < len) {
		ssize_t n = ::write(fd, data + off, len - off);
		if (n < 0) {
			if (errno == EINTR)
				continue;
			err = std::string("Write failed: ") + std::strerror(errno);
			return false;
		}
		off += static_cast<std::size_t>(n);
	}
	return true;
}


static void
best_effort_fsync_dir(const std::string &path)
{
	try {
		std::filesystem::path p(path);
		std::filesystem::path dir = p.parent_path();
		if (dir.empty())
			return;
		int dfd = kte::syscall::Open(dir.c_str(), O_RDONLY);
		if (dfd < 0)
			return;
		(void) kte::syscall::Fsync(dfd);
		(void) kte::syscall::Close(dfd);
	} catch (...) {
		// best-effort
	}
}


static bool
atomic_write_file(const std::string &path, const char *data, std::size_t len, std::string &err)
{
	// Create a temp file in the same directory so rename() is atomic.
	std::filesystem::path p(path);
	std::filesystem::path dir  = p.parent_path();
	std::string base           = p.filename().string();
	std::filesystem::path tmpl = dir / ("." + base + ".kte.tmp.XXXXXX");
	std::string tmpl_s         = tmpl.string();

	// mkstemp requires a mutable buffer.
	std::vector<char> buf(tmpl_s.begin(), tmpl_s.end());
	buf.push_back('\0');

	// Retry on transient errors for temp file creation
	int fd          = -1;
	auto mkstemp_fn = [&]() -> bool {
		// Reset buffer for each retry attempt
		buf.assign(tmpl_s.begin(), tmpl_s.end());
		buf.push_back('\0');
		fd = kte::syscall::Mkstemp(buf.data());
		return fd >= 0;
	};

	if (!kte::RetryOnTransientError(mkstemp_fn, kte::RetryPolicy::Aggressive(), err)) {
		if (fd < 0) {
			err = std::string("Failed to create temp file for save: ") + std::strerror(errno) + err;
		}
		return false;
	}
	std::string tmp_path(buf.data());

	// If the destination exists, carry over its permissions.
	struct stat dst_st{};
	if (::stat(path.c_str(), &dst_st) == 0) {
		(void) kte::syscall::Fchmod(fd, dst_st.st_mode);
	}

	bool ok = write_all_fd(fd, data, len, err);
	if (ok) {
		// Retry fsync on transient errors
		auto fsync_fn = [&]() -> bool {
			return kte::syscall::Fsync(fd) == 0;
		};

		std::string fsync_err;
		if (!kte::RetryOnTransientError(fsync_fn, kte::RetryPolicy::Aggressive(), fsync_err)) {
			err = std::string("fsync failed: ") + std::strerror(errno) + fsync_err;
			ok  = false;
		}
	}
	(void) kte::syscall::Close(fd);

	if (ok) {
		if (::rename(tmp_path.c_str(), path.c_str()) != 0) {
			err = std::string("rename failed: ") + std::strerror(errno);
			ok  = false;
		}
	}

	if (!ok) {
		(void) ::unlink(tmp_path.c_str());
		return false;
	}
	best_effort_fsync_dir(path);
	return true;
}


Buffer::Buffer(const std::string &path)
{
	std::string err;
	OpenFromFile(path, err);
}


// Copy constructor/assignment: perform a deep copy of core fields; reinitialize undo for the new buffer.
Buffer::Buffer(const Buffer &other)
{
	curx_             = other.curx_;
	cury_             = other.cury_;
	rx_               = other.rx_;
	nrows_            = other.nrows_;
	rowoffs_          = other.rowoffs_;
	coloffs_          = other.coloffs_;
	rows_             = other.rows_;
	content_          = other.content_;
	rows_cache_dirty_ = other.rows_cache_dirty_;
	filename_         = other.filename_;
	is_file_backed_   = other.is_file_backed_;
	dirty_            = other.dirty_;
	read_only_        = other.read_only_;
	mark_set_         = other.mark_set_;
	mark_curx_        = other.mark_curx_;
	mark_cury_        = other.mark_cury_;
	// Copy edit mode + syntax/highlighting flags
	edit_mode_          = other.edit_mode_;
	edit_mode_detected_ = other.edit_mode_detected_;
	version_            = other.version_;
	syntax_enabled_     = other.syntax_enabled_;
	filetype_           = other.filetype_;
	// Fresh undo system for the copy
	undo_tree_ = std::make_unique<UndoTree>();
	undo_sys_  = std::make_unique<UndoSystem>(*this, *undo_tree_);

	// Recreate a highlighter engine for this copy based on filetype/syntax state
	if (syntax_enabled_) {
		// Allocate engine and install an appropriate highlighter
		highlighter_ = std::make_unique<kte::HighlighterEngine>();
		if (!filetype_.empty()) {
			auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
			if (hl) {
				highlighter_->SetHighlighter(std::move(hl));
			} else {
				// Unsupported filetype -> NullHighlighter keeps syntax pipeline active
				highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
			}
		} else {
			// No filetype -> keep syntax enabled but use NullHighlighter
			highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
		}
		// Fresh engine has empty caches; nothing to invalidate
	}
}


Buffer &
Buffer::operator=(const Buffer &other)
{
	if (this == &other)
		return *this;
	curx_             = other.curx_;
	cury_             = other.cury_;
	rx_               = other.rx_;
	nrows_            = other.nrows_;
	rowoffs_          = other.rowoffs_;
	coloffs_          = other.coloffs_;
	rows_             = other.rows_;
	content_          = other.content_;
	rows_cache_dirty_ = other.rows_cache_dirty_;
	filename_         = other.filename_;
	is_file_backed_   = other.is_file_backed_;
	dirty_            = other.dirty_;
	read_only_        = other.read_only_;
	mark_set_         = other.mark_set_;
	mark_curx_          = other.mark_curx_;
	mark_cury_          = other.mark_cury_;
	edit_mode_          = other.edit_mode_;
	edit_mode_detected_ = other.edit_mode_detected_;
	version_            = other.version_;
	syntax_enabled_     = other.syntax_enabled_;
	filetype_           = other.filetype_;
	// Recreate undo system for this instance
	undo_tree_ = std::make_unique<UndoTree>();
	undo_sys_  = std::make_unique<UndoSystem>(*this, *undo_tree_);

	// Recreate highlighter engine consistent with syntax settings
	highlighter_.reset();
	if (syntax_enabled_) {
		highlighter_ = std::make_unique<kte::HighlighterEngine>();
		if (!filetype_.empty()) {
			auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
			if (hl) {
				highlighter_->SetHighlighter(std::move(hl));
			} else {
				highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
			}
		} else {
			highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
		}
	}
	return *this;
}


// Move constructor: move all fields and update UndoSystem's buffer reference
Buffer::Buffer(Buffer &&other) noexcept
	: curx_(other.curx_),
	  cury_(other.cury_),
	  rx_(other.rx_),
	  nrows_(other.nrows_),
	  rowoffs_(other.rowoffs_),
	  coloffs_(other.coloffs_),
	  rows_(std::move(other.rows_)),
	  filename_(std::move(other.filename_)),
	  is_file_backed_(other.is_file_backed_),
	  dirty_(other.dirty_),
	  read_only_(other.read_only_),
	  mark_set_(other.mark_set_),
	  mark_curx_(other.mark_curx_),
	  mark_cury_(other.mark_cury_),
	  undo_tree_(std::move(other.undo_tree_)),
	  undo_sys_(std::move(other.undo_sys_))
{
	// Move edit mode + syntax/highlighting state
	edit_mode_          = other.edit_mode_;
	edit_mode_detected_ = other.edit_mode_detected_;
	version_            = other.version_;
	syntax_enabled_     = other.syntax_enabled_;
	filetype_           = std::move(other.filetype_);
	highlighter_        = std::move(other.highlighter_);
	content_            = std::move(other.content_);
	rows_cache_dirty_   = other.rows_cache_dirty_;
	// Update UndoSystem's buffer reference to point to this object
	if (undo_sys_) {
		undo_sys_->UpdateBufferReference(*this);
	}
}


// Move assignment: move all fields and update UndoSystem's buffer reference
Buffer &
Buffer::operator=(Buffer &&other) noexcept
{
	if (this == &other)
		return *this;

	curx_           = other.curx_;
	cury_           = other.cury_;
	rx_             = other.rx_;
	nrows_          = other.nrows_;
	rowoffs_        = other.rowoffs_;
	coloffs_        = other.coloffs_;
	rows_           = std::move(other.rows_);
	filename_       = std::move(other.filename_);
	is_file_backed_ = other.is_file_backed_;
	dirty_          = other.dirty_;
	read_only_      = other.read_only_;
	mark_set_       = other.mark_set_;
	mark_curx_      = other.mark_curx_;
	mark_cury_      = other.mark_cury_;
	undo_tree_      = std::move(other.undo_tree_);
	undo_sys_       = std::move(other.undo_sys_);

	// Move edit mode + syntax/highlighting state
	edit_mode_          = other.edit_mode_;
	edit_mode_detected_ = other.edit_mode_detected_;
	version_            = other.version_;
	syntax_enabled_     = other.syntax_enabled_;
	filetype_           = std::move(other.filetype_);
	highlighter_        = std::move(other.highlighter_);
	content_            = std::move(other.content_);
	rows_cache_dirty_   = other.rows_cache_dirty_;
	// Update UndoSystem's buffer reference to point to this object
	if (undo_sys_) {
		undo_sys_->UpdateBufferReference(*this);
	}

	return *this;
}


bool
Buffer::OpenFromFile(const std::string &path, std::string &err)
{
	auto normalize_path = [](const std::string &in) -> std::string {
		std::string expanded = in;
		// Expand leading '~' to HOME
		if (!expanded.empty() && expanded[0] == '~') {
			const char *home = std::getenv("HOME");
			if (home && expanded.size() >= 2 && (expanded[1] == '/' || expanded[1] == '\\')) {
				expanded = std::string(home) + expanded.substr(1);
			} else if (home && expanded.size() == 1) {
				expanded = std::string(home);
			}
		}
		try {
			std::filesystem::path p(expanded);
			if (std::filesystem::exists(p)) {
				return std::filesystem::canonical(p).string();
			}
			return std::filesystem::absolute(p).string();
		} catch (...) {
			// On any error, fall back to input
			return expanded;
		}
	};

	const std::string norm = normalize_path(path);
	// If the file doesn't exist, initialize an empty, non-file-backed buffer
	// with the provided filename. Do not touch the filesystem until Save/SaveAs.
	if (!std::filesystem::exists(norm)) {
		rows_.clear();
		nrows_          = 0;
		filename_       = norm;
		is_file_backed_ = false;
		dirty_          = false;

		// Reset cursor/viewport state
		curx_      = cury_    = rx_ = 0;
		rowoffs_   = coloffs_ = 0;
		mark_set_  = false;
		mark_curx_ = mark_cury_ = 0;

		// Empty PieceTable
		content_.Clear();
		rows_cache_dirty_ = true;

		return true;
	}

	std::ifstream in(norm, std::ios::in | std::ios::binary);
	if (!in) {
		err = "Failed to open file: " + norm;
		kte::ErrorHandler::Instance().Error("Buffer", err, norm);
		return false;
	}

	// Read entire file into PieceTable as-is
	std::string data;
	in.seekg(0, std::ios::end);
	if (!in) {
		err = "Failed to seek to end of file: " + norm;
		kte::ErrorHandler::Instance().Error("Buffer", err, norm);
		return false;
	}
	auto sz = in.tellg();
	if (sz < 0) {
		err = "Failed to get file size: " + norm;
		kte::ErrorHandler::Instance().Error("Buffer", err, norm);
		return false;
	}
	if (sz > 0) {
		data.resize(static_cast<std::size_t>(sz));
		in.seekg(0, std::ios::beg);
		if (!in) {
			err = "Failed to seek to beginning of file: " + norm;
			kte::ErrorHandler::Instance().Error("Buffer", err, norm);
			return false;
		}
		in.read(data.data(), static_cast<std::streamsize>(data.size()));
		if (!in && !in.eof()) {
			err = "Failed to read file: " + norm;
			kte::ErrorHandler::Instance().Error("Buffer", err, norm);
			return false;
		}
		// Validate we read the expected number of bytes
		const std::streamsize bytes_read = in.gcount();
		if (bytes_read != static_cast<std::streamsize>(data.size())) {
			err = "Partial read of file (expected " + std::to_string(data.size()) +
			      " bytes, got " + std::to_string(bytes_read) + "): " + norm;
			kte::ErrorHandler::Instance().Error("Buffer", err, norm);
			return false;
		}
	}
	content_.Clear();
	if (!data.empty())
		content_.Append(data.data(), data.size());
	rows_cache_dirty_ = true;
	nrows_            = 0; // not used under PieceTable
	filename_         = norm;
	is_file_backed_   = true;
	dirty_            = false;
	RefreshOnDiskIdentity();

	// Reset/initialize undo system for this loaded file
	if (!undo_tree_)
		undo_tree_ = std::make_unique<UndoTree>();
	if (!undo_sys_)
		undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
	// Clear any existing history for a fresh load
	undo_sys_->clear();

	// Reset cursor/viewport state
	curx_      = cury_    = rx_ = 0;
	rowoffs_   = coloffs_ = 0;
	mark_set_  = false;
	mark_curx_ = mark_cury_ = 0;

	return true;
}


bool
Buffer::Save(std::string &err) const
{
	if (!is_file_backed_ || filename_.empty()) {
		err = "Buffer is not file-backed; use SaveAs()";
		return false;
	}
	const std::size_t sz = content_.Size();
	const char *data     = sz ? content_.Data() : nullptr;
	if (sz && !data) {
		err = "Internal error: buffer materialization failed";
		return false;
	}
	if (!atomic_write_file(filename_, data ? data : "", sz, err)) {
		kte::ErrorHandler::Instance().Error("Buffer", err, filename_);
		return false;
	}
	// Update observed on-disk identity after a successful save.
	const_cast<Buffer *>(this)->RefreshOnDiskIdentity();
	// Note: const method cannot change dirty_. Intentionally const to allow UI code
	// to decide when to flip dirty flag after successful save.
	return true;
}


bool
Buffer::SaveAs(const std::string &path, std::string &err)
{
	// Normalize output path first
	std::string out_path;
	try {
		std::filesystem::path p(path);
		// Do a light expansion of '~'
		std::string expanded = path;
		if (!expanded.empty() && expanded[0] == '~') {
			const char *home = std::getenv("HOME");
			if (home && expanded.size() >= 2 && (expanded[1] == '/' || expanded[1] == '\\'))
				expanded = std::string(home) + expanded.substr(1);
			else if (home && expanded.size() == 1)
				expanded = std::string(home);
		}
		std::filesystem::path ep(expanded);
		out_path = std::filesystem::absolute(ep).string();
	} catch (...) {
		out_path = path;
	}

	const std::size_t sz = content_.Size();
	const char *data     = sz ? content_.Data() : nullptr;
	if (sz && !data) {
		err = "Internal error: buffer materialization failed";
		return false;
	}
	if (!atomic_write_file(out_path, data ? data : "", sz, err)) {
		kte::ErrorHandler::Instance().Error("Buffer", err, out_path);
		return false;
	}

	filename_       = out_path;
	is_file_backed_ = true;
	dirty_          = false;
	RefreshOnDiskIdentity();
	return true;
}


std::string
Buffer::AsString() const
{
	std::stringstream ss;
	ss << "Buffer<" << this->filename_;
	if (this->Dirty()) {
		ss << "*";
	}
	ss << ">: " << content_.LineCount() << " lines";
	return ss.str();
}


// --- Raw editing APIs (no undo recording, cursor untouched) ---
void
Buffer::insert_text(int row, int col, std::string_view text)
{
	if (row < 0)
		row = 0;
	if (col < 0)
		col = 0;
	const std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row),
	                                                     static_cast<std::size_t>(col));
	if (!text.empty()) {
		content_.Insert(off, text.data(), text.size());
		rows_cache_dirty_ = true;
		if (swap_rec_)
			swap_rec_->OnInsert(row, col, text);
	}
}


// ===== Adapter helpers for PieceTable-backed Buffer =====
std::string_view
Buffer::GetLineView(std::size_t row) const
{
	// Get byte range for the logical line and return a view into materialized data
	auto range       = content_.GetLineRange(row); // [start,end) in bytes
	const char *base = content_.Data(); // materializes if needed
	if (!base)
		return std::string_view();
	const std::size_t start = range.first;
	const std::size_t len   = (range.second > range.first) ? (range.second - range.first) : 0;
	return std::string_view(base + start, len);
}


void
Buffer::ensure_rows_cache() const
{
	std::lock_guard<std::mutex> lock(buffer_mutex_);
	if (!rows_cache_dirty_)
		return;
	rows_.clear();
	const std::size_t lc = content_.LineCount();
	rows_.reserve(lc);
	for (std::size_t i = 0; i < lc; ++i) {
		rows_.emplace_back(content_.GetLine(i));
	}
	// Keep nrows_ in sync for any legacy code that still reads it
	const_cast<Buffer *>(this)->nrows_ = rows_.size();
	rows_cache_dirty_                  = false;
}


std::size_t
Buffer::content_LineCount_() const
{
	return content_.LineCount();
}


#if defined(KTE_TESTS)
std::string
Buffer::BytesForTests() const
{
	const std::size_t sz = content_.Size();
	if (sz == 0)
		return std::string();
	const char *data = content_.Data();
	if (!data)
		return std::string();
	return std::string(data, data + sz);
}
#endif


void
Buffer::delete_text(int row, int col, std::size_t len)
{
	if (len == 0)
		return;
	if (row < 0)
		row = 0;
	if (col < 0)
		col = 0;

	const std::size_t start = content_.LineColToByteOffset(static_cast<std::size_t>(row),
	                                                       static_cast<std::size_t>(col));
	std::size_t r         = static_cast<std::size_t>(row);
	std::size_t c         = static_cast<std::size_t>(col);
	std::size_t remaining = len;
	const std::size_t lc  = content_.LineCount();

	while (remaining > 0 && r < lc) {
		const std::string line = content_.GetLine(r); // logical line (without trailing '\n')
		const std::size_t L    = line.size();
		if (c < L) {
			const std::size_t take = std::min(remaining, L - c);
			c                      += take;
			remaining              -= take;
		}
		if (remaining == 0)
			break;
		// Consume newline between lines as one char, if there is a next line
		if (r + 1 < lc) {
			remaining -= 1; // the newline
			r         += 1;
			c         = 0;
		} else {
			// At last line and still remaining: delete to EOF
			const std::size_t total  = content_.Size();
			const std::size_t actual = (total > start) ? (total - start) : 0;
			if (actual == 0)
				return;
			content_.Delete(start, actual);
			rows_cache_dirty_ = true;
			if (swap_rec_)
				swap_rec_->OnDelete(row, col, actual);
			return;
		}
	}

	// Compute end offset at (r,c)
	std::size_t end = content_.LineColToByteOffset(r, c);
	if (end > start) {
		const std::size_t actual = end - start;
		content_.Delete(start, actual);
		rows_cache_dirty_ = true;
		if (swap_rec_)
			swap_rec_->OnDelete(row, col, actual);
	}
}


void
Buffer::split_line(int row, const int col)
{
	int c = col;
	if (row < 0)
		row = 0;
	if (c < 0)
		c = 0;
	const std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row),
	                                                     static_cast<std::size_t>(c));
	const char nl = '\n';
	content_.Insert(off, &nl, 1);
	rows_cache_dirty_ = true;
	if (swap_rec_)
		swap_rec_->OnInsert(row, c, std::string_view("\n", 1));
}


void
Buffer::join_lines(int row)
{
	if (row < 0)
		row = 0;
	std::size_t r = static_cast<std::size_t>(row);
	if (r + 1 >= content_.LineCount())
		return;
	const int col = static_cast<int>(content_.GetLine(r).size());
	// Delete the newline between line r and r+1
	std::size_t end_of_line = content_.LineColToByteOffset(r, std::numeric_limits<std::size_t>::max());
	// end_of_line now equals line end (clamped before newline). The newline should be exactly at this position.
	content_.Delete(end_of_line, 1);
	rows_cache_dirty_ = true;
	if (swap_rec_)
		swap_rec_->OnDelete(row, col, 1);
}


void
Buffer::insert_row(int row, const std::string_view text)
{
	if (row < 0)
		row = 0;
	std::size_t off = content_.LineColToByteOffset(static_cast<std::size_t>(row), 0);
	if (!text.empty())
		content_.Insert(off, text.data(), text.size());
	const char nl = '\n';
	content_.Insert(off + text.size(), &nl, 1);
	rows_cache_dirty_ = true;
	if (swap_rec_) {
		// Avoid allocation: emit the row text insertion (if any) and the newline insertion.
		if (!text.empty())
			swap_rec_->OnInsert(row, 0, text);
		swap_rec_->OnInsert(row, static_cast<int>(text.size()), std::string_view("\n", 1));
	}
}


void
Buffer::delete_row(int row)
{
	if (row < 0)
		row = 0;
	std::size_t r = static_cast<std::size_t>(row);
	if (r >= content_.LineCount())
		return;
	auto range = content_.GetLineRange(r); // [start,end)
	// If not last line, ensure we include the separating newline by using end as-is (which points to next line start)
	// If last line, end may equal total_size_. We still delete [start,end) which removes the last line content.
	const std::size_t start  = range.first;
	const std::size_t end    = range.second;
	const std::size_t actual = (end > start) ? (end - start) : 0;
	if (actual == 0)
		return;
	content_.Delete(start, actual);
	rows_cache_dirty_ = true;
	if (swap_rec_)
		swap_rec_->OnDelete(row, 0, actual);
}


void
Buffer::replace_all_bytes(const std::string_view bytes)
{
	content_.Clear();
	if (!bytes.empty())
		content_.Append(bytes.data(), bytes.size());
	rows_cache_dirty_ = true;
}


// Undo system accessors
UndoSystem *
Buffer::Undo()
{
	return undo_sys_.get();
}


const UndoSystem *
Buffer::Undo() const
{
	return undo_sys_.get();
}