// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.

using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using Markdig.Helpers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;

namespace Markdig.Parsers.Inlines;

/// <summary>
/// An inline parser for <see cref="EmphasisInline"/>.
/// </summary>
/// <seealso cref="InlineParser" />
/// <seealso cref="IPostInlineProcessor" />
public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
{
    private CharacterMap<EmphasisDescriptor>? emphasisMap;
    private readonly DelimitersObjectCache inlinesCache = new();
    /// <summary>
    /// Represents the EmphasisInline type.
    /// </summary>
    public delegate EmphasisInline? TryCreateEmphasisInlineDelegate(char emphasisChar, int delimiterCount);

    /// <summary>
    /// Initializes a new instance of the <see cref="EmphasisInlineParser"/> class.
    /// </summary>
    public EmphasisInlineParser()
    {
        EmphasisDescriptors =
        [
            new EmphasisDescriptor('*', 1, 2, true),
            new EmphasisDescriptor('_', 1, 2, false)
        ];
    }

    /// <summary>
    /// Gets the emphasis descriptors.
    /// </summary>
    public List<EmphasisDescriptor> EmphasisDescriptors { get; }

    /// <summary>
    /// Gets or toggles whether the emphasis parser should be CJK-friendly.
    /// </summary>
    /// <seealso href="https://github.com/tats-u/markdown-cjk-friendly"/>
    public bool CjkFriendlyEmphasis { get; set; } = false;

    /// <summary>
    /// Determines whether this parser is using the specified character as an emphasis delimiter.
    /// </summary>
    /// <param name="c">The character to look for.</param>
    /// <returns><c>true</c> if this parser is using the specified character as an emphasis delimiter; otherwise <c>false</c></returns>
    public bool HasEmphasisChar(char c)
    {
        foreach (var emphasis in EmphasisDescriptors)
        {
            if (emphasis.Character == c)
            {
                return true;
            }
        }
        return false;
    }

    /// <summary>
    /// Gets or sets the try create emphasis inline list.
    /// </summary>
    public readonly List<TryCreateEmphasisInlineDelegate> TryCreateEmphasisInlineList = [];

    /// <summary>
    /// Performs the initialize operation.
    /// </summary>
    public override void Initialize()
    {
        OpeningCharacters = new char[EmphasisDescriptors.Count];

        var tempMap = new List<KeyValuePair<char, EmphasisDescriptor>>();
        for (int i = 0; i < EmphasisDescriptors.Count; i++)
        {
            var emphasis = EmphasisDescriptors[i];
            if (Array.IndexOf(OpeningCharacters, emphasis.Character) >= 0)
            {
                ThrowHelper.InvalidOperationException(
                    $"The character `{emphasis.Character}` is already used by another emphasis descriptor");
            }

            OpeningCharacters[i] = emphasis.Character;

            tempMap.Add(new KeyValuePair<char, EmphasisDescriptor>(emphasis.Character, emphasis));
        }

        emphasisMap = new CharacterMap<EmphasisDescriptor>(tempMap);
    }

    /// <summary>
    /// Performs the post process operation.
    /// </summary>
    public bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, int postInlineProcessorIndex, bool isFinalProcessing)
    {
        if (root is null || !root.IsContainerInline)
        {
            return true;
        }

        ContainerInline container = Unsafe.As<ContainerInline>(root);

        List<EmphasisDelimiterInline>? delimiters = null;
        if (container is EmphasisDelimiterInline emphasisDelimiter)
        {
            delimiters = inlinesCache.Get();
            delimiters.Add(emphasisDelimiter);
        }

        // Collect all EmphasisDelimiterInline by searching from the root container
        var child = container.FirstChild;
        while (child != null)
        {
            // Stop the search on the delimitation child
            if (child == lastChild)
            {
                break;
            }

            if (child.IsContainer && child is DelimiterInline delimiterInline)
            {
                // If we have a delimiter, we search into it as we should have a tree of EmphasisDelimiterInline
                if (delimiterInline is EmphasisDelimiterInline delimiter)
                {
                    delimiters ??= inlinesCache.Get();
                    delimiters.Add(delimiter);
                }

                // Follow DelimiterInline (EmphasisDelimiter, TableDelimiter...)
                // If the delimiter has IsClosed=true (e.g., pipe table delimiter), it has no children
                // In that case, continue to next sibling instead of stopping
                var firstChild = delimiterInline.FirstChild;
                child = firstChild ?? delimiterInline.NextSibling;
            }
            else
            {
                child = child.NextSibling;
            }
        }

        if (delimiters != null)
        {
            ProcessEmphasis(state, delimiters);
            inlinesCache.Release(delimiters);
        }
        return true;
    }

    /// <summary>
    /// Attempts to match the parser at the current position.
    /// </summary>
    public override bool Match(InlineProcessor processor, ref StringSlice slice)
    {
        // First, some definitions.
        // A delimiter run is a sequence of one or more delimiter characters that is not preceded or followed by the same delimiter character
        // The amount of delimiter characters in the delimiter run may exceed emphasisDesc.MaximumCount, as that is handeled in `ProcessEmphasis`

        var delimiterChar = slice.CurrentChar;
        var emphasisDesc = emphasisMap![delimiterChar]!;

        Rune pc = (Rune)0;
        Rune twoPreviousChar = default;

        if (processor.Inline is HtmlEntityInline htmlEntityInline)
        {
            if (htmlEntityInline.Transcoded.Length > 0)
            {
                var transcoded = htmlEntityInline.Transcoded.Text.AsSpan(htmlEntityInline.Transcoded.Start, htmlEntityInline.Transcoded.Length);
                Rune.DecodeLastFromUtf16(transcoded, out pc, out int charsConsumed);

                if (CjkFriendlyEmphasis && charsConsumed < transcoded.Length)
                {
                    Rune.DecodeLastFromUtf16(transcoded.Slice(0, transcoded.Length - charsConsumed), out twoPreviousChar, out _);
                }
            }
        }
        if (pc.Value == 0)
        {
            pc = slice.PeekRuneExtra(-1);
            if (CjkFriendlyEmphasis)
            {
                // This cannot be a delegate (Func<Rune>?) because slice is a reference
                twoPreviousChar = slice.PeekRuneExtra(-1 - pc.Utf16SequenceLength);
            }
            // delimiterChar is BMP, so slice.PeekCharExtra(-2) is (a part of) the character two positions back.
            if (pc == (Rune)delimiterChar && slice.PeekCharExtra(-2) != '\\')
            {
                // If we get here, we determined that either:
                // a) there weren't enough delimiters in the delimiter run to satisfy the MinimumCount condition
                // b) the previous character couldn't open/close
                return false;
            }
        }
        var startPosition = slice.Start;

        int delimiterCount = slice.CountAndSkipChar(delimiterChar);

        // If the emphasis doesn't have the minimum required character
        if (delimiterCount < emphasisDesc.MinimumCount)
        {
            return false;
        }

        Rune c = slice.CurrentRune;

        // The following character is actually an entity, we need to decode it
        if (HtmlEntityParser.TryParse(ref slice, out string? htmlString, out int htmlLength))
        {
            // Note: c is U+FFFD when decode error
            Rune.DecodeFromUtf16(htmlString, out c, out _);
        }

        bool canOpen;
        bool canClose;
        // Calculate Open-Close for current character
        if (CjkFriendlyEmphasis)
        {
            CharHelper.CheckOpenCloseDelimiterCjkFriendly(pc, c, twoPreviousChar, emphasisDesc.EnableWithinWord, out canOpen, out canClose);
        }
        else
        {
            CharHelper.CheckOpenCloseDelimiter(pc, c, emphasisDesc.EnableWithinWord, out canOpen, out canClose);
        }

        // We have potentially an open or close emphasis
        if (canOpen || canClose)
        {
            var delimiterType = DelimiterType.Undefined;
            if (canOpen) delimiterType |= DelimiterType.Open;
            if (canClose) delimiterType |= DelimiterType.Close;

            var delimiter = new EmphasisDelimiterInline(this, emphasisDesc, new StringSlice(slice.Text, startPosition, slice.Start - 1))
            {
                DelimiterCount = delimiterCount,
                Type = delimiterType,
                Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)),
                Column = column,
                Line = line,
            };

            processor.Inline = delimiter;
            return true;
        }

        // We don't have an emphasis
        return false;
    }

    private void ProcessEmphasis(InlineProcessor processor, List<EmphasisDelimiterInline> delimiters)
    {
        // The following method is inspired by the "An algorithm for parsing nested emphasis and links"
        // at the end of the CommonMark specs.

        // TODO: Benchmark difference between using List and LinkedList here since there could be a few Remove calls

        // Move current_position forward in the delimiter stack (if needed) until
        // we find the first potential closer with delimiter * or _. (This will be the potential closer closest to the beginning of the input – the first one in parse order.)
        for (int i = 0; i < delimiters.Count; i++)
        {
            var closeDelimiter = delimiters[i];
            // Skip delimiters not supported by this instance
            EmphasisDescriptor? emphasisDesc = emphasisMap![closeDelimiter.DelimiterChar];
            if (emphasisDesc is null)
            {
                continue;
            }

            if ((closeDelimiter.Type & DelimiterType.Close) != 0)
            {
                while (closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount)
                {
                    // Now, look back in the stack (staying above stack_bottom and the openers_bottom for this delimiter type)
                    // for the first matching potential opener (“matching” means same delimiter).
                    EmphasisDelimiterInline? openDelimiter = null;
                    int openDelimiterIndex = -1;
                    for (int j = i - 1; j >= 0; j--)
                    {
                        var previousOpenDelimiter = delimiters[j];

                        var isOddMatch = ((closeDelimiter.Type & DelimiterType.Open) != 0 || (previousOpenDelimiter.Type & DelimiterType.Close) != 0) &&
                                         previousOpenDelimiter.DelimiterCount != closeDelimiter.DelimiterCount &&
                                         (previousOpenDelimiter.DelimiterCount + closeDelimiter.DelimiterCount) % 3 == 0 &&
                                         (previousOpenDelimiter.DelimiterCount % 3 != 0 || closeDelimiter.DelimiterCount % 3 != 0);

                        if (previousOpenDelimiter.DelimiterChar == closeDelimiter.DelimiterChar &&
                            (previousOpenDelimiter.Type & DelimiterType.Open) != 0 &&
                            previousOpenDelimiter.DelimiterCount >= emphasisDesc.MinimumCount && !isOddMatch)
                        {
                            openDelimiter = previousOpenDelimiter;
                            openDelimiterIndex = j;
                            break;
                        }
                    }

                    if (openDelimiter != null)
                    {
                    process_delims:
                        Debug.Assert(openDelimiter.DelimiterCount >= emphasisDesc.MinimumCount, "Extra emphasis should have been discarded by now");
                        Debug.Assert(closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount, "Extra emphasis should have been discarded by now");
                        int delimiterDelta = Math.Min(Math.Min(openDelimiter.DelimiterCount, closeDelimiter.DelimiterCount), emphasisDesc.MaximumCount);

                        // Insert an emph or strong emph node accordingly, after the text node corresponding to the opener.
                        EmphasisInline? emphasis = null;

                        // Go in backwards order to give priority to newer delegates
                        for (int delegateIndex = TryCreateEmphasisInlineList.Count - 1; delegateIndex >= 0; delegateIndex--)
                        {
                            emphasis = TryCreateEmphasisInlineList[delegateIndex].Invoke(closeDelimiter.DelimiterChar, delimiterDelta);
                            if (emphasis != null) break;
                        }

                        emphasis ??= new EmphasisInline
                        {
                            DelimiterChar = closeDelimiter.DelimiterChar,
                            DelimiterCount = delimiterDelta
                        };

                        // Update position for emphasis
                        var openDelimitercount = openDelimiter.DelimiterCount;
                        var closeDelimitercount = closeDelimiter.DelimiterCount;

                        emphasis!.Span.Start = openDelimiter.Span.Start + openDelimitercount - delimiterDelta;
                        emphasis.Line = openDelimiter.Line;
                        emphasis.Column = openDelimiter.Column + openDelimitercount - delimiterDelta;
                        emphasis.Span.End = closeDelimiter.Span.End - closeDelimitercount + delimiterDelta;

                        openDelimiter.Span.End -= delimiterDelta;
                        openDelimiter.Content.End -= delimiterDelta;
                        closeDelimiter.Content.Start += delimiterDelta;
                        closeDelimiter.Span.Start += delimiterDelta;
                        closeDelimiter.Column += delimiterDelta;

                        openDelimiter.DelimiterCount -= delimiterDelta;
                        closeDelimiter.DelimiterCount -= delimiterDelta;

                        var embracer = (ContainerInline)openDelimiter;

                        // Copy attributes attached to delimiter to the emphasis
                        var attributes = closeDelimiter.TryGetAttributes();
                        if (attributes != null)
                        {
                            emphasis.SetAttributes(attributes);
                        }

                        // Embrace all delimiters
                        embracer.EmbraceChildrenBy(emphasis);

                        // Remove any intermediate emphasis
                        for (int k = i - 1; k >= openDelimiterIndex + 1; k--)
                        {
                            var literalDelimiter = delimiters[k];
                            literalDelimiter.ReplaceBy(literalDelimiter.AsLiteralInline());
                            delimiters.RemoveAt(k);
                            i--;
                        }

                        if (closeDelimiter.DelimiterCount == 0)
                        {
                            var newParent = openDelimiter.DelimiterCount > 0 ? emphasis : emphasis.Parent!;
                            closeDelimiter.MoveChildrenAfter(newParent);
                            closeDelimiter.Remove();
                            delimiters.RemoveAt(i);
                            i--;

                            // Remove the open delimiter if it is also empty
                            if (openDelimiter.DelimiterCount == 0)
                            {
                                openDelimiter.MoveChildrenAfter(openDelimiter);
                                openDelimiter.Remove();
                                delimiters.RemoveAt(openDelimiterIndex);
                                i--;
                            }
                            break;
                        }

                        // The current delimiters are matching
                        if (openDelimiter.DelimiterCount >= emphasisDesc.MinimumCount &&
                            closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount)
                        {
                            goto process_delims;
                        }
                        else if (openDelimiter.DelimiterCount > 0)
                        {
                            // There are still delimiter characters left, there's just not enough of them
                            openDelimiter.ReplaceBy(openDelimiter.AsLiteralInline());
                            delimiters.RemoveAt(openDelimiterIndex);
                            i--;
                        }
                        else
                        {
                            // Remove the open delimiter if it is also empty
                            var firstChild = openDelimiter.FirstChild!;
                            firstChild.Remove();
                            openDelimiter.ReplaceBy(firstChild);
                            firstChild.IsClosed = true;
                            closeDelimiter.Remove();
                            firstChild.InsertAfter(closeDelimiter);
                            delimiters.RemoveAt(openDelimiterIndex);
                            i--;
                        }
                    }
                    else if ((closeDelimiter.Type & DelimiterType.Open) == 0)
                    {
                        closeDelimiter.ReplaceBy(closeDelimiter.AsLiteralInline());
                        delimiters.RemoveAt(i);
                        i--;
                        break;
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }

        // Any delimiters left must be literal
        for (int i = 0; i < delimiters.Count; i++)
        {
            var delimiter = delimiters[i];
            delimiter.ReplaceBy(delimiter.AsLiteralInline());
        }
        delimiters.Clear();
    }

    /// <summary>
    /// Represents the DelimitersObjectCache type.
    /// </summary>
    public class DelimitersObjectCache : ObjectCache<List<EmphasisDelimiterInline>>
    {
        /// <summary>
        /// Performs the new instance operation.
        /// </summary>
        protected override List<EmphasisDelimiterInline> NewInstance()
        {
            return new List<EmphasisDelimiterInline>(4);
        }

        /// <summary>
        /// Performs the reset operation.
        /// </summary>
        protected override void Reset(List<EmphasisDelimiterInline> instance)
        {
            instance.Clear();
        }
    }
}
