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

namespace SabreTools.Library.Writers
{
    /// <summary>
    /// ClrMamePro writer patterned heavily off of XmlTextWriter
    /// </summary>
    /// <see cref="https://referencesource.microsoft.com/#System.Xml/System/Xml/Core/XmlTextWriter.cs"/>
    public class ClrMameProWriter : IDisposable
    {
        /// <summary>
        /// State machine state for use in the table
        /// </summary>
        private enum State
        {
            Start,
            Prolog,
            Element,
            Attribute,
            Content,
            AttrOnly,
            Epilog,
            Error,
            Closed,
        }

        /// <summary>
        /// Potential token types
        /// </summary>
        private enum Token
        {
            None,
            Standalone,
            StartElement,
            EndElement,
            LongEndElement,
            StartAttribute,
            EndAttribute,
            Content,
        }

        /// <summary>
        /// Tag information for the stack
        /// </summary>
        private struct TagInfo
        {
            public string Name;
            public bool Mixed;

            public void Init()
            {
                Name = null;
                Mixed = false;
            }
        }

        /// <summary>
        /// Internal stream writer
        /// </summary>
        private StreamWriter sw;

        /// <summary>
        /// Stack for tracking current node
        /// </summary>
        private TagInfo[] stack;
        
        /// <summary>
        /// Pointer to current top element in the stack
        /// </summary>
        private int top;

        /// <summary>
        /// State table for determining the state machine
        /// </summary>
        private readonly State[] stateTable = {
            //                         State.Start      State.Prolog     State.Element    State.Attribute  State.Content   State.AttrOnly   State.Epilog
            //
            /* Token.None           */ State.Prolog,    State.Prolog,    State.Content,   State.Content,   State.Content,  State.Error,     State.Epilog,
            /* Token.Standalone     */ State.Prolog,    State.Prolog,    State.Content,   State.Content,   State.Content,  State.Error,     State.Epilog,
            /* Token.StartElement   */ State.Element,   State.Element,   State.Element,   State.Element,   State.Element,  State.Error,     State.Element,
            /* Token.EndElement     */ State.Error,     State.Error,     State.Content,   State.Content,   State.Content,  State.Error,     State.Error,
            /* Token.LongEndElement */ State.Error,     State.Error,     State.Content,   State.Content,   State.Content,  State.Error,     State.Error,
            /* Token.StartAttribute */ State.AttrOnly,  State.Error,     State.Attribute, State.Attribute, State.Error,    State.Error,     State.Error,
            /* Token.EndAttribute   */ State.Error,     State.Error,     State.Error,     State.Element,   State.Error,    State.Epilog,    State.Error,
            /* Token.Content        */ State.Content,   State.Content,   State.Content,   State.Attribute, State.Content,  State.Attribute, State.Epilog,
        };

        /// <summary>
        /// Current state in the machine
        /// </summary>
        private State currentState;

        /// <summary>
        /// Last seen token
        /// </summary>
        private Token lastToken;

        /// <summary>
        /// Get if quotes should surround attribute values
        /// </summary>
        public bool Quotes { get; set; }

        /// <summary>
        /// Constructor for opening a write from a file
        /// </summary>
        public ClrMameProWriter(string filename)
        {
            sw = new StreamWriter(filename);
            Quotes = true;

            // Element stack
            stack = new TagInfo[10];
            top = 0;
            stack[top].Init();
        }

        /// <summary>
        /// Constructor for opening a write from a stream and encoding
        /// </summary>
        public ClrMameProWriter(Stream stream, Encoding encoding)
        {
            sw = new StreamWriter(stream, encoding);
            Quotes = true;
            
            // Element stack
            stack = new TagInfo[10];
            top = 0;
            stack[top].Init();
        }

        /// <summary>
        /// Write the start of an element node
        /// </summary>
        public void WriteStartElement(string name)
        {
            try
            {
                AutoComplete(Token.StartElement);
                PushStack();
                stack[top].Name = name;
                sw.Write(name);
                sw.Write(" (");
            }
            catch
            {
                currentState = State.Error;
                throw;
            }
        }

        /// <summary>
        /// Write the end of an element node
        /// </summary>
        public void WriteEndElement()
        {
            InternalWriteEndElement(false);
        }

        /// <summary>
        /// Write the end of a mixed element node
        /// </summary>
        public void WriteFullEndElement()
        {
            InternalWriteEndElement(true);
        }

        /// <summary>
        /// Write a complete element with content
        /// </summary>
        public void WriteElementString(string name, string value)
        {
            WriteStartElement(name);
            WriteString(value);
            WriteEndElement();
        }

        /// <summary>
        /// Write the start of an attribute node
        /// </summary>
        public void WriteStartAttribute(string name)
        {
            try
            {
                AutoComplete(Token.StartAttribute);
                sw.Write(name);
                sw.Write(" ");
                if (Quotes)
                    sw.Write("\"");
            }
            catch
            {
                currentState = State.Error;
                throw;
            }
        }

        /// <summary>
        /// Write the end of an attribute node
        /// </summary>
        public void WriteEndAttribute()
        {
            try
            {
                AutoComplete(Token.EndAttribute);
            }
            catch
            {
                currentState = State.Error;
                throw;
            }
        }

        /// <summary>
        /// Write a complete attribute with content
        /// </summary>
        public void WriteAttributeString(string name, string value)
        {
            WriteStartAttribute(name);
            WriteString(value);
            WriteEndAttribute();
        }

