// Copyright 2023-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace ZB.MOM.NatsNet.Server.Internal.DataStructures;

/// <summary>
/// Utility methods for NATS subject matching, wildcard part decomposition,
/// common prefix computation, and byte manipulation used by SubjectTree.
/// </summary>
internal static class SubjectTreeParts
{
    // NATS subject special bytes.
    internal const byte Pwc = (byte)'*';  // single-token wildcard
    internal const byte Fwc = (byte)'>';  // full wildcard (terminal)
    internal const byte TSep = (byte)'.'; // token separator

    // Sentinel pivot returned when subject position is past end.
    internal const byte NoPivot = 127;

    /// <summary>
    /// Returns the pivot byte at <paramref name="pos"/> in <paramref name="subject"/>,
    /// or <see cref="NoPivot"/> if the position is at or beyond the end.
    /// </summary>
    internal static byte Pivot(ReadOnlySpan<byte> subject, int pos)
        => pos >= subject.Length ? NoPivot : subject[pos];

    /// <summary>
    /// Returns the pivot byte at <paramref name="pos"/> in <paramref name="subject"/>,
    /// or <see cref="NoPivot"/> if the position is at or beyond the end.
    /// </summary>
    internal static byte Pivot(byte[] subject, int pos)
        => pos >= subject.Length ? NoPivot : subject[pos];

    /// <summary>
    /// Computes the number of leading bytes that are equal between two spans.
    /// </summary>
    internal static int CommonPrefixLen(ReadOnlySpan<byte> s1, ReadOnlySpan<byte> s2)
    {
        var limit = Math.Min(s1.Length, s2.Length);
        var i = 0;
        while (i < limit && s1[i] == s2[i])
            i++;
        return i;
    }

    /// <summary>
    /// Returns a copy of <paramref name="src"/>, or an empty array if src is empty.
    /// </summary>
    internal static byte[] CopyBytes(ReadOnlySpan<byte> src)
    {
        if (src.IsEmpty) return Array.Empty<byte>();
        return src.ToArray();
    }

    /// <summary>
    /// Returns a copy of <paramref name="src"/>, or an empty array if src is null or empty.
    /// </summary>
    internal static byte[] CopyBytes(byte[]? src)
    {
        if (src == null || src.Length == 0) return Array.Empty<byte>();
        var dst = new byte[src.Length];
        src.CopyTo(dst, 0);
        return dst;
    }

    /// <summary>
    /// Converts a byte array to a string using Latin-1 (ISO-8859-1) encoding,
    /// which preserves a 1:1 byte-to-char mapping for all byte values 0-255.
    /// </summary>
    internal static string BytesToString(byte[] bytes)
    {
        if (bytes.Length == 0) return string.Empty;
        return System.Text.Encoding.Latin1.GetString(bytes);
    }

    /// <summary>
    /// Breaks a filter subject into parts separated by wildcards ('*' and '>').
    /// Each literal segment between wildcards becomes one part; each wildcard
    /// becomes its own single-byte part.
    /// </summary>
    internal static byte[][] GenParts(byte[] filter)
    {
        var parts = new List<byte[]>(8);
        var start = 0;
        var e = filter.Length - 1;

        for (var i = 0; i < filter.Length; i++)
        {
            if (filter[i] == TSep)
            {
                // Check if next token is pwc (internal or terminal).
                if (i < e && filter[i + 1] == Pwc &&
                    ((i + 2 <= e && filter[i + 2] == TSep) || i + 1 == e))
                {
                    if (i > start)
                        parts.Add(filter[start..(i + 1)]);
                    parts.Add(filter[(i + 1)..(i + 2)]);
                    i++; // skip pwc
                    if (i + 2 <= e)
                        i++; // skip next tsep from next part
                    start = i + 1;
                }
                else if (i < e && filter[i + 1] == Fwc && i + 1 == e)
                {
                    if (i > start)
                        parts.Add(filter[start..(i + 1)]);
                    parts.Add(filter[(i + 1)..(i + 2)]);
                    i++; // skip fwc
                    start = i + 1;
                }
            }
            else if (filter[i] == Pwc || filter[i] == Fwc)
            {
                // Wildcard must be preceded by tsep (or be at start).
                var prev = i - 1;
                if (prev >= 0 && filter[prev] != TSep)
                    continue;

                // Wildcard must be at end or followed by tsep.
                var next = i + 1;
                if (next == e || (next < e && filter[next] != TSep))
                    continue;

                // Full wildcard must be terminal.
                if (filter[i] == Fwc && i < e)
                    break;

                // Leading wildcard.
                parts.Add(filter[i..(i + 1)]);
                if (i + 1 <= e)
                    i++; // skip next tsep
                start = i + 1;
            }
        }

        if (start < filter.Length)
        {
            // Eat leading tsep if present.
            if (filter[start] == TSep)
                start++;
            if (start < filter.Length)
                parts.Add(filter[start..]);
        }

        return parts.ToArray();
    }

    /// <summary>
    /// Matches parts against a fragment (prefix or suffix).
    /// Returns the remaining parts and whether matching succeeded.
    /// </summary>
    internal static (byte[][] remainingParts, bool matched) MatchParts(byte[][] parts, byte[] frag)
    {
        var lf = frag.Length;
        if (lf == 0) return (parts, true);

        var si = 0;
        var lpi = parts.Length - 1;

        for (var i = 0; i < parts.Length; i++)
        {
            if (si >= lf)
                return (parts[i..], true);

            var part = parts[i];
            var lp = part.Length;

            // Check for wildcard placeholders.
            if (lp == 1)
            {
                if (part[0] == Pwc)
                {
                    // Find the next token separator.
                    var index = Array.IndexOf(frag, TSep, si);
                    if (index < 0)
                    {
                        // No tsep found.
                        if (i == lpi)
                            return (Array.Empty<byte[]>(), true);
                        return (parts[i..], true);
                    }
                    si = index + 1;
                    continue;
                }
                else if (part[0] == Fwc)
                {
                    return (Array.Empty<byte[]>(), true);
                }
            }

            var end = Math.Min(si + lp, lf);
            // If part is larger than the remaining fragment, adjust.
            var comparePart = part;
            if (si + lp > end)
                comparePart = part[..(end - si)];

            if (!frag.AsSpan(si, end - si).SequenceEqual(comparePart))
                return (parts, false);

            // Fragment still has bytes left.
            if (end < lf)
            {
                si = end;
                continue;
            }

            // We matched a partial part.
            if (end < si + lp)
            {
                if (end >= lf)
                {
                    // Create a copy of parts with the current part trimmed.
                    var newParts = new byte[parts.Length][];
                    parts.CopyTo(newParts, 0);
                    newParts[i] = parts[i][(lf - si)..];
                    return (newParts[i..], true);
                }
                else
                {
                    return (parts[(i + 1)..], true);
                }
            }

            if (i == lpi)
                return (Array.Empty<byte[]>(), true);

            si += part.Length;
        }

        return (parts, false);
    }
}
