///
/// \file Buffer.cc
/// \author kyle 
/// \created 10/10/23
/// \brief Buffer implementation.
///
/// \section COPYRIGHT
/// Copyright 2023 K. Isom <kyle@imap.cc>
///
/// Permission to use, copy, modify, and/or distribute this software for
/// any purpose with or without fee is hereby granted, provided that the
/// above copyright notice and this permission notice appear in all copies.
///
/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
/// BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
/// OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
/// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
/// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
/// SOFTWARE.
///


#include <filesystem>
#include <iterator>
#include <optional>
#include <random>
#include <sstream>

#include "Buffer.h"


static constexpr auto statusOK = Buffer::FileStatus::FileStatusOK;


static std::string
anonymousName()
{
	std::uniform_int_distribution<> dist(1000, 9999);
	std::random_device              randomDevice;
	std::mt19937                    rng(randomDevice());
	std::stringstream               ss;

	ss << "Buffer<" << dist(rng) << ">";
	return ss.str();
}


Buffer::Buffer()
    : dirty(false), name(anonymousName()), status(statusOK)
{
}


Buffer::Buffer(std::string fName)
    : dirty(false), name(std::move(fName))
{

}


Buffer::Buffer(std::filesystem::path fPath)
    : dirty(false), path(OptString(fPath.string()))
{
	this->name = fPath.filename().string();
	this->cursor = Cursor();
	this->file = OptFile(fPath.string());
	if (this->Exists()) {
		/// \todo Should I signal an error here, or is it
		/// okay for this to be a best-effort thing?
		this->status = this->Refresh();
	}
}



Buffer::Buffer(std::string fName, std::string fPath)
    : dirty(false), name(std::move(fName)), path(fPath)
{
	if (this->path) {
		this->file = OptFile (File(fPath));
	}

}


Buffer::FileStatus
Buffer::Flush(OptString altPath)
{
	OptOutFileStream handle = std::nullopt;

	if (altPath) {
		auto altFile = File(altPath.value());
		handle = altFile.Flush();
	} else if (this->file) {
		handle = this->file.value().Flush();
	} else {
		// At this point, we have no alternate path to write out
		// and this Buffer isn't file-backed. From this point
		// forward, we can operate under the assumption that
		// `this->file` is a valid file.
		return Buffer::FileStatus::FileStatusVirtual;
	}

	auto realFile = this->file.value();
	if (!handle) {
		if (realFile.IsReadOnly()) {
			return Buffer::FileStatus::FileStatusReadOnly;
		}

		if (!realFile.IsWriteable()) {
			return Buffer::FileStatus::FileStatusInvalidPermissions;
		}

		realFile.MarkReadOnly();
		return Buffer::FileStatus::FileStatusIOFailed;
	}

	/// At this point, we know we're working with a valid file handle.
	auto realHandle = handle.value();
	if (!realHandle->good()) {
		return Buffer::FileStatus::FileStatusIOFailed;
	}

	for (auto line : this->contents) {
		std::copy(line.begin(), line.end(),
			  std::ostream_iterator<uint8_t>(*realHandle, ""));
	}

	realHandle->flush();
	realHandle->close();
	delete realHandle;
	this->dirty = false;
	return Buffer::FileStatus::FileStatusOK;
}


Buffer::FileStatus
Buffer::Refresh()
{
	/// We can't actually refresh a virtual buffer. Unlike flush,
	/// it doesn't make sense to Refresh from an alternate file.
	if (this->IsVirtual()) {
		return Buffer::FileStatus::FileStatusVirtual;
	}

	auto realFile = this->file.value();
	auto handle = realFile.Refresh();
	if (!handle) {
		return FileStatus::FileStatusNonExistent;
	}

	auto realHandle = handle.value();
	this->clearContents();
	this->dirty = false; // clean slate, kiddo

	/// \todo Moral quandary: if the file disappears from disk, we
	///       probably want to know that, but maybe clearing the
	///       contents first isn't the right move. Maybe mark the
	///	  file as ReadOnly?
	if (!realHandle->good()) {
		realHandle->close();
		delete realHandle;
		return FileStatus::FileStatusIOFailed;
	}

	while (realHandle->good()) {
		std::string temp;
		while (std::getline(*realHandle, temp)) {
			std::vector<uint8_t> row(temp.begin(), temp.end());
			row.push_back('\n');
			this->contents.push_back(row);
		}
	}

	auto status = Buffer::FileStatus::FileStatusOK;
	if (!realHandle->eof()) {
		status = FileStatus::FileStatusIOFailed;
	}
	realHandle->close();
	delete realHandle;

	return status;
}


void
Buffer::ChangePath(std::string newPath)
{
	this->path = OptString(std::move(newPath));
}


void
Buffer::clearContents()
{
	for (auto & line : this->contents) {
		line.clear();
	}

	this->contents.clear();
}


void
Buffer::MarkDirty()
{
	if (this->dirty) {
		return;
	}

	this->dirty = true;
}


size_t
Buffer::Size()
{
	size_t 	size = 0;

	for (const auto& line : this->contents) {
		size += line.size();
	}

	return size;
}


void
Buffer::Close()
{
	this->clearContents();
	this->dirty = false;
	this->file = std::nullopt;
	this->path = std::nullopt;
	this->name = std::string("deleted buffer (" + this->name + ")");
}


std::string
Buffer::FileStatusToString(Buffer::FileStatus status)
{
	switch (status) {
	case FileStatus::FileStatusOK:
		return std::string("OK");
	case FileStatus::FileStatusReadOnly:
		return std::string("read-only file");
	case FileStatus::FileStatusIOFailed:
		return std::string("I/O failure");
	case FileStatus::FileStatusVirtual:
		return std::string("virtual buffer");
	case FileStatus::FileStatusInvalidPermissions:
		return std::string("invalid permissions");
	case FileStatus::FileStatusNonExistent:
		return std::string("file does not exist");
	default:
		abort();

	}
}


bool
Buffer::Exists()
{
	if (!this->file) {
		return false;
	}

	return this->file.value().Exists();
}



void
Buffer::PrintBufferStatus(std::ostream &os)
{
	os << "buffer:<" << this->name << ">[";
	if (this->IsVirtual()) {
		os << "virt]";
	} else {
		os << "file:";
		if (this->Exists()) {
			os << "real";
		} else {
			os << "new";
		}
		os << ":";
		auto &realFile = this->file.value();
		if (realFile.IsReadWriteable()) {
			os << "rw";
		} else if (realFile.IsReadable()) {
			os << "r-";
		} else if (realFile.IsWriteable()) {
			os << "-w";
		} else {
			os << "  ";
		}

		if (realFile.IsReadOnly()) {
			os << "@";
		} else {
			os << "-";
		}

		if (this->IsDirty()) {
			os << "!";
		} else {
			os << "-";
		}

		os << ":" << this->Size() << "B";
	}

	os << "\n";
	return;
}
