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

using System.Drawing;
using Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;

namespace System.Windows.Forms;

public partial class DataGridView
{
    protected class DataGridViewAccessibleObject : ControlAccessibleObject
    {
        private int[]? _runtimeId;
        private bool? _isModal;

        private DataGridViewTopRowAccessibleObject? _topRowAccessibilityObject;
        private DataGridViewSelectedCellsAccessibleObject? _selectedCellsAccessibilityObject;

        public DataGridViewAccessibleObject(DataGridView owner)
            : base(owner)
        {
        }

        internal override bool IsReadOnly => this.TryGetOwnerAs(out DataGridView? owner) ? owner.ReadOnly : base.IsReadOnly;

        private bool IsModal
        {
            get
            {
                _isModal ??= this.TryGetOwnerAs(out DataGridView? owner) && owner.TopMostParent is Form { Modal: true };
                return _isModal.Value;
            }
        }

        internal void ReleaseChildUiaProviders()
        {
            if (!OsVersion.IsWindows8OrGreater())
            {
                return;
            }

            PInvoke.UiaDisconnectProvider(_topRowAccessibilityObject, skipOSCheck: true);
            _topRowAccessibilityObject = null;

            PInvoke.UiaDisconnectProvider(_selectedCellsAccessibilityObject, skipOSCheck: true);
            _selectedCellsAccessibilityObject = null;
        }

        public override AccessibleRole Role => this.GetOwnerAccessibleRole(AccessibleRole.Table);

        private DataGridViewTopRowAccessibleObject? TopRowAccessibilityObject =>
            _topRowAccessibilityObject ??= this.TryGetOwnerAs(out DataGridView? owner) ? new(owner) : null;

        private DataGridViewSelectedCellsAccessibleObject SelectedCellsAccessibilityObject =>
            _selectedCellsAccessibilityObject ??= new(this);

        public override AccessibleObject? GetChild(int index)
        {
            if (index < 0 || !this.TryGetOwnerAs(out DataGridView? owner))
            {
                return null;
            }

            if (owner.Columns.Count == 0)
            {
                Debug.Assert(GetChildCount() == 0);
                return null;
            }

            if (index < 1 && owner.ColumnHeadersVisible)
            {
                return TopRowAccessibilityObject;
            }

            if (owner.ColumnHeadersVisible)
            {
                index--;
            }

            if (index < owner.Rows.GetRowCount(DataGridViewElementStates.Visible))
            {
                int actualRowIndex = owner.Rows.DisplayIndexToRowIndex(index);
                return owner.Rows[actualRowIndex].AccessibilityObject;
            }

            index -= owner.Rows.GetRowCount(DataGridViewElementStates.Visible);

            if (owner._horizScrollBar.Visible)
            {
                if (index == 0)
                {
                    return owner._horizScrollBar.AccessibilityObject;
                }
                else
                {
                    index--;
                }
            }

            if (owner._vertScrollBar.Visible)
            {
                if (index == 0)
                {
                    return owner._vertScrollBar.AccessibilityObject;
                }
            }

            return null;
        }

        public override int GetChildCount()
        {
            if (!this.TryGetOwnerAs(out DataGridView? owner) || owner.Columns.Count == 0)
            {
                return 0;
            }

            int childCount = owner.Rows.GetRowCount(DataGridViewElementStates.Visible);

            // the column header collection Accessible Object
            if (owner.ColumnHeadersVisible)
            {
                childCount++;
            }

            if (owner._horizScrollBar.Visible)
            {
                childCount++;
            }

            if (owner._vertScrollBar.Visible)
            {
                childCount++;
            }

            return childCount;
        }

        public override AccessibleObject? GetFocused()
        {
            if (this.TryGetOwnerAs(out DataGridView? owner) && owner.Focused && owner.CurrentCell is not null)
            {
                return owner.CurrentCell.AccessibilityObject;
            }
            else
            {
                return null;
            }
        }

        public override AccessibleObject GetSelected() => SelectedCellsAccessibilityObject;

