context: //
  // DO NOT REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  //
  // @Authors:
  //       timop
  //
  // Copyright 2004-2019 by OM International
  //
  // This file is part of OpenPetra.org.
  //
  // OpenPetra.org is free software: you can redistribute it and/or modify
  // it under the terms of the GNU General Public License as published by
  // the Free Software Foundation, either version 3 of the License, or
  // (at your option) any later version.
  //
  // OpenPetra.org is distributed in the hope that it will be useful,
  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  // GNU General Public License for more details.
  //
  // You should have received a copy of the GNU General Public License
  // along with OpenPetra.org.  If not, see <http://www.gnu.org/licenses/>.
  //
  using System;
  using System.IO;
  using System.Xml;
  using System.Collections;
  using System.Collections.Generic;
  using System.Collections.Specialized;
  using System.Text;
  using Ict.Common;
  using Ict.Common.IO;
  
  namespace Ict.Common.IO
  {
      /// <summary>
      /// TYml2Xml is able to parse a YML file and store it in an XmlDocument
      ///
      /// Simple YAML to XML converter
      /// could have also used http://yaml-net-parser.sourceforge.net/default.html#intro
      /// But I prefer to write YAML files but to work with XML in the program.
      /// The yaml net parser would just be another interface to understand how to step through the document.
      ///
      /// See also the spec http://yaml.org/spec/1.2/
      ///
      /// We only support:
      ///   indentation: http://yaml.org/spec/1.2/#id2577368
      ///        only use spaces, no tabs for indentation
      ///        Each node must be indented further than its parent node.
      ///        All sibling nodes must use the exact same indentation level.
      ///
      ///        comment lines start with #, can be indented
      ///        separators are comma and colon, lists are defined by curly and square brackets {} and []
      ///        values can be quoted to escape the colon or comma
      ///
      ///        name and colon (name:) is converted into an XML element as a parent for the following indented lines
      ///          indention only happens after node and colon without content
      ///        scalar: name and colon and a literal is converted into an attribute of the current XML element
      ///        sequence: [element1, element2, element3] are translated to &lt;element name=&quot;element1&quot;/&gt;&lt;element name=&quot;element2&quot;/&gt; below the parent node
      ///        mapping: name and value assignments {size=10, help=Test with spaces} are converted into attributes of the parent node
      ///
      ///        not supported:
      ///           node names cannot be quoted to escape colons
      ///           {params: size=10, help=Test with spaces}: leave out the params
      ///           [=name, =id, address] sorting columns are not identified this way
      ///
      ///        This class supports the loading of several derived files,
      ///        and overwriting the data of the base files;
      ///        todo: make a tag so that everything is pushed into base, before the last file is loaded
      ///              change the code: move only attributes into base tag
      ///                               add an attribute to elements that have been in base, base=&quot;yes&quot;
      ///                               this way the order of elements can be maintained easily,
      ///                               but it is still known which elements and attributes have been added since the tag
      ///              reactivate the check again, in ProcessXAML.cs: if (TXMLParser.GetAttribute(rootNode, &quot;ClassType&quot;) != &quot;abstract&quot;)
      ///        todo: save the last file after modification have been done to the xml structure (eg. loaded from csharp file)
      /// </summary>
      public class TYml2Xml
      {
          /// <summary>
          /// contains the lines of the yml document
          /// </summary>
          protected string[] lines = null;
          /// <summary>
          /// the current line that we are parsing
          /// </summary>
          protected Int32 currentLine = -1;
          /// <summary>
          /// the filename of the file that we are parsing
          /// </summary>
          protected string filename = "";
  
          /// <summary>
          /// constructor
          /// </summary>
          /// <param name="AFilename"></param>
          public TYml2Xml(string AFilename)
          {
              filename = AFilename;
  
              // file should be in unicode encoding
              // StreamReader DetectEncodingFromByteOrderMarks does not work for ANSI?
              TextReader reader = new StreamReader(filename, TTextFile.GetFileEncoding(filename), false);
              lines = reader.ReadToEnd().Replace("\r", "").Split(new char[] { '\n' });
              reader.Close();
              currentLine = -1;
          }
  
          /// <summary>
          /// constructor
          /// </summary>
          public TYml2Xml(string[] ALines)
          {
              filename = String.Empty;
              lines = ALines;
              currentLine = -1;
          }
  
          /// <summary>
          /// return either the name from the attribute, or the element name
          /// </summary>
          public static string GetElementName(XmlNode ANode)
          {
              if (TXMLParser.HasAttribute(ANode, "name"))
              {
                  return TXMLParser.GetAttribute(ANode, "name");
              }
  
              return ANode.Name;
          }
  
          /// <summary>
          /// check if the name can be used as an element name for XML
          /// </summary>
          public static bool CheckName(string AElementName)
          {
              if ((AElementName.Length > 0) && Char.IsDigit(AElementName[0]))
              {
                  return false;
              }
  
              try
              {
                  new XmlDocument().CreateElement(AElementName);
                  return true;
              }
              catch (XmlException)
              {
                  return false;
              }
          }
  
          private const Int32 DEFAULTINDENT = 4;
          private static void WriteXmlNode2Yml(StringBuilder AYmlDocument,
              Int32 ACurrentIndent,
              XmlNode ANode,
              SortedList <string, string>AInheritedAttributes)
          {
              string Indent = "".PadLeft(ACurrentIndent);
  
              if (ACurrentIndent == 0)
              {
                  if (ROOTNODEINTERNAL.StartsWith(ANode.Name))
                  {
                      AYmlDocument.Append(ANode.Name + ":");
                  }
                  else
                  {
                      AYmlDocument.Append(ROOTNODEINTERNAL + ":");
                  }
              }
              else
              {
                  if ((ANode.Name == XMLELEMENT) && TXMLParser.HasAttribute(ANode, "name"))
                  {
                      AYmlDocument.Append(Indent + TXMLParser.GetAttribute(ANode, "name") + ":");
                  }
                  else
                  {
                      AYmlDocument.Append(Indent + ANode.Name + ":");
                  }
              }
  
              // only write attributes if they are different from the parent node;
              // always write explicitly defined empty attributes
              List <XmlAttribute>attributesToWrite = new List <XmlAttribute>();
  
              foreach (XmlAttribute attr in ANode.Attributes)
              {
                  if ((ANode.ParentNode == null) || (attr.Value == String.Empty)
                      || !AInheritedAttributes.ContainsKey(attr.Name)
                      || (AInheritedAttributes[attr.Name] != attr.Value))
                  {
                      if (!((ANode.Name == XMLELEMENT) && (attr.Name == "name")) && (attr.Name != "depth"))
                      {
                          attributesToWrite.Add(attr);
                      }
                  }
              }
  
              if (ACurrentIndent == 0)
              {
                  AYmlDocument.Append(Environment.NewLine);
  
                  // for the root node, write all attributes into a separate line
                  foreach (XmlAttribute attrToWrite in attributesToWrite)
                  {
                      attrToWrite.Value = attrToWrite.Value.Replace("\\", "\\\\");
                      attrToWrite.Value = attrToWrite.Value.Replace("\r", "").Replace("\n", "\\\\n");
  
                      string value = attrToWrite.Value;
  
                      if (value.Contains(",") || value.Contains(":") || value.Contains("=")
                          || value.Contains("\"") || value.Contains("#"))
                      {
                          value = "\"" + value.Replace("\"", "\\\"") + "\"";
                      }
  
                      AYmlDocument.Append("".PadLeft(ACurrentIndent + DEFAULTINDENT) + attrToWrite.Name + ":" + value + Environment.NewLine);
                  }
              }
              else
              {
                  StringBuilder attributesYml = new StringBuilder(400);
                  bool firstAttribute = true;
  
                  foreach (XmlAttribute attrToWrite in attributesToWrite)
                  {
                      if (!firstAttribute)
                      {
                          attributesYml.Append(", ");
                      }
  
                      firstAttribute = false;
  
                      attributesYml.Append(attrToWrite.Name + "=");
  
                      attrToWrite.Value = attrToWrite.Value.Replace("\\", "\\\\");
                      attrToWrite.Value = attrToWrite.Value.Replace("\r", "").Replace("\n", "\\\\n");
  
                      if (attrToWrite.Value.Contains(",") || attrToWrite.Value.Contains(":") || attrToWrite.Value.Contains("=")
                          || attrToWrite.Value.Contains("\"") || attrToWrite.Value.Contains("#"))
                      {
                          attributesYml.Append("\"" + attrToWrite.Value.Replace("\"", "\\\"") + "\"");
                      }
                      else
                      {
                          attributesYml.Append(attrToWrite.Value);
                      }
                  }
  
                  if (!firstAttribute)
                  {
                      AYmlDocument.Append("{" + attributesYml.ToString() + "}" + Environment.NewLine);
                  }
                  else
                  {
                      AYmlDocument.Append(Environment.NewLine);
                  }
              }
  
              foreach (XmlNode childNode in ANode.ChildNodes)
              {
                  if (childNode.HasChildNodes && (childNode.FirstChild.Name == XMLLIST))
                  {
                      // write a list of values, eg: mylist: [test1, test2, test3]
                      AYmlDocument.Append("".PadLeft(ACurrentIndent + DEFAULTINDENT) + childNode.Name + ": [");
  
                      foreach (XmlNode elementNode in childNode.ChildNodes)
                      {
                          if (elementNode != childNode.FirstChild)
                          {
                              AYmlDocument.Append(", ");
                          }
  
                          AYmlDocument.Append(TXMLParser.GetAttribute(elementNode, "name"));
                      }
  
                      AYmlDocument.Append("]" + Environment.NewLine);
                      continue;
                  }
  
                  // make a deep copy of the sorted list, so that we don't modify the original list
                  SortedList <string, string>NewAttributesList = new SortedList <string, string>();
  
                  foreach (string key in AInheritedAttributes.Keys)
                  {
                      NewAttributesList.Add(key, AInheritedAttributes[key]);
                  }
  
                  // add only the attributes written in this loop
                  foreach (XmlAttribute attrToWrite in attributesToWrite)
                  {
                      if (NewAttributesList.ContainsKey(attrToWrite.Name))
                      {
                          NewAttributesList[attrToWrite.Name] = attrToWrite.Value;
                      }
                      else
                      {
                          NewAttributesList.Add(attrToWrite.Name, attrToWrite.Value);
                      }
                  }
  
                  WriteXmlNode2Yml(AYmlDocument, ACurrentIndent + DEFAULTINDENT, childNode, NewAttributesList);
              }
          }
  
          /// <summary>
          /// format the XML into zipped YML and return as Base64 string
          /// </summary>
          public static string Xml2YmlGz(XmlDocument ADoc)
          {
              StringBuilder sb = new StringBuilder(1024 * 1024 * 5);
  
              WriteXmlNode2Yml(sb, 0, ADoc.DocumentElement, new SortedList <string, string>());
  
              return PackTools.ZipString(sb.ToString());
          }
  
          /// <summary>
          /// format the XML into YML and return as string
          /// </summary>
          public static string Xml2Yml(XmlDocument ADoc)
          {
              StringBuilder sb = new StringBuilder(1024 * 1024 * 5);
  
              WriteXmlNode2Yml(sb, 0, ADoc.DocumentElement, new SortedList <string, string>());
  
              return sb.ToString();
          }
  
          /// <summary>
          /// format the XML into YML to increase readability and save to file
          /// </summary>
          public static bool Xml2Yml(XmlDocument ADoc, string AOutYMLFile)
          {
              StringBuilder sb = new StringBuilder(1024 * 1024 * 5);
  
              if ((ADoc.DocumentElement.Name == ROOTNODEINTERNAL) && ROOTNODEINTERNAL.StartsWith(ADoc.DocumentElement.FirstChild.Name))
              {
                  WriteXmlNode2Yml(sb, 0, ADoc.DocumentElement.FirstChild, new SortedList <string, string>());
              }
              else
              {
                  WriteXmlNode2Yml(sb, 0, ADoc.DocumentElement, new SortedList <string, string>());
              }
  
              StreamWriter sw = new StreamWriter(AOutYMLFile, false, System.Text.Encoding.UTF8);
  
              sw.Write(sb.ToString());
  
              sw.Close();
  
              return true;
          }
  
          /// <summary>
          /// This method can be used to check if this is the correct file type,
          /// and also find out the baseyaml or baseclass
          /// </summary>
          /// <returns>false if the yaml file cannot be read or interpreted</returns>
          public static bool ReadHeader(string AFilename, out string baseYamlOrClass)
          {
              // file should be in unicode encoding
              TextReader reader = new StreamReader(AFilename, true);
  
              baseYamlOrClass = "";
              string line = reader.ReadLine();
  
              while ((line != null) && line.Trim().StartsWith("#"))
              {
                  line = reader.ReadLine();
              }
  
              // first line should be RootNode
              line = reader.ReadLine();
  
              // second line should be the BaseYaml, if it is there, or BaseClass
              reader.Close();
  
              if (line != null)
              {
                  string nodeName, nodeContent;
  
                  if (SplitNodeStatic(line, out nodeName, out nodeContent))
                  {
                      if (nodeName == "BaseYaml")
                      {
                          baseYamlOrClass = nodeContent;
                      }
                      else if (nodeName == "BaseClass")
                      {
                          baseYamlOrClass = nodeContent;
                      }
                  }
              }
  
              return baseYamlOrClass.Length > 0;
          }
  
          /// throw an exception and tell the current position while reading the yaml file
          protected void ThrowException(string AMessage, Int32 lineNr)
          {
              throw new Exception("Error in Yml2Xml: " + AMessage + "; " +
                  "(file: " + filename + " line " + (lineNr + 1).ToString() + ")");
          }
  
          /// returns -1 for comments or invalid line numbers,
          /// otherwise the number of spaces
          /// throws exception if there is a tab in the indentation
          protected Int32 GetAbsoluteIndentation(Int32 lineNr)
          {
              if ((lineNr < 0) || (lineNr >= lines.Length))
              {
                  return -1;
              }
  
              string line = lines[lineNr];
  
              if (line.Trim().StartsWith("#") || (line.Trim().Length == 0))
              {
                  return -1;
              }
  
              Int32 counterSpaces = 0;
  
              while (counterSpaces < line.Length)
              {
                  if ((byte)line[counterSpaces] == 9)
                  {
                      ThrowException("There is a Tab character in the indentation", lineNr);
                  }
  
                  if (line[counterSpaces] == ' ')
                  {
                      counterSpaces++;
                  }
                  else
                  {
                      break;
                  }
              }
  
              return counterSpaces;
          }
  
          /// @returns the absolute indentation of the next valid line (skipping comments)
          protected Int32 GetAbsoluteIndentationNext(Int32 lineNr)
          {
              Int32 nextIndentation = -1;
  
              while (lineNr < lines.Length - 1 && nextIndentation == -1)
              {
                  lineNr++;
                  nextIndentation = GetAbsoluteIndentation(lineNr);
              }
  
              return nextIndentation;
          }
  
          /// @returns +1 if next line is more indented than the current line (skipping comments),
          /// -1 if next line is less indented, or current line is last line
          /// 0 if current line has same identation than the next line
          protected Int32 GetIndentationNext(Int32 lineNr)
          {
              Int32 currentIndentation = GetAbsoluteIndentation(lineNr);
  
              if (currentIndentation == -1)
              {
                  ThrowException("Invalid request for relative indentation", lineNr);
              }
  
              Int32 nextIndentation = -1;
  
              while (lineNr < lines.Length - 1 && nextIndentation == -1)
              {
                  lineNr++;
                  nextIndentation = GetAbsoluteIndentation(lineNr);
              }
  
              if ((nextIndentation == -1) || (nextIndentation < currentIndentation))
              {
                  return -1;
              }
              else if (nextIndentation > currentIndentation)
              {
                  return +1;
              }
  
              return 0;
          }
  
          /// <summary>
          /// does not return comment lines
          /// </summary>
          /// <returns>null if no line available</returns>
          protected string GetNextLine()
          {
              string line = null;
  
              currentLine++;
  
              while (currentLine > -1 && currentLine < lines.Length && line == null)
              {
                  line = lines[currentLine];
  
                  // skip over the comment lines
                  if (GetAbsoluteIndentation(currentLine) == -1)
                  {
                      line = null;
                      currentLine++;
                  }
              }
  
              return line;
          }
  
          /// <summary>
          /// split a line into the node name and the node content
          /// </summary>
          protected bool SplitNode(string line, out string nodeName, out string nodeContent)
          {
              Int32 posFirstColon = -1;
  
              if (line != null)
              {
                  posFirstColon = line.IndexOf(':');
              }
  
              if (posFirstColon == -1)
              {
                  nodeName = "";
                  nodeContent = "";
                  Console.WriteLine(line);
                  ThrowException("cannot find a colon in line", currentLine);
                  return false;
              }
  
              nodeName = line.Substring(0, posFirstColon).Trim();
  
              if (posFirstColon == line.Length - 1)
              {
                  // colon is the last character in the line
                  nodeContent = "";
              }
              else
              {
                  nodeContent = line.Substring(posFirstColon + 1).Trim();
              }
  
              return true;
          }
  
          /// <summary>
          /// same as SplitNode, but without an Exception, because that requires attributes from the class
          /// </summary>
          /// <param name="line"></param>
          /// <param name="nodeName"></param>
          /// <param name="nodeContent"></param>
          /// <returns></returns>
          private static bool SplitNodeStatic(string line, out string nodeName, out string nodeContent)
          {
              Int32 posFirstColon = -1;
  
              if (line != null)
              {
                  posFirstColon = line.IndexOf(':');
              }
  
              if (posFirstColon == -1)
              {
                  nodeName = "";
                  nodeContent = "";
                  Console.WriteLine(line);
  
                  // does not work in static mode: ThrowException("cannot find a colon in line", currentLine);
                  return false;
              }
  
              nodeName = line.Substring(0, posFirstColon).Trim();
  
              if (posFirstColon == line.Length - 1)
              {
                  // colon is the last character in the line
                  nodeContent = "";
              }
              else
              {
                  nodeContent = line.Substring(posFirstColon + 1).Trim();
              }
  
              return true;
          }
  
          /// <summary>
          /// strip the quotes from a string
          /// </summary>
          protected string StripQuotes(string s)
          {
              if ((s != null) && (s.Length > 1))
              {
                  if (s.StartsWith("\"") && s.EndsWith("\""))
                  {
                      s = s.Substring(1, s.Length - 2);
                  }
              }
  
              return s;
          }
  
          private bool PrintedOriginalError = false;
  
          /// <summary>
          /// this function just parses a YML file into an empty XmlDocument (created with CreateXmlDocument).
          /// No merging of documents is done here.
          /// </summary>
          private void ParseNode(XmlNode parent, int ADepth)
          {
              string line = GetNextLine();
  
              if (line != null)
              {
                  string nodeName, nodeContent;
  
                  // strip end of line comment, starting with #, that is not inside a quotation
                  if (line.LastIndexOf('#') > line.LastIndexOf('"'))
                  {
                      line = line.Substring(0, line.LastIndexOf('#'));
                  }
  
                  try
                  {
                      if (SplitNode(line, out nodeName, out nodeContent))
                      {
                          XmlNode newElement = null;
  
                          if (!CheckName(nodeName))
                          {
                              // work around invalid XML element name
                              newElement = TYml2Xml.LoadChild(parent, TYml2Xml.XMLELEMENT, ADepth);
  //                            newElement = parent.OwnerDocument.CreateElement("", TYml2Xml.XMLELEMENT, "");
                              ((XmlElement)newElement).SetAttribute("name", nodeName);
                          }
                          else
                          {
                              newElement = TYml2Xml.LoadChild(parent, nodeName, ADepth);
  //                            newElement = parent.OwnerDocument.CreateElement("", nodeName, "");
                          }
  
                          ((XmlElement)newElement).SetAttribute("depth", ADepth.ToString());
  
                          if (nodeContent.Length > 0)
                          {
                              // there is some content directly in the line
                              // can be scalar, sequence, or mapping
                              if (nodeContent.StartsWith("{"))
                              {
                                  if (!nodeContent.EndsWith("}"))
                                  {
                                      throw new Exception("missing closing curly bracket");
                                  }
  
                                  // mapping
                                  // first get the list without brackets
                                  string list = nodeContent.Substring(1, nodeContent.Length - 2).Trim();
  
                                  // now use getNextCSV which is able to deal with quoted strings
                                  while (list.Length > 0)
                                  {
                                      string mapping = StringHelper.GetNextCSV(ref list, ",");
                                      string mappingName = StringHelper.GetNextCSV(ref mapping, new string[] { "=", ":" }).Trim();
                                      string mappingValue = StripQuotes(mapping.Trim()).Replace("\\n", Environment.NewLine);
                                      try
                                      {
                                          TYml2Xml.SetAttribute(newElement, mappingName, mappingValue);
                                      }
                                      catch (XmlException e)
                                      {
                                          ThrowException(e.Message, currentLine);
                                      }
                                  }
                              }
                              else if (nodeContent.StartsWith("["))
                              {
                                  if (!nodeContent.EndsWith("]"))
                                  {
                                      throw new Exception("missing closing square bracket");
                                  }
  
                                  TYml2Xml.SetAttribute(newElement, "IsYMLSequence", "true");
  
                                  // sequence
                                  // first get the list without brackets
                                  string list = nodeContent.Substring(1, nodeContent.Length - 2).Trim();
  
                                  // now use getNextCSV which is able to deal with quoted strings
                                  while (list.Length > 0)
                                  {
                                      string value = StripQuotes(StringHelper.GetNextCSV(ref list, ",").Trim());
  
                                      bool negated = false;
  
                                      if (value[0] == '~')
                                      {
                                          negated = true;
                                          value = value.Substring(1);
                                      }
  
                                      XmlElement sequenceElement = parent.OwnerDocument.CreateElement("", TYml2Xml.XMLLIST, "");
                                      newElement.AppendChild(sequenceElement);
                                      ((XmlElement)sequenceElement).SetAttribute("name", value);
  
                                      if (negated)
                                      {
                                          ((XmlElement)sequenceElement).SetAttribute("negated", "true");
                                      }
                                  }
                              }
                              else
                              {
                                  // scalar
                                  // this should not be an element, but an attribute of the parent
                                  parent.RemoveChild(newElement);
                                  nodeContent = nodeContent.Trim().Replace("\\n", "\n");
  
                                  if (nodeContent.StartsWith("\"") && nodeContent.EndsWith("\""))
                                  {
                                      nodeContent = nodeContent.Substring(1, nodeContent.Length - 2);
                                  }
  
                                  TYml2Xml.SetAttribute(parent, nodeName, nodeContent);
                              }
                          }
  
                          // this is a parent node, so read the child nodes as well
                          if (GetIndentationNext(currentLine) > 0)
                          {
                              // There are children
                              Int32 childrenAbsoluteIndentation = GetAbsoluteIndentationNext(currentLine);
  
                              do
                              {
                                  ParseNode(newElement, ADepth);
                              } while (GetAbsoluteIndentationNext(currentLine) == childrenAbsoluteIndentation);
                          }
                      }
                  }
                  catch (Exception e)
                  {
                      if (!PrintedOriginalError)
                      {
                          TLogging.Log("Problem in line " + currentLine.ToString() + " " + line);
  
                          if (TLogging.DebugLevel > 0)
                          {
                              TLogging.Log(e.StackTrace);
                          }
  
                          PrintedOriginalError = true;
                      }
  
                      throw;
                  }
              }
          }
  
          /// <summary>
          /// this function merges two XML documents.
          /// this function does not know and care about base and hierarchical structures; use Tag for that.
          /// only for one thing it needs to consider base: check if the leaf to be added is already in base
          /// </summary>
          private static void MergeNode(XmlNode parent, XmlNode ANewNode, int ADepth)
          {
              // check if the node already exists
              // at this stage existing elements are just modified and added to
              // to get different versions, use the method Tag()
              // TYml2Xml.LoadChild will either
              // reuse an element, move an existing leaf from base to the main node, or create a new element
              XmlNode newElement = TYml2Xml.LoadChild(parent, ANewNode.Name, ADepth);
  
              // Sequences
              if (TXMLParser.GetAttribute(ANewNode, "IsYMLSequence") == "true")
              {
                  // if we find one value that is already part of the base list, then we want to overwrite the whole list
                  StringCollection baseElements = TYml2Xml.GetElements(newElement);
  
                  foreach (XmlNode element in ANewNode.ChildNodes)
                  {
                      string elementName = element.Name;
  
                      if (element.Attributes["name"] != null)
                      {
                          elementName = element.Attributes["name"].Value;
                      }
  
                      if (baseElements.Contains(elementName)
  
gt:                         && (element.Attributes["negated"] == null))