using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace Wayne.Lib
{
    /// <summary>
    /// A util class to create <see cref="XmlDocument"/> and <see cref="XmlTextReader"/> classes resistant to XXE attacks.>
    /// </summary>
    /// <seealso>
    ///     <cref>https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet</cref>
    /// </seealso>
    public static class Xml
    {
        #region XmlDocument Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="XmlDocument"/> class.
        /// </summary>        
        public static XmlDocument Document()
        {
            return FixDocument(new XmlDocument());
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="XmlDocument"/> class with the specified <see cref="XmlNameTable"/>.
        /// </summary>       
        public static XmlDocument Document(XmlNameTable nt)
        {
            return FixDocument(new XmlDocument(nt));
        }

        #endregion

        #region XmlReader Constructors

        /// <summary>
        /// Replace <see cref="XmlReader"/> Create method. Creates a new <see cref="XmlReader"/> instance using the specified <see cref="Stream"/> with default settings.
        /// </summary>  
        public static XmlReader Reader(Stream input)
        {
            return XmlReader.Create(input, FixSettings());
        }

        /// <summary>
        /// Replace <see cref="XmlReader"/> Create method. Creates a new <see cref="XmlReader"/> instance using the specified <see cref="Stream"/> and <see cref="XmlReaderSettings"/>.
        /// </summary>  
        public static XmlReader Reader(Stream input, XmlReaderSettings settings)
        {
            return XmlReader.Create(input, FixSettings(settings));
        }

        /// <summary>
        /// Replace <see cref="XmlReader"/> Create method. Creates a new <see cref="XmlReader"/> instance by using the specified <see cref="System.IO.TextReader"/>.
        /// </summary>  
        public static XmlReader Reader(TextReader input)
        {
            return XmlReader.Create(input, FixSettings());
        }

        /// <summary>
        /// Replace <see cref="XmlReader"/> Create method. Creates a new <see cref="XmlReader"/> instance by using the specified <see cref="System.IO.TextReader"/> and <see cref="XmlReaderSettings"/>.
        /// </summary>  
        public static XmlReader Reader(TextReader input, XmlReaderSettings settings)
        {
            return XmlReader.Create(input, FixSettings(settings));
        }

        #endregion        

        #region GetEncoding 

        /// <summary>
        /// Gets the encoded <see cref="string"/> of the undeling xml data. Uses <see cref="XmlTextReader"/> to read the data.
        /// </summary>
        public static string GetEncodedString(byte[] data, int offset, int length)
        {
            string ret = null;

            System.Text.Encoding encoding = GetEncoding(data, offset, length);            

            if (encoding != null && encoding.CodePage != 0)
            {
                ret = encoding.GetString(data, offset, length);
            }
            else
            {
                ret = System.Text.Encoding.UTF8.GetString(data, offset, length);
            }

            return ret;
        }

        #endregion

        #region Serialization

        /// <summary>
        /// Serializes a class instance to <see cref="String"/>.
        /// </summary>  
        public static string Serialize<T>(T instance) where T : class
        {
            var xmlWriterSettings = new XmlWriterSettings
            {
                Encoding = new UTF8Encoding(false)
            };

            using (var stream = new MemoryStream())
            using (var writer = XmlWriter.Create(stream, xmlWriterSettings))
            {
                var serializer = new XmlSerializer(typeof(T));
                serializer.Serialize(writer, instance);
                return System.Text.Encoding.UTF8.GetString(stream.ToArray(), 0, (int)stream.Length);
            }
        }

        /// <summary>
        /// Deserialize a xml <see cref="String"/>.
        /// </summary>
        public static T Deserialize<T>(string xml) where T : class
        {
            using (var reader = new StringReader(xml))
            {
                var serializer = new XmlSerializer(typeof(T));

                return serializer.Deserialize(reader) as T;
            }
        }

        /// <summary>
        /// Deserializes XmlDocument object to Serializable object of type T.
        /// </summary>
        /// <typeparam name="T">Serializable object type as output type.</typeparam>
        /// <param name="document">XmlDocument object to be deserialized.</param>
        /// <returns>Deserialized serializable object of type T.</returns>
        public static T Deserialize<T>(XmlDocument document) where T : class
        {
            using (var reader = new XmlNodeReader(document))
            {
                var serializer = new XmlSerializer(typeof(T));
                
                return serializer.Deserialize(reader) as T;
            }            
        }

        #endregion

        #region Implementation

        private static System.Text.Encoding GetEncoding(byte[] data, int offset, int length)
        {
            System.Text.Encoding encoding = null;

            using (var stream = new MemoryStream(data, offset, length))
            {
                using (var reader = FixTextReader(new XmlTextReader(stream)))
                {
                    reader.Read();
                    encoding = reader.Encoding;
                }
            }

            return encoding;
        }

        private static XmlDocument FixDocument(XmlDocument document)
        {
            document.XmlResolver = null; // Setting XmlResolver to NULL disables DTDs - Its NOT null by default.

            return document;
        }

        private static XmlTextReader FixTextReader(XmlTextReader reader)
        {

#if WindowsCE || OldFramework //Dtd processing is not supprted in .Net Compact Framework
#else
            reader.DtdProcessing = DtdProcessing.Prohibit; // Needed because the default is Parse!!                
#endif

            return reader;
        }

        private static XmlReaderSettings FixSettings()
        {
            return FixSettings(null);
        }

        private static XmlReaderSettings FixSettings(XmlReaderSettings settings)
        {
            if (settings == null)
            {
                settings = new XmlReaderSettings();
            }

#if WindowsCE || OldFramework //Dtd processing is not supprted in .Net Compact Framework
#else
            settings.DtdProcessing = DtdProcessing.Prohibit; // Needed because the default is Parse!!     
#endif

            return settings;
        }

        #endregion
    }
}