// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Drawing;
using System.Drawing.Interop;
using System.Runtime.CompilerServices;
using Windows.Win32.UI.Controls.Dialogs;

namespace System.Windows.Forms;

/// <summary>
///  Represents a common dialog box that displays a list of fonts that are currently installed on the system.
/// </summary>
[DefaultEvent(nameof(Apply))]
[DefaultProperty(nameof(Font))]
[SRDescription(nameof(SR.DescriptionFontDialog))]
public class FontDialog : CommonDialog
{
    protected static readonly object EventApply = new();

    private const int DefaultMinSize = 0;
    private const int DefaultMaxSize = 0;

    private CHOOSEFONT_FLAGS _options;
    private Font? _font;
    private Color _color;
    private int _minSize = DefaultMinSize;
    private int _maxSize = DefaultMaxSize;
    private bool _usingDefaultIndirectColor;

    /// <summary>
    ///  Initializes a new instance of the <see cref="FontDialog"/> class.
    /// </summary>
    public FontDialog() => Reset();

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box allows graphics device interface
    ///  (GDI) font simulations.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.FnDallowSimulationsDescr))]
    public bool AllowSimulations
    {
        get => !GetOption(CHOOSEFONT_FLAGS.CF_NOSIMULATIONS);
        set => SetOption(CHOOSEFONT_FLAGS.CF_NOSIMULATIONS, !value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box allows vector font selections.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.FnDallowVectorFontsDescr))]
    public bool AllowVectorFonts
    {
        get => !GetOption(CHOOSEFONT_FLAGS.CF_NOVECTORFONTS);
        set => SetOption(CHOOSEFONT_FLAGS.CF_NOVECTORFONTS, !value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether
    ///  the dialog box displays both vertical and horizontal fonts or only
    ///  horizontal fonts.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.FnDallowVerticalFontsDescr))]
    public bool AllowVerticalFonts
    {
        get => !GetOption(CHOOSEFONT_FLAGS.CF_NOVERTFONTS);
        set => SetOption(CHOOSEFONT_FLAGS.CF_NOVERTFONTS, !value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the user can change the character set specified in the Script combo
    ///  box to display a character set other than the one currently displayed.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.FnDallowScriptChangeDescr))]
    public bool AllowScriptChange
    {
        get => !GetOption(CHOOSEFONT_FLAGS.CF_SELECTSCRIPT);
        set => SetOption(CHOOSEFONT_FLAGS.CF_SELECTSCRIPT, !value);
    }

    /// <summary>
    ///  Gets or sets a value indicating the selected font color.
    /// </summary>
    [SRCategory(nameof(SR.CatData))]
    [SRDescription(nameof(SR.FnDcolorDescr))]
    [DefaultValue(typeof(Color), "Black")]
    public Color Color
    {
        get
        {
            // Convert to RGB and back to resolve indirect colors like SystemColors.ControlText
            // to real color values like Color.Lime
            return !_usingDefaultIndirectColor ? _color : ColorTranslator.FromWin32(ColorTranslator.ToWin32(_color));
        }
        set
        {
            if (!value.IsEmpty)
            {
                _color = value;
                _usingDefaultIndirectColor = false;
            }
            else
            {
                _color = SystemColors.ControlText;
                _usingDefaultIndirectColor = true;
            }
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box allows only the selection of fixed-pitch fonts.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FnDfixedPitchOnlyDescr))]
    public bool FixedPitchOnly
    {
        get => GetOption(CHOOSEFONT_FLAGS.CF_FIXEDPITCHONLY);
        set => SetOption(CHOOSEFONT_FLAGS.CF_FIXEDPITCHONLY, value);
    }

    /// <summary>
    ///  Gets or sets a value indicating the selected font.
    /// </summary>
    [SRCategory(nameof(SR.CatData))]
    [SRDescription(nameof(SR.FnDfontDescr))]
    public Font Font
    {
        get
        {
            Font? result = _font ?? Control.DefaultFont;

            float actualSize = result.SizeInPoints;
            if (_minSize != DefaultMinSize && actualSize < MinSize)
            {
                result = new Font(result.FontFamily, MinSize, result.Style, GraphicsUnit.Point);
            }

            if (_maxSize != DefaultMaxSize && actualSize > MaxSize)
            {
                result = new Font(result.FontFamily, MaxSize, result.Style, GraphicsUnit.Point);
            }

            return result;
        }
        set => _font = value;
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box specifies an error condition if the
    ///  user attempts to select a font or style that does not exist.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FnDfontMustExistDescr))]
    public bool FontMustExist
    {
        get => GetOption(CHOOSEFONT_FLAGS.CF_FORCEFONTEXIST);
        set => SetOption(CHOOSEFONT_FLAGS.CF_FORCEFONTEXIST, value);
    }

    /// <summary>
    ///  Gets or sets the maximum point size a user can select.
    /// </summary>
    [SRCategory(nameof(SR.CatData))]
    [DefaultValue(DefaultMaxSize)]
    [SRDescription(nameof(SR.FnDmaxSizeDescr))]
    public int MaxSize
    {
        get => _maxSize;
        set
        {
            if (value < 0)
            {
                value = 0;
            }

            _maxSize = value;

            if (_maxSize > 0 && _maxSize < _minSize)
            {
                _minSize = _maxSize;
            }
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating the minimum point size a user can select.
    /// </summary>
    [SRCategory(nameof(SR.CatData))]
    [DefaultValue(DefaultMinSize)]
    [SRDescription(nameof(SR.FnDminSizeDescr))]
    public int MinSize
    {
        get => _minSize;
        set
        {
            if (value < 0)
            {
                value = 0;
            }

            _minSize = value;

            if (_maxSize > 0 && _maxSize < _minSize)
            {
                _maxSize = _minSize;
            }
        }
    }

    /// <summary>
    ///  Gets the value passed to CHOOSEFONT.Flags.
    /// </summary>
    protected int Options => (int)_options;

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box allows selection of fonts for all non-OEM and Symbol
    ///  character sets, as well as the ANSI character set.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FnDscriptsOnlyDescr))]
    public bool ScriptsOnly
    {
        get => GetOption(CHOOSEFONT_FLAGS.CF_SCRIPTSONLY);
        set => SetOption(CHOOSEFONT_FLAGS.CF_SCRIPTSONLY, value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box contains an Apply button.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FnDshowApplyDescr))]
    public bool ShowApply
    {
        get => GetOption(CHOOSEFONT_FLAGS.CF_APPLY);
        set => SetOption(CHOOSEFONT_FLAGS.CF_APPLY, value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box displays the color choice.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FnDshowColorDescr))]
    public bool ShowColor { get; set; }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box contains controls that allow the
    ///  user to specify strikethrough, underline, and text color options.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.FnDshowEffectsDescr))]
    public bool ShowEffects
    {
        get => GetOption(CHOOSEFONT_FLAGS.CF_EFFECTS);
        set => SetOption(CHOOSEFONT_FLAGS.CF_EFFECTS, value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box displays a Help button.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FnDshowHelpDescr))]
    public bool ShowHelp
    {
        get => GetOption(CHOOSEFONT_FLAGS.CF_SHOWHELP);
        set => SetOption(CHOOSEFONT_FLAGS.CF_SHOWHELP, value);
    }

    /// <summary>
    ///  Occurs when the user clicks the Apply button in the font dialog box.
    /// </summary>
    [SRDescription(nameof(SR.FnDapplyDescr))]
    public event EventHandler? Apply
    {
        add => Events.AddHandler(EventApply, value);
        remove => Events.RemoveHandler(EventApply, value);
    }

    /// <summary>
    ///  Returns the state of the given option flag.
    /// </summary>
    internal bool GetOption(CHOOSEFONT_FLAGS option) => (_options & option) != 0;

    /// <summary>
    ///  Specifies the common dialog box hook procedure that is overridden to add
    ///  specific functionality to a common dialog box.
    /// </summary>
    protected override IntPtr HookProc(IntPtr hWnd, int msg, IntPtr wparam, IntPtr lparam)
    {
        switch ((uint)msg)
        {
            case PInvokeCore.WM_COMMAND:
                if (wparam != 0x402)
                {
                    break;
                }

                LOGFONT logFont = default;
                PInvokeCore.SendMessage((HWND)hWnd, PInvokeCore.WM_CHOOSEFONT_GETLOGFONT, (WPARAM)0, ref logFont);
                UpdateFont(ref logFont);
                int index = (int)PInvoke.SendDlgItemMessage((HWND)hWnd, (int)PInvoke.cmb4, PInvoke.CB_GETCURSEL, 0, 0);
                if (index != PInvoke.CB_ERR)
                {
                    UpdateColor((COLORREF)(int)PInvoke.SendDlgItemMessage(
                        (HWND)hWnd,
                        (int)PInvoke.cmb4,
                        PInvoke.CB_GETITEMDATA,
                        (WPARAM)index,
                        0));
                }

                if (NativeWindow.WndProcShouldBeDebuggable)
                {
                    OnApply(EventArgs.Empty);
                }
                else
                {
                    try
                    {
                        OnApply(EventArgs.Empty);
                    }
                    catch (Exception e)
                    {
                        Application.OnThreadException(e);
                    }
                }

                break;
            case PInvokeCore.WM_INITDIALOG:
                if (!ShowColor)
                {
                    HWND hWndCtl = PInvoke.GetDlgItem((HWND)hWnd, (int)PInvoke.cmb4);
                    PInvoke.ShowWindow(hWndCtl, SHOW_WINDOW_CMD.SW_HIDE);
                    hWndCtl = PInvoke.GetDlgItem((HWND)hWnd, (int)PInvoke.stc4);
                    PInvoke.ShowWindow(hWndCtl, SHOW_WINDOW_CMD.SW_HIDE);
                }

                break;
        }

        return base.HookProc(hWnd, msg, wparam, lparam);
    }

    /// <summary>
    ///  Raises the <see cref="Apply"/> event.
    /// </summary>
    protected virtual void OnApply(EventArgs e) => (Events[EventApply] as EventHandler)?.Invoke(this, e);

    /// <summary>
    ///  Resets all dialog box options to their default values.
    /// </summary>
    public override void Reset()
    {
        _options = CHOOSEFONT_FLAGS.CF_SCREENFONTS | CHOOSEFONT_FLAGS.CF_EFFECTS;
        _font = null;
        _color = SystemColors.ControlText;
        _usingDefaultIndirectColor = true;
        ShowColor = false;
        _minSize = DefaultMinSize;
        _maxSize = DefaultMaxSize;
        SetOption(CHOOSEFONT_FLAGS.CF_TTONLY, true);
    }

    private void ResetFont() => _font = null;

    protected override unsafe bool RunDialog(IntPtr hWndOwner)
    {
        using var dc = GetDcScope.ScreenDC;
        using Graphics graphics = Graphics.FromHdcInternal(dc);
        LOGFONTW logFont = Font.ToLogicalFont(graphics);

        CHOOSEFONTW cf = new()
        {
            lStructSize = (uint)sizeof(CHOOSEFONTW),
            hwndOwner = (HWND)hWndOwner,
            lpLogFont = &logFont,
            Flags = (CHOOSEFONT_FLAGS)Options | CHOOSEFONT_FLAGS.CF_INITTOLOGFONTSTRUCT | CHOOSEFONT_FLAGS.CF_ENABLEHOOK,
            lpfnHook = HookProcFunctionPointer,
            hInstance = PInvoke.GetModuleHandle((PCWSTR)null),
            nSizeMin = _minSize,
            nSizeMax = _maxSize == 0 ? int.MaxValue : _maxSize,
            rgbColors = ShowColor || ShowEffects ? _color : SystemColors.ControlText
        };

        if (_minSize > 0 || _maxSize > 0)
        {
            cf.Flags |= CHOOSEFONT_FLAGS.CF_LIMITSIZE;
        }

        // if ShowColor=true then try to draw the sample text in color,
        // if ShowEffects=false then we will draw the sample text in standard control text color regardless.
        // (limitation of windows control)

        Debug.Assert(cf.nSizeMin <= cf.nSizeMax, "min and max font sizes are the wrong way around");

        // The native font dialog does not currently support Per Monitor V2 mode. We are setting DpiAwareness
        // to SystemAware as a workaround. This action has no effect when the application is not running in
        // Per Monitor V2 mode.
        //
        // https://microsoft.visualstudio.com/OS/_workitems/edit/42835582
        using (ScaleHelper.EnterDpiAwarenessScope(DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_SYSTEM_AWARE))
        {
            if (!PInvoke.ChooseFont(&cf))
            {
                return false;
            }
        }

        if (!logFont.FaceName.IsEmpty)
        {
            UpdateFont(ref Unsafe.As<LOGFONTW, LOGFONT>(ref logFont));
            UpdateColor(cf.rgbColors);
        }

        return true;
    }

    /// <summary>
    ///  Sets the given option to the given boolean value.
    /// </summary>
    internal void SetOption(CHOOSEFONT_FLAGS option, bool value)
    {
        if (value)
        {
            _options |= option;
        }
        else
        {
            _options &= ~option;
        }
    }

    /// <summary>
    ///  Indicates whether the <see cref="Font"/> property should be persisted.
    /// </summary>
    private bool ShouldSerializeFont() => !Font.Equals(Control.DefaultFont);

    public override string ToString() => $"{base.ToString()},  Font: {Font}";

    private void UpdateColor(Color color)
    {
        if (_color != color)
        {
            _color = color;
            _usingDefaultIndirectColor = false;
        }
    }

    private void UpdateFont(ref LOGFONT lf)
    {
        using var dc = GetDcScope.ScreenDC;
        using Font fontInWorldUnits = Font.FromLogFont(in lf, dc);

        // The dialog claims its working in points (a device-independent unit),
        // but actually gives us something in world units (device-dependent).
        _font = ControlPaint.FontInPoints(fontInWorldUnits);
    }
}
