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

using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Xml;

namespace System.Data
{
    /// <summary>
    /// Represents a row of data in a <see cref='System.Data.DataTable'/>.
    /// </summary>
    public class DataRow
    {
        private readonly DataTable _table;
        private readonly DataColumnCollection _columns;

        internal int _oldRecord = -1;
        internal int _newRecord = -1;
        internal int _tempRecord;
        internal long _rowID = -1;

        internal DataRowAction _action;

        internal bool _inChangingEvent;
        internal bool _inDeletingEvent;
        internal bool _inCascade;

        private DataColumn? _lastChangedColumn; // last successfully changed column
        private int _countColumnChange;        // number of columns changed during edit mode

        private DataError? _error;
        private object? _element;

        private int _rbTreeNodeId; // if row is not detached, Id used for computing index in rows collection

        private static int s_objectTypeCount; // Bid counter
        internal readonly int _objectID = System.Threading.Interlocked.Increment(ref s_objectTypeCount);

        /// <summary>
        /// Initializes a new instance of the DataRow.
        /// Constructs a row from the builder. Only for internal usage..
        /// </summary>
        protected internal DataRow(DataRowBuilder builder)
        {
            _tempRecord = builder._record;
            _table = builder._table;
            _columns = _table.Columns;
        }

        internal XmlBoundElement? Element
        {
            get { return (XmlBoundElement?)_element; }
            set { _element = value; }
        }

        internal DataColumn? LastChangedColumn
        {
            get
            {
                // last successfully changed column or if multiple columns changed: null
                return _countColumnChange != 1 ? null : _lastChangedColumn;
            }
            set
            {
                _countColumnChange++;
                _lastChangedColumn = value;
            }
        }

        internal bool HasPropertyChanged => 0 < _countColumnChange;

        internal int RBTreeNodeId
        {
            get { return _rbTreeNodeId; }
            set
            {
                DataCommonEventSource.Log.Trace("<ds.DataRow.set_RBTreeNodeId|INFO> {0}, value={1}", _objectID, value);
                _rbTreeNodeId = value;
            }
        }

        /// <summary>
        /// Gets or sets the custom error description for a row.
        /// </summary>
        [AllowNull]
        public string RowError
        {
            get { return _error == null ? string.Empty : _error.Text; }
            set
            {
                DataCommonEventSource.Log.Trace("<ds.DataRow.set_RowError|API> {0}, value='{1}'", _objectID, value);
                if (_error == null)
                {
                    if (!string.IsNullOrEmpty(value))
                    {
                        _error = new DataError(value);
                    }
                    RowErrorChanged();
                }
                else if (_error.Text != value)
                {
                    _error.Text = value;
                    RowErrorChanged();
                }
            }
        }

        private void RowErrorChanged()
        {
            // We don't know which record was used by view index. try to use both.
            if (_oldRecord != -1)
            {
                _table.RecordChanged(_oldRecord);
            }
            if (_newRecord != -1)
            {
                _table.RecordChanged(_newRecord);
            }
        }

        internal long rowID
        {
            get { return _rowID; }
            set
            {
                ResetLastChangedColumn();
                _rowID = value;
            }
        }

        /// <summary>
        /// Gets the current state of the row in regards to its relationship to the table.
        /// </summary>
        public DataRowState RowState
        {
            get
            {
                if (_oldRecord == _newRecord)
                {
                    if (_oldRecord == -1)
                    {
                        return DataRowState.Detached; // 2
                    }
                    if (0 < _columns.ColumnsImplementingIChangeTrackingCount)
                    {
                        foreach (DataColumn dc in _columns.ColumnsImplementingIChangeTracking)
                        {
                            object value = this[dc];
                            if ((DBNull.Value != value) && ((IChangeTracking)value).IsChanged)
                            {
                                return DataRowState.Modified; // 3 + _columns.columnsImplementingIChangeTracking.Count
                            }
                        }
                    }
                    return DataRowState.Unchanged; // 3
                }
                else if (_oldRecord == -1)
                {
                    return DataRowState.Added; // 2
                }
                else if (_newRecord == -1)
                {
                    return DataRowState.Deleted; // 3
                }
                return DataRowState.Modified; // 3
            }
        }

        /// <summary>
        /// Gets the <see cref='System.Data.DataTable'/>
        /// for which this row has a schema.
        /// </summary>
        public DataTable Table => _table;

        /// <summary>
        /// Gets or sets the data stored in the column specified by index.
        /// </summary>
        [AllowNull]
        public object this[int columnIndex]
        {
            get
            {
                DataColumn column = _columns[columnIndex];
                int record = GetDefaultRecord();
                _table._recordManager.VerifyRecord(record, this);
                VerifyValueFromStorage(column, DataRowVersion.Default, column[record]);
                return column[record];
            }
            set
            {
                DataColumn column = _columns[columnIndex];
                this[column] = value;
            }
        }

        internal void CheckForLoops(DataRelation rel)
        {
            // don't check for loops in the diffgram
            // because there may be some holes in the rowCollection
            // and index creation may fail. The check will be done
            // after all the loading is done _and_ we are sure there
            // are no holes in the collection.
            if (_table._fInLoadDiffgram || (_table.DataSet != null && _table.DataSet._fInLoadDiffgram))
            {
                return;
            }

            int count = _table.Rows.Count, i = 0;
            // need to optimize this for count > 100
            DataRow? parent = GetParentRow(rel);
            while (parent != null)
            {
                if ((parent == this) || (i > count))
                {
                    throw ExceptionBuilder.NestedCircular(_table.TableName);
                }
                i++;
                parent = parent.GetParentRow(rel);
            }
        }

