// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.Extensions.DependencyInjection;
using System.Collections;
using System.Reflection;

namespace Grand.Web.Common.TagHelpers.Admin.Extend;

internal class TemplateRenderer
{
    private const string DisplayTemplateViewPath = "DisplayTemplates";
    private const string EditorTemplateViewPath = "EditorTemplates";
    public const string IEnumerableOfIFormFileName = "IEnumerable`" + nameof(IFormFile);

    private static readonly Dictionary<string, Func<IHtmlHelper, Task<IHtmlContent>>> _defaultDisplayActions =
        new(StringComparer.OrdinalIgnoreCase) {
            { "Collection", DefaultDisplayTemplates.CollectionTemplate },
            { "EmailAddress", DefaultDisplayTemplates.EmailAddressTemplate },
            { "HiddenInput", DefaultDisplayTemplates.HiddenInputTemplate },
            { "Html", DefaultDisplayTemplates.HtmlTemplate },
            { "Text", DefaultDisplayTemplates.StringTemplate },
            { "Url", DefaultDisplayTemplates.UrlTemplate },
            { typeof(bool).Name, DefaultDisplayTemplates.BooleanTemplate },
            { typeof(double).Name, DefaultDisplayTemplates.DecimalTemplate },
            { typeof(string).Name, DefaultDisplayTemplates.StringTemplate },
            { typeof(object).Name, DefaultDisplayTemplates.ObjectTemplate }
        };

    private static readonly Dictionary<string, Func<IHtmlHelper, Task<IHtmlContent>>> _defaultEditorActions =
        new(StringComparer.OrdinalIgnoreCase) {
            { "Collection", DefaultEditorTemplates.CollectionTemplate },
            { "EmailAddress", DefaultEditorTemplates.EmailAddressInputTemplate },
            { "HiddenInput", DefaultEditorTemplates.HiddenInputTemplate },
            { "MultilineText", DefaultEditorTemplates.MultilineTemplate },
            { "Password", DefaultEditorTemplates.PasswordTemplate },
            { "PhoneNumber", DefaultEditorTemplates.PhoneNumberInputTemplate },
            { "Text", DefaultEditorTemplates.StringTemplate },
            { "Url", DefaultEditorTemplates.UrlInputTemplate },
            { "Date", DefaultEditorTemplates.DateInputTemplate },
            { "DateTime", DefaultEditorTemplates.DateTimeLocalInputTemplate },
            { "DateTime-local", DefaultEditorTemplates.DateTimeLocalInputTemplate },
            { nameof(DateTimeOffset), DefaultEditorTemplates.DateTimeOffsetTemplate },
            { "Time", DefaultEditorTemplates.TimeInputTemplate },
            { "Month", DefaultEditorTemplates.MonthInputTemplate },
            { "Week", DefaultEditorTemplates.WeekInputTemplate },
            { typeof(byte).Name, DefaultEditorTemplates.NumberInputTemplate },
            { typeof(sbyte).Name, DefaultEditorTemplates.NumberInputTemplate },
            { typeof(short).Name, DefaultEditorTemplates.NumberInputTemplate },
            { typeof(ushort).Name, DefaultEditorTemplates.NumberInputTemplate },
            { typeof(int).Name, DefaultEditorTemplates.NumberInputTemplate },
            { typeof(uint).Name, DefaultEditorTemplates.NumberInputTemplate },
            { typeof(long).Name, DefaultEditorTemplates.NumberInputTemplate },
            { typeof(ulong).Name, DefaultEditorTemplates.NumberInputTemplate },
            { typeof(bool).Name, DefaultEditorTemplates.BooleanTemplate },
            { typeof(double).Name, DefaultEditorTemplates.DecimalTemplate },
            { typeof(string).Name, DefaultEditorTemplates.StringTemplate },
            { typeof(object).Name, DefaultEditorTemplates.ObjectTemplate },
            { typeof(IFormFile).Name, DefaultEditorTemplates.FileInputTemplate },
            { IEnumerableOfIFormFileName, DefaultEditorTemplates.FileCollectionInputTemplate }
        };

    private readonly IViewBufferScope _bufferScope;
    private readonly bool _readOnly;
    private readonly string _templateName;
    private readonly ViewContext _viewContext;
    private readonly ViewDataDictionary _viewData;

    private readonly IViewEngine _viewEngine;

    public TemplateRenderer(
        IViewEngine viewEngine,
        IViewBufferScope bufferScope,
        ViewContext viewContext,
        ViewDataDictionary viewData,
        string templateName,
        bool readOnly)
    {
        ArgumentNullException.ThrowIfNull(viewEngine);
        ArgumentNullException.ThrowIfNull(bufferScope);
        ArgumentNullException.ThrowIfNull(viewContext);
        ArgumentNullException.ThrowIfNull(viewData);

        _viewEngine = viewEngine;
        _bufferScope = bufferScope;
        _viewContext = viewContext;
        _viewData = viewData;
        _templateName = templateName;
        _readOnly = readOnly;
    }

