using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace RebootKit.Engine.Foundation {
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Method)]
    public class InjectAttribute : Attribute {
    }

    public class DIContext {
        private const BindingFlags k_fieldsBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
        private const BindingFlags k_methodsBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

        public interface IFieldInjector {
            bool Inject(FieldInfo field, object target, DIContext context);
        }

        private static readonly Logger Logger = new(nameof(DIContext));
        
        private readonly Dictionary<Type, object> _bindingsMaps = new();
        private readonly List<IFieldInjector> _fieldInjectors = new();
        
        public DIContext() {
            Bind(this);

            AddInjector(new InjectAttributeFieldInjector());
        }

        public void AddInjector(IFieldInjector injector) {
            _fieldInjectors.Add(injector);
        }

        public void Bind(Type type, object obj) {
            if (!_bindingsMaps.TryAdd(type, obj)) {
                Logger.Error($"Cannot bind to '{type}', slot is already occupied");
            }
        }

        public void Bind<TBind>(TBind obj) {
            Bind(typeof(TBind), obj);
        }

        public object Resolve(Type type) {
            if (_bindingsMaps.TryGetValue(type, out object obj)) {
                return obj;
            }

            Logger.Error($"Couldn't resolve `{type}`");
            return null;
        }

        public T Resolve<T>() {
            return (T) Resolve(typeof(T));
        }

        public void Inject<T>(T target) {
            Type type = typeof(T);

            foreach (FieldInfo field in type.GetFields(k_fieldsBindingFlags)) {
                if (!InjectField(field, target)) {
                    return;
                }
            }

            foreach (MethodInfo method in type.GetMethods(k_methodsBindingFlags)) {
                if (!Attribute.IsDefined(method, typeof(InjectAttribute))) {
                    continue;
                }

                Type[] paramsTypes = method.GetParameters()
                                           .Select(t => t.ParameterType)
                                           .ToArray();

                object[] instances = new object[paramsTypes.Length];

                for (int i = 0; i < paramsTypes.Length; ++i) {
                    instances[i] = Resolve(paramsTypes[i]);

                    if (instances[i] == null) {
                        Logger.Error($"Failed to resolve method parameter of type `{paramsTypes[i]}`");
                    }
                }

                method.Invoke(target, instances);
            }
        }

        private bool InjectField(FieldInfo field, object target) {
            for (int i = _fieldInjectors.Count - 1; i >= 0; i--) {
                if (_fieldInjectors[i].Inject(field, target, this)) {
                    return true;
                }
            }

            return false;
        }

        private class InjectAttributeFieldInjector : IFieldInjector {
            public bool Inject(FieldInfo field, object target, DIContext context) {
                if (!Attribute.IsDefined(field, typeof(InjectAttribute))) {
                    return false;
                }
                
                object instance = context.Resolve(field.FieldType);

                if (instance == null) {
                    Logger.Error($"Cannot resolve `{field.FieldType}`");
                    return false;
                }

                field.SetValue(target, instance);
                return true;
            }
        }
    }
}