        internal int GetNestedParentCount()
        {
            int count = 0;
            DataRelation[] nestedParentRelations = _table.NestedParentRelations;
            foreach (DataRelation rel in nestedParentRelations)
            {
                if (rel == null) // don't like this but done for backward code compatibility
                {
                    continue;
                }
                if (rel.ParentTable == _table) // self-nested table
                {
                    CheckForLoops(rel);
                }
                DataRow? row = GetParentRow(rel);
                if (row != null)
                {
                    count++;
                }
            }
            return count;
            // Rule 1: At all times, only ONE FK  "(in a row) can be non-Null
            // we won't allow a row to have multiple parents, as we cant handle it , also in diffgram
        }

        /// <summary>
        /// Gets or sets the data stored in the column specified by name.
        /// </summary>
        [AllowNull]
        public object this[string columnName]
        {
            get
            {
                DataColumn column = GetDataColumn(columnName);
                int record = GetDefaultRecord();
                _table._recordManager.VerifyRecord(record, this);
                VerifyValueFromStorage(column, DataRowVersion.Default, column[record]);
                return column[record];
            }
            set
            {
                DataColumn column = GetDataColumn(columnName);
                this[column] = value;
            }
        }

        /// <summary>
        /// Gets or sets the data stored in the specified <see cref='System.Data.DataColumn'/>.
        /// </summary>
        [AllowNull]
        public object this[DataColumn column]
        {
            get
            {
                CheckColumn(column);
                int record = GetDefaultRecord();
                _table._recordManager.VerifyRecord(record, this);
                VerifyValueFromStorage(column, DataRowVersion.Default, column[record]);
                return column[record];
            }
            set
            {
                CheckColumn(column);
                if (_inChangingEvent)
                {
                    throw ExceptionBuilder.EditInRowChanging();
                }
                if ((-1 != rowID) && column.ReadOnly)
                {
                    throw ExceptionBuilder.ReadOnly(column.ColumnName);
                }

                // allow users to tailor the proposed value, or throw an exception.
                // note we intentionally do not try/catch this event.
                // note: we also allow user to do anything at this point
                // infinite loops are possible if user calls Item or ItemArray during the event
                DataColumnChangeEventArgs? e = null;
                if (_table.NeedColumnChangeEvents)
                {
                    e = new DataColumnChangeEventArgs(this, column, value);
                    _table.OnColumnChanging(e);
                }

                if (column.Table != _table)
                {
                    // user removed column from table during OnColumnChanging event
                    throw ExceptionBuilder.ColumnNotInTheTable(column.ColumnName, _table.TableName);
                }
                if ((-1 != rowID) && column.ReadOnly)
                {
                    // user adds row to table during OnColumnChanging event
                    throw ExceptionBuilder.ReadOnly(column.ColumnName);
                }

                object? proposed = ((null != e) ? e.ProposedValue : value);
                if (null == proposed)
                {
                    if (column.IsValueType)
                    {
                        throw ExceptionBuilder.CannotSetToNull(column);
                    }
                    proposed = DBNull.Value;
                }

                bool immediate = BeginEditInternal();
                try
                {
                    int record = GetProposedRecordNo();
                    _table._recordManager.VerifyRecord(record, this);
                    column[record] = proposed;
                }
                catch (Exception e1) when (Common.ADP.IsCatchableOrSecurityExceptionType(e1))
                {
                    if (immediate)
                    {
                        Debug.Assert(!_inChangingEvent, "how are we in a changing event to cancel?");
                        Debug.Assert(-1 != _tempRecord, "how no propsed record to cancel?");
                        CancelEdit();
                    }
                    throw;
                }
                LastChangedColumn = column;

                // note: we intentionally do not try/catch this event.
                // infinite loops are possible if user calls Item or ItemArray during the event
                if (null != e)
                {
                    _table.OnColumnChanged(e); // user may call CancelEdit or EndEdit
                }

                if (immediate)
                {
                    Debug.Assert(!_inChangingEvent, "how are we in a changing event to end?");
                    EndEdit();
                }
            }
        }

        /// <summary>
        /// Gets the data stored in the column, specified by index and version of the data to retrieve.
        /// </summary>
        [AllowNull]
        public object this[int columnIndex, DataRowVersion version]
        {
            get
            {
                DataColumn column = _columns[columnIndex];
                int record = GetRecordFromVersion(version);
                _table._recordManager.VerifyRecord(record, this);
                VerifyValueFromStorage(column, version, column[record]);
                return column[record];
            }
        }

        /// <summary>
        ///  Gets the specified version of data stored in the named column.
        /// </summary>
        [AllowNull]
        public object this[string columnName, DataRowVersion version]
        {
            get
            {
                DataColumn column = GetDataColumn(columnName);
                int record = GetRecordFromVersion(version);
                _table._recordManager.VerifyRecord(record, this);
                VerifyValueFromStorage(column, version, column[record]);
                return column[record];
            }
        }

