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

#nullable disable

using System;
using System.Collections.Generic;
using System.Linq;

namespace Microsoft.AspNetCore.Razor.Language;

/// <summary>
/// A descriptor type for a directive that can be parsed by the Razor parser.
/// </summary>
public abstract class DirectiveDescriptor
{
    /// <summary>
    /// Gets the description of the directive.
    /// </summary>
    /// <remarks>
    /// The description is used for information purposes, and has no effect on parsing.
    /// </remarks>
    public abstract string Description { get; }

    /// <summary>
    /// Gets the directive keyword without the leading <c>@</c> token.
    /// </summary>
    public abstract string Directive { get; }

    /// <summary>
    /// Gets the display name of the directive.
    /// </summary>
    /// <remarks>
    /// The display name is used for information purposes, and has no effect on parsing.
    /// </remarks>
    public abstract string DisplayName { get; }

    /// <summary>
    /// Gets the kind of the directive. The kind determines whether or not a directive has an associated block.
    /// </summary>
    public abstract DirectiveKind Kind { get; }

    /// <summary>
    /// Gets the way a directive can be used. The usage determines how many, and where directives can exist per document.
    /// </summary>
    public abstract DirectiveUsage Usage { get; }

    /// <summary>
    /// Gets the list of directive tokens that can follow the directive keyword.
    /// </summary>
    public abstract IReadOnlyList<DirectiveTokenDescriptor> Tokens { get; }

    /// <summary>
    /// Creates a new <see cref="DirectiveDescriptor"/>.
    /// </summary>
    /// <param name="directive">The directive keyword.</param>
    /// <param name="kind">The directive kind.</param>
    /// <returns>A <see cref="DirectiveDescriptor"/> for the created directive.</returns>
    public static DirectiveDescriptor CreateDirective(string directive, DirectiveKind kind)
    {
        if (directive == null)
        {
            throw new ArgumentNullException(nameof(directive));
        }

        return CreateDirective(directive, kind, configure: null);
    }

    /// <summary>
    /// Creates a new <see cref="DirectiveDescriptor"/>.
    /// </summary>
    /// <param name="directive">The directive keyword.</param>
    /// <param name="kind">The directive kind.</param>
    /// <param name="configure">A configuration delegate for the directive.</param>
    /// <returns>A <see cref="DirectiveDescriptor"/> for the created directive.</returns>
    public static DirectiveDescriptor CreateDirective(string directive, DirectiveKind kind, Action<IDirectiveDescriptorBuilder> configure)
    {
        if (directive == null)
        {
            throw new ArgumentNullException(nameof(directive));
        }

        var builder = new DefaultDirectiveDescriptorBuilder(directive, kind);
        configure?.Invoke(builder);
        return builder.Build();
    }

    /// <summary>
    /// Creates a new <see cref="DirectiveDescriptor"/> with <see cref="Kind"/> set to <see cref="DirectiveKind.SingleLine"/>
    /// </summary>
    /// <param name="directive">The directive keyword.</param>
    /// <returns>A <see cref="DirectiveDescriptor"/> for the created directive.</returns>
    public static DirectiveDescriptor CreateSingleLineDirective(string directive)
    {
        if (directive == null)
        {
            throw new ArgumentNullException(nameof(directive));
        }

        return CreateDirective(directive, DirectiveKind.SingleLine, configure: null);
    }

    /// <summary>
    /// Creates a new <see cref="DirectiveDescriptor"/> with <see cref="Kind"/> set to <see cref="DirectiveKind.SingleLine"/>
    /// </summary>
    /// <param name="directive">The directive keyword.</param>
    /// <param name="configure">A configuration delegate for the directive.</param>
    /// <returns>A <see cref="DirectiveDescriptor"/> for the created directive.</returns>
    public static DirectiveDescriptor CreateSingleLineDirective(string directive, Action<IDirectiveDescriptorBuilder> configure)
    {
        if (directive == null)
        {
            throw new ArgumentNullException(nameof(directive));
        }

        return CreateDirective(directive, DirectiveKind.SingleLine, configure);
    }

    /// <summary>
    /// Creates a new <see cref="DirectiveDescriptor"/> with <see cref="Kind"/> set to <see cref="DirectiveKind.RazorBlock"/>
    /// </summary>
    /// <param name="directive">The directive keyword.</param>
    /// <returns>A <see cref="DirectiveDescriptor"/> for the created directive.</returns>
    public static DirectiveDescriptor CreateRazorBlockDirective(string directive)
    {
        if (directive == null)
        {
            throw new ArgumentNullException(nameof(directive));
        }

        return CreateDirective(directive, DirectiveKind.RazorBlock, configure: null);
    }

