// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X;

public class AssemblyAttributeInjectionPass : IntermediateNodePassBase, IRazorOptimizationPass
{
    private const string RazorViewAttribute = "global::Microsoft.AspNetCore.Mvc.Razor.Compilation.RazorViewAttribute";
    private const string RazorPageAttribute = "global::Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.RazorPageAttribute";

    protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
    {
        if (documentNode.Options.DesignTime)
        {
            return;
        }

        var @namespace = documentNode.FindPrimaryNamespace();
        if (@namespace == null)
        {
            // No namespace node. Skip.
            return;
        }

        var @class = documentNode.FindPrimaryClass();
        if (@class == null || string.IsNullOrEmpty(@class.Name))
        {
            // No class node or it's incomplete. Skip.
            return;
        }

        var generatedTypeName = string.IsNullOrEmpty(@namespace.Name)
            ? @class.Name
            : $"{@namespace.Name}.{@class.Name}";

        // The MVC attributes require a relative path to be specified so that we can make a view engine path.
        // We can't use a rooted path because we don't know what the project root is.
        //
        // If we can't sanitize the path, we'll just set it to null and let is blow up at runtime - we don't
        // want to create noise if this code has to run in some unanticipated scenario.
        var escapedPath = MakeVerbatimStringLiteral(ConvertToViewEnginePath(codeDocument.Source.RelativePath));

        string attribute;
        if (documentNode.DocumentKind == MvcViewDocumentClassifierPass.MvcViewDocumentKind)
        {
            attribute = $"[assembly:{RazorViewAttribute}({escapedPath}, typeof({generatedTypeName}))]";
        }
        else if (documentNode.DocumentKind == RazorPageDocumentClassifierPass.RazorPageDocumentKind &&
            PageDirective.TryGetPageDirective(documentNode, out var pageDirective))
        {
            var escapedRoutePrefix = MakeVerbatimStringLiteral(pageDirective.RouteTemplate);
            attribute = $"[assembly:{RazorPageAttribute}({escapedPath}, typeof({generatedTypeName}), {escapedRoutePrefix})]";
        }
        else
        {
            return;
        }

        var index = documentNode.Children.IndexOf(@namespace);
        Debug.Assert(index >= 0);

        var pageAttribute = new CSharpCodeIntermediateNode();
        pageAttribute.Children.Add(IntermediateNodeFactory.CSharpToken(attribute));

        documentNode.Children.Insert(index, pageAttribute);
    }

    private static string MakeVerbatimStringLiteral(string? value)
    {
        if (value == null)
        {
            return "null";
        }

        value = value.Replace("\"", "\"\"");
        return $"@\"{value}\"";
    }

    [return: NotNullIfNotNull(nameof(relativePath))]
    private static string? ConvertToViewEnginePath(string? relativePath)
    {
        if (string.IsNullOrEmpty(relativePath))
        {
            return null;
        }

        // Checking for both / and \ because a \ will become a /.
        if (!relativePath.StartsWith("/", StringComparison.Ordinal) && !relativePath.StartsWith("\\", StringComparison.Ordinal))
        {
            relativePath = "/" + relativePath;
        }

        relativePath = relativePath.Replace('\\', '/');
        return relativePath;
    }
}