        /// <summary>
        /// Gets the specified version of data stored in the specified <see cref='System.Data.DataColumn'/>.
        /// </summary>
        [AllowNull]
        public object this[DataColumn column, DataRowVersion version]
        {
            get
            {
                CheckColumn(column);
                int record = GetRecordFromVersion(version);
                _table._recordManager.VerifyRecord(record, this);
                VerifyValueFromStorage(column, version, column[record]);
                return column[record];
            }
        }

        /// <summary>
        /// Gets or sets all of the values for this row through an array.
        /// </summary>
        /// <remarks>
        /// Arrays returned from this property will never contain <see langword="null" />.
        /// When setting this property, <see langword="null" /> indicates that the current column value
        /// should be left as-is. <see cref="DBNull.Value" /> can be used to set the value to null.
        /// </remarks>
        public object?[] ItemArray
        {
            get
            {
                int record = GetDefaultRecord();
                _table._recordManager.VerifyRecord(record, this);
                object[] values = new object[_columns.Count];
                for (int i = 0; i < values.Length; i++)
                {
                    DataColumn column = _columns[i];
                    VerifyValueFromStorage(column, DataRowVersion.Default, column[record]);
                    values[i] = column[record];
                }
                return values;
            }
            set
            {
                if (null == value)
                {
                    throw ExceptionBuilder.ArgumentNull(nameof(ItemArray));
                }
                if (_columns.Count < value.Length)
                {
                    throw ExceptionBuilder.ValueArrayLength();
                }
                DataColumnChangeEventArgs? e = null;
                if (_table.NeedColumnChangeEvents)
                {
                    e = new DataColumnChangeEventArgs(this);
                }
                bool immediate = BeginEditInternal();

                for (int i = 0; i < value.Length; ++i)
                {
                    // Empty means don't change the row.
                    if (value[i] is object item)
                    {
                        // may throw exception if user removes column from table during event
                        DataColumn column = _columns[i];

                        if ((-1 != rowID) && column.ReadOnly)
                        {
                            throw ExceptionBuilder.ReadOnly(column.ColumnName);
                        }

                        // allow users to tailor the proposed value, or throw an exception.
                        // note: we intentionally do not try/catch this event.
                        // note: we also allow user to do anything at this point
                        // infinite loops are possible if user calls Item or ItemArray during the event
                        if (null != e)
                        {
                            e.InitializeColumnChangeEvent(column, item);
                            _table.OnColumnChanging(e);
                        }

                        if (column.Table != _table)
                        {
                            // user removed column from table during OnColumnChanging event
                            throw ExceptionBuilder.ColumnNotInTheTable(column.ColumnName, _table.TableName);
                        }
                        if ((-1 != rowID) && column.ReadOnly)
                        {
                            // user adds row to table during OnColumnChanging event
                            throw ExceptionBuilder.ReadOnly(column.ColumnName);
                        }
                        if (_tempRecord == -1)
                        {
                            // user affected CancelEdit or EndEdit during OnColumnChanging event of the last value
                            BeginEditInternal();
                        }

                        object? proposed = (null != e) ? e.ProposedValue : item;
                        if (null == proposed)
                        {
                            if (column.IsValueType)
                            {
                                throw ExceptionBuilder.CannotSetToNull(column);
                            }
                            proposed = DBNull.Value;
                        }

                        try
                        {
                            // must get proposed record after each event because user may have
                            // called EndEdit(), AcceptChanges(), BeginEdit() during the event
                            int record = GetProposedRecordNo();
                            _table._recordManager.VerifyRecord(record, this);
                            column[record] = proposed;
                        }
                        catch (Exception e1) when (Common.ADP.IsCatchableOrSecurityExceptionType(e1))
                        {
                            if (immediate)
                            {
                                Debug.Assert(!_inChangingEvent, "how are we in a changing event to cancel?");
                                Debug.Assert(-1 != _tempRecord, "how no propsed record to cancel?");
                                CancelEdit();
                            }
                            throw;
                        }
                        LastChangedColumn = column;

                        // note: we intentionally do not try/catch this event.
                        // infinite loops are possible if user calls Item or ItemArray during the event
                        if (null != e)
                        {
                            _table.OnColumnChanged(e);  // user may call CancelEdit or EndEdit
                        }
                    }
                }

                // proposed breaking change: if (immediate){ EndEdit(); } because table currently always fires RowChangedEvent
                Debug.Assert(!_inChangingEvent, "how are we in a changing event to end?");
                EndEdit();
            }
        }

        /// <summary>
        /// Commits all the changes made to this row since the last time <see cref='System.Data.DataRow.AcceptChanges'/> was called.
        /// </summary>
        public void AcceptChanges()
        {
            long logScopeId = DataCommonEventSource.Log.EnterScope("<ds.DataRow.AcceptChanges|API> {0}", _objectID);
            try
            {
                EndEdit();

                if (RowState != DataRowState.Detached && RowState != DataRowState.Deleted)
                {
                    if (_columns.ColumnsImplementingIChangeTrackingCount > 0)
                    {
                        foreach (DataColumn dc in _columns.ColumnsImplementingIChangeTracking)
                        {
                            object value = this[dc];
                            if (DBNull.Value != value)
                            {
                                IChangeTracking tracking = (IChangeTracking)value;
                                if (tracking.IsChanged)
                                {
                                    tracking.AcceptChanges();
                                }
                            }
                        }
                    }
                }
                _table.CommitRow(this);
            }
            finally
            {
                DataCommonEventSource.Log.ExitScope(logScopeId);
            }
        }

