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

namespace Google.ProtocolBuffers.Serialization
{
    /// <summary>
    /// Extensions and helpers to abstract the reading/writing of messages by a client-specified content type.
    /// </summary>
    public static class MessageFormatFactory
    {
        /// <summary>
        /// Constructs an ICodedInputStream from the input stream based on the contentType provided
        /// </summary>
        /// <param name="options">Options specific to reading this message and/or content type</param>
        /// <param name="contentType">The mime type of the input stream content</param>
        /// <param name="input">The stream to read the message from</param>
        /// <returns>The ICodedInputStream that can be given to the IBuilder.MergeFrom(...) method</returns>
        public static ICodedInputStream CreateInputStream(MessageFormatOptions options, string contentType, Stream input)
        {
            FormatType inputType = ContentTypeToFormat(contentType, options.DefaultContentType);

            ICodedInputStream codedInput;
            if (inputType == FormatType.ProtoBuffer)
            {
                codedInput = CodedInputStream.CreateInstance(input);
            }
            else if (inputType == FormatType.Json)
            {
                JsonFormatReader reader = JsonFormatReader.CreateInstance(input);
                codedInput = reader.ReadStartMessage();
            }
            else if (inputType == FormatType.Xml)
            {
                XmlFormatReader reader = XmlFormatReader.CreateInstance(input);
                reader.RootElementName = options.XmlReaderRootElementName;
                reader.Options = options.XmlReaderOptions;
                codedInput = reader.ReadStartMessage();
            }
            else
                throw new NotSupportedException();

            return codedInput;
        }

        /// <summary>
        /// Merges the message from the input stream based on the contentType provided
        /// </summary>
        /// <typeparam name="TBuilder">A type derived from IBuilderLite</typeparam>
        /// <param name="builder">An instance of a message builder</param>
        /// <param name="options">Options specific to reading this message and/or content type</param>
        /// <param name="contentType">The mime type of the input stream content</param>
        /// <param name="input">The stream to read the message from</param>
        /// <returns>The same builder instance that was supplied in the builder parameter</returns>
        public static TBuilder MergeFrom<TBuilder>(this TBuilder builder, MessageFormatOptions options, string contentType, Stream input) where TBuilder : IBuilderLite
        {
            ICodedInputStream codedInput = CreateInputStream(options, contentType, input);
            return (TBuilder)builder.WeakMergeFrom(codedInput, options.ExtensionRegistry);
        }
        
        /// <summary>
        /// Writes the message instance to the stream using the content type provided
        /// </summary>
        /// <param name="options">Options specific to writing this message and/or content type</param>
        /// <param name="contentType">The mime type of the content to be written</param>
        /// <param name="output">The stream to write the message to</param>
        /// <remarks> If you do not dispose of ICodedOutputStream some formats may yield incomplete output </remarks>
        public static ICodedOutputStream CreateOutputStream(MessageFormatOptions options, string contentType, Stream output)
        {
            FormatType outputType = ContentTypeToFormat(contentType, options.DefaultContentType);

            ICodedOutputStream codedOutput;
            if (outputType == FormatType.ProtoBuffer)
            {
                codedOutput = CodedOutputStream.CreateInstance(output);
            }
            else if (outputType == FormatType.Json)
            {
                JsonFormatWriter writer = JsonFormatWriter.CreateInstance(output);
                if (options.FormattedOutput)
                {
                    writer.Formatted();
                }
                writer.StartMessage();
                codedOutput = writer;
            }
            else if (outputType == FormatType.Xml)
            {
                XmlFormatWriter writer;
                if (options.FormattedOutput)
                {
                    writer = XmlFormatWriter.CreateInstance(output);
                }
                else
                {
                    XmlWriterSettings settings = new XmlWriterSettings()
                                                     {
                                                         CheckCharacters = false,
                                                         NewLineHandling = NewLineHandling.Entitize,
                                                         OmitXmlDeclaration = true,
                                                         Encoding = Encoding.UTF8,
                                                         Indent = true,
                                                         IndentChars = "  ",
                                                         NewLineChars = Environment.NewLine,
                                                     };
                    writer = XmlFormatWriter.CreateInstance(XmlWriter.Create(output, settings));
                }
                writer.RootElementName = options.XmlWriterRootElementName;
                writer.Options = options.XmlWriterOptions;
                writer.StartMessage();
                codedOutput = writer;
            }
            else
                throw new NotSupportedException();

            return codedOutput;
        }

        /// <summary>
        /// Writes the message instance to the stream using the content type provided
        /// </summary>
        /// <param name="message">An instance of a message</param>
        /// <param name="options">Options specific to writing this message and/or content type</param>
        /// <param name="contentType">The mime type of the content to be written</param>
        /// <param name="output">The stream to write the message to</param>
        public static void WriteTo(this IMessageLite message, MessageFormatOptions options, string contentType, Stream output)
        {
            using (ICodedOutputStream codedOutput = CreateOutputStream(options, contentType, output))
            {
                message.WriteTo(codedOutput);

                // This is effectivly done by Dispose(); however, if you need to finalize a message
                // without disposing the underlying stream, this is the only way to do it.
                if (codedOutput is AbstractWriter)
                    ((AbstractWriter)codedOutput).EndMessage();

                codedOutput.Flush();
            }
        }


        enum FormatType { ProtoBuffer, Json, Xml };

        private static FormatType ContentTypeToFormat(string contentType, string defaultType)
        {
            switch ((contentType ?? String.Empty).Split(';')[0].Trim().ToLower())
            {
                case "application/json":
                case "application/x-json":
                case "application/x-javascript":
                case "text/javascript":
                case "text/x-javascript":
                case "text/x-json":
                case "text/json":
                    {
                        return FormatType.Json;
                    }

                case "text/xml":
                case "application/xml":
                    {
                        return FormatType.Xml;
                    }

                case "application/binary":
                case "application/x-protobuf":
                case "application/vnd.google.protobuf":
                    {
                        return FormatType.ProtoBuffer;
                    }

                case "":
                case null:
                    if (!String.IsNullOrEmpty(defaultType))
                    {
                        return ContentTypeToFormat(defaultType, null);
                    }
                    break;
            }

            throw new ArgumentOutOfRangeException("contentType");
        }
    }
}