// 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.Design;
using System.Runtime.InteropServices;
using System.Windows.Forms.ComponentModel.Com2Interop;
using Windows.Win32.System.Ole;

namespace System.Windows.Forms;

public abstract partial class AxHost
{
    internal class AxPropertyDescriptor : PropertyDescriptor
    {
        private readonly PropertyDescriptor _baseDescriptor;
        internal AxHost _owner;
        private readonly DispIdAttribute? _dispid;

        private TypeConverter? _converter;
        private UITypeEditor? _editor;
        private readonly List<Attribute> _updateAttributes = [];
        private int _flags;

        private const int FlagUpdatedEditorAndConverter = 0x00000001;
        private const int FlagCheckGetter = 0x00000002;
        private const int FlagGetterThrew = 0x00000004;
        private const int FlagIgnoreCanAccessProperties = 0x00000008;
        private const int FlagSettingValue = 0x00000010;

        internal AxPropertyDescriptor(PropertyDescriptor baseDescriptor, AxHost owner) : base(baseDescriptor)
        {
            _baseDescriptor = baseDescriptor;
            _owner = owner;

            // Get the category for this dispid.
            _dispid = baseDescriptor.GetAttribute<DispIdAttribute>();
            if (_dispid is not null)
            {
                // Look to see if this property has a property page.
                // If it does, then it needs to be Browsable(true).
                if (!IsBrowsable && !IsReadOnly)
                {
                    Guid g = GetPropertyPage(_dispid.Value);
                    if (!Guid.Empty.Equals(g))
                    {
                        AddAttribute(new BrowsableAttribute(true));
                    }
                }

                // Use the CategoryAttribute provided by the OCX.
                CategoryAttribute? cat = owner.GetCategoryForDispid(_dispid.Value);
                if (cat is not null)
                {
                    AddAttribute(cat);
                }

                // Check to see if this a DataSource property.
                // If it is, we can always get and set the value of this property.
                if (PropertyType.GUID.Equals(s_dataSource_Guid))
                {
                    SetFlag(FlagIgnoreCanAccessProperties, true);
                }
            }
        }

        public override Type ComponentType
        {
            get
            {
                return _baseDescriptor.ComponentType;
            }
        }

        public override TypeConverter Converter
        {
            [RequiresUnreferencedCode(TrimmingConstants.PropertyDescriptorPropertyTypeMessage)]
            get
            {
                if (_dispid is not null)
                {
                    UpdateTypeConverterAndTypeEditorInternal(force: false, Dispid);
                }

                return _converter ?? base.Converter;
            }
        }

        internal int Dispid
            => _baseDescriptor.TryGetAttribute(out DispIdAttribute? dispid)
                ? dispid.Value
                : PInvokeCore.DISPID_UNKNOWN;

        public override bool IsReadOnly => _baseDescriptor.IsReadOnly;

        public override Type PropertyType => _baseDescriptor.PropertyType;

        internal bool SettingValue => GetFlag(FlagSettingValue);

        private void AddAttribute(Attribute attr)
        {
            _updateAttributes.Add(attr);
        }

        public override bool CanResetValue(object o)
        {
            return _baseDescriptor.CanResetValue(o);
        }

        [RequiresUnreferencedCode(TrimmingConstants.EditorRequiresUnreferencedCode)]
        public override object? GetEditor(Type editorBaseType)
        {
            ArgumentNullException.ThrowIfNull(editorBaseType);

            if (_dispid is not null)
            {
                UpdateTypeConverterAndTypeEditorInternal(false, _dispid.Value);
            }

            return editorBaseType.Equals(typeof(UITypeEditor)) && _editor is not null
                ? _editor
                : base.GetEditor(editorBaseType);
        }

        private bool GetFlag(int flagValue)
        {
            return ((_flags & flagValue) == flagValue);
        }

        private unsafe Guid GetPropertyPage(int dispid)
        {
            try
            {
                using var propertyBrowsing = _owner.TryGetComScope<IPerPropertyBrowsing>(out HRESULT hr);
                if (hr.Failed)
                {
                    return Guid.Empty;
                }

                Guid rval = Guid.Empty;
                if (propertyBrowsing.Value->MapPropertyToPage(dispid, &rval).Succeeded)
                {
                    return rval;
                }
            }
            catch (COMException)
            {
            }
            catch (Exception t)
            {
                Debug.Fail(t.ToString());
            }

            return Guid.Empty;
        }

        public override object? GetValue(object? component)
        {
            if ((!GetFlag(FlagIgnoreCanAccessProperties) && !_owner.CanAccessProperties) || GetFlag(FlagGetterThrew))
            {
                return null;
            }

            try
            {
                // Some controls fire OnChanged() notifications when getting values of some properties.
                // To prevent this kind of recursion, we check to see if we are already inside a OnChanged() call.

                _owner.NoComponentChangeEvents++;
                return _baseDescriptor.GetValue(component);
            }
            catch (Exception)
            {
                if (!GetFlag(FlagCheckGetter))
                {
                    SetFlag(FlagCheckGetter, true);
                    AddAttribute(new BrowsableAttribute(false));
                    _owner.RefreshAllProperties = true;
                    SetFlag(FlagGetterThrew, true);
                }

                throw;
            }
            finally
            {
                _owner.NoComponentChangeEvents--;
            }
        }