        /// <summary>
        /// Begins an edit operation on a <see cref='System.Data.DataRow'/>object.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public void BeginEdit() => BeginEditInternal();

        private bool BeginEditInternal()
        {
            if (_inChangingEvent)
            {
                throw ExceptionBuilder.BeginEditInRowChanging();
            }
            if (_tempRecord != -1)
            {
                if (_tempRecord < _table._recordManager.LastFreeRecord)
                {
                    return false; // we will not call EndEdit
                }
                else
                {
                    // partial fix for detached row after Table.Clear scenario
                    // in debug, it will have asserted earlier, but with this
                    // it will go get a new record for editing
                    _tempRecord = -1;
                }
                // shifted VerifyRecord to first make the correction, then verify
                _table._recordManager.VerifyRecord(_tempRecord, this);
            }

            if (_oldRecord != -1 && _newRecord == -1)
            {
                throw ExceptionBuilder.DeletedRowInaccessible();
            }

            ResetLastChangedColumn(); // shouldn't have to do this

            _tempRecord = _table.NewRecord(_newRecord);
            Debug.Assert(-1 != _tempRecord, "missing temp record");
            Debug.Assert(0 == _countColumnChange, "unexpected column change count");
            Debug.Assert(null == _lastChangedColumn, "unexpected last column change");
            return true;
        }

        /// <summary>
        /// Cancels the current edit on the row.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public void CancelEdit()
        {
            if (_inChangingEvent)
            {
                throw ExceptionBuilder.CancelEditInRowChanging();
            }

            _table.FreeRecord(ref _tempRecord);
            Debug.Assert(-1 == _tempRecord, "unexpected temp record");
            ResetLastChangedColumn();
        }

        private void CheckColumn(DataColumn column)
        {
            if (column == null)
            {
                throw ExceptionBuilder.ArgumentNull(nameof(column));
            }

            if (column.Table != _table)
            {
                throw ExceptionBuilder.ColumnNotInTheTable(column.ColumnName, _table.TableName);
            }
        }

        /// <summary>
        /// Throws a RowNotInTableException if row isn't in table.
        /// </summary>
        internal void CheckInTable()
        {
            if (rowID == -1)
            {
                throw ExceptionBuilder.RowNotInTheTable();
            }
        }

        /// <summary>
        /// Deletes the row.
        /// </summary>
        public void Delete()
        {
            if (_inDeletingEvent)
            {
                throw ExceptionBuilder.DeleteInRowDeleting();
            }

            if (_newRecord == -1)
            {
                return;
            }

            _table.DeleteRow(this);
        }

        /// <summary>
        /// Ends the edit occurring on the row.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public void EndEdit()
        {
            if (_inChangingEvent)
            {
                throw ExceptionBuilder.EndEditInRowChanging();
            }

            if (_newRecord == -1)
            {
                return; // this is meaningless, detached row case
            }

            if (_tempRecord != -1)
            {
                try
                {
                    // suppressing the ensure property changed because it's possible that no values have been modified
                    _table.SetNewRecord(this, _tempRecord, suppressEnsurePropertyChanged: true);
                }
                finally
                {
                    // a constraint violation may be thrown during SetNewRecord
                    ResetLastChangedColumn();
                }
            }
        }

        /// <summary>
        /// Sets the error description for a column specified by index.
        /// </summary>
        public void SetColumnError(int columnIndex, string? error)
        {
            DataColumn column = _columns[columnIndex];
            if (column == null)
            {
                throw ExceptionBuilder.ColumnOutOfRange(columnIndex);
            }
            SetColumnError(column, error);
        }

        /// <summary>
        /// Sets the error description for a column specified by name.
        /// </summary>
        public void SetColumnError(string columnName, string? error)
        {
            DataColumn column = GetDataColumn(columnName);
            SetColumnError(column, error);
        }

        /// <summary>
        /// Sets the error description for a column specified as a <see cref='System.Data.DataColumn'/>.
        /// </summary>
        public void SetColumnError(DataColumn column, string? error)
        {
            CheckColumn(column);

            long logScopeId = DataCommonEventSource.Log.EnterScope("<ds.DataRow.SetColumnError|API> {0}, column={1}, error='{2}'", _objectID, column.ObjectID, error);
            try
            {
                _error ??= new DataError();
                if (GetColumnError(column) != error)
                {
                    _error.SetColumnError(column, error);
                    RowErrorChanged();
                }
            }
            finally
            {
                DataCommonEventSource.Log.ExitScope(logScopeId);
            }
        }

        /// <summary>
        /// Gets the error description for the column specified by index.
        /// </summary>
        public string GetColumnError(int columnIndex) => GetColumnError(_columns[columnIndex]);

        /// <summary>
        /// Gets the error description for a column, specified by name.
        /// </summary>
        public string GetColumnError(string columnName) => GetColumnError(GetDataColumn(columnName));

        /// <summary>
        /// Gets the error description of the specified <see cref='System.Data.DataColumn'/>.
        /// </summary>
        public string GetColumnError(DataColumn column)
        {
            CheckColumn(column);
            _error ??= new DataError();
            return _error.GetColumnError(column);
        }

