/*
 * Copyright (C) 2013 Glyptodon LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.glyptodon.guacamole.xml;

import java.util.Deque;
import java.util.LinkedList;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * A simple ContentHandler implementation which digests SAX document events and
 * produces simpler tag-level events, maintaining its own stack for the
 * convenience of the tag handlers.
 *
 * @author Mike Jumper
 */
public class DocumentHandler extends DefaultHandler {

    /**
     * The name of the root element of the document.
     */
    private String rootElementName;

    /**
     * The handler which will be used to handle element events for the root
     * element of the document.
     */
    private TagHandler root;

    /**
     * The stack of all states applicable to the current parser state. Each
     * element of the stack references the TagHandler for the element being
     * parsed at that level of the document, where the current element is
     * last in the stack, and the root element is first.
     */
    private Deque<DocumentHandlerState> stack =
            new LinkedList<DocumentHandlerState>();

    /**
     * Creates a new DocumentHandler which will use the given TagHandler
     * to handle the root element.
     *
     * @param rootElementName The name of the root element of the document
     *                        being handled.
     * @param root The TagHandler to use for the root element.
     */
    public DocumentHandler(String rootElementName, TagHandler root) {
        this.root = root;
        this.rootElementName = rootElementName;
    }

    /**
     * Returns the current element state. The current element state is the
     * state of the element the parser is currently within.
     *
     * @return The current element state.
     */
    private DocumentHandlerState getCurrentState() {

        // If no state, return null
        if (stack.isEmpty())
            return null;

        return stack.getLast();
    }

    @Override
    public void startElement(String uri, String localName, String qName,
        Attributes attributes) throws SAXException {

        // Get current state
        DocumentHandlerState current = getCurrentState();

        // Handler for tag just read
        TagHandler handler;

        // If no stack, use root handler
        if (current == null) {

            // Validate element name
            if (!localName.equals(rootElementName))
                throw new SAXException("Root element must be '" + rootElementName + "'");

            handler = root;
        }

        // Otherwise, get handler from parent
        else {
            TagHandler parent_handler = current.getTagHandler();
            handler = parent_handler.childElement(localName);
        }

        // If no handler returned, the element was not expected
        if (handler == null)
            throw new SAXException("Unexpected element: '" + localName + "'");

        // Initialize handler
        handler.init(attributes);

        // Append new element state to stack
        stack.addLast(new DocumentHandlerState(handler));

    }

    @Override
    public void endElement(String uri, String localName, String qName)
            throws SAXException {

        // Pop last element from stack
        DocumentHandlerState completed = stack.removeLast();

        // Finish element by sending text content
        completed.getTagHandler().complete(
                completed.getTextContent().toString());

    }

    @Override
    public void characters(char[] ch, int start, int length)
            throws SAXException {

        // Get current state
        DocumentHandlerState current = getCurrentState();
        if (current == null)
            throw new SAXException("Character data not allowed outside XML document.");
        
        // Append received chunk to text content
        current.getTextContent().append(ch, start, length);

    }

    /**
     * The current state of the DocumentHandler.
     */
    private static class DocumentHandlerState {

        /**
         * The current text content of the current element being parsed.
         */
        private StringBuilder textContent = new StringBuilder();

        /**
         * The TagHandler which must handle document events related to the
         * element currently being parsed.
         */
        private TagHandler tagHandler;

        /**
         * Creates a new DocumentHandlerState which will maintain the state
         * of parsing of the current element, as well as contain the TagHandler
         * which will receive events related to that element.
         *
         * @param tagHandler The TagHandler which should receive any events
         *                   related to the element being parsed.
         */
        public DocumentHandlerState(TagHandler tagHandler) {
            this.tagHandler = tagHandler;
        }

        /**
         * Returns the mutable StringBuilder which contains the current text
         * content of the element being parsed.
         *
         * @return The mutable StringBuilder which contains the current text
         *         content of the element being parsed.
         */
        public StringBuilder getTextContent() {
            return textContent;
        }

        /**
         * Returns the TagHandler which must handle any events relating to the
         * element being parsed.
         *
         * @return The TagHandler which must handle any events relating to the
         *         element being parsed.
         */
        public TagHandler getTagHandler() {
            return tagHandler;
        }

    }

}
