using System;
using System.Linq.Expressions;
using System.Reflection;

namespace Unity.VisualScripting
{
    public class InstancePropertyAccessor<TTarget, TProperty> : IOptimizedAccessor
    {
        public InstancePropertyAccessor(PropertyInfo propertyInfo)
        {
            if (OptimizedReflection.safeMode)
            {
                Ensure.That(nameof(propertyInfo)).IsNotNull(propertyInfo);

                if (propertyInfo.DeclaringType != typeof(TTarget))
                {
                    throw new ArgumentException("The declaring type of the property info doesn't match the generic type.", nameof(propertyInfo));
                }

                if (propertyInfo.PropertyType != typeof(TProperty))
                {
                    throw new ArgumentException("The property type of the property info doesn't match the generic type.", nameof(propertyInfo));
                }

                if (propertyInfo.IsStatic())
                {
                    throw new ArgumentException("The property is static.", nameof(propertyInfo));
                }
            }

            this.propertyInfo = propertyInfo;
        }

        private readonly PropertyInfo propertyInfo;
        private Func<TTarget, TProperty> getter;
        private Action<TTarget, TProperty> setter;

        public void Compile()
        {
            var getterInfo = propertyInfo.GetGetMethod(true);
            var setterInfo = propertyInfo.GetSetMethod(true);

            if (OptimizedReflection.useJit)
            {
                var targetExpression = Expression.Parameter(typeof(TTarget), "target");

                if (getterInfo != null)
                {
                    var propertyExpression = Expression.Property(targetExpression, propertyInfo);
                    getter = Expression.Lambda<Func<TTarget, TProperty>>(propertyExpression, targetExpression).Compile();
                }

                if (setterInfo != null)
                {
                    setter = (Action<TTarget, TProperty>)setterInfo.CreateDelegate(typeof(Action<TTarget, TProperty>));
                }
            }
            else
            {
                if (getterInfo != null)
                {
                    getter = (Func<TTarget, TProperty>)getterInfo.CreateDelegate(typeof(Func<TTarget, TProperty>));
                }

                if (setterInfo != null)
                {
                    setter = (Action<TTarget, TProperty>)setterInfo.CreateDelegate(typeof(Action<TTarget, TProperty>));
                }
            }
        }

        public object GetValue(object target)
        {
            if (OptimizedReflection.safeMode)
            {
                OptimizedReflection.VerifyInstanceTarget<TTarget>(target);

                if (getter == null)
                {
                    throw new TargetException($"The property '{typeof(TTarget)}.{propertyInfo.Name}' has no get accessor.");
                }

                try
                {
                    return GetValueUnsafe(target);
                }
                catch (TargetInvocationException)
                {
                    throw;
                }
                catch (Exception ex)
                {
                    throw new TargetInvocationException(ex);
                }
            }
            else
            {
                return GetValueUnsafe(target);
            }
        }

        private object GetValueUnsafe(object target)
        {
            return getter.Invoke((TTarget)target);
        }

        public void SetValue(object target, object value)
        {
            if (OptimizedReflection.safeMode)
            {
                OptimizedReflection.VerifyInstanceTarget<TTarget>(target);

                if (setter == null)
                {
                    throw new TargetException($"The property '{typeof(TTarget)}.{propertyInfo.Name}' has no set accessor.");
                }

                if (!typeof(TProperty).IsAssignableFrom(value))
                {
                    throw new ArgumentException($"The provided value for '{typeof(TTarget)}.{propertyInfo.Name}' does not match the property type.\nProvided: {value?.GetType()?.ToString() ?? "null"}\nExpected: {typeof(TProperty)}");
                }

                try
                {
                    SetValueUnsafe(target, value);
                }
                catch (TargetInvocationException)
                {
                    throw;
                }
                catch (Exception ex)
                {
                    throw new TargetInvocationException(ex);
                }
            }
            else
            {
                SetValueUnsafe(target, value);
            }
        }

        private void SetValueUnsafe(object target, object value)
        {
            setter.Invoke((TTarget)target, (TProperty)value);
        }
    }
}