// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;

namespace Microsoft.AspNetCore.HttpSys.Internal
{
    internal static class PathNormalizer
    {
        private const byte ByteSlash = (byte)'/';
        private const byte ByteDot = (byte)'.';

        // In-place implementation of the algorithm from https://tools.ietf.org/html/rfc3986#section-5.2.4
        public static unsafe int RemoveDotSegments(Span<byte> input)
        {
            fixed (byte* start = input)
            {
                var end = start + input.Length;
                return RemoveDotSegments(start, end);
            }
        }

        public static unsafe int RemoveDotSegments(byte* start, byte* end)
        {
            if (!ContainsDotSegments(start, end))
            {
                return (int)(end - start);
            }

            var src = start;
            var dst = start;

            while (src < end)
            {
                var ch1 = *src;
                Debug.Assert(ch1 == '/', "Path segment must always start with a '/'");

                byte ch2, ch3, ch4;

                switch (end - src)
                {
                    case 1:
                        break;
                    case 2:
                        ch2 = *(src + 1);

                        if (ch2 == ByteDot)
                        {
                            // B.  if the input buffer begins with a prefix of "/./" or "/.",
                            //     where "." is a complete path segment, then replace that
                            //     prefix with "/" in the input buffer; otherwise,
                            src += 1;
                            *src = ByteSlash;
                            continue;
                        }

                        break;
                    case 3:
                        ch2 = *(src + 1);
                        ch3 = *(src + 2);

                        if (ch2 == ByteDot && ch3 == ByteDot)
                        {
                            // C.  if the input buffer begins with a prefix of "/../" or "/..",
                            //     where ".." is a complete path segment, then replace that
                            //     prefix with "/" in the input buffer and remove the last
                            //     segment and its preceding "/" (if any) from the output
                            //     buffer; otherwise,
                            src += 2;
                            *src = ByteSlash;

                            if (dst > start)
                            {
                                do
                                {
                                    dst--;
                                } while (dst > start && *dst != ByteSlash);
                            }

                            continue;
                        }
                        else if (ch2 == ByteDot && ch3 == ByteSlash)
                        {
                            // B.  if the input buffer begins with a prefix of "/./" or "/.",
                            //     where "." is a complete path segment, then replace that
                            //     prefix with "/" in the input buffer; otherwise,
                            src += 2;
                            continue;
                        }

                        break;
                    default:
                        ch2 = *(src + 1);
                        ch3 = *(src + 2);
                        ch4 = *(src + 3);

                        if (ch2 == ByteDot && ch3 == ByteDot && ch4 == ByteSlash)
                        {
                            // C.  if the input buffer begins with a prefix of "/../" or "/..",
                            //     where ".." is a complete path segment, then replace that
                            //     prefix with "/" in the input buffer and remove the last
                            //     segment and its preceding "/" (if any) from the output
                            //     buffer; otherwise,
                            src += 3;

                            if (dst > start)
                            {
                                do
                                {
                                    dst--;
                                } while (dst > start && *dst != ByteSlash);
                            }

                            continue;
                        }
                        else if (ch2 == ByteDot && ch3 == ByteSlash)
                        {
                            // B.  if the input buffer begins with a prefix of "/./" or "/.",
                            //     where "." is a complete path segment, then replace that
                            //     prefix with "/" in the input buffer; otherwise,
                            src += 2;
                            continue;
                        }

                        break;
                }

                // E.  move the first path segment in the input buffer to the end of
                //     the output buffer, including the initial "/" character (if
                //     any) and any subsequent characters up to, but not including,
                //     the next "/" character or the end of the input buffer.
                do
                {
                    *dst++ = ch1;
                    ch1 = *++src;
                } while (src < end && ch1 != ByteSlash);
            }

            if (dst == start)
            {
                *dst++ = ByteSlash;
            }

            return (int)(dst - start);
        }

        public static unsafe bool ContainsDotSegments(byte* start, byte* end)
        {
            var src = start;
            var dst = start;

            while (src < end)
            {
                var ch1 = *src;
                Debug.Assert(ch1 == '/', "Path segment must always start with a '/'");

                byte ch2, ch3, ch4;

                switch (end - src)
                {
                    case 1:
                        break;
                    case 2:
                        ch2 = *(src + 1);

                        if (ch2 == ByteDot)
                        {
                            return true;
                        }

                        break;
                    case 3:
                        ch2 = *(src + 1);
                        ch3 = *(src + 2);

                        if ((ch2 == ByteDot && ch3 == ByteDot) ||
                            (ch2 == ByteDot && ch3 == ByteSlash))
                        {
                            return true;
                        }

                        break;
                    default:
                        ch2 = *(src + 1);
                        ch3 = *(src + 2);
                        ch4 = *(src + 3);

                        if ((ch2 == ByteDot && ch3 == ByteDot && ch4 == ByteSlash) ||
                            (ch2 == ByteDot && ch3 == ByteSlash))
                        {
                            return true;
                        }

                        break;
                }

                do
                {
                    ch1 = *++src;
                } while (src < end && ch1 != ByteSlash);
            }

            return false;
        }
    }
}
