using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using Apache.NMS;

namespace Spring.Messaging.Nms.Support.Converter
{
    /// <summary>
    /// Convert an object via XML serialization for sending via an ITextMessage
    /// </summary>
    /// <author>Mark Pollack</author>
    public class XmlMessageConverter : IMessageConverter
    {
        private IMessageConverter defaultMessageConverter = new SimpleMessageConverter();


        private ITypeMapper typeMapper = new TypeMapper();

        private bool encoderShouldEmitUTF8Identifier = false;

        private bool throwOnInvalidBytes = true;


        /// <summary>
        /// Sets the type mapper.
        /// </summary>
        /// <value>The type mapper.</value>
        public ITypeMapper TypeMapper
        {
            set { typeMapper = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether encoder should emit UTF8 byte order mark.  Default is false.
        /// </summary>
        /// <value>
        /// 	<c>true</c> to specify that a Unicode byte order mark is provided; otherwise, <c>false</c>.
        /// </value>
        public bool EncoderShouldEmitUtf8Identifier
        {
            get { return encoderShouldEmitUTF8Identifier; }
            set { encoderShouldEmitUTF8Identifier = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether to throw an exception on invalid bytes.  Default is true. </summary>
        /// <value>
        /// 	<c>true</c> to specify that an exception be thrown when an invalid encoding is detected; otherwise, <c>false</c>.
        /// </value>
        public bool ThrowOnInvalidBytes
        {
            get { return throwOnInvalidBytes; }
            set { throwOnInvalidBytes = value; }
        }

        /// <summary>
        /// Convert a .NET object to a NMS Message using the supplied session
        /// to create the message object.
        /// </summary>
        /// <param name="objectToConvert">the object to convert</param>
        /// <param name="session">the Session to use for creating a NMS Message</param>
        /// <returns>the NMS Message</returns>
        /// <throws>NMSException if thrown by NMS API methods </throws>
        /// <throws>MessageConversionException in case of conversion failure </throws>
        public IMessage ToMessage(object objectToConvert, ISession session)
        {
            if (objectToConvert == null)
            {
                throw new MessageConversionException("Can't convert null object");
            }
            try
            {
                if (objectToConvert.GetType().Equals(typeof (string)) ||
                    typeof (IDictionary).IsAssignableFrom(objectToConvert.GetType()) ||
                    objectToConvert.GetType().Equals(typeof (Byte[])))
                {
                    return defaultMessageConverter.ToMessage(objectToConvert, session);
                }
                string xmlString = GetXmlString(objectToConvert);
                IMessage msg = session.CreateTextMessage(xmlString);
                msg.Properties.SetString(typeMapper.TypeIdFieldName, typeMapper.FromType(objectToConvert.GetType()));
                return msg;
            } catch (Exception e)
            {
                throw new MessageConversionException("Can't convert object of type " + objectToConvert.GetType(), e);
            }
        }

        /// <summary>
        /// Gets the XML string for an object
        /// </summary>
        /// <param name="objectToConvert">The object to convert.</param>
        /// <returns>XML string</returns>
        protected virtual string GetXmlString(object objectToConvert)
        {
            string xmlString;
            try
            {
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    using (XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, new UTF8Encoding(EncoderShouldEmitUtf8Identifier)))
                    {
                        XmlSerializer xs = new XmlSerializer(objectToConvert.GetType());
                        xs.Serialize(xmlTextWriter, objectToConvert);
                        xmlString = UTF8ByteArrayToString(((MemoryStream)xmlTextWriter.BaseStream).ToArray());
                    }
                }
            }
            catch (Exception e)
            {
                throw new MessageConversionException("Can't convert object of type " + objectToConvert.GetType(), e);
            }
            return xmlString;
        }

        /// <summary>
        /// Convert from a NMS Message to a .NET object.
        /// </summary>
        /// <param name="messageToConvert">the message to convert</param>
        /// <returns>the converted .NET object</returns>
        /// <throws>MessageConversionException in case of conversion failure </throws>
        public object FromMessage(IMessage messageToConvert)
        {
            if (messageToConvert == null)
            {
                throw new MessageConversionException("Can't convert null message");
            }
            try
            {
                string converterId = messageToConvert.Properties.GetString(typeMapper.TypeIdFieldName);
                if (converterId == null)
                {
                    return defaultMessageConverter.FromMessage(messageToConvert);
                }
                ITextMessage textMessage = messageToConvert as ITextMessage;
                if (textMessage == null)
                {
                    throw new MessageConversionException("Can't convert message of type " +
                                                         messageToConvert.GetType());
                }

                using (MemoryStream memoryStream = new MemoryStream(StringToUTF8ByteArray(textMessage.Text)))
                {

                    XmlSerializer xs = new XmlSerializer(GetTargetType(textMessage));
                    XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8);                        
                    return xs.Deserialize(memoryStream);
                }
            } catch (Exception e)
            {
                throw new MessageConversionException("Can't convert message of type " + messageToConvert.GetType(), e);
            }
        }

        /// <summary>
        /// Gets the type of the target given the message.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <returns>Type of the target</returns>
        protected virtual Type GetTargetType(ITextMessage message)
        {
            return typeMapper.ToType(message.Properties.GetString(typeMapper.TypeIdFieldName));
        }


        /// <summary>
        /// Converts a byte array to a UTF8 string.
        /// </summary>
        /// <param name="characters">The characters.</param>
        /// <returns>UTF8 string</returns>
        protected virtual String UTF8ByteArrayToString(Byte[] characters)
        {
            UTF8Encoding encoding = new UTF8Encoding(EncoderShouldEmitUtf8Identifier, ThrowOnInvalidBytes);

            String constructedString = encoding.GetString(characters);

            return (constructedString);
        }


        /// <summary>
        /// Converts a UTF8 string to a byte array
        /// </summary>
        /// <param name="xmlString">The p XML string.</param>
        /// <returns></returns>
        protected virtual Byte[] StringToUTF8ByteArray(String xmlString)
        {
            UTF8Encoding encoding = new UTF8Encoding();

            Byte[] byteArray = encoding.GetBytes(xmlString);

            return byteArray;
        }
    }
}