// 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.Collections.Immutable;
using System.Linq;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Razor.Language;

public sealed partial class RazorParserOptions
    : IEquatable<RazorParserOptions>
{
    private static RazorLanguageVersion DefaultLanguageVersion => RazorLanguageVersion.Latest;
    private static RazorFileKind DefaultFileKind => RazorFileKind.Legacy;

    public static RazorParserOptions Default { get; } = new(
        languageVersion: DefaultLanguageVersion,
        fileKind: DefaultFileKind,
        directives: [],
        csharpParseOptions: CSharpParseOptions.Default,
        flags: GetDefaultFlags(DefaultLanguageVersion, DefaultFileKind));

    public RazorLanguageVersion LanguageVersion { get; }
    internal RazorFileKind FileKind { get; }

    public ImmutableArray<DirectiveDescriptor> Directives { get; }
    public CSharpParseOptions CSharpParseOptions { get; }

    private readonly Flags _flags;

    private RazorParserOptions(
        RazorLanguageVersion languageVersion,
        RazorFileKind fileKind,
        ImmutableArray<DirectiveDescriptor> directives,
        CSharpParseOptions csharpParseOptions,
        Flags flags)
    {
        if (flags.IsFlagSet(Flags.ParseLeadingDirectives) &&
            flags.IsFlagSet(Flags.UseRoslynTokenizer))
        {
            throw new ArgumentException($"Cannot set {nameof(Flags.ParseLeadingDirectives)} and {nameof(Flags.UseRoslynTokenizer)} to true simultaneously.");
        }

        LanguageVersion = languageVersion ?? DefaultLanguageVersion;
        FileKind = fileKind;
        Directives = directives;
        CSharpParseOptions = csharpParseOptions;
        _flags = flags;
    }

    public static RazorParserOptions Create(RazorLanguageVersion languageVersion, RazorFileKind fileKind, Action<Builder>? configure = null)
    {
        var builder = new Builder(languageVersion, fileKind);
        configure?.Invoke(builder);

        return builder.ToOptions();
    }

    public bool DesignTime
        => _flags.IsFlagSet(Flags.DesignTime);

    /// <summary>
    /// Gets a value which indicates whether the parser will parse only the leading directives. If <c>true</c>
    /// the parser will halt at the first HTML content or C# code block. If <c>false</c> the whole document is parsed.
    /// </summary>
    /// <remarks>
    /// Currently setting this option to <c>true</c> will result in only the first line of directives being parsed.
    /// In a future release this may be updated to include all leading directive content.
    /// </remarks>
    public bool ParseLeadingDirectives
        => _flags.IsFlagSet(Flags.ParseLeadingDirectives);

    public bool UseRoslynTokenizer
        => _flags.IsFlagSet(Flags.UseRoslynTokenizer);

    internal bool EnableSpanEditHandlers
        => _flags.IsFlagSet(Flags.EnableSpanEditHandlers);

    internal bool AllowMinimizedBooleanTagHelperAttributes
        => _flags.IsFlagSet(Flags.AllowMinimizedBooleanTagHelperAttributes);

    internal bool AllowHtmlCommentsInTagHelpers
        => _flags.IsFlagSet(Flags.AllowHtmlCommentsInTagHelpers);

    internal bool AllowComponentFileKind
        => _flags.IsFlagSet(Flags.AllowComponentFileKind);

    internal bool AllowRazorInAllCodeBlocks
        => _flags.IsFlagSet(Flags.AllowRazorInAllCodeBlocks);

    internal bool AllowUsingVariableDeclarations
        => _flags.IsFlagSet(Flags.AllowUsingVariableDeclarations);

    internal bool AllowConditionalDataDashAttributes
        => _flags.IsFlagSet(Flags.AllowConditionalDataDashAttributes);

    internal bool AllowCSharpInMarkupAttributeArea
        => _flags.IsFlagSet(Flags.AllowCSharpInMarkupAttributeArea);

    internal bool AllowNullableForgivenessOperator
        => _flags.IsFlagSet(Flags.AllowNullableForgivenessOperator);

    public RazorParserOptions WithDirectives(params ImmutableArray<DirectiveDescriptor> value)
        => Directives.SequenceEqual(value)
            ? this
            : new(LanguageVersion, FileKind, value, CSharpParseOptions, _flags);

    public RazorParserOptions WithCSharpParseOptions(CSharpParseOptions value)
        => CSharpParseOptions.Equals(value)
            ? this
            : new(LanguageVersion, FileKind, Directives, value, _flags);

    public RazorParserOptions WithFlags(
        Optional<bool> designTime = default,
        Optional<bool> parseLeadingDirectives = default,
        Optional<bool> useRoslynTokenizer = default,
        Optional<bool> enableSpanEditHandlers = default,
        Optional<bool> allowMinimizedBooleanTagHelperAttributes = default,
        Optional<bool> allowHtmlCommentsInTagHelpers = default,
        Optional<bool> allowComponentFileKind = default,
        Optional<bool> allowRazorInAllCodeBlocks = default,
        Optional<bool> allowUsingVariableDeclarations = default,
        Optional<bool> allowConditionalDataDashAttributes = default,
        Optional<bool> allowCSharpInMarkupAttributeArea = default,
        Optional<bool> allowNullableForgivenessOperator = default)
    {
        var flags = _flags;

        if (designTime.HasValue)
        {
            flags.UpdateFlag(Flags.DesignTime, designTime.Value);
        }

        if (parseLeadingDirectives.HasValue)
        {
            flags.UpdateFlag(Flags.ParseLeadingDirectives, parseLeadingDirectives.Value);
        }

        if (useRoslynTokenizer.HasValue)
        {
            flags.UpdateFlag(Flags.UseRoslynTokenizer, useRoslynTokenizer.Value);
        }

        if (enableSpanEditHandlers.HasValue)
        {
            flags.UpdateFlag(Flags.EnableSpanEditHandlers, enableSpanEditHandlers.Value);
        }

        if (allowMinimizedBooleanTagHelperAttributes.HasValue)
        {
            flags.UpdateFlag(Flags.AllowMinimizedBooleanTagHelperAttributes, allowMinimizedBooleanTagHelperAttributes.Value);
        }

        if (allowHtmlCommentsInTagHelpers.HasValue)
        {
            flags.UpdateFlag(Flags.AllowHtmlCommentsInTagHelpers, allowHtmlCommentsInTagHelpers.Value);
        }

        if (allowComponentFileKind.HasValue)
        {
            flags.UpdateFlag(Flags.AllowComponentFileKind, allowComponentFileKind.Value);
        }

        if (allowRazorInAllCodeBlocks.HasValue)
        {
            flags.UpdateFlag(Flags.AllowRazorInAllCodeBlocks, allowRazorInAllCodeBlocks.Value);
        }

        if (allowUsingVariableDeclarations.HasValue)
        {
            flags.UpdateFlag(Flags.AllowUsingVariableDeclarations, allowUsingVariableDeclarations.Value);
        }

        if (allowConditionalDataDashAttributes.HasValue)
        {
            flags.UpdateFlag(Flags.AllowConditionalDataDashAttributes, allowConditionalDataDashAttributes.Value);
        }

        if (allowCSharpInMarkupAttributeArea.HasValue)
        {
            flags.UpdateFlag(Flags.AllowCSharpInMarkupAttributeArea, allowCSharpInMarkupAttributeArea.Value);
        }

        if (allowNullableForgivenessOperator.HasValue)
        {
            flags.UpdateFlag(Flags.AllowNullableForgivenessOperator, allowNullableForgivenessOperator.Value);
        }

        return flags == _flags
            ? this
            : new(LanguageVersion, FileKind, Directives, CSharpParseOptions, flags);
    }

    public bool Equals(RazorParserOptions? other)
    {
        return
            other is not null
            && _flags == other._flags
            && FileKind == other.FileKind
            && LanguageVersion == other.LanguageVersion
            && CSharpParseOptions == other.CSharpParseOptions
            && Directives.SequenceEqual(other.Directives, ReferenceEqualityComparer<DirectiveDescriptor>.Instance);
    }

    public override bool Equals(object? obj)
    {
        return Equals(obj as RazorParserOptions);
    }

    public override int GetHashCode()
    {
        var combiner = HashCodeCombiner.Start();
        combiner.Add(_flags);
        combiner.Add(FileKind);
        combiner.Add(LanguageVersion);
        combiner.Add(CSharpParseOptions);
        combiner.Add(Directives, ReferenceEqualityComparer<DirectiveDescriptor>.Instance);

        return combiner.CombinedHash;
    }
}
