using R3;
using UnityEngine;
using System;
using KitsuneCafe.System;
using System.Collections.Generic;
using System.Threading;

namespace KitsuneCafe.Extension
{
    public static class ObservableExtensions
    {
        public static Observable<TResult> Compose<TSource, TResult>(this Observable<TSource> source, Func<Observable<TSource>, Observable<TResult>> transform)
        {
            return transform(source);
        }

        private struct SmoothDampState
        {
            public float CurrentValue;
            public float CurrentVelocityRef;
        }

        public static Observable<float> EveryUpdateDelta(this TimeProvider provider)
        {
            if (provider == null)
            {
                provider = UnityTimeProvider.Update;
            }

            if (provider.TryGetFrameProvider(out var frameProvider))
            {
                return Observable.EveryUpdate(frameProvider)
                    .Select(_ => provider.GetDeltaTime(Time.deltaTime));
            }
            else
            {
                return Observable.Empty<float>();
            }
        }

        /// <summary>
        /// Smoothly dampens a value from its current state towards a target value over time,
        /// driven by a FrameProvider.
        /// </summary>
        /// <param name="source">The observable stream providing the *target* values for damping.</param>
        /// <param name="valueGetter">A function that provides the initial value for the damping process when a new target is received (e.g., current value from Animator).</param>
        /// <param name="smoothTime">The approximate time it takes for the value to reach the target.</param>
        /// <param name="maxSpeed">The maximum speed the value can change per second.</param>
        /// <param name="provider">The FrameProvider to use for update ticks (e.g., UnityFrameProvider.FixedUpdate).</param>
        /// <returns>An observable stream of the smoothed values.</returns>
        public static Observable<float> SmoothDamp(
            this Observable<float> source,
            Func<float> valueGetter,
            float smoothTime,
            float maxSpeed,
            TimeProvider provider)
        {
            return source
                .DistinctUntilChanged(FEqualityComparer<float>.Create(KMath.IsApproxEqual))
                .Select(target =>
                {
                    float initialCurrentValue = valueGetter();

                    return Observable.EveryUpdate(provider.GetFrameProvider())
                        .Scan(new SmoothDampState { CurrentValue = initialCurrentValue, CurrentVelocityRef = 0f }, (state, _) =>
                        {
                            float currentVelocity = state.CurrentVelocityRef;
                            float nextValue = Mathf.SmoothDamp(
                                state.CurrentValue,
                                target,
                                ref currentVelocity,
                                smoothTime,
                                maxSpeed,
                                provider.GetDeltaTime()
                            );
                            return new SmoothDampState { CurrentValue = nextValue, CurrentVelocityRef = currentVelocity };
                        })
                        .Select(state => state.CurrentValue)
                        .TakeWhile(val => Mathf.Abs(val - target) > float.Epsilon)
                        .Concat(Observable.Return(target));
                })
                .Switch();
        }

        public static Observable<float> SmoothDamp(
            this Observable<float> source,
            Func<float> valueGetter,
            float smoothTime)
        {
            return SmoothDamp(source, valueGetter, smoothTime, Mathf.Infinity, UnityTimeProvider.Update);
        }

        public static Observable<float> SmoothDamp(
            this Observable<float> source,
            Func<float> valueGetter,
            float smoothTime,
            float maxSpeed)
        {
            return SmoothDamp(source, valueGetter, smoothTime, maxSpeed, UnityTimeProvider.Update);
        }

        public static Observable<float> SmoothDamp(
            this Observable<float> source,
            Func<float> valueGetter,
            float smoothTime,
            TimeProvider provider)
        {
            return SmoothDamp(source, valueGetter, smoothTime, Mathf.Infinity, provider);
        }

        public static float SqrDistance(this Vector3 vector, Vector3 other)
        {
            return (vector - other).sqrMagnitude;
        }

        /// <summary>
        /// Returns the element from a sequence that has the minimum value for a specified key.
        /// If the source sequence is empty, returns default(TSource).
        /// </summary>
        /// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
        /// <typeparam name="TKey">The type of the key to compare elements by.</typeparam>
        /// <param name="source">The sequence to find the minimum element from.</param>
        /// <param name="keySelector">A function to extract the key from an element.</param>
        /// <returns>The element that has the minimum key value, or default(TSource) if the source is empty.</returns>
        public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
            where TKey : IComparable<TKey>
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));

            using (var enumerator = source.GetEnumerator())
            {
                if (!enumerator.MoveNext())
                {
                    return default;
                }

                TSource minElement = enumerator.Current;
                TKey minKey = keySelector(minElement);

                while (enumerator.MoveNext())
                {
                    TSource currentElement = enumerator.Current;
                    TKey currentKey = keySelector(currentElement);

                    if (currentKey.CompareTo(minKey) < 0)
                    {
                        minKey = currentKey;
                        minElement = currentElement;
                    }
                }
                return minElement;
            }
        }

        public static Observable<TResult> SelectSwitch<T, TResult>(this Observable<T> source, Func<T, Observable<TResult>> selector)
        {
            return source.Select(selector).Switch();
        }

        public static Observable<T> SwitchIf<T>(this Observable<T> source, Observable<bool> selectionSource, Observable<T> other)
        {
            return selectionSource.SelectSwitch(value => value ? other : source);
        }

        public static Observable<T> SwitchIf<T>(this Observable<T> source, Observable<bool> selectionSource, Func<Observable<T>> other)
        {
            return selectionSource.SelectSwitch(value => value ? other() : source);
        }

        public static Observable<T> WhereEquals<T>(this Observable<T> source, T value) where T : IEquatable<T>
        {
            return source.Where(value.Equals);
        }

        public static Observable<bool> WhereTrue(this Observable<bool> source)
        {
            return source.Where(Func.Identity);
        }
    }
}

