// 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.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;

namespace Microsoft.Extensions.DependencyModel
{
    public class DependencyContextLoader
    {
        private const string DepsJsonExtension = ".deps.json";

        private readonly string? _entryPointDepsLocation;
        private readonly IEnumerable<string> _nonEntryPointDepsPaths;
        private readonly IFileSystem _fileSystem;
        private readonly Func<IDependencyContextReader> _jsonReaderFactory;

        public DependencyContextLoader() : this(
            DependencyContextPaths.Current.Application,
            DependencyContextPaths.Current.NonApplicationPaths,
            FileSystemWrapper.Default,
            () => new DependencyContextJsonReader())
        {
        }

        internal DependencyContextLoader(
            string? entryPointDepsLocation,
            IEnumerable<string> nonEntryPointDepsPaths,
            IFileSystem fileSystem,
            Func<IDependencyContextReader> jsonReaderFactory)
        {
            _entryPointDepsLocation = entryPointDepsLocation;
            _nonEntryPointDepsPaths = nonEntryPointDepsPaths;
            _fileSystem = fileSystem;
            _jsonReaderFactory = jsonReaderFactory;
        }

        public static DependencyContextLoader Default { get; } = new();

        private static bool IsEntryAssembly(Assembly assembly)
        {
            return assembly.Equals(Assembly.GetEntryAssembly());
        }

        private static Stream? GetResourceStream(Assembly assembly, string name)
        {
            return assembly.GetManifestResourceStream(name);
        }

        [RequiresAssemblyFiles("DependencyContext for an assembly from a application published as single-file is not supported. The method will return null. Make sure the calling code can handle this case.")]
        public DependencyContext? Load(Assembly assembly)
        {
            ThrowHelper.ThrowIfNull(assembly);

            DependencyContext? context = null;
            using (IDependencyContextReader reader = _jsonReaderFactory())
            {
                if (IsEntryAssembly(assembly))
                {
                    context = LoadEntryAssemblyContext(reader);
                }

                context ??= LoadAssemblyContext(assembly, reader);

                if (context != null)
                {
                    foreach (string extraPath in _nonEntryPointDepsPaths)
                    {
                        DependencyContext? extraContext = LoadContext(reader, extraPath);
                        if (extraContext != null)
                        {
                            context = context.Merge(extraContext);
                        }
                    }
                }
            }
            return context;
        }

        private DependencyContext? LoadEntryAssemblyContext(IDependencyContextReader reader)
        {
            return LoadContext(reader, _entryPointDepsLocation);
        }

        private DependencyContext? LoadContext(IDependencyContextReader reader, string? location)
        {
            if (!string.IsNullOrEmpty(location))
            {
                Debug.Assert(_fileSystem.File.Exists(location));
                using (Stream stream = _fileSystem.File.OpenRead(location))
                {
                    return reader.Read(stream);
                }
            }
            return null;
        }

        [RequiresAssemblyFiles("DependencyContext for an assembly from a application published as single-file is not supported. The method will return null. Make sure the calling code can handle this case.")]
        private DependencyContext? LoadAssemblyContext(Assembly assembly, IDependencyContextReader reader)
        {
            using (Stream? stream = GetResourceStream(assembly, assembly.GetName().Name + DepsJsonExtension))
            {
                if (stream != null)
                {
                    return reader.Read(stream);
                }
            }

            string? depsJsonFile = GetDepsJsonPath(assembly);
            if (!string.IsNullOrEmpty(depsJsonFile))
            {
                using (Stream stream = _fileSystem.File.OpenRead(depsJsonFile))
                {
                    return reader.Read(stream);
                }
            }

            return null;
        }

        [RequiresAssemblyFiles("The use of DependencyContextLoader is not supported when publishing as single-file")]
        private string? GetDepsJsonPath(Assembly assembly)
        {
            // Assemblies loaded in memory (e.g. single file) return empty string from Location.
            // In these cases, don't try probing next to the assembly.
            string assemblyLocation = assembly.Location;
            if (string.IsNullOrEmpty(assemblyLocation))
            {
                return null;
            }

            string depsJsonFile = Path.ChangeExtension(assemblyLocation, DepsJsonExtension);
            bool depsJsonFileExists = _fileSystem.File.Exists(depsJsonFile);

            if (!depsJsonFileExists)
            {
                // in some cases (like .NET Framework shadow copy) the Assembly Location
                // and CodeBase will be different, so also try the CodeBase
                string? assemblyCodeBase = GetNormalizedCodeBasePath(assembly);
                if (!string.IsNullOrEmpty(assemblyCodeBase) &&
                    assemblyLocation != assemblyCodeBase)
                {
                    depsJsonFile = Path.ChangeExtension(assemblyCodeBase, DepsJsonExtension);
                    depsJsonFileExists = _fileSystem.File.Exists(depsJsonFile);
                }
            }

            return depsJsonFileExists ?
                depsJsonFile :
                null;
        }

        [RequiresAssemblyFiles]
        private static string? GetNormalizedCodeBasePath(Assembly assembly)
        {
#pragma warning disable SYSLIB0012 // CodeBase is obsolete
            if (Uri.TryCreate(assembly.CodeBase, UriKind.Absolute, out Uri? codeBase)
#pragma warning restore SYSLIB0012 // CodeBase is obsolete
                && codeBase.IsFile)
            {
                return codeBase.LocalPath;
            }
            else
            {
                return null;
            }
        }
    }
}