    public async Task<IHtmlContent> Render()
    {
        var defaultActions = GetDefaultActions();
        var modeViewPath = _readOnly ? DisplayTemplateViewPath : EditorTemplateViewPath;

        foreach (var viewName in GetViewNames())
        {
            var viewEngineResult = _viewEngine.GetView(_viewContext.ExecutingFilePath, viewName, false);
            if (!viewEngineResult.Success)
            {
                var fullViewName = modeViewPath + "/" + viewName;
                viewEngineResult = _viewEngine.FindView(_viewContext, fullViewName, false);
            }

            if (viewEngineResult.Success)
            {
                var viewBuffer = new ViewBuffer(_bufferScope, viewName, ViewBuffer.PartialViewPageSize);
                using (var writer = new ViewBufferTextWriter(viewBuffer, _viewContext.Writer.Encoding))
                {
                    // Forcing synchronous behavior so users don't have to await templates.
                    var view = viewEngineResult.View;
                    using (view as IDisposable)
                    {
                        var viewContext = new ViewContext(_viewContext, viewEngineResult.View, _viewData, writer);
                        await viewEngineResult.View.RenderAsync(viewContext);
                        return viewBuffer;
                    }
                }
            }

            if (defaultActions.TryGetValue(viewName, out var defaultAction))
                return await defaultAction(MakeHtmlHelper(_viewContext, _viewData));
        }

        throw new InvalidOperationException(_viewData.ModelExplorer.ModelType.FullName);
    }

    private Dictionary<string, Func<IHtmlHelper, Task<IHtmlContent>>> GetDefaultActions()
    {
        return _readOnly ? _defaultDisplayActions : _defaultEditorActions;
    }

    private IEnumerable<string> GetViewNames()
    {
        var metadata = _viewData.ModelMetadata;
        var templateHints = new[] {
            _templateName,
            metadata.TemplateHint,
            metadata.DataTypeName
        };

        foreach (var templateHint in templateHints.Where(s => !string.IsNullOrEmpty(s))) yield return templateHint;

        // We don't want to search for Nullable<T>, we want to search for T (which should handle both T and
        // Nullable<T>).
        var fieldType = metadata.UnderlyingOrModelType;
        foreach (var typeName in GetTypeNames(metadata, fieldType)) yield return typeName;
    }

    public static IEnumerable<string> GetTypeNames(ModelMetadata modelMetadata, Type fieldType)
    {
        // Not returning type name here for IEnumerable<IFormFile> since we will be returning
        // a more specific name, IEnumerableOfIFormFileName.
        var fieldTypeInfo = fieldType.GetTypeInfo();

        if (typeof(IEnumerable<IFormFile>) != fieldType) yield return fieldType.Name;

        if (fieldType == typeof(string))
            // Nothing more to provide
            yield break;

        if (!modelMetadata.IsComplexType)
        {
            // IsEnum is false for the Enum class itself
            if (fieldTypeInfo.IsEnum)
                // Same as fieldType.BaseType.Name in this case
                yield return "Enum";
            else if (fieldType == typeof(DateTimeOffset)) yield return "DateTime";

            yield return "String";
            yield break;
        }

        if (!fieldTypeInfo.IsInterface)
        {
            var type = fieldType;
            while (true)
            {
                type = type.GetTypeInfo().BaseType;
                if (type == null || type == typeof(object)) break;

                yield return type.Name;
            }
        }

        if (typeof(IEnumerable).IsAssignableFrom(fieldType))
        {
            if (typeof(IEnumerable<IFormFile>).IsAssignableFrom(fieldType))
            {
                yield return IEnumerableOfIFormFileName;

                // Specific name has already been returned, now return the generic name.
                if (typeof(IEnumerable<IFormFile>) == fieldType) yield return fieldType.Name;
            }

            yield return "Collection";
        }
        else if (typeof(IFormFile) != fieldType && typeof(IFormFile).IsAssignableFrom(fieldType))
        {
            yield return nameof(IFormFile);
        }

        yield return "Object";
    }

    private static IHtmlHelper MakeHtmlHelper(ViewContext viewContext, ViewDataDictionary viewData)
    {
        var newHelper = viewContext.HttpContext.RequestServices.GetRequiredService<IHtmlHelper>();

        if (newHelper is IViewContextAware contextable)
        {
            var newViewContext = new ViewContext(viewContext, viewContext.View, viewData, viewContext.Writer);
            contextable.Contextualize(newViewContext);
        }

        return newHelper;
    }
}