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 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;


        [SerializeField]
        private Color outlineColor = Color.orangeRed;

        [SerializeField]
        private int outlineWidth = 3;

        [SerializeField]
        private Outline.Mode outlineMode = Outline.Mode.OutlineAll;

        private readonly ObservableHashSet<IInteractable> interactables = new ObservableHashSet<IInteractable>();
        public ReactiveProperty<IInteractable> CurrentHighlightedInteractable { get; private set; }

        private Vector3 lastPosition;

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

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

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

        private void Awake()
        {
            CurrentHighlightedInteractable = new ReactiveProperty<IInteractable>(null);
            lastPosition = transform.position;
            sqrtMovementDelta = Mathf.Sqrt(minimumChangeDelta);

            var d = Disposable.CreateBuilder();

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

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

            Observable.Merge(InteractablesChanged(), PlayerMoved())
                .ThrottleFirstFrame(updateFrequency)
                .DefaultIfEmpty()
                .Select(_ =>
                {
                    var closest = interactables
                        .Where(x => x.IsInteractable && x.gameObject != null)
                        .MinBy(x => Vector3.SqrMagnitude(gameObject.transform.position - x.gameObject.transform.position));

                    return closest;
                })
                .DistinctUntilChanged()
                .Subscribe(highlighted =>
                {
                    CurrentHighlightedInteractable.Value = highlighted;
                })
                .AddTo(ref d);

            interactables.ObserveChanged()
                .ThrottleFirstFrame(updateFrequency, UnityFrameProvider.FixedUpdate)
                .Subscribe(_ =>
                {
                    var highlighted = interactables
                        .Where(x => x.IsInteractable)
                        .MinBy(x => collider.transform.position.SqrDistance(x.gameObject.transform.position));

                    if (CurrentHighlightedInteractable.Value != highlighted)
                    {
                        CurrentHighlightedInteractable.Value = highlighted;
                    }
                })
                .AddTo(ref d);

            var highlightedInteractable = CurrentHighlightedInteractable
                .Scan((prev, cur) =>
                {
                    UnhighlightInteractable(prev);
                    HighlightInteractable(cur);
                    return cur;
                });

            interactSource
                .AsObservable()
                .WithLatestFrom(highlightedInteractable, (_, highlighted) => highlighted)
                .WhereNotNull()
                .Subscribe(interactable =>
                {
                    var result = interactable.Interact(this);
                    if (result.IsOk)
                    {
                        interactables.Remove(interactable);
                    }
                    else
                    {
                        Debug.Log($"interaction failed: {result}");
                    }
                })
                .AddTo(ref d);

            d.RegisterTo(destroyCancellationToken);
        }

        private Observable<Unit> InteractablesChanged()
        {
            return interactables.ObserveChanged().Select(_ => Unit.Default);
        }

        private Observable<Unit> PlayerMoved()
        {
            return Observable.EveryUpdate(UnityFrameProvider.FixedUpdate)
                .Select(_ => transform.position)
                .Where(pos => pos.SqrDistance(lastPosition) > sqrtMovementDelta)
                .Do(pos => lastPosition = pos)
                .Select(_ => Unit.Default);
        }

        private void HighlightInteractable(IInteractable interactable)
        {
            if (interactable == null) { return; }

            if (!interactable.TryGetComponent(out Outline outline))
            {
                outline = interactable.gameObject.AddComponent<Outline>();

                outline.OutlineColor = outlineColor;
                outline.OutlineWidth = outlineWidth;
                outline.OutlineMode = outlineMode;
            }

            outline.enabled = true;
        }

        private void UnhighlightInteractable(IInteractable interactable)
        {
            if (interactable == null) { return; }

            if (interactable.TryGetComponent(out Outline outline))
            {
                outline.enabled = false;
            }
        }
    }
}