using System.Buffers;
using System.Text;

using Avalonia.Collections;

using AvaloniaEdit.Document;

using InkForge.Data;

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

namespace InkForge.Desktop.Models;

public class TextDocumentStore(IDbContextFactory<NoteDbContext> dbContextFactory)
{
	private readonly AvaloniaDictionary<int, TextDocument> _cache = [];

	public void Close(int id)
	{
		if (_cache.Remove(id, out var document))
		{
			document.Remove(0, document.TextLength);
		}
	}

	public TextDocument Get(int id)
	{
		if (!_cache.TryGetValue(id, out var document))
		{
			using var dbContext = dbContextFactory.CreateDbContext();
			var connection = dbContext.Database.GetDbConnection();
			using var command = connection.CreateCommand();
			command.CommandText = $"SELECT rowid FROM Blobs WHERE Id={id}";
			using var commandReader = command.ExecuteReader();
			document = new();
			if (commandReader.Read())
			{
				using SqliteBlob stream = new((SqliteConnection)connection, "Blob", "Value", commandReader.GetInt64(0), true);
				CopyDocumentContent(stream, document);
			}

			_cache.Add(id, document);
		}

		return document;

		static void CopyDocumentContent(Stream stream, TextDocument document)
		{
			DocumentTextWriter writer = new(document, 0);
			StreamReader reader = new(stream, Encoding.UTF8);
			Span<char> buffer = stackalloc char[128];
			while (!reader.EndOfStream)
			{
				var read = reader.Read(buffer);
				writer.Write(buffer[..read]);
			}
		}
	}

	public int? Save(int? id, TextDocument textDocument)
	{
		var state = textDocument.CreateSnapshot();
		var bytes = TextBytes(state);

		using var dbContext = dbContextFactory.CreateDbContext();
		var connection = dbContext.Database.GetDbConnection();
		using var transaction = dbContext.Database.BeginTransaction();
		using var command = connection.CreateCommand();
		command.CommandText = id switch
		{
			null => $"INSERT INTO Blobs(Value) VALUES(zeroblob({bytes})) RETURNING (Id, rowid)",
			_ => $"UPDATE Blobs SET Value=zeroblob({bytes}) WHERE Id={id} RETURNING (Id, rowid)"
		};
		using var commandReader = command.ExecuteReader();
		if (!commandReader.Read())
		{
			return null;
		}

		id = commandReader.GetInt32(0);
		using (SqliteBlob stream = new((SqliteConnection)connection, "Blobs", "Value", commandReader.GetInt64(1)))
		{
			using StreamWriter writer = new(stream, Encoding.UTF8);
			state.WriteTextTo(writer);
		}

		transaction.Commit();
		_cache.TryAdd((int)id, textDocument);
		return id;

		static int TextBytes(ITextSource text)
		{
			int length = 0;
			Span<byte> byteBuffer = stackalloc byte[128];
			using var reader = text.CreateReader();
			ArrayBufferWriter<char> buffer = new();
			var encoder = Encoding.UTF8.GetEncoder();
			while (reader.Peek() != -1)
			{
				var memory = buffer.GetMemory();
				buffer.Advance(reader.Read(memory.Span));
				ReadOnlySpan<char> chars = buffer.WrittenSpan;

			convert:
				encoder.Convert(buffer.WrittenSpan, byteBuffer, false, out var charsUsed, out var bytesUsed, out _);
				chars = chars[charsUsed..];
				if (bytesUsed > 0)
				{
					length += bytesUsed;
					goto convert;
				}

				Span<char> remaining = [];
				if (!chars.IsEmpty)
				{
					remaining = buffer.GetSpan(chars.Length);
					chars.CopyTo(remaining);
				}

				buffer.Clear();
				if (!remaining.IsEmpty)
				{
					buffer.Write(remaining);
				}
			}

			return length;
		}
	}
}
