// **********************************************************************************************************
//    BytePackingXml.cs
//    5/18/2022
//    NGI - Next Generation Interceptor
//    
//    Contract No. HQ0856-21-C-0003/1022000209
//    
//    THIS DOCUMENT DOES NOT CONTAIN TECHNOLOGY OR TECHNICAL DATA CONTROLLED UNDER EITHER THE U.S. 
//    INTERNATIONAL TRAFFIC IN ARMS REGULATIONS OR THE U.S. EXPORT ADMINISTRATION REGULATIONS.
//    
//    RAYTHEON PROPRIETARY: THIS DOCUMENT CONTAINS DATA OR INFORMATION PROPRIETARY TO RAYTHEON 
//    COMPANY AND IS RESTRICTED TO USE ONLY BY PERSONS AUTHORIZED BY RAYTHEON COMPANY IN WRITING TO USE IT. 
//    DISCLOSURE TO UNAUTHORIZED PERSONS WOULD LIKELY CAUSE SUBSTANTIAL COMPETITIVE HARM TO RAYTHEON 
//    COMPANY'S BUSINESS POSITION. NEITHER SAID DOCUMENT NOR ITS CONTENTS SHALL BE FURNISHED OR DISCLOSED 
//    TO OR COPIED OR USED BY PERSONS OUTSIDE RAYTHEON COMPANY WITHOUT THE EXPRESS WRITTEN APPROVAL OF 
//    RAYTHEON COMPANY.
//    
//    UNPUBLISHED WORK - COPYRIGHT RAYTHEON COMPANY.
//    
//    DESTRUCTION NOTICE: FOR CLASSIFIED DOCUMENTS FOLLOW THE PROCEDURES IN DOD 5220.22-M, 
//    NATIONAL INDUSTRIAL SECURITY PROGRAM OPERATING MANUAL, FEBRUARY 2006, 
//    INCORPORATING CHANGE 1, MARCH 28, 2013, CHAPTER 5, SECTION 7, OR DODM 5200.01-VOLUME 3, 
//    DOD INFORMATION SECURITY PROGRAM: PROTECTION OF CLASSIFIED INFORMATION, ENCLOSURE 3, 
//    SECTION 17. FOR CONTROLLED UNCLASSIFIED INFORMATION FOLLOW THE PROCEDURES IN DODM 5200.01-VOLUME 4, 
//    INFORMATION SECURITY PROGRAM: CONTROLLED UNCLASSIFIED INFORMATION.
//    
//    CONTROLLED BY: MISSILE DEFENSE AGENCY
//    CONTROLLED BY: GROUND-BASED MIDCOURSE DEFENSE PROGRAM OFFICE
//    CUI CATEGORY: CTI 
//    DISTRIBUTION/DISSEMINATION CONTROL: F 
//    POC: Alex Kravchenko (1118268)
// **********************************************************************************************************
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.XPath;

namespace Raytheon.Common.Coe
{
   // Takes an XML ICD and adds in nodes to represent the byte packing
   // The padding added will be displayed in the tool as arrays of characters
   internal class BytePackingXml : XmlDocument
   {
      private readonly int m_Packing = 0;
      private readonly Dictionary<string, double> m_ConstMap;
      private readonly Dictionary<string, int> m_SizeMap;
      private readonly Dictionary<string, int> m_PadMap;

