package com.izforge.izpack.adaptator.impl;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLFilterImpl;

import javax.xml.transform.dom.DOMResult;
import java.util.Queue;
import java.util.Stack;
import java.util.LinkedList;


/**
 * A custom SAX XML filter, used to add line numbers to a DOM document.
 * This filter stores line numbers while parsing, and the applyLN method set
 * line numbers on the result.
 * Line numbers are stored in the user data of the Element,
 * so require Java 5 (DOM 3) or higher.
 *
 * @author Anthonin Bonnefoy
 * @author David Duponchel
 */
public class LineNumberFilter extends XMLFilterImpl
{

    /**
     * a queue to store line numbers while parsing.
     */
    private Queue<Integer> lnQueue;

    /**
     * The locator given while parsing.
     */
    private Locator locator;

    public LineNumberFilter(XMLReader xmlReader)
    {
        super(xmlReader);
    }

    @Override
    public void startDocument() throws SAXException
    {
        super.startDocument();
        lnQueue = new LinkedList<Integer>();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException
    {
        super.startElement(uri, localName, qName, atts);
        lnQueue.add(locator.getLineNumber());
    }

    @Override
    public void setDocumentLocator(Locator locator)
    {
        super.setDocumentLocator(locator);
        this.locator = locator;
    }

    /**
     * Return the first element found from the given Node.
     *
     * @param elt The Node start point.
     *
     * @return The first Element found, or null.
     */
    private Element getFirstFoundElement(Node elt)
    {
        while (elt != null && elt.getNodeType() != Node.ELEMENT_NODE)
        {
            elt = elt.getNextSibling();
        }
        return (Element) elt;
    }

    /**
     * Return the next element sibling found from the given Node.
     *
     * @param elt The Node start point.
     *
     * @return The next sibling Element, or null if nto found.
     */
    private Element getNextSibling(Node elt)
    {
        return getFirstFoundElement(elt.getNextSibling());
    }

    /**
     * Return the first element child found from the given Node.
     *
     * @param elt The Node start point.
     *
     * @return The first child Element found, or null.
     */
    private Element getFirstChild(Node elt)
    {
        return getFirstFoundElement(elt.getFirstChild());
    }

    /**
     * Returns whether the given node has any element children.
     *
     * @param elt The node to check.
     *
     * @return Returns <code>true</code> if this node has any children,
     *         <code>false</code> otherwise.
     */
    private boolean hasChildElements(Node elt)
    {
        return getFirstChild(elt) != null;
    }

    /**
     * Apply a line number on the given element.
     * We assume that the line number queue has been correctly filled, and that
     * the current DOM tree correspond to the parsed XML.
     *
     * @param elt the element to apply the line number.
     */
    private void applyLN(Element elt)
    {
        elt.setUserData("ln", lnQueue.poll(), null);
    }

    /**
     * Apply line numbers stored by a parse using this object on the xml elements.
     *
     * @param result The result of the parse.
     */
    public void applyLN(DOMResult result)
    {
        Element elt = getFirstChild(result.getNode());
        boolean end = false;
        Stack<Element> stack = new Stack<Element>();

        while (!end)
        {
            if (hasChildElements(elt))
            { // not a leaf
                stack.push(elt);
                applyLN(elt);
                elt = getFirstChild(elt); // go down
            }
            else
            { // a leaf
                applyLN(elt);
                Element sibling = getNextSibling(elt);
                if (sibling != null)
                { // has a sibling
                    elt = sibling;
                }
                else
                { // no sibling
                    do
                    {
                        if (stack.isEmpty())
                        {
                            end = true;
                        }
                        else
                        {
                            elt = stack.pop();
                            elt = getNextSibling(elt);
                        }
                    } while (!end && elt == null);
                }
            }
        }
    }
}
