////////////////////////////////////////////////////////////////////////////////
//
//  Microsoft Research Singularity
//
//  Copyright (c) Microsoft Corporation.  All rights reserved.
//
//  Note:   This is a very limited Xml Reader.
//
//  BUG:    The parser cannot handle attributes in single quotes
//          Actually, that's a feature -- XML requires the quotes.
//
namespace Microsoft.Singularity.Xml
{
    using System;
    using System.IO;
    using System.Collections;
    using System.Text;

    /// <summary>
    /// This is a simple XML Parser that does no validation.  All it does it parse the syntax of XML
    /// </summary>
    public class XmlReader
    {
        private States state;
        private int lineNumber;
        XmlNode doc; 
        
        public XmlReader()
        {
            state = States.START;
            lineNumber = 1;
        }

        private enum TokenType
        {
            NAME,
            STRING_LITERAL,
            OPERATOR,
            NONE
        }

        private enum States
        {
            START,
            NEED_ELEMENT_NAME,
            NEED_ATTRIBUTE_NAME,
            NEED_EQUALS_SIGN,
            NEED_ATTRIBUTE_VALUE,
            NEED_CLOSURE_BRACKET,
            NEED_CLOSURE_NAME,
            NEED_FINAL_BRACKET,
            QUESTION_MARK_ELEMENT,
            GET_STRINGS
        }

        public XmlNode Parse(byte[] xmlBytes)
        {
            MemoryStream ms = new MemoryStream(xmlBytes);
            StreamReader sr = new StreamReader(ms);
            doc = new XmlNode("::xml");
            return ParseHelper(doc, sr);
        }

        public XmlNode Parse(string filePath)
        {
            StreamReader sr = new StreamReader(filePath);
            doc = new XmlNode("::xml");
            return ParseHelper(doc, sr);
        }


