// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
using System.Text;

#pragma warning disable SA1509 // Opening braces should not be preceded by blank line

namespace MessagePack.Internal
{
    // Key = long, Value = int for UTF8String Dictionary

    /// <remarks>
    /// This code is used by dynamically generated code as well as AOT generated code,
    /// and thus must be public for the "C# generated and compiled into saved assembly" scenario.
    /// </remarks>
    public class AutomataDictionary : IEnumerable<KeyValuePair<string, int>>
    {
        private readonly AutomataNode root;

        public AutomataDictionary()
        {
            this.root = new AutomataNode(0);
        }

        public void Add(string str, int value)
        {
            ReadOnlySpan<byte> bytes = Encoding.UTF8.GetBytes(str);
            AutomataNode node = this.root;

            while (bytes.Length > 0)
            {
                var key = AutomataKeyGen.GetKey(ref bytes);

                if (bytes.Length == 0)
                {
                    node = node.Add(key, value, str);
                }
                else
                {
                    node = node.Add(key);
                }
            }
        }

        public bool TryGetValue(in ReadOnlySequence<byte> bytes, out int value) => this.TryGetValue(bytes.ToArray(), out value);

        public bool TryGetValue(ReadOnlySpan<byte> bytes, out int value)
        {
            AutomataNode node = this.root;

            while (bytes.Length > 0 && node != null)
            {
                node = node.SearchNext(ref bytes);
            }

            if (node == null)
            {
                value = -1;
                return false;
            }
            else
            {
                value = node.Value;
                return true;
            }
        }

        // for debugging
        public override string ToString()
        {
            var sb = new StringBuilder();
            ToStringCore(this.root.YieldChildren(), sb, 0);
            return sb.ToString();
        }