      public BytePackingXml(string icdStr) : base()
      {
         // Create the dictionaries
         m_ConstMap = new Dictionary<string, double>();
         m_PadMap = new Dictionary<string, int>();
         m_SizeMap = new Dictionary<string, int>();

         // Create an XML document from the string         
         LoadXml(icdStr);

         XPathNavigator node = CreateNavigator();
         //Get the type of packing
         XPathNodeIterator nodeset = node.Select("/interface/packing");
         if (nodeset.MoveNext() == true)
         {
            if (!Parse.Try(nodeset.Current.Value.Trim(), out m_Packing))
            {
               switch (nodeset.Current.Value.Trim())
               {
                  case "char":
                  case "1":
                     m_Packing = 1;
                     break;
                  case "short":
                  case "2":
                     m_Packing = 2;
                     break;
                  case "long":
                  case "4":
                     m_Packing = 4;
                     break;
                  case "double":
                  case "8":
                  default:
                     m_Packing = 8;
                     break;
               }
            }
         }

         // Handle all of the constants
         nodeset = node.Select("/interface/constant|/interface/namespace/constant");
         while (nodeset.MoveNext())
         {
            ProcessConstantNode(nodeset.Current);
         }

         nodeset = node.Select("/interface/typedef|/interface/namespace/typedef");
         while (nodeset.MoveNext())
         {
            ProcessTypedefNode(nodeset.Current);
         }

         nodeset = node.Select("/interface/enum|/interface/namespace/enum");
         while (nodeset.MoveNext())
         {
            ProcessEnumerationNode(nodeset.Current);
         }

         nodeset = node.Select("/interface/structure|/interface/message|/interface/namespace/structure|/interface/namespace/message");
         while (nodeset.MoveNext())
         {
            ProcessStructureNode(nodeset.Current);
         }

         NormalizeIcdLabels();
      }

      // This function takes all of the messages in the ICD
      // and converts the labels into decimal, so when we do
      // a string lookup, they will all be in the same known format
      private void NormalizeIcdLabels()
      {
         XPathNavigator navigator = CreateNavigator();
         XPathNodeIterator nodeset;

         nodeset = navigator.Select("/interface/message/label|/interface/namespace/message/label");
         while (nodeset.MoveNext())
         {
            try
            {
               double dLabel = GetConstFromString(nodeset.Current.Value);
               nodeset.Current.SetValue(((int)dLabel).ToString());
            }
            catch
            {
               throw new Exception("Message Label, " + nodeset.Current.Value + ", can not be converted to an integer");
            }
         }
      }

      private void ProcessConstantNode(XPathNavigator node)
      {
         string name = "";
         string constStr = "";
         double constNum = 0;

         if (node.MoveToChild("name", ""))
         {
            name = node.Value.Trim();
            node.MoveToParent();
         }
         if (node.MoveToChild("value", ""))
         {
            constStr = node.Value.Trim();
            if ((constStr.Length != 0) && (constStr[0] != '\"'))
            {
               constNum = GetConstFromString(constStr);
            }
            else
            {
               constNum = 0;
            }
            node.MoveToParent();
         }

         // Verify the correctnes of the <constant> tag
         if ((name != "") && (constStr != ""))
         {
            AddItemToMap(m_ConstMap, name, constNum);
         }
         else
         {
            throw new Exception(
                  "ERROR: Constant Definition Incorrect - <name>:" + name +
                  " <value>:" + constStr);
         }
      }

      private void ProcessTypedefNode(XPathNavigator node)
      {
         string name = "";
         string type = "";
         int typeSize = 0; // Size of the item
         int typePad = 0;  //Size of the largest item to pad to

         if (node.MoveToChild("name", ""))
         {
            name = node.Value.Trim();
            node.MoveToParent();
         }
         if (node.MoveToChild("value", ""))
         {
            type = node.Value.Trim();
            GetSizeFromType(type, out typeSize, out typePad);
            node.MoveToParent();
         }

         // Verify the correctnes of the <typedef> tag
         if ((name != "") && (type != ""))
         {
            AddItemToMap(m_PadMap, name, typePad);
            AddItemToMap(m_SizeMap, name, typeSize);
         }
         else
         {
            throw new Exception(
                  "ERROR: Typedef Definition Incorrect - <name>:" + name +
                  " <value>:" + type);
         }
      }