        private XmlNode ParseHelper(XmlNode doc, TextReader sr)
        {
        
            TokenType type;
            string token = null;
            ArrayList xmlNodes = new ArrayList();
            XmlNode curNode = null;
            Stack st = new Stack();
            string curAttributeName = null;

            while (ReadToken(sr, out token, out type)) {
                switch (state) {
                    case States.START:
                        //Console.WriteLine("START");
                        if (type == TokenType.OPERATOR && token.Equals("<")) {
                            state = States.NEED_ELEMENT_NAME;
                        }
                        else {
                            // All other text is interpreted as freestanding text.
                            // It had better occur within a tag!
                            if (st.Count == 0) {
                                throw new XmlException("Line " + lineNumber + ": Text must be inside a tag");
                            }

                            XmlNode lastNode = (XmlNode)st.Peek();
                            lastNode.AddText(token);

                            // Next state depends on whether we're at the end of the text-within-a-tag
                            // already or not.
                            if ((char)sr.Peek() == '<') {
                                // Looks like we're already at the end
                                state = States.START;
                            }
                            else {
                                state = States.GET_STRINGS;
                            }
                        }
                        break;

                    case States.NEED_ELEMENT_NAME:
                        //Console.WriteLine("NEED_ELEMENT_NAME");
                        if (type == TokenType.NAME) {
                            state = States.NEED_ATTRIBUTE_NAME;
                            curNode = new XmlNode(token, st.Count);

                            //Console.WriteLine("Saw beginning of element: " + token);
                        }
                        else if (type == TokenType.OPERATOR && token.Equals("/")) {
                            state = States.NEED_CLOSURE_NAME;
                        }
                        else if (type == TokenType.OPERATOR &&
                                 (token.Equals("?") || token.Equals("!") || token.Equals('-'))) {
                            // then go to the question mark state and ignore this element
                            state = States.QUESTION_MARK_ELEMENT;
                        }
                        else {
                            throw new XmlException("Line " + lineNumber + ": Can only begin a tag with / or a name");
                        }
                        break;

                    case States.NEED_ATTRIBUTE_NAME:
                        //Console.WriteLine("NEED_ATTRIBUTE_NAME");
                        if (type == TokenType.NAME) {
                            state = States.NEED_EQUALS_SIGN;
                            curAttributeName = token;
                        }
                        else if (type == TokenType.OPERATOR && token.Equals(">")) {
                            // hold onto this node so we can check it later
                            state = States.START;
                            bool stackEmpty = st.Count == 0;

                            if (!stackEmpty) {
                                XmlNode parent = (XmlNode)st.Peek();
                                parent.AddChild(curNode);
                                //DebugStub.WriteLine("add {1} to {0}", 
                                //    __arglist(parent.Name, curNode.Name));
                            }
                            else doc.AddChild(curNode);
                            st.Push(curNode);

                            curNode = null;
                        }
                        else if (type == TokenType.OPERATOR && token.Equals("/")) {
                            // this node is almost complete
                            state = States.NEED_CLOSURE_BRACKET;
                        }
                        else {
                            throw new XmlException("Line " + lineNumber + ": Must have either attributes, '/>', or '>' after the name of an element");
                        }
                        break;

                    case States.NEED_EQUALS_SIGN:
                        //Console.WriteLine("NEED_EQUALS_SIGN");
                        if (type == TokenType.OPERATOR && token.Equals("=")) {
                            state = States.NEED_ATTRIBUTE_VALUE;
                        }
                        else {
                            throw new XmlException("Line " + lineNumber + ": Need an '=' after an attribute name");
                        }
                        break;

                    case States.NEED_ATTRIBUTE_VALUE:
                        //Console.WriteLine("NEED_ATTRIBUTE_VALUE");
                        if (type == TokenType.STRING_LITERAL) {
                            state = States.NEED_ATTRIBUTE_NAME;
                            string unescaped_attribute_value = ExpandEntityReferences(token);
                            curNode[curAttributeName] = unescaped_attribute_value;
                            //DebugStub.WriteLine("  {2}.attr[{0}]={1}",
                            //    __arglist(curAttributeName ,unescaped_attribute_value, curNode.Name));
                            curAttributeName = null;
                        }
                        else {
                            throw new XmlException("Line " + lineNumber + ": Must have an attribute value after the '=' in an XML node");
                        }
                        break;

                    case States.NEED_CLOSURE_BRACKET:
                        //Console.WriteLine("NEED_CLOSURE_BRACKET");
                        if (type == TokenType.OPERATOR && token.Equals(">")) {
                            // this node is done, and we don't have to check it
                            state = States.START;
                            bool stackEmpty = st.Count == 0;

                            if (!stackEmpty) {
                                XmlNode parent = (XmlNode)st.Peek();
                                parent.AddChild(curNode);
                            }
                            else doc.AddChild(curNode);
                        }
                        else {
                            throw new XmlException("Line " + lineNumber + ": Must have a '>' after a closing '/' in an XML node");
                        }
                        break;

                    case States.NEED_CLOSURE_NAME:
                        //Console.WriteLine("NEED_CLOSURE_NAME");
                        if (type == TokenType.NAME) {
                            // pop the last XmlNode and make sure that this name matches.
                            // Otherwise we don't have balanced open and close tags
                            state = States.NEED_FINAL_BRACKET;
                            XmlNode xmln = (XmlNode)st.Pop();
                            if (!token.Equals(xmln.Name)) {
                                throw new XmlException("Line " + lineNumber + ": " + token + " does not match " + xmln.Name);
                            }
                            //Console.WriteLine("Saw end of element: " + token);
                        }
                        else {
                            throw new XmlException("Line " + lineNumber + ": Must have a name after an opening />");
                        }

                        break;
                    case States.NEED_FINAL_BRACKET:
                        //Console.WriteLine("NEED_FINAL_BRACKET");
                        if (type == TokenType.OPERATOR && token.Equals(">")) {
                            state = States.START;
                        }
                        else {
                            throw new XmlException("Line " + lineNumber + ": Must have a > after a closure tag's name");
                        }
                        break;

                    case States.QUESTION_MARK_ELEMENT:
                        // just stay in this state until you see a '>'
                        while ('>' != ReadCharacter(sr)) {
                            ;
                        }
                        state = States.START;
                        break;

                    case States.GET_STRINGS:
                        {
                            // stay in this state until you see a '<'
                            StringBuilder sb = new StringBuilder();
                            XmlNode prevNode = (XmlNode)st.Peek();

                            if (type == TokenType.OPERATOR && token.Equals("<")) {
                                throw new XmlException("Unexpected tag beginning while in text state");
                            }

                            sb.Append(token);
                            while ((char)sr.Peek() != '<') {
                                sb.Append((char)sr.Read());
                            }

                            prevNode.AddText(sb.ToString());
                            state = States.START;
                            //Console.WriteLine("Grabbed string data");
                        }
                        break;
                }
            }

            sr.Close();
            return doc;
        }

        /// <summary>
        /// This method expands XML entity references found in an input string.
        /// If an invalid entity reference is encountered, this method will throw
        /// XmlException.
        /// </summary>
        /// <param name="input">The string to search for entity references.</param>
        /// <returns>The expanded string.</returns>
        private static string ExpandEntityReferences(string input)
        {
            // In most cases, there are no entity references.  Check for that case now.
            // If we do find an entity reference, then the work isn't wasted.
            int start = input.IndexOf('&');
            if (start == -1)
                return input;

            StringBuilder buffer = new StringBuilder();
            buffer.Append(input, 0, start);
            start++;

            for (;;) {
                // At this point, 'start' points to a named XML entity.
                // locate the entity name.
                int end = input.IndexOf(';', start);
                if (end == -1)
                    throw new XmlException("An invalid entity reference was found.  '&' is present, but there is no matching ';'.");

                int name_length = end - start;
                if (name_length == 0)
                    throw new XmlException("An invalid entity reference was found.  '&;' is not a valid entity reference.");

                string entity_name = input.Substring(start, name_length);
                string value;

                switch (entity_name) {
                    case "amp": value = "&"; break;
                    case "lt": value = "<"; break;
                    case "gt": value = ">"; break;
                    case "quot": value = "\""; break;
                    default:
                        throw new XmlException(String.Format("An invalid entity reference was found.  The entity '&{0};' is not recognized.", entity_name));
                }

                buffer.Append(value);

                // Are there any more entity references in this string?
                int next = input.IndexOf('&', end + 1);
                if (next == -1) {
                    // There are no more entity references in the string.
                    // Append the rest of the string.
                    buffer.Append(input, end + 1, input.Length - end - 1);
                    return buffer.ToString();
                }

                // Yes, there are more references.  Keep looping, preserving
                // the same meaning of 'start' as when we entered this loop.
                buffer.Append(input, end + 1, next - (end + 1));
                start = next + 1; // skip over next '&'
            }
        }