        public override AccessibleObject? HitTest(int x, int y)
        {
            if (!this.IsOwnerHandleCreated(out DataGridView? owner))
            {
                return null;
            }

            Point pt = owner.PointToClient(new Point(x, y));
            HitTestInfo hti = owner.HitTest(pt.X, pt.Y);

            switch (hti.Type)
            {
                case DataGridViewHitTestType.Cell:
                    return owner.Rows[hti.RowIndex].Cells[hti.ColumnIndex].AccessibilityObject;
                case DataGridViewHitTestType.ColumnHeader:
                    // map the column index to the actual display index
                    int actualDisplayIndex = owner.Columns.ColumnIndexToActualDisplayIndex(hti.ColumnIndex, DataGridViewElementStates.Visible);
                    if (owner.RowHeadersVisible)
                    {
                        // increment the childIndex because the first child in the TopRowAccessibleObject is the TopLeftHeaderCellAccObj
                        return TopRowAccessibilityObject?.GetChild(actualDisplayIndex + 1);
                    }

                    return TopRowAccessibilityObject?.GetChild(actualDisplayIndex);

                case DataGridViewHitTestType.RowHeader:
                    return owner.Rows[hti.RowIndex].HeaderCell.AccessibilityObject;
                case DataGridViewHitTestType.TopLeftHeader:
                    return owner.TopLeftHeaderCell.AccessibilityObject;
                case DataGridViewHitTestType.VerticalScrollBar:
                    return owner.VerticalScrollBar.AccessibilityObject;
                case DataGridViewHitTestType.HorizontalScrollBar:
                    return owner.HorizontalScrollBar.AccessibilityObject;
                default:
                    return null;
            }
        }

        public override AccessibleObject? Navigate(AccessibleNavigation navigationDirection) => navigationDirection switch
        {
            AccessibleNavigation.FirstChild => GetChild(0),
            AccessibleNavigation.LastChild => GetChild(GetChildCount() - 1),
            _ => null,
        };

        internal override int[] RuntimeId => _runtimeId ??=
        [
            RuntimeIDFirstItem,
            GetHashCode()
        ];

        internal override bool IsIAccessibleExSupported() => true;

        internal override VARIANT GetPropertyValue(UIA_PROPERTY_ID propertyID)
        {
            DataGridView? owner;

            switch (propertyID)
            {
                case UIA_PROPERTY_ID.UIA_ControlTypePropertyId:
                    return (this.TryGetOwnerAs(out owner) && owner.AccessibleRole == AccessibleRole.Default)
                        ? (VARIANT)(int)UIA_CONTROLTYPE_ID.UIA_DataGridControlTypeId
                        : base.GetPropertyValue(propertyID);
                case UIA_PROPERTY_ID.UIA_HasKeyboardFocusPropertyId:
                    // If no inner cell entire DGV should be announced as focused by Narrator.
                    // Else only inner cell should be announced as focused by Narrator but not entire DGV.
                    return (VARIANT)(this.TryGetOwnerAs(out owner) && (IsModal || RowCount == 0) && owner.Focused);
                case UIA_PROPERTY_ID.UIA_IsControlElementPropertyId:
                    return VARIANT.True;
                case UIA_PROPERTY_ID.UIA_IsKeyboardFocusablePropertyId:
                    return (VARIANT)(this.TryGetOwnerAs(out owner) && owner.CanFocus);
                case UIA_PROPERTY_ID.UIA_ItemStatusPropertyId:
                    bool canSort = false;
                    if (!this.TryGetOwnerAs(out owner))
                    {
                        return base.GetPropertyValue(propertyID);
                    }

                    for (int i = 0; i < ColumnCount; i++)
                    {
                        int columnIndex = owner.Columns.ActualDisplayIndexToColumnIndex(i, DataGridViewElementStates.Visible);
                        if (owner.IsSortable(owner.Columns[columnIndex]))
                        {
                            canSort = true;
                            break;
                        }
                    }

                    if (canSort)
                    {
                        switch (owner.SortOrder)
                        {
                            case SortOrder.None:
                                return (VARIANT)SR.NotSortedAccessibleStatus;
                            case SortOrder.Ascending:
                                return (VARIANT)string.Format(SR.DataGridViewSortedAscendingAccessibleStatusFormat, owner.SortedColumn?.HeaderText);
                            case SortOrder.Descending:
                                return (VARIANT)string.Format(SR.DataGridViewSortedDescendingAccessibleStatusFormat, owner.SortedColumn?.HeaderText);
                        }
                    }

                    if (ColumnCount > 0 && RowCount > 0)
                    {
                        return (VARIANT)SR.NotSortedAccessibleStatus;
                    }

                    return base.GetPropertyValue(propertyID);
                default:
                    return base.GetPropertyValue(propertyID);
            }
        }

