﻿#region usings

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Prismactic.Core.Extensions;
using Prismactic.Core.Properties;

#endregion

namespace Prismactic.Core.Modularity
{
    /// <summary>
    ///     Handles AppDomain's AssemblyResolve event to be able to load assemblies dynamically in
    ///     the LoadFrom context, but be able to reference the type from assemblies loaded in the Load context.
    /// </summary>
    public class AssemblyResolver : IAssemblyResolver, IDisposable
    {
        #region Fields

        private readonly List<AssemblyInfo> _registeredAssemblies;
        private bool _handlesAssemblyResolve;

        #endregion

        #region Constructors

        public AssemblyResolver()
        {
            _registeredAssemblies = new List<AssemblyInfo>();
        }

        #endregion

        #region Methods

        /// <summary>
        ///     Registers the specified assembly and resolves the types in it when the AppDomain requests for it.
        /// </summary>
        /// <param name="assemblyFilePath">The path to the assemly to load in the LoadFrom context.</param>
        /// <remarks>
        ///     This method does not load the assembly immediately, but lazily until someone requests a <see cref="Type" />
        ///     declared in the assembly.
        /// </remarks>
        public void LoadAssemblyFrom(string assemblyFilePath)
        {
            if (!_handlesAssemblyResolve)
            {
                AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
                _handlesAssemblyResolve = true;
            }

            var assemblyUri = GetFileUri(assemblyFilePath);

            assemblyUri.EnsureParameterNotNull("assemblyFilePath", Resources.InvalidArgumentAssemblyUri);

            if (!File.Exists(assemblyUri.LocalPath))
            {
                throw new FileNotFoundException();
            }

            var assemblyName = AssemblyName.GetAssemblyName(assemblyUri.LocalPath);
            var assemblyInfo = _registeredAssemblies.FirstOrDefault(a => assemblyName == a.AssemblyName);

            if (assemblyInfo != null)
            {
                return;
            }

            assemblyInfo = new AssemblyInfo {AssemblyName = assemblyName, AssemblyUri = assemblyUri};
            _registeredAssemblies.Add(assemblyInfo);
        }

        /// <summary>
        ///     Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <remarks>Calls <see cref="Dispose(bool)" /></remarks>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private static Uri GetFileUri(string filePath)
        {
            if (String.IsNullOrEmpty(filePath))
            {
                return null;
            }

            Uri uri;
            if (!Uri.TryCreate(filePath, UriKind.Absolute, out uri))
            {
                return null;
            }

            return !uri.IsFile ? null : uri;
        }

        private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            var assemblyName = new AssemblyName(args.Name);

            var assemblyInfo =
                _registeredAssemblies.FirstOrDefault(
                    a => AssemblyName.ReferenceMatchesDefinition(assemblyName, a.AssemblyName));

            if (assemblyInfo != null)
            {
                return assemblyInfo.Assembly ??
                       (assemblyInfo.Assembly = Assembly.LoadFrom(assemblyInfo.AssemblyUri.LocalPath));
            }

            return null;
        }

        /// <summary>
        ///     Disposes the associated <see cref="AssemblyResolver" />.
        /// </summary>
        /// <param name="disposing">When <see langword="true" />, it is being called from the Dispose method.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!_handlesAssemblyResolve) return;
            AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
            _handlesAssemblyResolve = false;
        }

        #endregion

        private class AssemblyInfo
        {
            public AssemblyName AssemblyName { get; set; }

            public Uri AssemblyUri { get; set; }

            public Assembly Assembly { get; set; }
        }
    }
}