// 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.Concurrent;
using System.IO;
using System.Reflection;
using System.Resources;
using Microsoft.AspNetCore.Shared;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.Localization;

/// <summary>
/// An <see cref="IStringLocalizerFactory"/> that creates instances of <see cref="ResourceManagerStringLocalizer"/>.
/// </summary>
/// <remarks>
/// <see cref="ResourceManagerStringLocalizerFactory"/> offers multiple ways to set the relative path of
/// resources to be used. They are, in order of precedence:
/// <see cref="ResourceLocationAttribute"/> -> <see cref="LocalizationOptions.ResourcesPath"/> -> the project root.
/// </remarks>
public class ResourceManagerStringLocalizerFactory : IStringLocalizerFactory
{
    private readonly IResourceNamesCache _resourceNamesCache = new ResourceNamesCache();
    private readonly ConcurrentDictionary<string, ResourceManagerStringLocalizer> _localizerCache =
        new ConcurrentDictionary<string, ResourceManagerStringLocalizer>();
    private readonly string _resourcesRelativePath;
    private readonly ILoggerFactory _loggerFactory;

    /// <summary>
    /// Creates a new <see cref="ResourceManagerStringLocalizer"/>.
    /// </summary>
    /// <param name="localizationOptions">The <see cref="IOptions{LocalizationOptions}"/>.</param>
    /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
    public ResourceManagerStringLocalizerFactory(
        IOptions<LocalizationOptions> localizationOptions,
        ILoggerFactory loggerFactory)
    {
        ArgumentNullThrowHelper.ThrowIfNull(localizationOptions);
        ArgumentNullThrowHelper.ThrowIfNull(loggerFactory);

        _resourcesRelativePath = localizationOptions.Value.ResourcesPath ?? string.Empty;
        _loggerFactory = loggerFactory;

        if (!string.IsNullOrEmpty(_resourcesRelativePath))
        {
            _resourcesRelativePath = _resourcesRelativePath.Replace(Path.AltDirectorySeparatorChar, '.')
                .Replace(Path.DirectorySeparatorChar, '.') + ".";
        }
    }

    /// <summary>
    /// Gets the resource prefix used to look up the resource.
    /// </summary>
    /// <param name="typeInfo">The type of the resource to be looked up.</param>
    /// <returns>The prefix for resource lookup.</returns>
    protected virtual string GetResourcePrefix(TypeInfo typeInfo)
    {
        ArgumentNullThrowHelper.ThrowIfNull(typeInfo);

        return GetResourcePrefix(typeInfo, GetRootNamespace(typeInfo.Assembly), GetResourcePath(typeInfo.Assembly));
    }

    /// <summary>
    /// Gets the resource prefix used to look up the resource.
    /// </summary>
    /// <param name="typeInfo">The type of the resource to be looked up.</param>
    /// <param name="baseNamespace">The base namespace of the application.</param>
    /// <param name="resourcesRelativePath">The folder containing all resources.</param>
    /// <returns>The prefix for resource lookup.</returns>
    /// <remarks>
    /// For the type "Sample.Controllers.Home" if there's a resourceRelativePath return
    /// "Sample.Resourcepath.Controllers.Home" if there isn't one then it would return "Sample.Controllers.Home".
    /// </remarks>
    protected virtual string GetResourcePrefix(TypeInfo typeInfo, string? baseNamespace, string? resourcesRelativePath)
    {
        ArgumentNullThrowHelper.ThrowIfNull(typeInfo);
        ArgumentThrowHelper.ThrowIfNullOrEmpty(baseNamespace);

        if (string.IsNullOrEmpty(typeInfo.FullName))
        {
            throw new ArgumentException(Resources.FormatLocalization_TypeMustHaveTypeName(typeInfo));
        }

        if (string.IsNullOrEmpty(resourcesRelativePath))
        {
            return typeInfo.FullName;
        }
        else
        {
            // This expectation is defined by dotnet's automatic resource storage.
            // We have to conform to "{RootNamespace}.{ResourceLocation}.{FullTypeName - RootNamespace}".
            return baseNamespace + "." + resourcesRelativePath + TrimPrefix(typeInfo.FullName, baseNamespace + ".");
        }
    }

    /// <summary>
    /// Gets the resource prefix used to look up the resource.
    /// </summary>
    /// <param name="baseResourceName">The name of the resource to be looked up</param>
    /// <param name="baseNamespace">The base namespace of the application.</param>
    /// <returns>The prefix for resource lookup.</returns>
    protected virtual string GetResourcePrefix(string baseResourceName, string baseNamespace)
    {
        ArgumentThrowHelper.ThrowIfNullOrEmpty(baseResourceName);
        ArgumentThrowHelper.ThrowIfNullOrEmpty(baseNamespace);

        var assemblyName = new AssemblyName(baseNamespace);
        var assembly = Assembly.Load(assemblyName);
        var rootNamespace = GetRootNamespace(assembly);
        var resourceLocation = GetResourcePath(assembly);
        var locationPath = rootNamespace + "." + resourceLocation;

        baseResourceName = locationPath + TrimPrefix(baseResourceName, baseNamespace + ".");

        return baseResourceName;
    }

