context: using System;
  using System.Collections.Generic;
  using System.Collections.ObjectModel;
  using System.Text;
  using System.Data;
  using SenseNet.ContentRepository.Storage.Data;
  using System.Globalization;
  using System.Xml;
  using SenseNet.Diagnostics;
  using System.Linq;
  using System.Threading;
  using SenseNet.Configuration;
  using SenseNet.ContentRepository.Storage.DataModel;
  // ReSharper disable ArrangeThisQualifier
  
  // ReSharper disable once CheckNamespace
  namespace SenseNet.ContentRepository.Storage.Schema
  {
      public abstract class SchemaRoot : ISchemaRoot
      {
          public static readonly string RepositoryStorageSchemaXmlNamespace = "http://schemas.sensenet.com/SenseNet/ContentRepository/Storage/Schema";
  
          private class NodeTypeInfo
          {
              public readonly int Id;
              public readonly int ParentId;
              public readonly string Name;
              public readonly string ClassName;
  
              public NodeTypeInfo(int id, int parentId, string name, string className)
              {
                  Id = id;
                  ParentId = parentId;
                  Name = name;
                  ClassName = className;
              }
          }
          private enum PropertySetType
          {
              NodeType, ContentListType
          }
  
          // ================================================================================ Fields
  
          private TypeCollection<PropertyType> _propertyTypes;
          private TypeCollection<NodeType> _nodeTypes;
          private TypeCollection<ContentListType> _contentListTypes;
  
          // ================================================================================ Properties
  
          public TypeCollection<PropertyType> PropertyTypes
          {
              get { return _propertyTypes; }
          }
          public TypeCollection<NodeType> NodeTypes
          {
              get { return _nodeTypes; }
          }
          public TypeCollection<ContentListType> ContentListTypes
          {
              get { return _contentListTypes; }
          }
  
          public long SchemaTimestamp { get; private set; }
  
          // ================================================================================ Construction
  
          protected SchemaRoot()
          {
              _propertyTypes = new TypeCollection<PropertyType>(this);
              _nodeTypes = new TypeCollection<NodeType>(this);
              _contentListTypes = new TypeCollection<ContentListType>(this);
          }
  
          // ================================================================================ Methods
  
          public void Clear()
          {
              _propertyTypes.Clear();
              _nodeTypes.Clear();
              _contentListTypes.Clear();
          }
  
          // -------------------------------------------------------------------------------- Load
  
          public void Load()
          {
              using (var op = SnTrace.Database.StartOperation("Load storage schema."))
              {
                  Clear();
                  var schemaData = Providers.Instance.DataStore.LoadSchemaAsync(CancellationToken.None).GetAwaiter().GetResult();
                  Load(schemaData);
                  op.Successful = true;
              }
          }
          public void Load(string schemaXml)
          {
              var xml = new XmlDocument();
              xml.LoadXml(schemaXml);
              Load(xml);
          }
          public void Load(XmlDocument schemaXml)
          {
              Clear();
              Load(BuildDataSetFromXml(schemaXml));
          }
  
          // -------------------------------------------------------------------------------- Load from RepositorySchemaData
  
          private void Load(RepositorySchemaData schemaData)
          {
              BuildPropertyTypes(schemaData.PropertyTypes);
              BuildNodeTypes(schemaData.NodeTypes);
              BuildContentListTypes(schemaData.ContentListTypes);
  
              SchemaTimestamp = schemaData.Timestamp;
          }
  
          private void BuildPropertyTypes(IEnumerable<PropertyTypeData> data)
          {
              foreach (var item in data)
                  CreatePropertyType(item.Id, item.Name, item.DataType, item.Mapping, item.IsContentListProperty);
          }
          private void BuildNodeTypes(IEnumerable<NodeTypeData> data)
          {
              var nodeTypes = data.ToList();
  
              while (nodeTypes.Count > 0)
              {
                  for (var i = nodeTypes.Count - 1; i >= 0; i--)
                  {
                      var nti = nodeTypes[i];
                      NodeType parent = null;
                      if (nti.ParentName == null || (parent = _nodeTypes[nti.ParentName]) != null)
                      {
                          var type = CreateNodeType(nti.Id, parent, nti.Name, nti.ClassName);
                          BuildProperties(type, nti.Properties);
                          nodeTypes.Remove(nti);
                      }
                  }
              }
          }
          private void BuildContentListTypes(IEnumerable<ContentListTypeData> data)
          {
              foreach (var item in data)
              {
                  var type = CreateContentListType(item.Id, item.Name);
                  BuildProperties(type, item.Properties);
              }
          }
          private void BuildProperties(PropertySet type, IEnumerable<string> propertyNames)
          {
              foreach (var propertyName in propertyNames)
              {
                  var propertyType = _propertyTypes[propertyName];
                  if (propertyType == null)
                      throw new InvalidSchemaException(
                          SR.Exceptions.Schema.Msg_PropertyTypeDoesNotExist + ": name=" + type.Name + "." + propertyName);
  
                  AddPropertyTypeToPropertySet(propertyType, type);
              }
          }
  
          // -------------------------------------------------------------------------------- ToRepositorySchemaData
  
          public RepositorySchemaData ToRepositorySchemaData()
          {
              return new RepositorySchemaData
              {
                  PropertyTypes = _propertyTypes.Select(p => new PropertyTypeData
                  {
                      Id = p.Id,
                      Name = p.Name,
                      DataType = p.DataType,
                      Mapping = p.Mapping,
                      IsContentListProperty = p.IsContentListProperty
                  }).ToList(),
                  NodeTypes = _nodeTypes.Select(n => new NodeTypeData
                  {
                      Id = n.Id,
                      Name = n.Name,
                      ParentName = n.Parent?.Name,
                      ClassName = n.ClassName,
                      Properties = n.DeclaredPropertyTypes.Select(p => p.Name).ToList()
                  }).ToList(),
                  ContentListTypes = _contentListTypes.Select(c => new ContentListTypeData
                  {
                      Id = c.Id,
                      Name = c.Name,
                      Properties = c.PropertyTypes.Select(p => p.Name).ToList()
                  }).ToList(),
              };
          }
  
          // -------------------------------------------------------------------------------- Load from DataSet
  
          private void Load(DataSet dataSet)
          {
              Dictionary<int, DataType> dataTypeHelper = BuildDataTypeHelper(dataSet);
              Dictionary<int, PropertySetType> propertySetTypeHelper = BuildPropertySetTypeHelper(dataSet);
  
              BuildPropertyTypes(dataSet.Tables["PropertyTypes"], dataTypeHelper);
              BuildPropertySets(dataSet.Tables["PropertySets"], propertySetTypeHelper);
              BuildPropertyTypeAssignments(dataSet.Tables["PropertySetsPropertyTypes"]);
  
              SchemaTimestamp = GetSchemaTimestamp(dataSet);
          }
  
          private static long GetSchemaTimestamp(DataSet dataSet)
          {
              if (dataSet == null) // usecase: in tests
                  return 0;
              var table = dataSet.Tables["SchemaModification"];
              if (table == null) // usecase: in tests
                  return 0;
              var rows = table.Rows;
              if (rows.Count == 0)
                  return 0;
              var row = rows[0];
              //return DataProvider.GetLongFromBytes((byte[])row["Timestamp"]);
              throw new NotSupportedException();
          }
          private static Dictionary<int, DataType> BuildDataTypeHelper(DataSet dataSet)
          {
              Dictionary<int, DataType> dataTypeHelper = new Dictionary<int, DataType>();
              foreach (DataRow row in dataSet.Tables["DataTypes"].Rows)
                  dataTypeHelper.Add(TypeConverter.ToInt32(row["DataTypeID"]), (DataType)Enum.Parse(typeof(DataType), TypeConverter.ToString(row["Name"])));
              return dataTypeHelper;
          }
          private static Dictionary<int, PropertySetType> BuildPropertySetTypeHelper(DataSet dataSet)
          {
              Dictionary<int, PropertySetType> dataTypeHelper = new Dictionary<int, PropertySetType>();
              foreach (DataRow row in dataSet.Tables["PropertySetTypes"].Rows)
              {
                  switch (TypeConverter.ToString(row["Name"]))
                  {
                      case "NodeType":
                          dataTypeHelper.Add(TypeConverter.ToInt32(row["PropertySetTypeID"]), PropertySetType.NodeType);
                          break;
                      case "ContentListType":
                          dataTypeHelper.Add(TypeConverter.ToInt32(row["PropertySetTypeID"]), PropertySetType.ContentListType);
                          break;
                  }
              }
              return dataTypeHelper;
          }
  
          private void BuildPropertyTypes(DataTable table, Dictionary<int, DataType> dataTypes)
          {
              foreach (DataRow row in table.Rows)
              {
                  int id = TypeConverter.ToInt32(row["PropertyTypeID"]);
                  string name = TypeConverter.ToString(row["Name"]);
                  DataType dataType = dataTypes[TypeConverter.ToInt32(row["DataTypeID"])];
                  int mapping = TypeConverter.ToInt32(row["Mapping"]);
                  bool isContentListProperty = TypeConverter.ToBoolean(row["IsContentListProperty"]);
  
                  CreatePropertyType(id, name, dataType, mapping, isContentListProperty);
              }
          }
          private void BuildPropertySets(DataTable table, Dictionary<int, PropertySetType> propertySetTypes)
          {
              List<NodeTypeInfo> ntiList = new List<NodeTypeInfo>();
              foreach (DataRow row in table.Rows)
              {
                  int id = TypeConverter.ToInt32(row["PropertySetID"]);
                  int parentID = row["ParentID"] is DBNull ? 0 : TypeConverter.ToInt32(row["ParentID"]);
                  string name = TypeConverter.ToString(row["Name"]);
                  string className = row["ClassName"] is DBNull ? null : TypeConverter.ToString(row["ClassName"]);
                  PropertySetType propertySetType = propertySetTypes[TypeConverter.ToInt32(row["PropertySetTypeID"])];
                  switch (propertySetType)
                  {
                      case PropertySetType.NodeType:
                          ntiList.Add(new NodeTypeInfo(id, parentID, name, className));
                          break;
                      case PropertySetType.ContentListType:
                          CreateContentListType(id, name);
                          break;
                      default:
                          throw new InvalidSchemaException(String.Concat(SR.Exceptions.Schema.Msg_UnknownPropertySetType, propertySetType));
                  }
              }
              while (ntiList.Count > 0)
              {
                  for (int i = ntiList.Count - 1; i >= 0; i--)
                  {
                      NodeTypeInfo nti = ntiList[i];
                      NodeType parent = null;
                      if (nti.ParentId == 0 || ((parent = _nodeTypes.GetItemById(nti.ParentId)) != null))
                      {
                          CreateNodeType(nti.Id, _nodeTypes.GetItemById(nti.ParentId), nti.Name, nti.ClassName);
                          ntiList.Remove(nti);
                      }
                  }
              }
          }
          private void BuildPropertyTypeAssignments(DataTable table)
          {
              foreach (DataRow row in table.Rows)
              {
                  int propertyTypeId = TypeConverter.ToInt32(row["PropertyTypeId"]);
                  int propertySetId = TypeConverter.ToInt32(row["PropertySetID"]);
                  bool isDeclared = TypeConverter.ToBoolean(row["IsDeclared"]);
                  PropertyType propertyType = _propertyTypes.GetItemById(propertyTypeId);
                  if (propertyType == null)
                      throw new InvalidSchemaException(String.Concat(SR.Exceptions.Schema.Msg_PropertyTypeDoesNotExist, ": id=", propertyTypeId));
                  PropertySet propertySet = null;
                  if (propertyType.IsContentListProperty)
                      propertySet = _contentListTypes.GetItemById(propertySetId);
                  else
                      propertySet = _nodeTypes.GetItemById(propertySetId);
  
                  try
                  {
                      if (isDeclared)
                          AddPropertyTypeToPropertySet(propertyType, propertySet);
                  }
                  catch (ArgumentNullException ex)
                  {
                      SnLog.WriteWarning("Unknown property set: " + propertySetId, properties: new Dictionary<string, object>
                      {
                          {"PropertyTypeId", propertyType.Id},
                          {"Content list types: ", string.Join(", ", _contentListTypes.Select(cl => cl.Id))},
                          {"Node types: ", string.Join(", ", _nodeTypes.Select(nt => string.Format("{0} ({1})", nt.Name, nt.Id)))},
                      });
  
                      throw new InvalidSchemaException("Unknown property set: " + propertySetId, ex);
                  }
              }
          }
  
          // -------------------------------------------------------------------------------- ToXml
  
          public string ToXml()
          {
              StringBuilder sb = new StringBuilder();
              sb.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\" ?>");
              sb.Append("<StorageSchema xmlns=\"").Append(RepositoryStorageSchemaXmlNamespace).AppendLine("\">");
  
              if (_propertyTypes.Count > 0)
              {
                  sb.AppendLine("	<UsedPropertyTypes>");
                  foreach (PropertyType propertyType in _propertyTypes)
                      PropertyTypeToXml(propertyType, sb, "\t\t");
                  sb.AppendLine("	</UsedPropertyTypes>");
              }
  
              if (_nodeTypes.Count > 0)
              {
                  sb.AppendLine("	<NodeTypeHierarchy>");
                  foreach (NodeType nt in _nodeTypes)
                      if (nt.Parent == null)
                          NodeTypeToXml(nt, sb, "\t\t");
                  sb.AppendLine("	</NodeTypeHierarchy>");
              }
  
              if (_contentListTypes.Count > 0)
              {
                  sb.AppendLine("	<ContentListTypes>");
                  foreach (var lt in _contentListTypes)
                      ContentListTypeToXml(lt, sb, "\t\t");
                  sb.AppendLine("	</ContentListTypes>");
              }
  
              sb.AppendLine("</StorageSchema>");
              return sb.ToString();
          }
          private static void NodeTypeToXml(NodeType nt, StringBuilder sb, string indent)
          {
              // <NodeType itemID="1" name="NodeType1">
              sb.Append(indent).Append("<NodeType");
              sb.Append(" itemID=\"").Append(nt.Id).Append("\"");
              sb.Append(" name=\"").Append(nt.Name).Append("\"");
              if (nt.ClassName != null)
                  sb.Append(" className=\"").Append(nt.ClassName).Append("\"");
  
              if (nt.DeclaredPropertyTypes.Count == 0 && nt.Children.Count == 0)
              {
                  sb.AppendLine(" />");
                  return;
              }
              sb.AppendLine(">");
              // Inherited PropertyTypes are not be written
              foreach (PropertyType pt in nt.DeclaredPropertyTypes)
                  PropertyTypeReferenceToXml(pt/*, nt*/, sb, indent + "\t");
  
              // Types that are inherited from "nt"
              foreach (NodeType cnt in nt.Children)
                  NodeTypeToXml(cnt, sb, indent + "\t");
              sb.Append(indent).AppendLine("</NodeType>");
          }
          private static void ContentListTypeToXml(ContentListType lt, StringBuilder sb, string indent)
          {
              // <NodeType itemID="1" name="NodeType1">
              sb.Append(indent).Append("<ContentListType");
              sb.Append(" itemID=\"").Append(lt.Id).Append("\"");
              sb.Append(" name=\"").Append(lt.Name).Append("\"");
  
  
gt:             if (lt.PropertyTypes.Count == 0)