using System.Buffers.Binary;
using System.Runtime.InteropServices;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Core.Indexing.Internal;

namespace ZB.MOM.WW.CBDD.Core.Storage;

/// <summary>
/// Page for storing R-Tree nodes for Geospatial Indexing.
/// </summary>
internal struct SpatialPage
{
    // Layout:
    // [PageHeader (32)]
    // [IsLeaf (1)]
    // [Level (1)]
    // [EntryCount (2)]
    // [ParentPageId (4)]
    // [Padding (8)]
    // [Entries (Contiguous)...]
    // 
    // Each Entry: [MBR (4 * 8 = 32)] [Pointer (6)] = 38 bytes

    private const int IsLeafOffset = 32;
    private const int LevelOffset = 33;
    private const int EntryCountOffset = 34;
    private const int ParentPageIdOffset = 36;
    private const int DataOffset = 48;

    public const int EntrySize = 38; // 32 (GeoBox) + 6 (Pointer)

    /// <summary>
    /// Initializes a spatial page.
    /// </summary>
    /// <param name="page">The page buffer to initialize.</param>
    /// <param name="pageId">The page identifier.</param>
    /// <param name="isLeaf">Whether this page is a leaf node.</param>
    /// <param name="level">The tree level for this page.</param>
    public static void Initialize(Span<byte> page, uint pageId, bool isLeaf, byte level)
    {
        var header = new PageHeader
        {
            PageId = pageId,
            PageType = PageType.Spatial,
            FreeBytes = (ushort)(page.Length - DataOffset),
            NextPageId = 0,
            TransactionId = 0
        };
        header.WriteTo(page);

        page[IsLeafOffset] = (byte)(isLeaf ? 1 : 0);
        page[LevelOffset] = level;
        BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(EntryCountOffset), 0);
        BinaryPrimitives.WriteUInt32LittleEndian(page.Slice(ParentPageIdOffset), 0);
    }

    /// <summary>
    /// Gets a value indicating whether the page is a leaf node.
    /// </summary>
    /// <param name="page">The page buffer.</param>
    /// <returns><see langword="true"/> if the page is a leaf node; otherwise, <see langword="false"/>.</returns>
    public static bool GetIsLeaf(ReadOnlySpan<byte> page) => page[IsLeafOffset] == 1;

    /// <summary>
    /// Gets the tree level stored in the page.
    /// </summary>
    /// <param name="page">The page buffer.</param>
    /// <returns>The level value.</returns>
    public static byte GetLevel(ReadOnlySpan<byte> page) => page[LevelOffset];

    /// <summary>
    /// Gets the number of entries in the page.
    /// </summary>
    /// <param name="page">The page buffer.</param>
    /// <returns>The number of entries.</returns>
    public static ushort GetEntryCount(ReadOnlySpan<byte> page) => BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(EntryCountOffset));

    /// <summary>
    /// Sets the number of entries in the page.
    /// </summary>
    /// <param name="page">The page buffer.</param>
    /// <param name="count">The entry count to set.</param>
    public static void SetEntryCount(Span<byte> page, ushort count) => BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(EntryCountOffset), count);

    /// <summary>
    /// Gets the parent page identifier.
    /// </summary>
    /// <param name="page">The page buffer.</param>
    /// <returns>The parent page identifier.</returns>
    public static uint GetParentPageId(ReadOnlySpan<byte> page) => BinaryPrimitives.ReadUInt32LittleEndian(page.Slice(ParentPageIdOffset));

    /// <summary>
    /// Sets the parent page identifier.
    /// </summary>
    /// <param name="page">The page buffer.</param>
    /// <param name="parentId">The parent page identifier.</param>
    public static void SetParentPageId(Span<byte> page, uint parentId) => BinaryPrimitives.WriteUInt32LittleEndian(page.Slice(ParentPageIdOffset), parentId);

    /// <summary>
    /// Gets the maximum number of entries that can fit in a page.
    /// </summary>
    /// <param name="pageSize">The page size in bytes.</param>
    /// <returns>The maximum number of entries.</returns>
    public static int GetMaxEntries(int pageSize) => (pageSize - DataOffset) / EntrySize;

    /// <summary>
    /// Writes an entry at the specified index.
    /// </summary>
    /// <param name="page">The page buffer.</param>
    /// <param name="index">The entry index.</param>
    /// <param name="mbr">The minimum bounding rectangle for the entry.</param>
    /// <param name="pointer">The document location pointer.</param>
    public static void WriteEntry(Span<byte> page, int index, GeoBox mbr, DocumentLocation pointer)
    {
        int offset = DataOffset + (index * EntrySize);
        var entrySpan = page.Slice(offset, EntrySize);

        // Write MBR (4 doubles)
        var doubles = MemoryMarshal.Cast<byte, double>(entrySpan.Slice(0, 32));
        doubles[0] = mbr.MinLat;
        doubles[1] = mbr.MinLon;
        doubles[2] = mbr.MaxLat;
        doubles[3] = mbr.MaxLon;

        // Write Pointer (6 bytes)
        pointer.WriteTo(entrySpan.Slice(32, 6));
    }

    /// <summary>
    /// Reads an entry at the specified index.
    /// </summary>
    /// <param name="page">The page buffer.</param>
    /// <param name="index">The entry index.</param>
    /// <param name="mbr">When this method returns, contains the entry MBR.</param>
    /// <param name="pointer">When this method returns, contains the entry document location.</param>
    public static void ReadEntry(ReadOnlySpan<byte> page, int index, out GeoBox mbr, out DocumentLocation pointer)
    {
        int offset = DataOffset + (index * EntrySize);
        var entrySpan = page.Slice(offset, EntrySize);

        var doubles = MemoryMarshal.Cast<byte, double>(entrySpan.Slice(0, 32));
        mbr = new GeoBox(doubles[0], doubles[1], doubles[2], doubles[3]);
        pointer = DocumentLocation.ReadFrom(entrySpan.Slice(32, 6));
    }

    /// <summary>
    /// Calculates the combined MBR of all entries in the page.
    /// </summary>
    /// <param name="page">The page buffer.</param>
    /// <returns>The combined MBR, or <see cref="GeoBox.Empty"/> when the page has no entries.</returns>
    public static GeoBox CalculateMBR(ReadOnlySpan<byte> page)
    {
        ushort count = GetEntryCount(page);
        if (count == 0) return GeoBox.Empty;

        GeoBox result = GeoBox.Empty;
        for (int i = 0; i < count; i++)
        {
            ReadEntry(page, i, out var mbr, out _);
            if (i == 0) result = mbr;
            else result = result.ExpandTo(mbr);
        }
        return result;
    }
}