        /// <summary>
        /// Write a standalone attribute
        /// </summary>
        public void WriteStandalone(string name, string value, bool? quoteOverride = null)
        {
            try
            {
                if (string.IsNullOrEmpty(name))
                    throw new ArgumentException();

                AutoComplete(Token.Standalone);
                sw.Write(name);
                sw.Write(" ");
                if ((quoteOverride == null && Quotes)
                    || (quoteOverride == true))
                {
                    sw.Write("\"");
                }
                sw.Write(value);
                if ((quoteOverride == null && Quotes)
                    || (quoteOverride == true))
                {
                    sw.Write("\"");
                }
            }
            catch
            {
                currentState = State.Error;
                throw;
            }
        }

        /// <summary>
        /// Write a string content value
        /// </summary>
        public void WriteString(string value)
        {
            try
            {
                if (!string.IsNullOrEmpty(value))
                {
                    AutoComplete(Token.Content);
                    sw.Write(value);
                }
            }
            catch
            {
                currentState = State.Error;
                throw;
            }
        }

        /// <summary>
        /// Close the writer
        /// </summary>
        public void Close()
        {
            try
            {
                AutoCompleteAll();
            }
            catch
            {
                // Don't fail at this step
            }
            finally
            {
                currentState = State.Closed;
                sw.Close();
            }
        }

        /// <summary>
        /// Close and dispose
        /// </summary>
        public void Dispose()
        {
            Close();
            sw.Dispose();
        }

        /// <summary>
        /// Flush the base TextWriter
        /// </summary>
        public void Flush()
        {
            sw.Flush();
        }

        /// <summary>
        /// Prepare for the next token to be written
        /// </summary>
        private void AutoComplete(Token token)
        {
            // Handle the error cases
            if (currentState == State.Closed)
                throw new InvalidOperationException();
            else if (currentState == State.Error)
                throw new InvalidOperationException();

            State newState = stateTable[(int)token * 7 + (int)currentState];
            if (newState == State.Error)
                throw new InvalidOperationException();

            // TODO: Figure out how to get attributes on their own lines ONLY if an element contains both attributes and elements
            switch (token)
            {
                case Token.StartElement:
                case Token.Standalone:
                    if (currentState == State.Attribute)
                    {
                        WriteEndAttributeQuote();
                        WriteEndStartTag(false);
                    }
                    else if (currentState == State.Element)
                    {
                        WriteEndStartTag(false);
                    }

                    if (currentState != State.Start)
                        Indent(false);

                    break;

                case Token.EndElement:
                case Token.LongEndElement:
                    if (currentState == State.Attribute)
                        WriteEndAttributeQuote();

                    if (currentState == State.Content)
                        token = Token.LongEndElement;
                    else
                        WriteEndStartTag(token == Token.EndElement);

                    break;

                case Token.StartAttribute:
                    if (currentState == State.Attribute)
                    {
                        WriteEndAttributeQuote();
                        sw.Write(' ');
                    }
                    else if (currentState == State.Element)
                    {
                        sw.Write(' ');
                    }

                    break;

                case Token.EndAttribute:
                    WriteEndAttributeQuote();
                    break;

                case Token.Content:
                    if (currentState == State.Element && lastToken != Token.Content)
                        WriteEndStartTag(false);

                    if (newState == State.Content)
                        stack[top].Mixed = true;

                    break;

                default:
                    throw new InvalidOperationException();
            }

            currentState = newState;
            lastToken = token;
        }

        /// <summary>
        /// Autocomplete all open element nodes
        /// </summary>
        private void AutoCompleteAll()
        {
            while (top > 0)
            {
                WriteEndElement();
            }
        }

        /// <summary>
        /// Internal helper to write the end of an element
        /// </summary>
        private void InternalWriteEndElement(bool longFormat)
        {
            try
            {
                if (top <= 0)
                    throw new InvalidOperationException();

                AutoComplete(longFormat ? Token.LongEndElement : Token.EndElement);
                if (this.lastToken == Token.LongEndElement)
                {
                    Indent(true);
                    sw.Write(')');
                }

                top--;
            }
            catch
            {
                currentState = State.Error;
                throw;
            }
        }

        /// <summary>
        /// Internal helper to write the end of a tag
        /// </summary>
        private void WriteEndStartTag(bool empty)
        {
            if (empty)
                sw.Write(" )");
        }

        /// <summary>
        /// Internal helper to write the end of an attribute
        /// </summary>
        private void WriteEndAttributeQuote()
        {
            if (Quotes)
                sw.Write("\"");
        }

        /// <summary>
        /// Internal helper to indent a node, if necessary
        /// </summary>
        private void Indent(bool beforeEndElement)
        {
            if (top == 0)
            {
                sw.WriteLine();
            }
            else if (!stack[top].Mixed)
            {
                sw.WriteLine();
                int i = beforeEndElement ? top - 1 : top;
                for (; i > 0; i--)
                {
                    sw.Write('\t');
                }
            }
        }

        /// <summary>
        /// Move up one element in the stack
        /// </summary>
        private void PushStack()
        {
            if (top == stack.Length - 1)
            {
                TagInfo[] na = new TagInfo[stack.Length + 10];
                if (top > 0) Array.Copy(stack, na, top + 1);
                stack = na;
            }

            top++; // Move up stack
            stack[top].Init();
        }
    }
}