        /// <summary>
        /// Clears the errors for the row, including the <see cref='System.Data.DataRow.RowError'/>
        /// and errors set with <see cref='System.Data.DataRow.SetColumnError(DataColumn, string)'/>
        /// </summary>
        public void ClearErrors()
        {
            if (_error != null)
            {
                _error.Clear();
                RowErrorChanged();
            }
        }

        internal void ClearError(DataColumn column)
        {
            if (_error != null)
            {
                _error.Clear(column);
                RowErrorChanged();
            }
        }

        /// <summary>
        /// Gets a value indicating whether there are errors in a columns collection.
        /// </summary>
        public bool HasErrors => _error == null ? false : _error.HasErrors;

        /// <summary>
        /// Gets an array of columns that have errors.
        /// </summary>
        public DataColumn[] GetColumnsInError() => _error == null ?
            Array.Empty<DataColumn>() : _error.GetColumnsInError();

        public DataRow[] GetChildRows(string? relationName) =>
            GetChildRows(_table.ChildRelations[relationName], DataRowVersion.Default);

        public DataRow[] GetChildRows(string? relationName, DataRowVersion version) =>
            GetChildRows(_table.ChildRelations[relationName], version);

        /// <summary>
        /// Gets the child rows of this <see cref='System.Data.DataRow'/> using the
        /// specified <see cref='System.Data.DataRelation'/>.
        /// </summary>
        public DataRow[] GetChildRows(DataRelation? relation) =>
            GetChildRows(relation, DataRowVersion.Default);

        /// <summary>
        /// Gets the child rows of this <see cref='System.Data.DataRow'/> using the specified <see cref='System.Data.DataRelation'/> and the specified <see cref='System.Data.DataRowVersion'/>
        /// </summary>
        public DataRow[] GetChildRows(DataRelation? relation, DataRowVersion version)
        {
            if (relation == null)
            {
                return _table.NewRowArray(0);
            }

            if (relation.DataSet != _table.DataSet)
            {
                throw ExceptionBuilder.RowNotInTheDataSet();
            }
            if (relation.ParentKey.Table != _table)
            {
                throw ExceptionBuilder.RelationForeignTable(relation.ParentTable.TableName, _table.TableName);
            }
            return DataRelation.GetChildRows(relation.ParentKey, relation.ChildKey, this, version);
        }

        internal DataColumn GetDataColumn(string columnName)
        {
            DataColumn? column = _columns[columnName];
            if (null != column)
            {
                return column;
            }
            throw ExceptionBuilder.ColumnNotInTheTable(columnName, _table.TableName);
        }

        public DataRow? GetParentRow(string? relationName) =>
            GetParentRow(_table.ParentRelations[relationName], DataRowVersion.Default);

        public DataRow? GetParentRow(string? relationName, DataRowVersion version) =>
            GetParentRow(_table.ParentRelations[relationName], version);

        /// <summary>
        /// Gets the parent row of this <see cref='System.Data.DataRow'/> using the specified <see cref='System.Data.DataRelation'/> .
        /// </summary>
        public DataRow? GetParentRow(DataRelation? relation) =>
            GetParentRow(relation, DataRowVersion.Default);

        /// <summary>
        /// Gets the parent row of this <see cref='System.Data.DataRow'/>
        /// using the specified <see cref='System.Data.DataRelation'/> and <see cref='System.Data.DataRowVersion'/>.
        /// </summary>
        public DataRow? GetParentRow(DataRelation? relation, DataRowVersion version)
        {
            if (relation == null)
            {
                return null;
            }

            if (relation.DataSet != _table.DataSet)
            {
                throw ExceptionBuilder.RelationForeignRow();
            }

            if (relation.ChildKey.Table != _table)
            {
                throw ExceptionBuilder.GetParentRowTableMismatch(relation.ChildTable.TableName, _table.TableName);
            }

            return DataRelation.GetParentRow(relation.ParentKey, relation.ChildKey, this, version);
        }

        // a multiple nested child table's row can have only one non-null FK per row. So table has multiple
        // parents, but a row can have only one parent. Same nested row cannot below to 2 parent rows.
        internal DataRow? GetNestedParentRow(DataRowVersion version)
        {
            // 1) Walk over all FKs and get the non-null. 2) Get the relation. 3) Get the parent Row.
            DataRelation[] nestedParentRelations = _table.NestedParentRelations;
            foreach (DataRelation rel in nestedParentRelations)
            {
                if (rel == null) // don't like this but done for backward code compatibility
                {
                    continue;
                }
                if (rel.ParentTable == _table) // self-nested table
                {
                    CheckForLoops(rel);
                }

                DataRow? row = GetParentRow(rel, version);
                if (row != null)
                {
                    return row;
                }
            }
            return null; // Rule 1: At all times, only ONE FK  "(in a row) can be non-Null
        }
        // No Nested in 1-many

        public DataRow[] GetParentRows(string? relationName) =>
            GetParentRows(_table.ParentRelations[relationName], DataRowVersion.Default);

        public DataRow[] GetParentRows(string? relationName, DataRowVersion version) =>
            GetParentRows(_table.ParentRelations[relationName], version);

        /// <summary>
        /// Gets the parent rows of this <see cref='System.Data.DataRow'/> using the specified <see cref='System.Data.DataRelation'/> .
        /// </summary>
        public DataRow[] GetParentRows(DataRelation? relation) =>
            GetParentRows(relation, DataRowVersion.Default);