      private void ProcessEnumerationNode(XPathNavigator node)
      {
         string name;
         double constNum = 0;
         var constStr = string.Empty;

         if (node.MoveToChild("name", ""))
         {
            name = node.Value.Trim();
            AddItemToMap(m_PadMap, name, 4);
            AddItemToMap(m_SizeMap, name, 4);
            node.MoveToParent();
         }
         else
         {
            throw new Exception("ERROR: Enumeration Definition Incorrect. No <name> tag present.");
         }

         XPathNodeIterator nodeSet = node.Select("item|enum_item");
         while (nodeSet.MoveNext())
         {
            name = string.Empty;

            if ((nodeSet.Current.MoveToChild("name", "")) ||
                (nodeSet.Current.MoveToChild("item_name", "")))
            {
               name = nodeSet.Current.Value.Trim();
               nodeSet.Current.MoveToParent();
            }
            if (nodeSet.Current.MoveToChild("value", ""))
            {
               constStr = nodeSet.Current.Value.Trim();
               constNum = GetConstFromString(constStr);
               nodeSet.Current.MoveToParent();
            }

            // Verify the correctnes of the <enum><item> tag
            if ((name != "") && (constStr != ""))
            {
               AddItemToMap(m_ConstMap, name, constNum);
            }
            else
            {
               throw new Exception($"ERROR: Enumeration Item Definition Incorrect - <name>: {name} <value>: {constStr}");
            }
         }
      }

      private void ProcessStructureNode(XPathNavigator node)
      {
         string name;

         if (node.MoveToChild("name", ""))
         {
            name = node.Value.Trim();
            node.MoveToParent();
         }
         else
         {
            throw new Exception("ERROR: Stucture/Message Definition Incorrect. No <name> tag present.");
         }

         int maxSize = 0;
         int padCount = 0;
         uint bitCount = 0; // Used to see how many bits have been processed.
         int lastItemSize = 0;

         var nodeSet = node.Select("item|struct_item|msg_item");
         while (nodeSet.MoveNext())
         {
            GetItemSize(nodeSet.Current, out int padSize, out int itemSize, out int reps, out uint bits);
            if ((lastItemSize != itemSize) || ((bitCount + bits) > (uint)(itemSize * 8)))
            {
               bitCount = 0; // Size changed or bit rollover
            }

            if (bitCount == 0)
            {
               padCount += AddPadding(node, nodeSet.Current, padSize, padCount);

               // Set maxSize
               if (padSize > maxSize)
               {
                  maxSize = padSize;
               }

               // Keep up with the pad count
               padCount += (itemSize * reps);
            }

            lastItemSize = itemSize;
            bitCount += bits;
         }

         if (maxSize != 0)
         {
            // Add final padding
            padCount += AddPadding(node, null, maxSize, padCount);
         }

         AddItemToMap(m_PadMap, name, maxSize);
         AddItemToMap(m_SizeMap, name, padCount);
      }

      private void GetItemSize(XPathNavigator node, out int padSize, out int itemSize, out int reps, out uint bits)
      {
         string name = "";

         if ((node.MoveToChild("name", "")) ||
             (node.MoveToChild("item_name", "")))
         {
            name = node.Value.Trim();
            node.MoveToParent();
         }

         itemSize = -1;
         padSize = -1;
         reps = 1;
         bits = 0;

         var nodeSet = node.Select("type");
         while (nodeSet.MoveNext())
         {
            GetSizeFromType(nodeSet.Current.Value.Trim(), out padSize, out itemSize);
         }

         nodeSet = node.Select("bits");
         if (nodeSet.MoveNext())
         {
            bits = (uint)GetConstFromString(nodeSet.Current.Value.Trim());
         }

         nodeSet = node.Select("arrayLength|imageWidth|imageHeight");
         while (nodeSet.MoveNext())
         {
            try
            {
               reps *= (int)GetConstFromString(nodeSet.Current.Value.Trim());
            }
            catch (Exception e)
            {
               throw new Exception
                  (e.Message + "  item name = \"" + name + "\", tag = <" +
                        nodeSet.Current.Name + ">.");

            }
         }

         if ((itemSize == -1) || (padSize == -1))
         {
            throw new Exception
               ("ERROR: Item named " + name + "does not contain a <type> tag.");
         }

         if (bits == 0)
            bits = (uint)padSize * 8;
      }

