

namespace Terminal.Gui.Views;

/// <summary>Shows a checkbox that can be cycled between two or three states.</summary>
/// <remarks>
///     <para>
///         <see cref="RadioStyle"/> is used to display radio button style glyphs (●) instead of checkbox style glyphs (☑).
///     </para>
/// </remarks>
public class CheckBox : View
{
    /// <summary>
    ///     Gets or sets the default Highlight Style.
    /// </summary>
    [ConfigurationProperty (Scope = typeof (ThemeScope))]
    public static MouseState DefaultHighlightStates { get; set; } = MouseState.PressedOutside | MouseState.Pressed | MouseState.In;

    /// <summary>
    ///     Initializes a new instance of <see cref="CheckBox"/>.
    /// </summary>
    public CheckBox ()
    {
        Width = Dim.Auto (DimAutoStyle.Text);
        Height = Dim.Auto (DimAutoStyle.Text, 1);

        CanFocus = true;

        // Activate (Space key and single-click) - Raise Activate event and Advance
        // - DO NOT raise Accept
        // - DO NOT SetFocus
        AddCommand (Command.Select, ActivateAndAdvance);

        // Accept (Enter key and double-click) - Raise Accept event
        // - DO NOT advance state
        // The default Accept handler does that.
        MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept);

        TitleChanged += Checkbox_TitleChanged;