    /// <summary>
    /// Creates a new <see cref="DirectiveDescriptor"/> with <see cref="Kind"/> set to <see cref="DirectiveKind.RazorBlock"/>
    /// </summary>
    /// <param name="directive">The directive keyword.</param>
    /// <param name="configure">A configuration delegate for the directive.</param>
    /// <returns>A <see cref="DirectiveDescriptor"/> for the created directive.</returns>
    public static DirectiveDescriptor CreateRazorBlockDirective(string directive, Action<IDirectiveDescriptorBuilder> configure)
    {
        if (directive == null)
        {
            throw new ArgumentNullException(nameof(directive));
        }

        return CreateDirective(directive, DirectiveKind.RazorBlock, configure);
    }

    /// <summary>
    /// Creates a new <see cref="DirectiveDescriptor"/> with <see cref="Kind"/> set to <see cref="DirectiveKind.CodeBlock"/>
    /// </summary>
    /// <param name="directive">The directive keyword.</param>
    /// <returns>A <see cref="DirectiveDescriptor"/> for the created directive.</returns>
    public static DirectiveDescriptor CreateCodeBlockDirective(string directive)
    {
        if (directive == null)
        {
            throw new ArgumentNullException(nameof(directive));
        }

        return CreateDirective(directive, DirectiveKind.CodeBlock, configure: null);
    }

    /// <summary>
    /// Creates a new <see cref="DirectiveDescriptor"/> with <see cref="Kind"/> set to <see cref="DirectiveKind.CodeBlock"/>
    /// </summary>
    /// <param name="directive">The directive keyword.</param>
    /// <param name="configure">A configuration delegate for the directive.</param>
    /// <returns>A <see cref="DirectiveDescriptor"/> for the created directive.</returns>
    public static DirectiveDescriptor CreateCodeBlockDirective(string directive, Action<IDirectiveDescriptorBuilder> configure)
    {
        if (directive == null)
        {
            throw new ArgumentNullException(nameof(directive));
        }

        return CreateDirective(directive, DirectiveKind.CodeBlock, configure);
    }

    private class DefaultDirectiveDescriptorBuilder : IDirectiveDescriptorBuilder
    {
        public DefaultDirectiveDescriptorBuilder(string name, DirectiveKind kind)
        {
            Directive = name;
            Kind = kind;

            Tokens = new List<DirectiveTokenDescriptor>();
        }

        public string Description { get; set; }

        public string Directive { get; }

        public string DisplayName { get; set; }

        public DirectiveKind Kind { get; }

        public DirectiveUsage Usage { get; set; }

        public IList<DirectiveTokenDescriptor> Tokens { get; }

        public DirectiveDescriptor Build()
        {
            if (Directive.Length == 0)
            {
                throw new InvalidOperationException(Resources.FormatDirectiveDescriptor_InvalidDirectiveKeyword(Directive));
            }

            for (var i = 0; i < Directive.Length; i++)
            {
                if (!char.IsLetter(Directive[i]))
                {
                    throw new InvalidOperationException(Resources.FormatDirectiveDescriptor_InvalidDirectiveKeyword(Directive));
                }
            }

            var foundOptionalToken = false;
            for (var i = 0; i < Tokens.Count; i++)
            {
                var token = Tokens[i];
                foundOptionalToken |= token.Optional;

                if (foundOptionalToken && !token.Optional)
                {
                    throw new InvalidOperationException(Resources.DirectiveDescriptor_InvalidNonOptionalToken);
                }
            }

            return new DefaultDirectiveDescriptor(Directive, Kind, Usage, Tokens.ToArray(), DisplayName, Description);
        }
    }

    private class DefaultDirectiveDescriptor : DirectiveDescriptor
    {
        public DefaultDirectiveDescriptor(
            string directive,
            DirectiveKind kind,
            DirectiveUsage usage,
            DirectiveTokenDescriptor[] tokens,
            string displayName,
            string description)
        {
            Directive = directive;
            Kind = kind;
            Usage = usage;
            Tokens = tokens;
            DisplayName = displayName;
            Description = description;
        }

        public override string Description { get; }

        public override string Directive { get; }

        public override string DisplayName { get; }

        public override DirectiveKind Kind { get; }

        public override DirectiveUsage Usage { get; }

        public override IReadOnlyList<DirectiveTokenDescriptor> Tokens { get; }
    }
}
