using UnityEngine;
using System.Linq;

using R3;
using R3.Triggers;
using ObservableCollections;

using KitsuneCafe.Interaction;
using KitsuneCafe.Extension;
using KitsuneCafe.SOAP;

namespace KitsuneCafe.Player
{
    public class Interactor : MonoBehaviour, IInteractor
    {
        [SerializeField]
        private GameObject root;
        public GameObject Root => root;

        [SerializeField]
        private new Collider collider;

        [SerializeField]
        private ReactiveValue<GameObject> selectedObject;

        [SerializeField]
        private ReactiveEvent<float> interactSource;

        [SerializeField]
        private int updateFrequency = 10;

        [SerializeField, Tooltip("How far an object must move before being considered")]
        private float minimumChangeDelta = 0.1f;
        private float sqrtMovementDelta;

        public new T GetComponent<T>()
        {
            return Root.GetComponent<T>();
        }

        public new bool TryGetComponent<T>(out T component)
        {
            return root.TryGetComponent<T>(out component);
        }

        private void OnValidate()
        {
            root = gameObject;
            collider = GetComponent<Collider>();
        }

        private void Awake()
        {
            selectedObject.Value = null;
            sqrtMovementDelta = Mathf.Sqrt(minimumChangeDelta);

            var d = Disposable.CreateBuilder();

            var activeInteractables = TrackCollisions<IInteractable>(collider, ref d);

            var selectedObjects = ObserveNearestInteractable(
                    PositionChanged(transform, sqrtMovementDelta),
                    activeInteractables
                )
                .DefaultIfEmpty()
                .DistinctUntilChanged()
                .Do(selected => selectedObject.Value = selected?.gameObject)
                .SelectOrDefault(go => go.TryGetComponent(out IInteractable i) ? i : null)
                .Scan<IInteractable, IInteractable>(null, SwapSelected);

            interactSource
                .AsObservable()
                .WithLatestFrom(selectedObjects, (_, highlighted) => highlighted)
                .WhereNotNull()
                .Subscribe(interactable =>
                {
                    var result = interactable.Interact(this);

                    if (result.IsOk)
                    {
                        activeInteractables.Remove(interactable);
                    }
                    else
                    {
                        Debug.Log($"interaction failed: {result}");
                    }
                })
                .AddTo(ref d);

            d.RegisterTo(destroyCancellationToken);
        }

        private ObservableHashSet<T> TrackCollisions<T>(Collider collider, ref DisposableBuilder d)
        {
            var interactables = new ObservableHashSet<T>();

            collider.OnTriggerEnterAsObservable()
                .Subscribe(other =>
                {
                    if (other.TryGetComponent(out T interactable))
                    {
                        interactables.Add(interactable);
                    }
                })
                .AddTo(ref d);

            collider.OnTriggerExitAsObservable()
                .Subscribe(other =>
                {
                    if (other.TryGetComponent(out T interactable))
                    {
                        interactables.Remove(interactable);
                    }
                })
                .AddTo(ref d);

            return interactables;
        }

        private Observable<Vector3> PositionChanged(Transform target, float delta, FrameProvider frameProvider = null)
        {
            frameProvider ??= UnityFrameProvider.FixedUpdate;
            return Observable.EveryValueChanged(target, t => t.position, frameProvider)
                .Scan((prev, cur) => cur.SqrDistance(prev) > delta ? cur : prev);
        }

        private Observable<IInteractable> ObserveNearestInteractable(Observable<Vector3> positionSource, IObservableCollection<IInteractable> interactables)
        {
            return Observable.CombineLatest(
                interactables.ObserveChanged(),
                positionSource,
                (_, position) => Nearest(position, interactables)
            );
        }

        private IInteractable Nearest(Vector3 origin, IObservableCollection<IInteractable> interactables)
        {
            return interactables
                .Where(x => x.IsInteractable)
                .MinBy(x => origin.SqrDistance(x.gameObject.transform.position));
        }

        private IInteractable SwapSelected(IInteractable previous, IInteractable current)
        {
            previous?.Deselect(this);
            current?.Select(this);
            return current;
        }
    }
}