// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections;
using System.Data;
using System.Diagnostics;

#pragma warning disable 0618 // ignore obsolete warning about XmlDataDocument

namespace System.Xml
{
    //
    // Maps XML nodes to schema
    //
    // With the exception of some functions (the most important is SearchMatchingTableSchema) all functions expect that each region rowElem is already associated
    // w/ it's DataRow (basically the test to determine a rowElem is based on a != null associated DataRow). As a result of this, some functions will NOT work properly
    // when they are used on a tree for which rowElem's are not associated w/ a DataRow.
    //

    internal sealed class DataSetMapper
    {
        private Hashtable _tableSchemaMap;   // maps an string (currently this is localName:nsURI) to a DataTable. Used to quickly find if a bound-elem matches any data-table metadata..
        private Hashtable _columnSchemaMap;  // maps a string (table localName:nsURI) to a Hashtable. The 2nd hastable (the one that is stored as data in columnSchemaMap, maps a string to a DataColumn.

        private XmlDataDocument _doc;        // The document this mapper is related to
        private DataSet _dataSet;          // The dataset this mapper is related to
        internal const string strReservedXmlns = "http://www.w3.org/2000/xmlns/";

        internal DataSetMapper()
        {
            Debug.Assert(_dataSet == null);
            _tableSchemaMap = new Hashtable();
            _columnSchemaMap = new Hashtable();
        }

        internal void SetupMapping(XmlDataDocument xd, DataSet ds)
        {
            // If are already mapped, forget about our current mapping and re-do it again.
            if (IsMapped())
            {
                _tableSchemaMap = new Hashtable();
                _columnSchemaMap = new Hashtable();
            }
            _doc = xd;
            _dataSet = ds;
            foreach (DataTable t in _dataSet.Tables)
            {
                AddTableSchema(t);

                foreach (DataColumn c in t.Columns)
                {
                    // don't include auto-generated PK & FK to be part of mapping
                    if (!IsNotMapped(c))
                    {
                        AddColumnSchema(c);
                    }
                }
            }
        }

        internal bool IsMapped() => _dataSet != null;

        internal DataTable SearchMatchingTableSchema(string localName, string namespaceURI)
        {
            object tid = GetIdentity(localName, namespaceURI);
            return (DataTable)(_tableSchemaMap[tid]);
        }

        // SearchMatchingTableSchema function works only when the elem has not been bound to a DataRow. If you want to get the table associated w/ an element after 
        // it has been associated w/ a DataRow use GetTableSchemaForElement function.
        // rowElem is the parent region rowElem or null if there is no parent region (in case elem is a row elem, then rowElem will be the parent region; if elem is not
        //    mapped to a DataRow, then rowElem is the region elem is part of)
        //
        // Those are the rules for determing if elem is a row element:
        //  1. node is an element (already meet, since elem is of type XmlElement)
        //  2. If the node is already associated w/ a DataRow, then the node is a row element - not applicable, b/c this function is intended to be called on a
        //    to find out if the node s/b associated w/ a DataRow (see XmlDataDocument.LoadRows)
        //  3. If the node localName/ns matches a DataTable then
        //      3.1 Take the parent region DataTable (in our case rowElem.Row.DataTable)
        //          3.2 If no parent region, then the node is associated w/ a DataTable
        //          3.3 If there is a parent region
        //              3.3.1 If the node has no elem children and no attr other than namespace declaration, and the node can match
        //                  a column from the parent region table, then the node is NOT associated w/ a DataTable (it is a potential DataColumn in the parent region)
        //              3.3.2 Else the node is a row-element (and associated w/ a DataTable / DataRow )
        //
        internal DataTable SearchMatchingTableSchema(XmlBoundElement rowElem, XmlBoundElement elem)
        {
            Debug.Assert(elem != null);

            DataTable t = SearchMatchingTableSchema(elem.LocalName, elem.NamespaceURI);
            if (t == null)
            {
                return null;
            }

            if (rowElem == null)
            {
                return t;
            }

            // Currently we expect we map things from top of the tree to the bottom
            Debug.Assert(rowElem.Row != null);

            DataColumn col = GetColumnSchemaForNode(rowElem, elem);
            if (col == null)
            {
                return t;
            }

            foreach (XmlAttribute a in elem.Attributes)
            {
#if DEBUG
                // Some sanity check to catch errors like namespace attributes have the right localName/namespace value, but a wrong atomized namespace value
                if (a.LocalName == "xmlns")
                {
                    Debug.Assert(a.Prefix != null && a.Prefix.Length == 0);
                    Debug.Assert(a.NamespaceURI == (object)strReservedXmlns);
                }
                if (a.Prefix == "xmlns")
                {
                    Debug.Assert(a.NamespaceURI == (object)strReservedXmlns);
                }
                if (a.NamespaceURI == strReservedXmlns)
                {
                    Debug.Assert(a.NamespaceURI == (object)strReservedXmlns);
                }
#endif
                // No namespace attribute found, so elem cannot be a potential DataColumn, therefore is a row-elem
                if (a.NamespaceURI != (object)strReservedXmlns)
                {
                    return t;
                }
            }

            for (XmlNode n = elem.FirstChild; n != null; n = n.NextSibling)
            {
                if (n.NodeType == XmlNodeType.Element)
                {
                    // elem has an element child, so elem cannot be a potential DataColumn, therefore is a row-elem
                    return t;
                }
            }

            // Node is a potential DataColumn in rowElem region
            return null;
        }