        /// <summary>
        /// Gets the parent rows of this <see cref='System.Data.DataRow'/> using the specified <see cref='System.Data.DataRelation'/> .
        /// </summary>
        public DataRow[] GetParentRows(DataRelation? relation, DataRowVersion version)
        {
            if (relation == null)
            {
                return _table.NewRowArray(0);
            }

            if (relation.DataSet != _table.DataSet)
            {
                throw ExceptionBuilder.RowNotInTheDataSet();
            }

            if (relation.ChildKey.Table != _table)
            {
                throw ExceptionBuilder.GetParentRowTableMismatch(relation.ChildTable.TableName, _table.TableName);
            }

            return DataRelation.GetParentRows(relation.ParentKey, relation.ChildKey, this, version);
        }

        internal object[] GetColumnValues(DataColumn[] columns) =>
            GetColumnValues(columns, DataRowVersion.Default);

        internal object[] GetColumnValues(DataColumn[] columns, DataRowVersion version)
        {
            DataKey key = new DataKey(columns, false); // temporary key, don't copy columns
            return GetKeyValues(key, version);
        }

        internal object[] GetKeyValues(DataKey key)
        {
            int record = GetDefaultRecord();
            return key.GetKeyValues(record);
        }

        internal object[] GetKeyValues(DataKey key, DataRowVersion version)
        {
            int record = GetRecordFromVersion(version);
            return key.GetKeyValues(record);
        }

        internal int GetCurrentRecordNo()
        {
            if (_newRecord == -1)
            {
                throw ExceptionBuilder.NoCurrentData();
            }
            return _newRecord;
        }

        internal int GetDefaultRecord()
        {
            if (_tempRecord != -1)
            {
                return _tempRecord;
            }
            if (_newRecord != -1)
            {
                return _newRecord;
            }

            // If row has oldRecord - this is deleted row.
            throw _oldRecord == -1 ?
                ExceptionBuilder.RowRemovedFromTheTable() :
                ExceptionBuilder.DeletedRowInaccessible();
        }

        internal int GetOriginalRecordNo()
        {
            if (_oldRecord == -1)
            {
                throw ExceptionBuilder.NoOriginalData();
            }
            return _oldRecord;
        }

        private int GetProposedRecordNo()
        {
            if (_tempRecord == -1)
            {
                throw ExceptionBuilder.NoProposedData();
            }
            return _tempRecord;
        }

        internal int GetRecordFromVersion(DataRowVersion version)
        {
            return version switch
            {
                DataRowVersion.Original => GetOriginalRecordNo(),
                DataRowVersion.Current => GetCurrentRecordNo(),
                DataRowVersion.Proposed => GetProposedRecordNo(),
                DataRowVersion.Default => GetDefaultRecord(),
                _ => throw ExceptionBuilder.InvalidRowVersion(),
            };
        }

        internal DataRowVersion GetDefaultRowVersion(DataViewRowState viewState)
        {
            if (_oldRecord == _newRecord)
            {
                if (_oldRecord == -1)
                {
                    // should be DataView.addNewRow
                    return DataRowVersion.Default;
                }
                return DataRowVersion.Default;
            }
            else if (_oldRecord == -1)
            {
                Debug.Assert(0 != (DataViewRowState.Added & viewState), "not DataViewRowState.Added");
                return DataRowVersion.Default;
            }
            else if (_newRecord == -1)
            {
                Debug.Assert(_action == DataRowAction.Rollback || 0 != (DataViewRowState.Deleted & viewState), "not DataViewRowState.Deleted");
                return DataRowVersion.Original;
            }
            else if (0 != (DataViewRowState.ModifiedCurrent & viewState))
            {
                return DataRowVersion.Default;
            }
            else
            {
                Debug.Assert(0 != (DataViewRowState.ModifiedOriginal & viewState), "not DataViewRowState.ModifiedOriginal");
                return DataRowVersion.Original;
            }
        }

        internal DataViewRowState GetRecordState(int record)
        {
            if (record == -1)
            {
                return DataViewRowState.None;
            }

            if (record == _oldRecord && record == _newRecord)
            {
                return DataViewRowState.Unchanged;
            }

            if (record == _oldRecord)
            {
                return (_newRecord != -1) ? DataViewRowState.ModifiedOriginal : DataViewRowState.Deleted;
            }

            if (record == _newRecord)
            {
                return (_oldRecord != -1) ? DataViewRowState.ModifiedCurrent : DataViewRowState.Added;
            }

            return DataViewRowState.None;
        }

        internal bool HasKeyChanged(DataKey key) =>
            HasKeyChanged(key, DataRowVersion.Current, DataRowVersion.Proposed);

        internal bool HasKeyChanged(DataKey key, DataRowVersion version1, DataRowVersion version2)
        {
            if (!HasVersion(version1) || !HasVersion(version2))
            {
                return true;
            }

            return !key.RecordsEqual(GetRecordFromVersion(version1), GetRecordFromVersion(version2));
        }

        /// <summary>
        /// Gets a value indicating whether a specified version exists.
        /// </summary>
        public bool HasVersion(DataRowVersion version)
        {
            return version switch
            {
                DataRowVersion.Original => (_oldRecord != -1),
                DataRowVersion.Current => (_newRecord != -1),
                DataRowVersion.Proposed => (_tempRecord != -1),
                DataRowVersion.Default => (_tempRecord != -1 || _newRecord != -1),
                _ => throw ExceptionBuilder.InvalidRowVersion(),
            };
        }

