using System.Timers;
using Connected.Annotations;
using Connected.Utilities.BindingConverters;
using Microsoft.AspNetCore.Components;

namespace Connected.Components;

public abstract class DebouncedInput<T> : InputBase<T>
{
   private System.Timers.Timer _timer;
   private double _debounceInterval;

   /// <summary>
   /// Interval to be awaited in milliseconds before changing the Text value
   /// </summary>
   [Parameter]
   [Category(CategoryTypes.FormComponent.Behavior)]
   public double DebounceInterval
   {
      get => _debounceInterval;
      set
      {
         if (NumericConverter<double>.AreEqual(_debounceInterval, value))
            return;
         _debounceInterval = value;
         if (_debounceInterval == 0)
         {
            // not debounced, dispose timer if any
            ClearTimer(suppressTick: false);
            return;
         }
         SetTimer();
      }
   }

   /// <summary>
   /// callback to be called when the debounce interval has elapsed
   /// receives the Text as a parameter
   /// </summary>
   [Parameter] public EventCallback<string> OnDebounceIntervalElapsed { get; set; }

   protected Task OnChange()
   {
      if (DebounceInterval > 0 && _timer != null)
      {
         _timer.Stop();
         return base.UpdateValuePropertyAsync(false);
      }

      return Task.CompletedTask;
   }

   protected override Task UpdateValuePropertyAsync(bool updateText)
   {
      // This method is called when Value property needs to be refreshed from the current Text property, so typically because Text property has changed.
      // We want to debounce only text-input, not a value being set, so the debouncing is only done when updateText==false (because that indicates the
      // change came from a Text setter)
      if (updateText)
      {
         // we have a change coming not from the Text setter, no debouncing is needed
         return base.UpdateValuePropertyAsync(updateText);
      }
      // if debounce interval is 0 we update immediately
      if (DebounceInterval <= 0 || _timer == null)
         return base.UpdateValuePropertyAsync(updateText);
      // If a debounce interval is defined, we want to delay the update of Value property.
      _timer.Stop();
      // restart the timer while user is typing
      _timer.Start();
      return Task.CompletedTask;
   }

   protected override void OnParametersSet()
   {
      base.OnParametersSet();
      // if input is to be debounced, makes sense to bind the change of the text to oninput
      // so we set Immediate to true
      if (DebounceInterval > 0)
         Immediate = true;
   }

   private void SetTimer()
   {
      if (_timer == null)
      {
         _timer = new System.Timers.Timer();
         _timer.Elapsed += OnTimerTick;
         _timer.AutoReset = false;
      }
      _timer.Interval = DebounceInterval;
   }

   private void OnTimerTick(object sender, ElapsedEventArgs e)
   {
      InvokeAsync(OnTimerTickGuiThread).AndForget();
   }

   private async Task OnTimerTickGuiThread()
   {
      await base.UpdateValuePropertyAsync(false);
      await OnDebounceIntervalElapsed.InvokeAsync(Text);
   }

   private void ClearTimer(bool suppressTick = false)
   {
      if (_timer == null)
         return;
      var wasEnabled = _timer.Enabled;
      _timer.Stop();
      _timer.Elapsed -= OnTimerTick;
      _timer.Dispose();
      _timer = null;
      if (wasEnabled && !suppressTick)
         OnTimerTickGuiThread().AndForget();
   }

   protected override void Dispose(bool disposing)
   {
      base.Dispose(disposing);
      ClearTimer(suppressTick: true);
   }
}