        public void OnValueChanged(object? component)
        {
            OnValueChanged(component, EventArgs.Empty);
        }

        public override void ResetValue(object o)
        {
            _baseDescriptor.ResetValue(o);
        }

        private void SetFlag(int flagValue, bool value)
        {
            if (value)
            {
                _flags |= flagValue;
            }
            else
            {
                _flags &= ~flagValue;
            }
        }

        public override void SetValue(object? component, object? value)
        {
            if (!GetFlag(FlagIgnoreCanAccessProperties) && !_owner.CanAccessProperties)
            {
                return;
            }

            // State oldOcxState = owner.OcxState;

            try
            {
                SetFlag(FlagSettingValue, true);
                if (PropertyType.IsEnum && value is not null && value.GetType() != PropertyType)
                {
                    _baseDescriptor.SetValue(component, Enum.ToObject(PropertyType, value));
                }
                else
                {
                    _baseDescriptor.SetValue(component, value);
                }
            }
            finally
            {
                SetFlag(FlagSettingValue, false);
            }

            OnValueChanged(component);
            if (_owner == component)
            {
                _owner.SetAxState(s_valueChanged, true);
            }
        }

        public override bool ShouldSerializeValue(object o)
        {
            return _baseDescriptor.ShouldSerializeValue(o);
        }

        internal void UpdateAttributes()
        {
            if (_updateAttributes.Count == 0)
            {
                return;
            }

            List<Attribute> attributes = new(AttributeArray!);
            attributes.AddRange(_updateAttributes);
            AttributeArray = [.. attributes];
            _updateAttributes.Clear();
        }

        /// <summary>
        ///  Called externally to flag that we need to update the editor or type converter.
        /// </summary>
        internal void UpdateTypeConverterAndTypeEditor(bool force)
        {
            // If this is an external request, flip the flag to false so we do the update on demand.
            if (GetFlag(FlagUpdatedEditorAndConverter) && force)
            {
                SetFlag(FlagUpdatedEditorAndConverter, false);
            }
        }

        /// <summary>
        ///  Called externally to flag that we need to update the editor or type converter for a specific DISPID.
        /// </summary>
        internal unsafe void UpdateTypeConverterAndTypeEditorInternal(bool force, int dispid)
        {
            // Check to see if we're being forced here or if the work really needs to be done.
            if ((GetFlag(FlagUpdatedEditorAndConverter) && !force) || _owner.GetOcx() is null)
            {
                return;
            }

            try
            {
                using var propertyBrowsing = _owner.TryGetComScope<IPerPropertyBrowsing>(out HRESULT hr);
                if (hr.Failed)
                {
                    return;
                }

                // Check for enums
                CALPOLESTR caStrings = default;
                CADWORD caCookies = default;

                hr = propertyBrowsing.Value->GetPredefinedStrings(dispid, &caStrings, &caCookies);

                if (hr.Failed)
                {
                    Debug.Fail($"IPerPropertyBrowsing::GetPredefinedStrings(dispid={dispid}) failed: {hr}");
                }

                if (hr == HRESULT.S_OK)
                {
                    string?[] names = caStrings.ConvertAndFree();
                    uint[] cookies = caCookies.ConvertAndFree();

                    if (names.Length > 0 && cookies.Length > 0)
                    {
                        if (_converter is null)
                        {
                            _converter = new AxEnumConverter(
                                this,
                                new AxPerPropertyBrowsingEnum(
                                    this,
                                    _owner,
                                    names,
                                    cookies));
                        }
                        else if (_converter is AxEnumConverter enumConverter)
                        {
                            enumConverter.RefreshValues();
                            if (enumConverter._com2Enum is AxPerPropertyBrowsingEnum axEnum)
                            {
                                axEnum.RefreshArrays(names, cookies);
                            }
                        }
                    }

                    return;
                }

                // Destroy the existing editor if we created the current one so if the items have
                // disappeared, we don't hold onto the old items.
                if (_converter is Com2EnumConverter)
                {
                    _converter = null;
                }

                // If we didn't get any strings, try the property page editor.
                //
                // Check to see if this is a property that we have already massaged to be a .NET type. If it is, don't
                // bother with custom property pages. We already have a .NET Editor for this type.

                if (_baseDescriptor.GetAttribute<ComAliasNameAttribute>() is not null)
                {
                    return;
                }

                Guid pageGuid = GetPropertyPage(dispid);

                if (pageGuid != Guid.Empty)
                {
                    _editor = new AxPropertyTypeEditor(this, pageGuid);

                    // Show any non-browsable property that has an editor through a property page.
                    if (!IsBrowsable)
                    {
                        AddAttribute(new BrowsableAttribute(true));
                    }
                }
            }
            catch (Exception)
            {
            }
            finally
            {
                SetFlag(FlagUpdatedEditorAndConverter, true);
            }
        }
    }
}