        private char ReadCharacter(TextReader sr)
        {
            char tempChar = (char)sr.Read();
            if (tempChar == '\n') {
                lineNumber++;
            }
            return tempChar;
        }

        private bool ReadToken(TextReader sr, out string token, out TokenType type)
        {
            // fortunately for us, the first character tells us exactly which of the three
            // types of token this is. [a-zA-Z] means it's a NAME, ["] means it's a STRING_LITERAL
            // and [<>/=] means it's an operator
            int nextVal = sr.Peek();

            if (nextVal == -1) {
                // this is the end of the file
                token = "";
                type = TokenType.NONE;
                return false;
            }

            char firstChar = unchecked((char)nextVal);

            // if it's in [a-zA-Z]
            if (('a' <= firstChar && firstChar <= 'z') || ('A' <= firstChar && firstChar <= 'Z')) {
                token = ReadName(sr);
                type = TokenType.NAME;
                return true;
            }
            else if (firstChar == '"') {
                token = ReadString(sr);
                type = TokenType.STRING_LITERAL;
                return true;
            }
            else if (firstChar == '<' ||
                     firstChar == '>' ||
                     firstChar == '/' ||
                     firstChar == '=' ||
                     firstChar == '?' ||
                     firstChar == '!' ||
                     firstChar == '-') {
                sr.Read();
                token = firstChar.ToString();
                type = TokenType.OPERATOR;
                return true;
            }
            else if (firstChar == ' ' || firstChar == '\n' || (int)firstChar == 13 || firstChar == '\t') {
                // throw away all whitespace
                while (firstChar == ' ' || firstChar == '\n' || (int)firstChar == 13 || firstChar == '\t') {
                    char tempChar = (char)sr.Read();
                    if (tempChar == '\n') {
                        // increment the line count
                        lineNumber++;
                    }
                    firstChar = (char)sr.Peek();
                }
                return ReadToken(sr, out token, out type);
            }
            else {
                sr.Read();
                type = TokenType.NONE;
                token = firstChar.ToString();
                return true;
                //                throw new XmlException("Line " + lineNumber + ": '" + firstChar + "' is not a valid first character for a token");
            }
        }

        private string ReadName(TextReader sr)
        {
            StringBuilder sb = new StringBuilder();

            // add characters to our string until we see something that's not in [a-zA-Z0-9_-.:]
            char read = (char)sr.Peek();

            while (('a' <= read && read <= 'z') ||
                   ('A' <= read && read <= 'Z') ||
                   ('0' <= read && read <= '9') ||
                   read == '-' ||
                   read == '_' ||
                   read == '.' ||
                   read == ':') {
                sb.Append((char)sr.Read());
                read = (char)sr.Peek();
            }

            return sb.ToString();
        }

        private string ReadString(TextReader sr)
        {
            StringBuilder sb = new StringBuilder();
            // we need to read this string and translate it into a string literal.
            // this means that we have (for the moment) two magic characters: \ and "
            // "\\" means \ and "\"" means "
            bool translateNext = false;
            char read = (char)sr.Peek();
            if (read != '"') {
                throw new XmlException("Line " + lineNumber + ": Cannot start a string literal with " + (char)read + ".  You must use a '\"'");
            }
            // drop the '"' on the floor
            sr.Read();
            read = (char)sr.Read();

            while (read != '"' || translateNext) {
                if (!translateNext) {
                    if (read == '\\') {
                        translateNext = true;
                    }
                    else {
                        sb.Append(read);
                        read = (char)sr.Read();
                    }
                }
                else {
                    translateNext = false;
                    if (read == '\\') {
                        sb.Append("\\");
                        read = (char)sr.Read();
                    }
                    else if (read == '"') {
                        sb.Append("\"");
                        read = (char)sr.Read();
                    }
                    else {
                        throw new XmlException("Line " + lineNumber + ": Invalid escape sequence: \\" + read);
                    }
                }
            }

            return sb.ToString();
        }
    }
}
