using System.Globalization;
using System.Xml;
using System.Text;
namespace Wayne.Lib.Log
{
    /// <summary>
    /// Static support class for managing masking of debug logs.
    /// </summary>
    public static class DebugLoggerMask
    {
        #region Fields

        private static bool forceMask;

        #endregion

        #region XmlWriter Support

        /// <summary>
        /// Writes processing instructions to an XML document to mask a node.
        /// The masking will be active from this point forward in the XML document (until the end or the instruction is redefined).
        /// </summary>
        /// <param name="xmlWriter">The XML writer.</param>
        /// <param name="nodeName">The name of the XML node.</param>
        /// <param name="maskKind">The type of masking to use.</param>
        public static void MaskXmlNode(XmlWriter xmlWriter, string nodeName, DebugLogMaskKind maskKind)
        {
            xmlWriter.WriteProcessingInstruction(XmlLogObject.XmlMaskNode,
                string.Concat(XmlLogObject.XmlNodeName, "=\"", nodeName, "\" ",
                XmlLogObject.XmlMaskKind, "=\"", maskKind, "\""));
        }

        /// <summary>
        /// Writes processing instructions to an XML document to mask the next node.
        /// </summary>
        /// <param name="xmlWriter">The XML writer.</param>
        /// <param name="maskKind">The type of masking to use.</param>
        public static void MaskNextXmlNode(XmlWriter xmlWriter, DebugLogMaskKind maskKind)
        {
            xmlWriter.WriteProcessingInstruction(XmlLogObject.XmlMaskNode,
                string.Concat(XmlLogObject.XmlNodeName, "=\"", XmlLogObject.XmlMaskNextNodeName, "\" ",
                XmlLogObject.XmlMaskKind, "=\"", maskKind, "\""));
        }

        /// <summary>
        /// Writes processing instructions to an XML document to mask an attribute.
        /// The masking will be active from this point forward in the XML document (until the end or the instruction is redefined).
        /// </summary>
        /// <param name="xmlWriter">The XML writer.</param>
        /// <param name="nodeName">The name of the XML node.</param>
        /// <param name="attributeName">The name of the node attribute.</param>
        /// <param name="maskKind">The type of masking to use.</param>
        public static void MaskXmlAttribute(XmlWriter xmlWriter, string nodeName, string attributeName, DebugLogMaskKind maskKind)
        {
            xmlWriter.WriteProcessingInstruction(XmlLogObject.XmlMaskAttribute,
                string.Concat(XmlLogObject.XmlNodeName, "=\"", nodeName, "\" ",
                XmlLogObject.XmlAttributeName, "=\"", attributeName, "\" ",
                XmlLogObject.XmlMaskKind, "=\"", maskKind, "\""));
        }

        #endregion

        #region XmlDocument Support

        /// <summary>
        /// Returns the XmlNode needed to be added to an XML document to mask a node.
        /// The masking will be active from the point forward where the node is inserted (until the end or the instruction is redefined).
        /// </summary>
        /// <param name="xmlDocument">The XML document.</param>
        /// <param name="nodeName">The name of the XML node.</param>
        /// <param name="maskKind">The type of masking to use.</param>
        public static XmlProcessingInstruction CreateXmlNodeToMaskNode(XmlDocument xmlDocument, string nodeName, DebugLogMaskKind maskKind)
        {
            return xmlDocument.CreateProcessingInstruction(XmlLogObject.XmlMaskNode,
                string.Concat(XmlLogObject.XmlNodeName, "=\"", nodeName, "\" ",
                XmlLogObject.XmlMaskKind, "=\"", maskKind, "\""));
        }

        /// <summary>
        /// Returns the XmlNode needed to be added to an XML document to mask the next node.
        /// </summary>
        /// <param name="xmlDocument">The XML document.</param>
        /// <param name="maskKind">The type of masking to use.</param>
        public static XmlProcessingInstruction CreateXmlNodeToMaskNextNode(XmlDocument xmlDocument, DebugLogMaskKind maskKind)
        {
            return xmlDocument.CreateProcessingInstruction(XmlLogObject.XmlMaskNode,
                string.Concat(XmlLogObject.XmlNodeName, "=\"", XmlLogObject.XmlMaskNextNodeName, "\" ",
                XmlLogObject.XmlMaskKind, "=\"", maskKind, "\""));
        }

        /// <summary>
        /// Returns the XmlNode needed to be added to an XML document to mask an attribute.
        /// The masking will be active from this point forward in the XML document (until the end or the instruction is redefined).
        /// </summary>
        /// <param name="xmlDocument">The XML document.</param>
        /// <param name="nodeName">The name of the XML node.</param>
        /// <param name="attributeName">The name of the node attribute.</param>
        /// <param name="maskKind">The type of masking to use.</param>
        public static XmlProcessingInstruction CreateXmlNodeToMaskAttribute(XmlDocument xmlDocument, string nodeName, string attributeName, DebugLogMaskKind maskKind)
        {
            return xmlDocument.CreateProcessingInstruction(XmlLogObject.XmlMaskAttribute,
                string.Concat(XmlLogObject.XmlNodeName, "=\"", nodeName, "\" ",
                XmlLogObject.XmlAttributeName, "=\"", attributeName, "\" ",
                XmlLogObject.XmlMaskKind, "=\"", maskKind, "\""));
        }