        internal bool HasChanges()
        {
            if (!HasVersion(DataRowVersion.Original) || !HasVersion(DataRowVersion.Current))
            {
                return true; // if does not have original, its added row, if does not have current, its deleted row so it has changes
            }
            foreach (DataColumn dc in Table.Columns)
            {
                if (dc.Compare(_oldRecord, _newRecord) != 0)
                {
                    return true;
                }
            }
            return false;
        }

        internal bool HaveValuesChanged(DataColumn[] columns) =>
            HaveValuesChanged(columns, DataRowVersion.Current, DataRowVersion.Proposed);

        internal bool HaveValuesChanged(DataColumn[] columns, DataRowVersion version1, DataRowVersion version2)
        {
            for (int i = 0; i < columns.Length; i++)
            {
                CheckColumn(columns[i]);
            }
            DataKey key = new DataKey(columns, false); // temporary key, don't copy columns
            return HasKeyChanged(key, version1, version2);
        }

        /// <summary>
        /// Gets a value indicating whether the column at the specified index contains a
        /// null value.
        /// </summary>
        public bool IsNull(int columnIndex)
        {
            DataColumn column = _columns[columnIndex];
            int record = GetDefaultRecord();
            return column.IsNull(record);
        }

        /// <summary>
        /// Gets a value indicating whether the named column contains a null value.
        /// </summary>
        public bool IsNull(string columnName)
        {
            DataColumn column = GetDataColumn(columnName);
            int record = GetDefaultRecord();
            return column.IsNull(record);
        }

        /// <summary>
        /// Gets a value indicating whether the specified <see cref='System.Data.DataColumn'/>
        /// contains a null value.
        /// </summary>
        public bool IsNull(DataColumn column)
        {
            CheckColumn(column);
            int record = GetDefaultRecord();
            return column.IsNull(record);
        }

        public bool IsNull(DataColumn column, DataRowVersion version)
        {
            CheckColumn(column);
            int record = GetRecordFromVersion(version);
            return column.IsNull(record);
        }

        /// <summary>
        /// Rejects all changes made to the row since <see cref='System.Data.DataRow.AcceptChanges'/>
        /// was last called.
        /// </summary>
        public void RejectChanges()
        {
            long logScopeId = DataCommonEventSource.Log.EnterScope("<ds.DataRow.RejectChanges|API> {0}", _objectID);
            try
            {
                if (RowState != DataRowState.Detached)
                {
                    if (_columns.ColumnsImplementingIChangeTrackingCount != _columns.ColumnsImplementingIRevertibleChangeTrackingCount)
                    {
                        foreach (DataColumn dc in _columns.ColumnsImplementingIChangeTracking)
                        {
                            if (!dc.ImplementsIRevertibleChangeTracking)
                            {
                                object? value = null;
                                if (RowState != DataRowState.Deleted)
                                    value = this[dc];
                                else
                                    value = this[dc, DataRowVersion.Original];
                                if (DBNull.Value != value)
                                {
                                    if (((IChangeTracking)value).IsChanged)
                                    {
                                        throw ExceptionBuilder.UDTImplementsIChangeTrackingButnotIRevertible(dc.DataType.AssemblyQualifiedName!);
                                    }
                                }
                            }
                        }
                    }
                    foreach (DataColumn dc in _columns.ColumnsImplementingIChangeTracking)
                    {
                        object? value;
                        if (RowState != DataRowState.Deleted)
                            value = this[dc];
                        else
                            value = this[dc, DataRowVersion.Original];
                        if (DBNull.Value != value)
                        {
                            IChangeTracking tracking = (IChangeTracking)value;
                            if (tracking.IsChanged)
                            {
                                ((IRevertibleChangeTracking)value).RejectChanges();
                            }
                        }
                    }
                }
                _table.RollbackRow(this);
            }
            finally
            {
                DataCommonEventSource.Log.ExitScope(logScopeId);
            }
        }

        internal void ResetLastChangedColumn()
        {
            _lastChangedColumn = null;
            _countColumnChange = 0;
        }

        internal void SetKeyValues(DataKey key, object[] keyValues)
        {
            bool fFirstCall = true;
            bool immediate = (_tempRecord == -1);

            for (int i = 0; i < keyValues.Length; i++)
            {
                object value = this[key.ColumnsReference[i]];
                if (!value.Equals(keyValues[i]))
                {
                    if (immediate && fFirstCall)
                    {
                        fFirstCall = false;
                        BeginEditInternal();
                    }
                    this[key.ColumnsReference[i]] = keyValues[i];
                }
            }
            if (!fFirstCall)
            {
                EndEdit();
            }
        }

        /// <summary>
        /// Sets the specified column's value to a null value.
        /// </summary>
        protected void SetNull(DataColumn column)
        {
            this[column] = DBNull.Value;
        }

        internal void SetNestedParentRow(DataRow? parentRow, bool setNonNested)
        {
            if (parentRow == null)
            {
                SetParentRowToDBNull();
                return;
            }

            foreach (DataRelation relation in _table.ParentRelations)
            {
                if (relation.Nested || setNonNested)
                {
                    if (relation.ParentKey.Table == parentRow._table)
                    {
                        object[] parentKeyValues = parentRow.GetKeyValues(relation.ParentKey);
                        SetKeyValues(relation.ChildKey, parentKeyValues);

                        if (relation.Nested)
                        {
                            if (parentRow._table == _table)
                            {
                                CheckForLoops(relation);
                            }
                            else
                            {
                                GetParentRow(relation);
                            }
                        }
                    }
                }
            }
        }