        HighlightStates = DefaultHighlightStates;
    }

    /// <inheritdoc />
    protected override bool OnHandlingHotKey (CommandEventArgs args)
    {
        // Invoke Activate on ourselves
        if (InvokeCommand (Command.Select, args.Context) is true)
        {
            // Default behavior for View is to set Focus on hotkey. We need to return
            // true here to indicate Activate was handled. That will prevent the default
            // behavior from setting focus, so we do it here.
            SetFocus ();
            return true;
        }
        return base.OnHandlingHotKey (args);
    }

    private bool? ActivateAndAdvance (ICommandContext? commandContext)
    {
        if (RaiseSelecting (commandContext) is true)
        {
            return true;
        }

        bool? cancelled = AdvanceCheckState ();

        if (cancelled is true)
        {
            return true;
        }

        return commandContext?.Command == Command.HotKey ? cancelled : cancelled is false;
    }

    private void Checkbox_TitleChanged (object? sender, EventArgs<string> e)
    {
        base.Text = e.Value;
        TextFormatter.HotKeySpecifier = HotKeySpecifier;
    }

    /// <inheritdoc/>
    public override string Text
    {
        get => Title;
        set => base.Text = Title = value;
    }

    /// <inheritdoc/>
    public override Rune HotKeySpecifier
    {
        get => base.HotKeySpecifier;
        set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value;
    }

    private bool _allowNone;

    /// <summary>
    ///     If <see langword="true"/> allows <see cref="CheckedState"/> to be <see cref="CheckState.None"/>. The default is
    ///     <see langword="false"/>.
    /// </summary>
    public bool AllowCheckStateNone
    {
        get => _allowNone;
        set
        {
            if (_allowNone == value)
            {
                return;
            }

            _allowNone = value;

            if (CheckedState == CheckState.None)
            {
                CheckedState = CheckState.UnChecked;
            }
        }
    }

    private CheckState _checkedState = CheckState.UnChecked;

    /// <summary>
    ///     The state of the <see cref="CheckBox"/>.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         If <see cref="AllowCheckStateNone"/> is <see langword="true"/> and <see cref="CheckState.None"/>, the
    ///         <see cref="CheckBox"/>
    ///         will display the <c>Glyphs.CheckStateNone</c> character (☒).
    ///     </para>
    ///     <para>
    ///         If <see cref="CheckState.UnChecked"/>, the <see cref="CheckBox"/>
    ///         will display the <c>Glyphs.CheckStateUnChecked</c> character (☐).
    ///     </para>
    ///     <para>
    ///         If <see cref="CheckState.Checked"/>, the <see cref="CheckBox"/>
    ///         will display the <c>Glyphs.CheckStateChecked</c> character (☑).
    ///     </para>
    /// </remarks>
    public CheckState CheckedState
    {
        get => _checkedState;
        set => ChangeCheckedState (value);
    }

    /// <summary>
    ///     INTERNAL Sets CheckedState.
    /// </summary>
    /// <param name="value"></param>
    /// <returns>
    ///     <see langword="true"/> if state change was canceled, <see langword="false"/> if the state changed, and
    ///     <see langword="null"/> if the state was not changed for some other reason.
    /// </returns>
    private bool? ChangeCheckedState (CheckState value)
    {
        if (_checkedState == value || (value is CheckState.None && !AllowCheckStateNone))
        {
            return null;
        }

        ResultEventArgs<CheckState> e = new (value);

        if (OnCheckedStateChanging (e))
        {
            return true;
        }

        CheckedStateChanging?.Invoke (this, e);

        if (e.Handled)
        {
            return e.Handled;
        }

        _checkedState = value;
        UpdateTextFormatterText ();
        SetNeedsLayout ();

        EventArgs<CheckState> args = new (in _checkedState);
        OnCheckedStateChanged (args);

        CheckedStateChanged?.Invoke (this, args);

        return false;
    }

    /// <summary>Called when the <see cref="CheckBox"/> state is changing.</summary>
    /// <remarks>
    ///     <para>
    ///         The state change can be cancelled by setting the args.Cancel to <see langword="true"/>.
    ///     </para>
    /// </remarks>
    protected virtual bool OnCheckedStateChanging (ResultEventArgs<CheckState> args) { return false; }

    /// <summary>Raised when the <see cref="CheckBox"/> state is changing.</summary>
    /// <remarks>
    ///     <para>
    ///         This event can be cancelled. If cancelled, the <see cref="CheckBox"/> will not change its state.
    ///     </para>
    /// </remarks>
    public event EventHandler<ResultEventArgs<CheckState>>? CheckedStateChanging;

    /// <summary>Called when the <see cref="CheckBox"/> state has changed.</summary>
    protected virtual void OnCheckedStateChanged (EventArgs<CheckState> args) { }

    /// <summary>Raised when the <see cref="CheckBox"/> state has changed.</summary>
    public event EventHandler<EventArgs<CheckState>>? CheckedStateChanged;

    /// <summary>
    ///     Advances <see cref="CheckedState"/> to the next value. Invokes the cancelable <see cref="CheckedStateChanging"/>
    ///     event.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         Cycles through the states <see cref="CheckState.None"/>, <see cref="CheckState.Checked"/>, and
    ///         <see cref="CheckState.UnChecked"/>.
    ///     </para>
    ///     <para>
    ///         If the <see cref="CheckedStateChanging"/> event is not canceled, the <see cref="CheckedState"/> will be updated
    ///         and the <see cref="Command.Accept"/> event will be raised.
    ///     </para>
    /// </remarks>
    /// <returns>
    ///     <see langword="true"/> if state change was canceled, <see langword="false"/> if the state changed, and
    ///     <see langword="null"/> if the state was not changed for some other reason.
    /// </returns>
    public bool? AdvanceCheckState ()
    {
        CheckState oldValue = CheckedState;
        ResultEventArgs<CheckState> e = new (oldValue);

        switch (CheckedState)
        {
            case CheckState.None:
                e.Result = CheckState.Checked;

                break;
            case CheckState.Checked:
                e.Result = CheckState.UnChecked;

                break;
            case CheckState.UnChecked:
                if (AllowCheckStateNone)
                {
                    e.Result = CheckState.None;
                }
                else
                {
                    e.Result = CheckState.Checked;
                }

                break;
        }

        bool? cancelled = ChangeCheckedState (e.Result);

        return cancelled;
    }

    /// <inheritdoc />
    protected override bool OnClearingViewport ()
    {
        SetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Normal);
        return base.OnClearingViewport ();
    }

    /// <inheritdoc/>
    protected override void UpdateTextFormatterText ()
    {
        base.UpdateTextFormatterText ();

        Rune glyph = RadioStyle ? GetRadioGlyph () : GetCheckGlyph ();
        switch (TextAlignment)
        {
            case Alignment.Start:
            case Alignment.Center:
            case Alignment.Fill:
                TextFormatter.Text = $"{glyph} {Text}";

                break;
            case Alignment.End:
                TextFormatter.Text = $"{Text} {glyph}";

                break;
        }
    }

    private Rune GetCheckGlyph ()
    {
        return CheckedState switch
        {
            CheckState.Checked => Glyphs.CheckStateChecked,
            CheckState.UnChecked => Glyphs.CheckStateUnChecked,
            CheckState.None => Glyphs.CheckStateNone,
            _ => throw new ArgumentOutOfRangeException ()
        };
    }

    /// <summary>
    ///     If <see langword="true"/>, the <see cref="CheckBox"/> will display radio button style glyphs (●) instead of
    ///     checkbox style glyphs (☑).
    /// </summary>
    public bool RadioStyle { get; set; }

    private Rune GetRadioGlyph ()
    {
        return CheckedState switch
        {
            CheckState.Checked => Glyphs.Selected,
            CheckState.UnChecked => Glyphs.UnSelected,
            CheckState.None => Glyphs.Dot,
            _ => throw new ArgumentOutOfRangeException ()
        };
    }
}
