using System;
using System.Threading;
using KitsuneCafe.Extension;
using R3;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;
using static KitsuneCafe.Extension.R3Extensions;
using TimeUnit = KitsuneCafe.System.TimeUnit;

namespace KitsuneCafe
{
    public class Timer : MonoBehaviour
    {
        [SerializeField]
        private float duration = 1;
        public float Duration
        {
            get => duration;
            set => duration = value;
        }

        [SerializeField]
        private TimeUnit unit = TimeUnit.Seconds;
        public TimeUnit Unit
        {
            get => unit;
            set => unit = value;
        }

        [SerializeField]
        private PlayerLoopTiming timing = PlayerLoopTiming.Update;

        [SerializeField]
        private TimeKind timeKind = TimeKind.Time;

        [SerializeField]
        private bool autostart = false;

        [SerializeField]
        private bool oneShot = false;

        [SerializeField]
        private UnityEvent onComplete = default;

        private readonly ReactiveProperty<bool> started = new(false);
        private readonly ReactiveProperty<bool> paused = new(false);

        public bool Started => started.Value;
        public bool Paused => paused.Value;

        public IDisposable source;

        public event EventHandler Completed;

        private void Awake()
        {
            SubscribeToTimer();
        }

        private void SubscribeToTimer()
        {
            source?.Dispose();

            var counting = Observable.Merge(
                started.Compose(WhereTrue).Select(_ => true),
                paused.Compose(WhereTrue).Select(_ => false)
            );

            var stopped = started.Where(KMath.Not).Skip(1);

            var timeProvider = GetTimeProvider(timing, timeKind);
            var totalDuration = GetDuration();

            source = CreateTimer(GetDuration(), counting, timeProvider)
                .TakeUntil(stopped)
                // add onNext handler if remaining time is required
                .Do(onCompleted: OnCompleted)
                .Subscribe()
                .AddTo(this);
        }

        private void Start()
        {
            if (autostart)
            {
                StartTimer();
            }
        }

        private void OnEnable()
        {
            if (Started && Paused)
            {
                Resume();
            }
        }

        private void OnDisable()
        {
            if (Started)
            {
                Pause();
            }
        }

        public void StartTimer()
        {
            if (source == null)
            {
                SubscribeToTimer();
            }

            paused.Value = false;
            started.Value = true;
        }

        public void StopTimer()
        {
            source?.Dispose();
            source = null;
            paused.Value = false;
            started.Value = false;
        }

        public void Pause()
        {
            paused.Value = true;
        }

        public void Resume()
        {
            paused.Value = false;
        }

        private void OnCompleted(Result result)
        {
            StopTimer();

            if (result.IsSuccess)
            {
                Finish();

                if (!oneShot)
                {
                    StartTimer();
                }
            }
        }

        private void Finish()
        {
            Completed?.Invoke(this, EventArgs.Empty);
            onComplete.Invoke();
        }

        private Observable<TimeSpan> CreateTimer(TimeSpan duration, Observable<bool> isCounting, TimeProvider timeProvider)
        {
            return Observable.Empty<Unit>()
                .SwitchIf(isCounting, Observable.EveryUpdate(timeProvider.GetFrameProvider()))
                .Select(_ => DeltaTime(timeProvider))
                .Scan(duration, (acc, dt) => acc.Subtract(dt))
                .TakeWhile(ts => ts > TimeSpan.Zero)
                .DefaultIfEmpty(duration);
        }

        private Observable<Unit> WhereTrue(Observable<bool> source)
        {
            return source.WhereTrue().AsUnitObservable();
        }

        private TimeSpan GetDuration()
        {
            return System.Duration.From(duration, unit);
        }

        private TimeSpan DeltaTime(TimeProvider timeProvider)
        {
            return TimeSpan.FromSeconds(timeProvider.GetDeltaTime());
        }
    }
}
