// 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.Runtime.InteropServices;
using Windows.Win32.UI.Controls.Dialogs;

namespace System.Windows.Forms;

/// <summary>
///  Represents a common dialog box that displays available colors along with controls
///  that allow the user to define custom colors.
/// </summary>
[DefaultProperty(nameof(Color))]
[SRDescription(nameof(SR.DescriptionColorDialog))]
public class ColorDialog : CommonDialog
{
    private int _options;
    private readonly COLORREF[] _customColors = new COLORREF[16];

    private Color _color;

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

    /// <summary>
    ///  Gets or sets a value indicating whether the user can use the dialog box
    ///  to define custom colors.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.CDallowFullOpenDescr))]
    public virtual bool AllowFullOpen
    {
        get => !GetOption((int)CHOOSECOLOR_FLAGS.CC_PREVENTFULLOPEN);
        set => SetOption((int)CHOOSECOLOR_FLAGS.CC_PREVENTFULLOPEN, !value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box displays all available colors in
    ///  the set of basic colors.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.CDanyColorDescr))]
    public virtual bool AnyColor
    {
        get => GetOption((int)CHOOSECOLOR_FLAGS.CC_ANYCOLOR);
        set => SetOption((int)CHOOSECOLOR_FLAGS.CC_ANYCOLOR, value);
    }

    /// <summary>
    ///  Gets or sets the color selected by the user.
    /// </summary>
    [SRCategory(nameof(SR.CatData))]
    [SRDescription(nameof(SR.CDcolorDescr))]
    public Color Color
    {
        get => _color;
        set => _color = !value.IsEmpty ? value : Color.Black;
    }

    /// <summary>
    ///  Gets or sets the set of custom colors shown in the dialog box.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.CDcustomColorsDescr))]
    [AllowNull]
    public int[] CustomColors
    {
        get
        {
            int[] result = new int[_customColors.Length];
            MemoryMarshal.Cast<COLORREF, int>(_customColors).CopyTo(result);
            return result;
        }
        set
        {
            Span<COLORREF> customColors = _customColors;
            customColors.Fill(0x00FFFFFF);

            if (value is not null && value.Length > 0)
            {
                MemoryMarshal.Cast<int, COLORREF>(value)[..Math.Min(customColors.Length, value.Length)].CopyTo(customColors);
            }
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the controls used to create custom
    ///  colors are visible when the dialog box is opened.
    /// </summary>
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.CDfullOpenDescr))]
    public virtual bool FullOpen
    {
        get => GetOption((int)CHOOSECOLOR_FLAGS.CC_FULLOPEN);
        set => SetOption((int)CHOOSECOLOR_FLAGS.CC_FULLOPEN, value);
    }

    /// <summary>
    ///  Our HINSTANCE from Windows.
    /// </summary>
    protected virtual nint Instance => PInvoke.GetModuleHandle((PCWSTR)null);

    /// <summary>
    ///  Returns our CHOOSECOLOR options.
    /// </summary>
    protected virtual int Options => _options;

    /// <summary>
    ///  Gets or sets a value indicating whether a Help button appears in the color dialog box.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.CDshowHelpDescr))]
    public virtual bool ShowHelp
    {
        get => GetOption((int)CHOOSECOLOR_FLAGS.CC_SHOWHELP);
        set => SetOption((int)CHOOSECOLOR_FLAGS.CC_SHOWHELP, value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box will restrict users to selecting solid colors only.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.CDsolidColorOnlyDescr))]
    public virtual bool SolidColorOnly
    {
        get => GetOption((int)CHOOSECOLOR_FLAGS.CC_SOLIDCOLOR);
        set => SetOption((int)CHOOSECOLOR_FLAGS.CC_SOLIDCOLOR, value);
    }

    /// <summary>
    ///  Lets us control the CHOOSECOLOR options.
    /// </summary>
    private bool GetOption(int option) => (_options & option) != 0;

    /// <summary>
    ///  Resets all options to their default values, the last selected color to black, and the custom
    ///  colors to their default values.
    /// </summary>
    public override void Reset()
    {
        _options = 0;
        _color = Color.Black;
        CustomColors = null;
    }

    private void ResetColor() => Color = Color.Black;

    protected override unsafe bool RunDialog(IntPtr hwndOwner)
    {
        CHOOSECOLOR_FLAGS flags = (CHOOSECOLOR_FLAGS)Options | CHOOSECOLOR_FLAGS.CC_RGBINIT | CHOOSECOLOR_FLAGS.CC_ENABLEHOOK;

        // Our docs say AllowFullOpen takes precedence over FullOpen; ChooseColor implements the opposite
        if (!AllowFullOpen)
        {
            flags &= ~CHOOSECOLOR_FLAGS.CC_FULLOPEN;
        }

        // We have to copy the custom colors so we can ignore changes if the user cancels the dialog.
        Span<COLORREF> customColors = stackalloc COLORREF[16];
        _customColors.CopyTo(customColors);

        CHOOSECOLORW cc = new()
        {
            lStructSize = (uint)sizeof(CHOOSECOLORW),
            hwndOwner = (HWND)hwndOwner,
            hInstance = (HWND)Instance,
            rgbResult = (COLORREF)_color,
            Flags = flags,
            lpfnHook = HookProcFunctionPointer
        };

        fixed (COLORREF* c = customColors)
        {
            cc.lpCustColors = c;

            if (!PInvoke.ChooseColor(&cc))
            {
                return false;
            }

            customColors.CopyTo(_customColors);
            _color = cc.rgbResult;
        }

        return true;
    }

    /// <summary>
    ///  Allows us to manipulate the CHOOSECOLOR options
    /// </summary>
    private void SetOption(int option, bool value)
    {
        if (value)
        {
            _options |= option;
        }
        else
        {
            _options &= ~option;
        }
    }

    /// <summary>
    ///  Indicates whether the <see cref="Color"/> property should be persisted.
    /// </summary>
    private bool ShouldSerializeColor() => !Color.Equals(Color.Black);

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