using System;
using Autofac;
using Autofac.Builder;
using NitroxModel.DataStructures.Util;

namespace NitroxModel.Core;

/// <summary>
///     Dependency Injection (DI) class for resolving types as defined in the DI registrar, implementing <see cref="IAutoFacRegistrar" />.
/// </summary>
public static class NitroxServiceLocator
{
    private static IContainer DependencyContainer { get; set; }
    private static ILifetimeScope CurrentLifetimeScope { get; set; }
    public static event EventHandler LifetimeScopeEnded;

    public static void InitializeDependencyContainer(params IAutoFacRegistrar[] registrars)
    {
        ContainerBuilder builder = new();
        foreach (IAutoFacRegistrar registrar in registrars)
        {
            registrar.RegisterDependencies(builder);
        }

        // IgnoreStartableComponents - Prevents "phantom" executions of the Start() method
        // on a MonoBehaviour because someone accidentally did something funky with a DI registration.
        DependencyContainer = builder.Build(ContainerBuildOptions.IgnoreStartableComponents);
    }

    /// <summary>
    ///     Starts a new life time scope. A single instance per registered service will be returned while this scope is active.
    ///     Services can scoped to this life time using <see cref="IRegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.InstancePerLifetimeScope" />.
    /// </summary>
    /// <remarks>
    ///     A life time scope should be created when the game leaves the main menu and loads a level with multiplayer.
    ///     It should end when the game process unloads the level (e.g. player returns to the main menu).
    /// </remarks>
    public static void BeginNewLifetimeScope()
    {
        if (DependencyContainer == null)
        {
            throw new InvalidOperationException("You must install an Autofac container before initializing a new lifetime scope.");
        }

        CurrentLifetimeScope?.Dispose();
        CurrentLifetimeScope = DependencyContainer.BeginLifetimeScope();
    }

    /// <summary>
    ///     Ends the life time scoped services that were registered using <see cref="IRegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.InstancePerLifetimeScope" />.
    /// </summary>
    public static void EndCurrentLifetimeScope()
    {
        CurrentLifetimeScope?.Dispose();
        OnLifetimeScopeEnded();
    }

    /// <summary>
    ///     Only locates the service in the container, pre-lifetime scope.
    /// </summary>
    public static T LocateServicePreLifetime<T>()
    {
        return DependencyContainer.Resolve<T>();
    }

    /// <summary>
    ///     Retrieves a service which was registered into the DI container. Creates a new instance if required.<br />
    /// </summary>
    /// <remarks>
    ///     This method should not be used if the constructor is available for defining a parameter where its type is the service to inject.
    ///     For Unity monobehaviours the constructor is used by Unity and cannot be used to inject services. In this case, use this method.
    /// </remarks>
    public static T LocateService<T>()
        where T : class
    {
        CheckServiceResolutionViability();
        return CurrentLifetimeScope.Resolve<T>();
    }

    /// <summary>
    ///     Non-generic alternative to <see cref="LocateService{T}" />.
    /// </summary>
    public static object LocateService(Type serviceType)
    {
        CheckServiceResolutionViability();
        return CurrentLifetimeScope.Resolve(serviceType);
    }

    /// <summary>
    ///     Tries to locate the service if it exists. Can return an <see cref="Optional{T}" /> without a value.
    /// </summary>
    /// <typeparam name="T">Type of service to try to locate.</typeparam>
    /// <returns>Optional that might or might not hold the service instance.</returns>
    public static Optional<T> LocateOptionalService<T>() where T : class
    {
        CheckServiceResolutionViability();
        return Optional.OfNullable(CurrentLifetimeScope.ResolveOptional<T>());
    }

    /// <summary>
    ///     Tries to locate the service if it exists. Can return an <see cref="Optional{T}" /> without a value.
    /// </summary>
    /// <param name="serviceType">Type of service to try to locate.</param>
    /// <returns>Optional that might or might not hold the service instance.</returns>
    public static Optional<object> LocateOptionalService(Type serviceType)
    {
        CheckServiceResolutionViability();
        return Optional.OfNullable(CurrentLifetimeScope.ResolveOptional(serviceType));
    }

    /// <summary>
    ///     Throws if a service is asked for but without a proper life time scope.
    /// </summary>
    private static void CheckServiceResolutionViability()
    {
        if (DependencyContainer == null)
        {
            throw new InvalidOperationException("You must install an Autofac container before resolving dependencies.");
        }
        if (CurrentLifetimeScope == null)
        {
            throw new InvalidOperationException("You must begin a new lifetime scope before resolving dependencies.");
        }
    }

    private static void OnLifetimeScopeEnded() => LifetimeScopeEnded?.Invoke(null, EventArgs.Empty);

    /// <summary>
    ///     Generic static class to cache type with very fast lookups. Only use for singleton types.
    /// </summary>
    /// <typeparam name="T">Type in the cache, should be singleton.</typeparam>
    public static class Cache<T> where T : class
    {
        private static T value;
        public static T Value => value ??= LocateServiceAndRegister();
        public static T ValuePreLifetime => value ??= LocateServicePreLifetimeAndRegister();

        private static T LocateServiceAndRegister()
        {
            LifetimeScopeEnded += Invalidate;
            return LocateService<T>();
        }

        private static T LocateServicePreLifetimeAndRegister()
        {
            LifetimeScopeEnded += Invalidate;
            return LocateServicePreLifetime<T>();
        }

        /// <summary>
        ///     Invalidates the cache for type <see cref="T" />. The next <see cref="Value" /> access will request from <see cref="NitroxServiceLocator" /> again.
        /// </summary>
        private static void Invalidate(object _, EventArgs __)
        {
            value = null;
            LifetimeScopeEnded -= Invalidate;
        }
    }
}
