// 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 Markdig.Parsers;
using Markdig.Syntax;

namespace Markdig.Extensions.DefinitionLists;

/// <summary>
/// The block parser for a <see cref="DefinitionList"/>.
/// </summary>
/// <seealso cref="BlockParser" />
public class DefinitionListParser : BlockParser
{
    /// <summary>
    /// Initializes a new instance of the <see cref="DefinitionListParser"/> class.
    /// </summary>
    public DefinitionListParser()
    {
        OpeningCharacters = [':', '~'];
    }

    /// <summary>
    /// Attempts to open a block at the current parser position.
    /// </summary>
    public override BlockState TryOpen(BlockProcessor processor)
    {
        var paragraphBlock = processor.LastBlock as ParagraphBlock;
        if (processor.IsCodeIndent || paragraphBlock is null || paragraphBlock.LastLine - processor.LineIndex > 1)
        {
            return BlockState.None;
        }

        var startPosition = processor.Start;

        var column = processor.ColumnBeforeIndent;
        processor.NextChar();
        processor.ParseIndent();
        var delta = processor.Column - column;

        // We expect to have a least
        if (delta < 4)
        {
            // Return back to original position
            processor.GoToColumn(column);
            return BlockState.None;
        }

        if (delta > 4)
        {
            processor.GoToColumn(column + 4);
        }

        var previousParent = paragraphBlock.Parent!;
        var currentDefinitionList = GetCurrentDefinitionList(paragraphBlock, previousParent);

        processor.Discard(paragraphBlock);

        // If the paragraph block was not part of the opened blocks, we need to remove it manually from its parent container
        if (paragraphBlock.Parent != null)
        {
            paragraphBlock.Parent.Remove(paragraphBlock);
        }

        if (currentDefinitionList is null)
        {
            currentDefinitionList = new DefinitionList(this)
            {
                Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End),
                Column = paragraphBlock.Column,
                Line = paragraphBlock.Line,
            };
            previousParent.Add(currentDefinitionList);
        }

        var definitionItem = new DefinitionItem(this)
        {
            Line = processor.LineIndex,
            Column = column,
            Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End),
            OpeningCharacter = processor.CurrentChar,
        };

        for (int i = 0; i < paragraphBlock.Lines.Count; i++)
        {
            var line = paragraphBlock.Lines.Lines[i];
            var term = new DefinitionTerm(this)
            {
                Column =  paragraphBlock.Column,
                Line = line.Line,
                Span = new SourceSpan(paragraphBlock.Span.Start, paragraphBlock.Span.End),
                IsOpen = false
            };
            term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position, processor.TrackTrivia);
            definitionItem.Add(term);
        }
        currentDefinitionList.Add(definitionItem);
        processor.Open(definitionItem);

        // Update the end position
        currentDefinitionList.UpdateSpanEnd(processor.Line.End);

        return BlockState.Continue;
    }

    private static DefinitionList? GetCurrentDefinitionList(ParagraphBlock paragraphBlock, ContainerBlock previousParent)
    {
        var index = previousParent.IndexOf(paragraphBlock) - 1;
        if (index < 0) return null;
        switch (previousParent[index])
        {
            case DefinitionList definitionList:
                return definitionList;

            case BlankLineBlock:
                if (index > 0 && previousParent[index - 1] is DefinitionList definitionList2)
                {
                    previousParent.RemoveAt(index);
                    return definitionList2;
                }
                break;
        }
        return null;
    }

    /// <summary>
    /// Attempts to continue parsing the specified block.
    /// </summary>
    public override BlockState TryContinue(BlockProcessor processor, Block block)
    {
        var definitionItem = (DefinitionItem)block;
        if (processor.IsCodeIndent)
        {
            processor.GoToCodeIndent();
            return BlockState.Continue;
        }

        var list = (DefinitionList)definitionItem.Parent!;
        var lastBlankLine = definitionItem.LastChild as BlankLineBlock;

        // Check if we have another definition list
        if (Array.IndexOf(OpeningCharacters!, processor.CurrentChar) >= 0)
        {
            var startPosition = processor.Start;
            var column = processor.ColumnBeforeIndent;
            processor.NextChar();
            processor.ParseIndent();
            var delta = processor.Column - column;

            // We expect to have a least
            if (delta < 4)
            {
                // Remove the blankline before breaking this definition item
                if (lastBlankLine != null)
                {
                    definitionItem.RemoveAt(definitionItem.Count - 1);
                }

                list.Span.End = list.LastChild!.Span.End;
                return BlockState.None;
            }

            if (delta > 4)
            {
                processor.GoToColumn(column + 4);
            }

            processor.Close(definitionItem);
            var nextDefinitionItem = new DefinitionItem(this)
            {
                Span = new SourceSpan(startPosition, processor.Line.End),
                Line = processor.LineIndex,
                Column = processor.Column,
                OpeningCharacter = processor.CurrentChar,
            };
            list.Add(nextDefinitionItem);
            processor.Open(nextDefinitionItem);

            return BlockState.Continue;
        }

        var isBreakable = definitionItem.LastChild?.IsBreakable ?? true;
        if (processor.IsBlankLine)
        {
            if (lastBlankLine is null && isBreakable)
            {
                definitionItem.Add(new BlankLineBlock());
            }
            return isBreakable ? BlockState.ContinueDiscard : BlockState.Continue;
        }

        var paragraphBlock = definitionItem.LastChild as ParagraphBlock;
        if (lastBlankLine is null && paragraphBlock != null)
        {
            return BlockState.Continue;
        }

        // Remove the blankline before breaking this definition item
        if (lastBlankLine != null)
        {
            definitionItem.RemoveAt(definitionItem.Count - 1);
        }

        list.Span.End = list.LastChild!.Span.End;
        return BlockState.Break;
    }
}