      private double GetConstFromString(string constStr)
      {
         // remove namespace
         constStr = Regex.Replace(constStr, @"[^:]+::([^:]+)", "$1");
         if ((constStr.Length > 0) && (constStr[0] == '\''))
         {
            byte charData = (byte)constStr[1];
            constStr = charData.ToString();
         }

         if (Parse.Try(constStr, out double data) == false)
         {
            if (Parse.Try(constStr, out int iData) == false)
            {
               try
               {
                  data = m_ConstMap[constStr];
               }
               catch
               {
                  throw new Exception("ERROR: ConstantValue - \"" + constStr + "\" does not resolve to a number.");
               }
            }
            else
            {
               data = (double)iData;
            }
         }
         return data;
      }

      private void AddItemToMap(Dictionary<string, int> map, string name, int value)
      {
         if (map.ContainsKey(name))
         {
            throw new Exception("ERROR: Element " + name + " is defined multiple times.");
         }
         else
         {
            map.Add(name, value);
         }
      }

      private void AddItemToMap(Dictionary<string, double> map, string name, double value)
      {
         if (map.ContainsKey(name))
         {
            throw new Exception("ERROR: Element " + name + " is defined multiple times.");
         }
         else
         {
            map.Add(name, value);
         }
      }

      private void GetSizeFromType(string type, out int typePad, out int typeSize)
      {
         // Remove all whitespace
         type = type.Replace(" ", "");
         // remove namespace
         type = Regex.Replace(type, @"[^:]+::([^:]+)", "$1");

         switch (type)
         {
            case "uint8_t":
               type = "char";
               break;
            case "uint16_t":
            case "int16_t":
               type = "short";
               break;
            case "uint32_t":
            case "int32_t":
               type = "int";
               break;
            case "uint64_t":
            case "int64_t":
               type = "double";
               break;
         }

         if ((type == "char"))
         {
            typePad = 1;
            typeSize = 1;
         }
         else if ((type == "short"))
         {
            typePad = 2;
            typeSize = 2;
         }
         else if ((Regex.IsMatch(type, @"unsigned", RegexOptions.IgnoreCase)) || (type == "int") || (type == "float") ||
                  (type == "boolean") || (type == "address"))
         {
            typePad = 4;
            typeSize = 4;
         }
         else if ((type == "double"))
         {
            typePad = 8;
            typeSize = 8;
         }
         else // The type is complex and has already been defined
         {
            try
            {
               typePad = m_PadMap[type];
               typeSize = m_SizeMap[type];
            }
            catch
            {
               throw new Exception("ERROR: <type> - " + type + " used without being defined.");
            }
         }
      }

      private int AddPadding(XPathNavigator ParentElement, XPathNavigator CurrentElement, int padSize, int padCount)
      {
         int padAdd = 0;
         int padTo = padSize;

         if (m_Packing < padSize)
         {
            padTo = m_Packing;
         }

         if (m_Packing > 0 && padTo != 0 && (padCount % padTo != 0))
         {
            padAdd = padTo - (padCount % padTo);
            InsertPaddingNode(ParentElement, CurrentElement, padAdd);
         }

         return padAdd;
      }

      private void InsertPaddingNode(XPathNavigator ParentElement, XPathNavigator CurrentElement, int padAdd)
      {
         string pad = "<item>" +
                         "<name>" + Message.PADDING_ITEM_NAME + "</name>" +
                         "<type>char</type>" +
                         "<arrayLength>" + padAdd + "</arrayLength>" +
                         "<instruType>S</instruType>" +
                      "</item>";
         if (CurrentElement != null)
         {
            CurrentElement.InsertBefore(pad);
         }
         else // End padding
         {
            ParentElement.AppendChild(pad);
         }
      }

   }
}
