// Copyright (c) MudBlazor 2021
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.RegularExpressions;
using Connected.Annotations;
using Connected.Extensions;
using Connected.Services;
using Connected.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace Connected.Components;

public partial class Mask : InputBase<string>, IDisposable
{
   public Mask()
   {
      TextUpdateSuppression = false;
   }

   protected string Classname =>
      new CssBuilder("input")
         .AddClass($"input-{Variant.ToDescription()}")
         .AddClass($"input-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None)
         .AddClass($"input-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None)
         .AddClass("input-underline", when: () => DisableUnderLine == false && Variant != Variant.Outlined)
         .AddClass("shrink",
            when: () => !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start ||
                     !string.IsNullOrWhiteSpace(Placeholder))
         .AddClass("disabled", Disabled)
         .AddClass("input-error", HasErrors)
         .AddClass("ltr", GetInputType() == InputType.Email || GetInputType() == InputType.Telephone)
         .AddClass(AdditionalClassList)
         .Build();

   protected string InputClassname =>
      new CssBuilder("input-slot")
         .AddClass("input-root")
         .AddClass($"input-root-{Variant.ToDescription()}")
         .AddClass($"input-root-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None)
         .AddClass($"input-root-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None)
         .AddClass(AdditionalClassList)
         .Build();

   protected string AdornmentClassname =>
      new CssBuilder("input-adornment")
         .AddClass($"input-adornment-{Adornment.ToDescription()}", Adornment != Adornment.None)
         .AddClass($"text", !string.IsNullOrEmpty(AdornmentText))
         .AddClass($"input-root-filled-shrink", Variant == Variant.Filled)
         .AddClass(AdditionalClassList)
         .Build();

   protected string ClearButtonClassname =>
      new CssBuilder()
         // .AddClass("me-n1", Adornment == Adornment.End && HideSpinButtons == false)
         .AddClass("icon-button-edge-end", Adornment == Adornment.End)
         // .AddClass("me-6", Adornment != Adornment.End && HideSpinButtons == false)
         .AddClass("icon-button-edge-margin-end", Adornment != Adornment.End)
         .Build();


   private ElementReference _elementReference;
   private ElementReference _elementReference1;
   private IJsEvent _jsEvent;
   private IKeyInterceptor _keyInterceptor;

   [Inject] private IKeyInterceptorFactory _keyInterceptorFactory { get; set; }

   [Inject] private IJsEventFactory _jsEventFactory { get; set; }
   [Inject] private IJsApiService _jsApiService { get; set; }

   private string _elementId = "mask_" + Guid.NewGuid().ToString().Substring(0, 8);

   private IMask _mask = new PatternMask("** **-** **");

   /// <summary>
   /// ChildContent will only be displayed if InputType.Hidden and if its not null. Required for Select
   /// </summary>
   [Parameter]
   [Category(CategoryTypes.General.Appearance)]
   public RenderFragment ChildContent { get; set; }

   /// <summary>
   /// Provide a masking object. Built-in masks are PatternMask, MultiMask, RegexMask and BlockMask
   /// </summary>
   [Parameter]
   [Category(CategoryTypes.General.Data)]
   public IMask MaskKind
   {
      get => _mask;
      set => SetMask(value);
   }

   /// <summary>
   /// Type of the input element. It should be a valid HTML5 input type.
   /// </summary>
   [Parameter]
   [Category(CategoryTypes.FormComponent.ListAppearance)]
   public InputType InputType { get; set; } = InputType.Text;

   /// <summary>
   /// Show clear button.
   /// </summary>
   [Parameter]
   [Category(CategoryTypes.FormComponent.ListBehavior)]
   public bool Clearable { get; set; } = false;

   private bool _showClearable;

   private void UpdateClearable(object value)
   {
      var showClearable = Clearable && !string.IsNullOrWhiteSpace(Text);
      if (_showClearable != showClearable)
         _showClearable = showClearable;
   }

   /// <summary>
   /// Button click event for clear button. Called after text and value has been cleared.
   /// </summary>
   [Parameter]
   [Category(CategoryTypes.FormComponent.ListAppearance)]
   public EventCallback<MouseEventArgs> OnClearButtonClick { get; set; }

   /// <summary>
   /// Custom clear icon.
   /// </summary>
   [Parameter]
   [Category(CategoryTypes.General.Appearance)]
   public string ClearIcon { get; set; } = Icons.Material.Filled.Clear;

   protected override async Task OnInitializedAsync()
   {
      if (Text != MaskKind.Text)
         await SetTextAsync(MaskKind.Text, updateValue: false);
      await base.OnInitializedAsync();
   }

   protected override async Task OnAfterRenderAsync(bool firstRender)
   {
      if (firstRender)
      {
         _jsEvent = _jsEventFactory.Create();

         await _jsEvent.Connect(_elementId,
            new JsEventOptions
            {
               //EnableLogging = true,
               TargetClass = "input-slot",
               TagName = "INPUT"
            });
         _jsEvent.CaretPositionChanged += OnCaretPositionChanged;
         _jsEvent.Paste += OnPaste;
         _jsEvent.Select += OnSelect;

         _keyInterceptor = _keyInterceptorFactory.Create();

         await _keyInterceptor.Connect(_elementId, new KeyInterceptorOptions()
         {
            //EnableLogging = true,
            TargetClass = "input-slot",
            Keys =
             {
                new KeyOptions
                {
                   Key = " ", PreventDown = "key+none"
                }, //prevent scrolling page, toggle open/close
                     new KeyOptions { Key = "ArrowUp", PreventDown = "key+none" }, // prevent scrolling page
                     new KeyOptions { Key = "ArrowDown", PreventDown = "key+none" }, // prevent scrolling page
                     new KeyOptions { Key = "PageUp", PreventDown = "key+none" }, // prevent scrolling page
                     new KeyOptions { Key = "PageDown", PreventDown = "key+none" }, // prevent scrolling page
                     new KeyOptions { Key = @"/^.$/", PreventDown = "key+none|key+shift" },
                new KeyOptions { Key = "/./", SubscribeDown = true },
                new KeyOptions { Key = "Backspace", PreventDown = "key+none" },
                new KeyOptions { Key = "Delete", PreventDown = "key+none" },
             },
         });
         _keyInterceptor.KeyDown += HandleKeyDownInternally;
      }
      if (_isFocused && MaskKind.Selection == null)
         SetCaretPosition(MaskKind.CaretPos, _selection, render: false);
      await base.OnAfterRenderAsync(firstRender);
   }

   private async void HandleKeyDownInternally(KeyboardEventArgs args)
   {
      await HandleKeyDown(args);
   }

   protected internal async Task HandleKeyDown(KeyboardEventArgs e)
   {
      try
      {
         if ((e.CtrlKey && e.Key != "Backspace") || e.AltKey || ReadOnly)
            return;
         switch (e.Key)
         {
            case "Backspace":
               if (e.CtrlKey)
               {
                  MaskKind.Clear();
                  await Update();
                  return;
               }
               MaskKind.Backspace();
               await Update();
               return;
            case "Delete":
               MaskKind.Delete();
               await Update();
               return;
         }

         if (Regex.IsMatch(e.Key, @"^.$"))
         {
            MaskKind.Insert(e.Key);
            await Update();
         }
      }
      finally
      {
         // call user callback
         await OnKeyDown.InvokeAsync(e);
      }
   }

   private bool _updating;

   private async Task Update()
   {
      var caret = MaskKind.CaretPos;
      var selection = MaskKind.Selection;
      var text = MaskKind.Text;
      var cleanText = MaskKind.GetCleanText();
      _updating = true;
      try
      {
         await base.SetTextAsync(text, updateValue: false);
         if (Clearable)
            UpdateClearable(Text);
         var v = Converter.ConvertBack(cleanText);
         Value = v;
         await ValueChanged.InvokeAsync(v);
         SetCaretPosition(caret, selection);
      }
      finally
      {
         _updating = false;
      }
   }

   internal async void HandleClearButton(MouseEventArgs e)
   {
      MaskKind.Clear();
      await Update();
      await _elementReference.FocusAsync();
      await OnClearButtonClick.InvokeAsync(e);
   }

   protected override async Task UpdateTextPropertyAsync(bool updateValue)
   {
      // allow this only via changes from the outside
      if (_updating)
         return;
      var text = Converter.Convert(Value);
      var cleanText = MaskKind.GetCleanText();
      if (cleanText == text || string.IsNullOrEmpty(cleanText) && string.IsNullOrEmpty(text))
         return;
      var maskText = MaskKind.Text;
      MaskKind.SetText(text);
      if (maskText == MaskKind.Text)
         return; // no change, stop update loop
      await Update();
   }

   protected override async Task UpdateValuePropertyAsync(bool updateText)
   {
      // allow this only via changes from the outside
      if (_updating)
         return;
      var text = Text;
      if (MaskKind.Text == text)
         return;
      var maskText = MaskKind.Text;
      MaskKind.SetText(text);
      if (maskText == MaskKind.Text)
         return; // no change, stop update loop
      await Update();
   }

   internal override InputType GetInputType() => InputType;

   private string GetCounterText() => Counter == null
      ? string.Empty
      : (Counter == 0
         ? (string.IsNullOrEmpty(Text) ? "0" : $"{Text.Length}")
         : ((string.IsNullOrEmpty(Text) ? "0" : $"{Text.Length}") + $" / {Counter}"));

   /// <summary>
   /// Clear the text field. 
   /// </summary>
   /// <returns></returns>
   public Task Clear()
   {
      MaskKind.Clear();
      return Update();
   }

   public override ValueTask FocusAsync()
   {
      return _elementReference.FocusAsync();
   }

   public override ValueTask SelectAsync()
   {
      return _elementReference.SelectAsync();
   }

   public override ValueTask SelectRangeAsync(int pos1, int pos2)
   {
      return _elementReference.SelectRangeAsync(pos1, pos2);
   }

   internal void OnCopy()
   {
      var text = Text;
      if (MaskKind.Selection != null)
      {
         (_, text, _) = BaseMask.SplitSelection(text, MaskKind.Selection.Value);
      }
      _jsApiService.CopyToClipboardAsync(text);
   }

   internal async void OnPaste(string text)
   {
      if (text == null || ReadOnly)
         return;
      MaskKind.Insert(text);
      await Update();
   }

   public void OnSelect(int start, int end)
   {
      MaskKind.Selection = _selection = (start, end);
   }

   internal void OnFocused(FocusEventArgs obj)
   {
      _isFocused = true;
   }

   protected internal override void OnBlurred(FocusEventArgs obj)
   {
      base.OnBlurred(obj);
      _isFocused = false;
   }

   private int _caret;
   private (int, int)? _selection;

   private void SetCaretPosition(int caret, (int, int)? selection = null, bool render = true)
   {
      if (!_isFocused)
         return;
      _caret = caret;
      if (caret == 0)
         _caret = 0;
      _selection = selection;
      if (selection == null)
      {
         _elementReference.SelectRangeAsync(caret, caret).AndForget();
      }
      else
      {
         var sel = selection.Value;
         _elementReference.SelectRangeAsync(sel.Item1, sel.Item2).AndForget();
      }
   }

   // from JS event     
   internal void OnCaretPositionChanged(int pos)
   {
      if (MaskKind.Selection != null)
      {
         // do not clear selection if pos change is at selection border
         var sel = MaskKind.Selection.Value;
         if (pos == sel.Item1 || pos == sel.Item2)
            return;
      }

      if (pos == MaskKind.CaretPos)
         return;
      MaskKind.Selection = null;
      MaskKind.CaretPos = pos;
   }

   private void SetMask(IMask other)
   {
      if (_mask == null || other == null || _mask?.GetType() != other?.GetType())
      {
         _mask = other;
         if (_mask == null)
            _mask = new PatternMask("null ********"); // warn the user that the mask parameter is missing
         return;
      }

      // set new mask properties without loosing state
      _mask.UpdateFrom(other);
   }

   private async void OnCut(ClipboardEventArgs obj)
   {
      if (ReadOnly)
         return;

      if (_selection != null)
         MaskKind.Delete();
      await Update();
   }

   protected override void Dispose(bool disposing)
   {
      base.Dispose(disposing);

      if (disposing == true)
      {
         _jsEvent?.Dispose();

         if (_keyInterceptor != null)
         {
            _keyInterceptor.KeyDown -= HandleKeyDownInternally;
            _keyInterceptor.Dispose();
         }

         _keyInterceptor?.Dispose();
      }
   }
}