        internal override bool IsPatternSupported(UIA_PATTERN_ID patternId)
            => (patternId == UIA_PATTERN_ID.UIA_TablePatternId && RowCount > 0) ||
                patternId == UIA_PATTERN_ID.UIA_GridPatternId ||
                base.IsPatternSupported(patternId);

        internal override IRawElementProviderSimple.Interface[]? GetRowHeaders()
        {
            if (!this.TryGetOwnerAs(out DataGridView? owner) || !owner.RowHeadersVisible)
            {
                return null;
            }

            var result = new IRawElementProviderSimple.Interface[RowCount];
            for (int i = 0; i < RowCount; i++)
            {
                int rowIndex = owner.Rows.DisplayIndexToRowIndex(i);
                result[i] = owner.Rows[rowIndex].HeaderCell.AccessibilityObject;
            }

            return result;
        }

        internal override IRawElementProviderSimple.Interface[]? GetColumnHeaders()
        {
            if (!this.TryGetOwnerAs(out DataGridView? owner) || !owner.ColumnHeadersVisible)
            {
                return null;
            }

            var result = new IRawElementProviderSimple.Interface[ColumnCount];
            for (int i = 0; i < ColumnCount; i++)
            {
                int columnIndex = owner.Columns.ActualDisplayIndexToColumnIndex(i, DataGridViewElementStates.Visible);
                result[i] = owner.Columns[columnIndex].HeaderCell.AccessibilityObject;
            }

            return result;
        }

        internal override RowOrColumnMajor RowOrColumnMajor
        {
            get
            {
                return RowOrColumnMajor.RowOrColumnMajor_RowMajor;
            }
        }

        internal override IRawElementProviderSimple.Interface? GetItem(int row, int column)
        {
            if (!this.TryGetOwnerAs(out DataGridView? owner))
            {
                return null;
            }

            if (row >= 0 && row < RowCount && column >= 0 && column < ColumnCount)
            {
                row = owner.Rows.DisplayIndexToRowIndex(row);
                column = owner.Columns.ActualDisplayIndexToColumnIndex(column, DataGridViewElementStates.Visible);
                return owner.Rows[row].Cells[column].AccessibilityObject;
            }

            return null;
        }

        internal override int RowCount
        {
            get
            {
                return this.TryGetOwnerAs(out DataGridView? owner) ? owner.Rows.GetRowCount(DataGridViewElementStates.Visible) : base.RowCount;
            }
        }

        internal override int ColumnCount
        {
            get
            {
                return this.TryGetOwnerAs(out DataGridView? owner) ? owner.Columns.GetColumnCount(DataGridViewElementStates.Visible) : base.ColumnCount;
            }
        }

        #region IRawElementProviderFragment Implementation

        internal override IRawElementProviderFragmentRoot.Interface FragmentRoot => this;

        internal override IRawElementProviderFragment.Interface? FragmentNavigate(NavigateDirection direction)
        {
            switch (direction)
            {
                case NavigateDirection.NavigateDirection_FirstChild:
                    int childCount = GetChildCount();
                    if (childCount > 0)
                    {
                        return GetChild(0);
                    }

                    break;
                case NavigateDirection.NavigateDirection_LastChild:
                    childCount = GetChildCount();
                    if (childCount > 0)
                    {
                        int lastChildIndex = childCount - 1;
                        return GetChild(lastChildIndex);
                    }

                    break;
            }

            return base.FragmentNavigate(direction);
        }

        internal override void SetFocus()
        {
            if (this.IsOwnerHandleCreated(out DataGridView? owner) && owner.CanFocus)
            {
                owner.Focus();
            }
        }

        #endregion

        #region IRawElementProviderFragmentRoot Implementation

        internal override IRawElementProviderFragment.Interface? ElementProviderFromPoint(double x, double y)
            => this.IsOwnerHandleCreated(out DataGridView? _) ? HitTest((int)x, (int)y) : null;

        internal override IRawElementProviderFragment.Interface? GetFocus() => GetFocused();

        #endregion
    }
}