        #endregion

        #region Properties

        /// <summary>
        /// Turn on/off masking in debug mode.
        /// </summary>
        public static bool ForceMask
        {
            get { return forceMask; }
            set { forceMask = value; }
        }

        /// <summary>
        /// Should sensitive data be masked?
        /// </summary>
        public static bool MaskSensitiveData
        {
            get
            {
#if DEBUG
                return forceMask;
#else
                return true;
#endif
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Masks the data the requested way.
        /// </summary>
        /// <param name="data">The data to mask.</param>
        /// <param name="maskKind">The kind of masking to perform.</param>
        /// <returns></returns>
        public static string MaskData(string data, DebugLogMaskKind maskKind)
        {
#if DEBUG
            if (forceMask)
#endif
            {
                switch (maskKind)
                {
                    case DebugLogMaskKind.Remove: return string.Empty;
                    case DebugLogMaskKind.Empty: return string.Empty;
                    case DebugLogMaskKind.Mask: return "****";
                    case DebugLogMaskKind.MaskDigits:
                        {
                            StringBuilder maskedData = new StringBuilder();
                            if (!string.IsNullOrEmpty(data))
                            {
                                for (int i = 0; i < data.Length; i++)
                                {
                                    char dataChar = data[i];
                                    if ((dataChar >= '0') && (dataChar <= '9'))
                                        maskedData.Append('*');
                                    else
                                        maskedData.Append(dataChar);
                                }
                            }
                            return maskedData.ToString();
                        }
                    case DebugLogMaskKind.MaskDigitsAppendHash:
                        {
                            StringBuilder maskedData = new StringBuilder();
                            StringBuilder digits = new StringBuilder();
                            if (!string.IsNullOrEmpty(data))
                            {
                                for (int i = 0; i < data.Length; i++)
                                {
                                    char dataChar = data[i];
                                    if ((dataChar >= '0') && (dataChar <= '9'))
                                    {
                                        maskedData.Append('*');
                                        digits.Append(dataChar);
                                    }
                                    else
                                        maskedData.Append(dataChar);
                                }
                            }
                            return string.Concat(maskedData, "[#", GetHashCode(digits.ToString()), "#]");
                        }
                    case DebugLogMaskKind.MaskHex:
                        {
                            StringBuilder maskedData = new StringBuilder();
                            if (!string.IsNullOrEmpty(data))
                            {
                                for (int i = 0; i < data.Length; i++)
                                {
                                    char dataChar = data[i];
                                    if ((dataChar >= '0') && (dataChar <= '9') || (dataChar >= 'a') && (dataChar <= 'f') || (dataChar >= 'A') && (dataChar <= 'F'))
                                        maskedData.Append('*');
                                    else
                                        maskedData.Append(dataChar);
                                }
                            }
                            return maskedData.ToString();
                        }
                    case DebugLogMaskKind.MaskHexAppendHash:
                        {
                            StringBuilder maskedData = new StringBuilder();
                            StringBuilder hex = new StringBuilder();
                            if (!string.IsNullOrEmpty(data))
                            {
                                for (int i = 0; i < data.Length; i++)
                                {
                                    char dataChar = data[i];
                                    if ((dataChar >= '0') && (dataChar <= '9') || (dataChar >= 'a') && (dataChar <= 'f') || (dataChar >= 'A') && (dataChar <= 'F'))
                                    {
                                        maskedData.Append('*');
                                        hex.Append(dataChar);
                                    }
                                    else
                                        maskedData.Append(dataChar);
                                }
                            }
                            return string.Concat(maskedData, "[#", GetHashCode(hex.ToString()), "#]");
                        }
                    case DebugLogMaskKind.MaskPan:
                        {
                            if (!string.IsNullOrEmpty(data) && (data.Length > 8))
                                return string.Concat(data.Substring(0, 4), string.Empty.PadLeft(data.Length - 8, '*'), data.Substring(data.Length - 4, 4));
                            return data;
                        }
                    case DebugLogMaskKind.MaskPan64:
                        {
                            if (!string.IsNullOrEmpty(data) && (data.Length > 10))
                                return string.Concat(data.Substring(0, 6), string.Empty.PadLeft(data.Length - 10, '*'), data.Substring(data.Length - 4, 4));
                            return data;
                        }
                    case DebugLogMaskKind.MaskTrack64:
                        {
                            if (!string.IsNullOrEmpty(data) && (data.Length > 10))
                            {
                                int sepIndex = data.IndexOf("=");
                                if (sepIndex < 0)
                                {
                                    return string.Concat(data.Substring(0, 6), string.Empty.PadLeft(data.Length - 10, '*'), data.Substring(data.Length - 4, 4));
                                }
                                else
                                {
                                    string panData = data.Substring(0, sepIndex);
                                    string otherData = string.Empty;
                                    if (data.Length > sepIndex + 1)
                                    {
                                        otherData = data.Substring(sepIndex + 1, data.Length - sepIndex - 1);
                                    }
                                    return string.Concat(panData.Substring(0, 6), string.Empty.PadLeft(panData.Length - 10, '*'), panData.Substring(panData.Length - 4, 4), "=", otherData);
                                }
                            }
                            return data;
                        }
                    case DebugLogMaskKind.MaskPanAppendHash:
                        {
                            if (!string.IsNullOrEmpty(data) && (data.Length > 8))
                                return string.Concat(data.Substring(0, 4), string.Empty.PadLeft(data.Length - 8, '*'), data.Substring(data.Length - 4, 4), "[#", GetHashCode(data), "#]");
                            return data;
                        }
                    case DebugLogMaskKind.MaskEmbeddedPan:
                    case DebugLogMaskKind.MaskEmbeddedPanAppendHash:
                        {
                            StringBuilder maskedData = new StringBuilder();
                            int stringBuilderOffset = 0;
                            if (!string.IsNullOrEmpty(data))
                            {
                                int digitStart = -1;
                                for (int i = 0; i < data.Length; i++)
                                {
                                    char dataChar = data[i];
                                    bool isDigit = (dataChar >= '0') && (dataChar <= '9');
                                    bool isLastChar = (i == data.Length - 1);
                                    bool inDigitSequence = (digitStart > -1);

                                    maskedData.Append(dataChar);
                                    if (isDigit && (!isLastChar || !inDigitSequence))
                                    {
                                        if (digitStart == -1)
                                            digitStart = i + stringBuilderOffset;
                                    }
                                    else
                                    {
                                        if (digitStart > -1)
                                        {
                                            int digitLength = i + stringBuilderOffset - digitStart;
                                            if (isLastChar)
                                                digitLength++;
                                            if (digitLength > 8)
                                            {
                                                if (maskKind == DebugLogMaskKind.MaskEmbeddedPanAppendHash)
                                                {
                                                    string hashedPan = string.Concat("[#", GetHashCode(maskedData.ToString(digitStart, digitLength)), "#]");
                                                    maskedData.Remove(digitStart + 4, digitLength - 8).Insert(digitStart + 4, "*", digitLength - 8).Insert(digitStart + digitLength, hashedPan);
                                                    stringBuilderOffset += hashedPan.Length;
                                                }
                                                else
                                                    maskedData.Remove(digitStart + 4, digitLength - 8).Insert(digitStart + 4, "*", digitLength - 8);
                                            }
                                        }
                                        digitStart = -1;
                                    }
                                }
                            }
                            return maskedData.ToString();
                        }
                }
            }
            return data;
        }

        /// <summary>
        /// Masks the data the requested way.
        /// </summary>
        /// <param name="bytes">The binary data to mask.</param>
        /// <param name="showLength">Should the original length of the bytes be shown in the log?</param>
        /// <returns></returns>
        public static string MaskData(byte[] bytes, bool showLength)
        {
            return MaskData(bytes, showLength, false);
        }

        /// <summary>
        /// Masks the data the requested way.
        /// </summary>
        /// <param name="bytes">The binary data to mask.</param>
        /// <param name="showLength">Should the original length of the bytes be shown in the log?</param>
        /// <param name="showHashValue">Should a hash value be calculated and shown in the log?</param>
        /// <returns></returns>
        public static string MaskData(byte[] bytes, bool showLength, bool showHashValue)
        {
            bool isDebug = false;
#if DEBUG
            isDebug = true;
#endif
            if (forceMask || !isDebug)
            {
                StringBuilder text = new StringBuilder();
                if (!showLength)
                    text.Append("***# bytes***");
                else if (bytes == null)
                    text.Append("***null bytes***");
                else
                {
                    text.Append(bytes.Length);
                    text.Append(" bytes");
                }
                if (showHashValue)
                {
                    if (showLength)
                        text.Append(",");
                    if (bytes != null)
                    {
                        int hashValue = 0;
                        for (int i = 0; i < bytes.Length; i++)
                            hashValue = (hashValue * 31) ^ bytes[i]; // Just a simple "hash" calculation of a byte array.

                        text.Append("[#");
                        text.Append(hashValue.ToString("X8"));
                        text.Append("#]");
                    }
                    else
                        text.Append("[#--------#]");
                }

                return string.Concat("***", text, "***");
            }

            return Strings.ByteArrayToString(bytes, StringsByteArrayFormat.CompactHex);
        }

        private static string GetHashCode(string text)
        {
            string fullHashCode = text.GetHashCode().ToString("X8");
            int firstHashHalf = int.Parse(fullHashCode.Substring(0, 4), NumberStyles.HexNumber);
            int lastHashHalf = int.Parse(fullHashCode.Substring(4, 4), NumberStyles.HexNumber);
            int hashedHash = firstHashHalf ^ lastHashHalf;
            return hashedHash.ToString("X4");
        }

        #endregion
    }
}