        private static void ToStringCore(IEnumerable<AutomataNode> nexts, StringBuilder sb, int depth)
        {
            foreach (AutomataNode item in nexts)
            {
                if (depth != 0)
                {
                    sb.Append(' ', depth * 2);
                }

                sb.Append("[" + item.Key + "]");
                if (item.Value != -1)
                {
                    sb.Append("(" + item.OriginalKey + ")");
                    sb.Append(" = ");
                    sb.Append(item.Value);
                }

                sb.AppendLine();
                ToStringCore(item.YieldChildren(), sb, depth + 1);
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        public IEnumerator<KeyValuePair<string, int>> GetEnumerator()
        {
            return YieldCore(this.root.YieldChildren()).GetEnumerator();
        }

        private static IEnumerable<KeyValuePair<string, int>> YieldCore(IEnumerable<AutomataNode> nexts)
        {
            foreach (AutomataNode item in nexts)
            {
                if (item.Value != -1)
                {
                    yield return new KeyValuePair<string, int>(item.OriginalKey, item.Value);
                }

                foreach (KeyValuePair<string, int> x in YieldCore(item.YieldChildren()))
                {
                    yield return x;
                }
            }
        }

        /* IL Emit */

#if !NET_STANDARD_2_0

        public void EmitMatch(ILGenerator il, LocalBuilder bytesSpan, LocalBuilder key, Action<KeyValuePair<string, int>> onFound, Action onNotFound)
        {
            this.root.EmitSearchNext(il, bytesSpan, key, onFound, onNotFound);
        }

#endif

        private class AutomataNode : IComparable<AutomataNode>
        {
#pragma warning disable SA1401 // Fields should be private
            internal ulong Key;
            internal int Value;
            internal string OriginalKey;
#pragma warning restore SA1401 // Fields should be private

            private AutomataNode[] nexts;
            private ulong[] nextKeys;
            private int count;

            public bool HasChildren
            {
                get { return this.count != 0; }
            }

            public AutomataNode(ulong key)
            {
                this.Key = key;
                this.Value = -1;
                this.nexts = Array.Empty<AutomataNode>();
                this.nextKeys = Array.Empty<ulong>();
                this.count = 0;
                this.OriginalKey = null;
            }

            public AutomataNode Add(ulong key)
            {
                var index = Array.BinarySearch(this.nextKeys, 0, this.count, key);
                if (index < 0)
                {
                    if (this.nexts.Length == this.count)
                    {
                        Array.Resize<AutomataNode>(ref this.nexts, (this.count == 0) ? 4 : (this.count * 2));
                        Array.Resize<ulong>(ref this.nextKeys, (this.count == 0) ? 4 : (this.count * 2));
                    }

                    this.count++;

                    var nextNode = new AutomataNode(key);
                    this.nexts[this.count - 1] = nextNode;
                    this.nextKeys[this.count - 1] = key;
                    Array.Sort(this.nexts, 0, this.count);
                    Array.Sort(this.nextKeys, 0, this.count);
                    return nextNode;
                }
                else
                {
                    return this.nexts[index];
                }
            }

            public AutomataNode Add(ulong key, int value, string originalKey)
            {
                AutomataNode v = this.Add(key);
                v.Value = value;
                v.OriginalKey = originalKey;
                return v;
            }

            public AutomataNode SearchNext(ref ReadOnlySpan<byte> value)
            {
                var key = AutomataKeyGen.GetKey(ref value);
                if (this.count < 4)
                {
                    // linear search
                    for (int i = 0; i < this.count; i++)
                    {
                        if (this.nextKeys[i] == key)
                        {
                            return this.nexts[i];
                        }
                    }
                }
                else
                {
                    // binary search
                    var index = BinarySearch(this.nextKeys, 0, this.count, key);
                    if (index >= 0)
                    {
                        return this.nexts[index];
                    }
                }

                return null;
            }

            internal static int BinarySearch(ulong[] array, int index, int length, ulong value)
            {
                int lo = index;
                int hi = index + length - 1;
                while (lo <= hi)
                {
                    int i = lo + ((hi - lo) >> 1);

                    var arrayValue = array[i];
                    int order;
                    if (arrayValue < value)
                    {
                        order = -1;
                    }
                    else if (arrayValue > value)
                    {
                        order = 1;
                    }
                    else
                    {
                        order = 0;
                    }

                    if (order == 0)
                    {
                        return i;
                    }

                    if (order < 0)
                    {
                        lo = i + 1;
                    }
                    else
                    {
                        hi = i - 1;
                    }
                }

                return ~lo;
            }

            public int CompareTo(AutomataNode other)
            {
                return this.Key.CompareTo(other.Key);
            }

            public IEnumerable<AutomataNode> YieldChildren()
            {
                for (int i = 0; i < this.count; i++)
                {
                    yield return this.nexts[i];
                }
            }

#if !NET_STANDARD_2_0

            // SearchNext(ref ReadOnlySpan<byte> bytes)
            public void EmitSearchNext(ILGenerator il, LocalBuilder bytesSpan, LocalBuilder key, Action<KeyValuePair<string, int>> onFound, Action onNotFound)
            {
                // key = AutomataKeyGen.GetKey(ref bytesSpan);
                il.EmitLdloca(bytesSpan);
                il.EmitCall(AutomataKeyGen.GetKeyMethod);
                il.EmitStloc(key);

                // match children.
                EmitSearchNextCore(il, bytesSpan, key, onFound, onNotFound, this.nexts, this.count);
            }

            private static void EmitSearchNextCore(ILGenerator il, LocalBuilder bytesSpan, LocalBuilder key, Action<KeyValuePair<string, int>> onFound, Action onNotFound, AutomataNode[] nexts, int count)
            {
                if (count < 4)
                {
                    // linear-search
                    AutomataNode[] valueExists = nexts.Take(count).Where(x => x.Value != -1).ToArray();
                    AutomataNode[] childrenExists = nexts.Take(count).Where(x => x.HasChildren).ToArray();
                    Label gotoSearchNext = il.DefineLabel();
                    Label gotoNotFound = il.DefineLabel();

                    {
                              // bytesSpan.Length
                        il.EmitLdloca(bytesSpan);
                        il.EmitCall(typeof(ReadOnlySpan<byte>).GetRuntimeProperty(nameof(ReadOnlySpan<byte>.Length)).GetMethod);
                        if (childrenExists.Length != 0 && valueExists.Length == 0)
                        {
                            il.Emit(OpCodes.Brfalse, gotoNotFound); // if(bytesSpan.Length == 0)
                        }
                        else
                        {
                            il.Emit(OpCodes.Brtrue, gotoSearchNext); // if(bytesSpan.Length != 0)
                        }
                    }

                    {
                        Label[] ifValueNexts = Enumerable.Range(0, Math.Max(valueExists.Length - 1, 0)).Select(_ => il.DefineLabel()).ToArray();
                        for (int i = 0; i < valueExists.Length; i++)
                        {
                            Label notFoundLabel = il.DefineLabel();
                            if (i != 0)
                            {
                                il.MarkLabel(ifValueNexts[i - 1]);
                            }

                            il.EmitLdloc(key);
                            il.EmitULong(valueExists[i].Key);
                            il.Emit(OpCodes.Bne_Un, notFoundLabel);

                            // found
                            onFound(new KeyValuePair<string, int>(valueExists[i].OriginalKey, valueExists[i].Value));

                            // notfound
                            il.MarkLabel(notFoundLabel);
                            if (i != valueExists.Length - 1)
                            {
                                il.Emit(OpCodes.Br, ifValueNexts[i]);
                            }
                            else
                            {
                                onNotFound();
                            }
                        }
                    }

                    il.MarkLabel(gotoSearchNext);
                    Label[] ifRecNext = Enumerable.Range(0, Math.Max(childrenExists.Length - 1, 0)).Select(_ => il.DefineLabel()).ToArray();
                    for (int i = 0; i < childrenExists.Length; i++)
                    {
                        Label notFoundLabel = il.DefineLabel();
                        if (i != 0)
                        {
                            il.MarkLabel(ifRecNext[i - 1]);
                        }

                        il.EmitLdloc(key);
                        il.EmitULong(childrenExists[i].Key);
                        il.Emit(OpCodes.Bne_Un, notFoundLabel);

                        // found
                        childrenExists[i].EmitSearchNext(il, bytesSpan, key, onFound, onNotFound);

                        // notfound
                        il.MarkLabel(notFoundLabel);
                        if (i != childrenExists.Length - 1)
                        {
                            il.Emit(OpCodes.Br, ifRecNext[i]);
                        }
                        else
                        {
                            onNotFound();
                        }
                    }

                    il.MarkLabel(gotoNotFound);
                    onNotFound();
                }
                else
                {
                    // binary-search
                    var midline = count / 2;
                    var mid = nexts[midline].Key;
                    AutomataNode[] l = nexts.Take(count).Take(midline).ToArray();
                    AutomataNode[] r = nexts.Take(count).Skip(midline).ToArray();

                    Label gotoRight = il.DefineLabel();

                    // if(key < mid)
                    il.EmitLdloc(key);
                    il.EmitULong(mid);
                    il.Emit(OpCodes.Bge_Un, gotoRight);
                    EmitSearchNextCore(il, bytesSpan, key, onFound, onNotFound, l, l.Length);

                    // else
                    il.MarkLabel(gotoRight);
                    EmitSearchNextCore(il, bytesSpan, key, onFound, onNotFound, r, r.Length);
                }
            }

#endif
        }
    }

    /// <remarks>
    /// This is used by dynamically generated code. It can be made internal after we enable our dynamic assemblies to access internals.
    /// But that trick may require net46, so maybe we should leave this as public.
    /// </remarks>
    public static class AutomataKeyGen
    {
        public static readonly MethodInfo GetKeyMethod = typeof(AutomataKeyGen).GetRuntimeMethod(nameof(GetKey), new[] { typeof(ReadOnlySpan<byte>).MakeByRefType() });

        public static ulong GetKey(ref ReadOnlySpan<byte> span)
        {
            ulong key;

            unchecked
            {
                if (span.Length >= 8)
                {
                    key = SafeBitConverter.ToUInt64(span);
                    span = span.Slice(8);
                }
                else
                {
                    switch (span.Length)
                    {
                        case 1:
                            {
                                key = span[0];
                                span = span.Slice(1);
                                break;
                            }

                        case 2:
                            {
                                key = SafeBitConverter.ToUInt16(span);
                                span = span.Slice(2);
                                break;
                            }

                        case 3:
                            {
                                var a = span[0];
                                var b = SafeBitConverter.ToUInt16(span.Slice(1));
                                key = a | (ulong)b << 8;
                                span = span.Slice(3);
                                break;
                            }

                        case 4:
                            {
                                key = SafeBitConverter.ToUInt32(span);
                                span = span.Slice(4);
                                break;
                            }

                        case 5:
                            {
                                var a = span[0];
                                var b = SafeBitConverter.ToUInt32(span.Slice(1));
                                key = a | (ulong)b << 8;
                                span = span.Slice(5);
                                break;
                            }

                        case 6:
                            {
                                ulong a = SafeBitConverter.ToUInt16(span);
                                ulong b = SafeBitConverter.ToUInt32(span.Slice(2));
                                key = a | (b << 16);
                                span = span.Slice(6);
                                break;
                            }

                        case 7:
                            {
                                var a = span[0];
                                var b = SafeBitConverter.ToUInt16(span.Slice(1));
                                var c = SafeBitConverter.ToUInt32(span.Slice(3));
                                key = a | (ulong)b << 8 | (ulong)c << 24;
                                span = span.Slice(7);
                                break;
                            }

                        default:
                            throw new MessagePackSerializationException("Not Supported Length");
                    }
                }

                return key;
            }
        }
    }
}