    /// <summary>
    /// Creates a <see cref="ResourceManagerStringLocalizer"/> using the <see cref="Assembly"/> and
    /// <see cref="Type.FullName"/> of the specified <see cref="Type"/>.
    /// </summary>
    /// <param name="resourceSource">The <see cref="Type"/>.</param>
    /// <returns>The <see cref="ResourceManagerStringLocalizer"/>.</returns>
    public IStringLocalizer Create(Type resourceSource)
    {
        ArgumentNullThrowHelper.ThrowIfNull(resourceSource);

        // Get without Add to prevent unnecessary lambda allocation
        if (!_localizerCache.TryGetValue(resourceSource.AssemblyQualifiedName!, out var localizer))
        {
            var typeInfo = resourceSource.GetTypeInfo();
            var baseName = GetResourcePrefix(typeInfo);
            var assembly = typeInfo.Assembly;

            localizer = CreateResourceManagerStringLocalizer(assembly, baseName);

            _localizerCache[resourceSource.AssemblyQualifiedName!] = localizer;
        }

        return localizer;
    }

    /// <summary>
    /// Creates a <see cref="ResourceManagerStringLocalizer"/>.
    /// </summary>
    /// <param name="baseName">The base name of the resource to load strings from.</param>
    /// <param name="location">The location to load resources from.</param>
    /// <returns>The <see cref="ResourceManagerStringLocalizer"/>.</returns>
    public IStringLocalizer Create(string baseName, string location)
    {
        ArgumentNullThrowHelper.ThrowIfNull(baseName);
        ArgumentNullThrowHelper.ThrowIfNull(location);

        return _localizerCache.GetOrAdd($"B={baseName},L={location}", _ =>
        {
            var assemblyName = new AssemblyName(location);
            var assembly = Assembly.Load(assemblyName);
            baseName = GetResourcePrefix(baseName, location);

            return CreateResourceManagerStringLocalizer(assembly, baseName);
        });
    }

    /// <summary>Creates a <see cref="ResourceManagerStringLocalizer"/> for the given input.</summary>
    /// <param name="assembly">The assembly to create a <see cref="ResourceManagerStringLocalizer"/> for.</param>
    /// <param name="baseName">The base name of the resource to search for.</param>
    /// <returns>A <see cref="ResourceManagerStringLocalizer"/> for the given <paramref name="assembly"/> and <paramref name="baseName"/>.</returns>
    /// <remarks>This method is virtual for testing purposes only.</remarks>
    protected virtual ResourceManagerStringLocalizer CreateResourceManagerStringLocalizer(
        Assembly assembly,
        string baseName)
    {
        return new ResourceManagerStringLocalizer(
            new ResourceManager(baseName, assembly),
            assembly,
            baseName,
            _resourceNamesCache,
            _loggerFactory.CreateLogger<ResourceManagerStringLocalizer>());
    }

    /// <summary>
    /// Gets the resource prefix used to look up the resource.
    /// </summary>
    /// <param name="location">The general location of the resource.</param>
    /// <param name="baseName">The base name of the resource.</param>
    /// <param name="resourceLocation">The location of the resource within <paramref name="location"/>.</param>
    /// <returns>The resource prefix used to look up the resource.</returns>
    protected virtual string GetResourcePrefix(string location, string baseName, string resourceLocation)
    {
        // Re-root the base name if a resources path is set
        return location + "." + resourceLocation + TrimPrefix(baseName, location + ".");
    }

    /// <summary>Gets a <see cref="ResourceLocationAttribute"/> from the provided <see cref="Assembly"/>.</summary>
    /// <param name="assembly">The assembly to get a <see cref="ResourceLocationAttribute"/> from.</param>
    /// <returns>The <see cref="ResourceLocationAttribute"/> associated with the given <see cref="Assembly"/>.</returns>
    /// <remarks>This method is protected and virtual for testing purposes only.</remarks>
    protected virtual ResourceLocationAttribute? GetResourceLocationAttribute(Assembly assembly)
    {
        return assembly.GetCustomAttribute<ResourceLocationAttribute>();
    }

    /// <summary>Gets a <see cref="RootNamespaceAttribute"/> from the provided <see cref="Assembly"/>.</summary>
    /// <param name="assembly">The assembly to get a <see cref="RootNamespaceAttribute"/> from.</param>
    /// <returns>The <see cref="RootNamespaceAttribute"/> associated with the given <see cref="Assembly"/>.</returns>
    /// <remarks>This method is protected and virtual for testing purposes only.</remarks>
    protected virtual RootNamespaceAttribute? GetRootNamespaceAttribute(Assembly assembly)
    {
        return assembly.GetCustomAttribute<RootNamespaceAttribute>();
    }

    private string? GetRootNamespace(Assembly assembly)
    {
        var rootNamespaceAttribute = GetRootNamespaceAttribute(assembly);

        if (rootNamespaceAttribute != null)
        {
            return rootNamespaceAttribute.RootNamespace;
        }

        return assembly.GetName().Name;
    }

    private string GetResourcePath(Assembly assembly)
    {
        var resourceLocationAttribute = GetResourceLocationAttribute(assembly);

        // If we don't have an attribute assume all assemblies use the same resource location.
        var resourceLocation = resourceLocationAttribute == null
            ? _resourcesRelativePath
            : resourceLocationAttribute.ResourceLocation + ".";
        resourceLocation = resourceLocation
            .Replace(Path.DirectorySeparatorChar, '.')
            .Replace(Path.AltDirectorySeparatorChar, '.');

        return resourceLocation;
    }

    private static string? TrimPrefix(string name, string prefix)
    {
        if (name.StartsWith(prefix, StringComparison.Ordinal))
        {
            return name.Substring(prefix.Length);
        }

        return name;
    }
}