        internal DataColumn GetColumnSchemaForNode(XmlBoundElement rowElem, XmlNode node)
        {
            Debug.Assert(rowElem != null);
            // The caller must make sure that node is not a row-element
            Debug.Assert((node is XmlBoundElement) ? ((XmlBoundElement)node).Row == null : true);

            object tid = GetIdentity(rowElem.LocalName, rowElem.NamespaceURI);
            object cid = GetIdentity(node.LocalName, node.NamespaceURI);

            Hashtable columns = (Hashtable)_columnSchemaMap[tid];
            if (columns != null)
            {
                DataColumn col = (DataColumn)(columns[cid]);
                if (col == null)
                {
                    return null;
                }

                MappingType mt = col.ColumnMapping;

                if (node.NodeType == XmlNodeType.Attribute && mt == MappingType.Attribute)
                {
                    return col;
                }

                if (node.NodeType == XmlNodeType.Element && mt == MappingType.Element)
                {
                    return col;
                }

                // node's (localName, ns) matches a column, but the MappingType is different (i.e. node is elem, MT is attr)
                return null;
            }
            return null;
        }
        internal DataTable GetTableSchemaForElement(XmlElement elem)
        {
            XmlBoundElement be = elem as XmlBoundElement;
            if (be == null)
            {
                return null;
            }

            return GetTableSchemaForElement(be);
        }

        internal DataTable GetTableSchemaForElement(XmlBoundElement be) => be.Row?.Table;

        internal static bool IsNotMapped(DataColumn c) => c.ColumnMapping == MappingType.Hidden;

        // ATTENTION: GetRowFromElement( XmlElement ) and GetRowFromElement( XmlBoundElement ) should have the same functionality and side effects. 
        // See this code fragment for why:
        //     XmlBoundElement be = ...;
        //     XmlElement e = be;
        //     GetRowFromElement( be ); // Calls GetRowFromElement( XmlBoundElement )
        //     GetRowFromElement( e );  // Calls GetRowFromElement( XmlElement ), in spite of e beeing an instance of XmlBoundElement
        internal DataRow GetRowFromElement(XmlElement e) => (e as XmlBoundElement)?.Row;

        internal DataRow GetRowFromElement(XmlBoundElement be) => be.Row;

        // Get the row-elem associatd w/ the region node is in.
        // If node is in a region not mapped (like document element node) the function returns false and sets elem to null)
        // This function does not work if the region is not associated w/ a DataRow (it uses DataRow association to know what is the row element associated w/ the region)
        internal bool GetRegion(XmlNode node, out XmlBoundElement rowElem)
        {
            while (node != null)
            {
                XmlBoundElement be = node as XmlBoundElement;
                // Break if found a region
                if (be != null && GetRowFromElement(be) != null)
                {
                    rowElem = be;
                    return true;
                }

                if (node.NodeType == XmlNodeType.Attribute)
                {
                    node = ((XmlAttribute)node).OwnerElement;
                }
                else
                {
                    node = node.ParentNode;
                }
            }

            rowElem = null;
            return false;
        }