        public void SetParentRow(DataRow? parentRow)
        {
            SetNestedParentRow(parentRow, true);
        }

        /// <summary>
        /// Sets current row's parent row with specified relation.
        /// </summary>
        public void SetParentRow(DataRow? parentRow, DataRelation? relation)
        {
            if (relation == null)
            {
                SetParentRow(parentRow);
                return;
            }

            if (parentRow == null)
            {
                SetParentRowToDBNull(relation);
                return;
            }

            if (_table.DataSet != parentRow._table.DataSet)
            {
                throw ExceptionBuilder.ParentRowNotInTheDataSet();
            }

            if (relation.ChildKey.Table != _table)
            {
                throw ExceptionBuilder.SetParentRowTableMismatch(relation.ChildKey.Table.TableName, _table.TableName);
            }

            if (relation.ParentKey.Table != parentRow._table)
            {
                throw ExceptionBuilder.SetParentRowTableMismatch(relation.ParentKey.Table.TableName, parentRow._table.TableName);
            }

            object[] parentKeyValues = parentRow.GetKeyValues(relation.ParentKey);
            SetKeyValues(relation.ChildKey, parentKeyValues);
        }

        internal void SetParentRowToDBNull()
        {
            foreach (DataRelation relation in _table.ParentRelations)
            {
                SetParentRowToDBNull(relation);
            }
        }

        internal void SetParentRowToDBNull(DataRelation relation)
        {
            Debug.Assert(relation != null, "The relation should not be null here.");

            if (relation.ChildKey.Table != _table)
            {
                throw ExceptionBuilder.SetParentRowTableMismatch(relation.ChildKey.Table.TableName, _table.TableName);
            }

            object[] parentKeyValues = new object[1];
            parentKeyValues[0] = DBNull.Value;
            SetKeyValues(relation.ChildKey, parentKeyValues);
        }

        public void SetAdded()
        {
            if (RowState == DataRowState.Unchanged)
            {
                _table.SetOldRecord(this, -1);
            }
            else
            {
                throw ExceptionBuilder.SetAddedAndModifiedCalledOnnonUnchanged();
            }
        }

        public void SetModified()
        {
            if (RowState == DataRowState.Unchanged)
            {
                _tempRecord = _table.NewRecord(_newRecord);
                if (_tempRecord != -1)
                {
                    // suppressing the ensure property changed because no values have changed
                    _table.SetNewRecord(this, _tempRecord, suppressEnsurePropertyChanged: true);
                }
            }
            else
            {
                throw ExceptionBuilder.SetAddedAndModifiedCalledOnnonUnchanged();
            }
        }

        // RecordList contains the empty column storage needed. We need to copy the existing record values into this storage.

        internal int CopyValuesIntoStore(ArrayList storeList, ArrayList nullbitList, int storeIndex)
        {
            int recordCount = 0;
            if (_oldRecord != -1)
            {
                //Copy original record for the row in Unchanged, Modified, Deleted state.
                for (int i = 0; i < _columns.Count; i++)
                {
                    _columns[i].CopyValueIntoStore(_oldRecord, storeList[i]!, (BitArray)nullbitList[i]!, storeIndex);
                }
                recordCount++;
                storeIndex++;
            }

            DataRowState state = RowState;
            if ((DataRowState.Added == state) || (DataRowState.Modified == state))
            {
                //Copy current record for the row in Added, Modified state.
                for (int i = 0; i < _columns.Count; i++)
                {
                    _columns[i].CopyValueIntoStore(_newRecord, storeList[i]!, (BitArray)nullbitList[i]!, storeIndex);
                }
                recordCount++;
                storeIndex++;
            }

            if (-1 != _tempRecord)
            {
                //Copy temp record for the row in edit mode.
                for (int i = 0; i < _columns.Count; i++)
                {
                    _columns[i].CopyValueIntoStore(_tempRecord, storeList[i]!, (BitArray)nullbitList[i]!, storeIndex);
                }
                recordCount++;
            }

            return recordCount;
        }

        [Conditional("DEBUG")]
        private void VerifyValueFromStorage(DataColumn column, DataRowVersion version, object valueFromStorage)
        {
            // ignore deleted rows by adding "newRecord != -1" condition - we do not evaluate computed rows if they are deleted
            if (column.DataExpression != null && !_inChangingEvent && _tempRecord == -1 && _newRecord != -1)
            {
                // for unchanged rows, check current if original is asked for.
                // this is because by design, there is only single storage for an unchanged row.
                if (version == DataRowVersion.Original && _oldRecord == _newRecord)
                {
                    version = DataRowVersion.Current;
                }

                Debug.Assert(valueFromStorage.Equals(column.DataExpression.Evaluate(this, version)),
                    "Value from storage does lazily computed expression value");
            }
        }
    }

    public sealed class DataRowBuilder
    {
        internal readonly DataTable _table;
        internal int _record;

        internal DataRowBuilder(DataTable table, int record)
        {
            _table = table;
            _record = record;
        }
    }
}