        internal bool IsRegionRadical(XmlBoundElement rowElem)
        {
            // You must pass a row element (which s/b associated w/ a DataRow)
            Debug.Assert(rowElem.Row != null);

            if (rowElem.ElementState == ElementState.Defoliated)
            {
                return true;
            }

            DataTable table = GetTableSchemaForElement(rowElem);
            DataColumnCollection columns = table.Columns;
            int iColumn = 0;

            // check column attributes...
            int cAttrs = rowElem.Attributes.Count;
            for (int iAttr = 0; iAttr < cAttrs; iAttr++)
            {
                XmlAttribute attr = rowElem.Attributes[iAttr];

                // only specified attributes are radical
                if (!attr.Specified)
                {
                    return false;
                }

                // only mapped attrs are valid
                DataColumn schema = GetColumnSchemaForNode(rowElem, attr);
                if (schema == null)
                {
                    return false;
                }

                // check to see if column is in order
                if (!IsNextColumn(columns, ref iColumn, schema))
                {
                    return false;
                }

                // must have exactly one text node (XmlNodeType.Text) child
                XmlNode fc = attr.FirstChild;
                if (fc == null || fc.NodeType != XmlNodeType.Text || fc.NextSibling != null)
                {
                    return false;
                }
            }

            // check column elements
            iColumn = 0;
            XmlNode n = rowElem.FirstChild;
            for (; n != null; n = n.NextSibling)
            {
                // only elements can exist in radically structured data
                if (n.NodeType != XmlNodeType.Element)
                {
                    return false;
                }
                XmlElement e = n as XmlElement;

                // only checking for column mappings in this loop
                if (GetRowFromElement(e) != null)
                {
                    break;
                }

                // element's must have schema to be radically structured
                DataColumn schema = GetColumnSchemaForNode(rowElem, e);
                if (schema == null)
                {
                    return false;
                }

                // check to see if column is in order
                if (!IsNextColumn(columns, ref iColumn, schema))
                {
                    return false;
                }

                // must have no attributes
                if (e.HasAttributes)
                {
                    return false;
                }

                // must have exactly one text node child
                XmlNode fc = e.FirstChild;
                if (fc == null || fc.NodeType != XmlNodeType.Text || fc.NextSibling != null)
                {
                    return false;
                }
            }

            // check for remaining sub-regions
            for (; n != null; n = n.NextSibling)
            {
                // only elements can exist in radically structured data
                if (n.NodeType != XmlNodeType.Element)
                {
                    return false;
                }

                // element's must be regions in order to be radially structured
                DataRow row = GetRowFromElement((XmlElement)n);
                if (row == null)
                {
                    return false;
                }
            }

            return true;
        }

        private void AddTableSchema(DataTable table)
        {
            object idTable = GetIdentity(table.EncodedTableName, table.Namespace);
            _tableSchemaMap[idTable] = table;
        }
        private void AddColumnSchema(DataColumn col)
        {
            DataTable table = col.Table;
            object idTable = GetIdentity(table.EncodedTableName, table.Namespace);
            object idColumn = GetIdentity(col.EncodedColumnName, col.Namespace);

            Hashtable columns = (Hashtable)_columnSchemaMap[idTable];
            if (columns == null)
            {
                columns = new Hashtable();
                _columnSchemaMap[idTable] = columns;
            }
            columns[idColumn] = col;
        }
        private static object GetIdentity(string localName, string namespaceURI)
        {
            // we need access to XmlName to make this faster
            return localName + ":" + namespaceURI;
        }

        private bool IsNextColumn(DataColumnCollection columns, ref int iColumn, DataColumn col)
        {
            for (; iColumn < columns.Count; iColumn++)
            {
                if (columns[iColumn] == col)
                {
                    iColumn++; // advance before we return...
                    return true;
                }
            }

            return false;
        }
    }
}
