/*
 * The MIT License
 *
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
 *
 * 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 hudson.util;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import org.apache.commons.jelly.XMLOutput;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.XMLFilter;
import org.xml.sax.helpers.XMLFilterImpl;

/**
 * {@link XMLFilter} that checks the proper nesting of table related tags.
 *
 * <p>
 * Browser often "fixes" HTML by moving tables into the right place,
 * so failure to generate proper tables can result in a hard-to-track bugs.
 *
 * <p>
 * TODO: where to apply this in stapler?
 * JellyClassTearOff creates XMLOutput. Perhaps we define a decorator?
 * We can also wrap Script. would that work better?
 *
 * @author Kohsuke Kawaguchi
 */
public class TableNestChecker extends XMLFilterImpl {

    private final Stack<Checker> elements = new Stack<>();
    private final Stack<String> tagNames = new Stack<>();

    public static void applyTo(XMLOutput xo) {
        xo.setContentHandler(new TableNestChecker(xo.getContentHandler()));
    }

    public TableNestChecker() {
        elements.push(ALL_ALLOWED);
    }

    public TableNestChecker(ContentHandler target) {
        this();
        setContentHandler(target);
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        String tagName = localName.toUpperCase(Locale.ENGLISH);

        // make sure that this tag occurs in the proper context
        if (!elements.peek().isAllowed(tagName))
            throw new SAXException(tagName + " is not allowed inside " + tagNames.peek());

        Checker next = CHECKERS.get(tagName);
        if (next == null)  next = ALL_ALLOWED;
        elements.push(next);
        tagNames.push(tagName);

        super.startElement(uri, localName, qName, atts);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        elements.pop();
        tagNames.pop();
        super.endElement(uri, localName, qName);
    }


    private interface Checker {
        boolean isAllowed(String childTag);
    }

    private static final Checker ALL_ALLOWED = childTag -> true;

    private static final class InList implements Checker {
        private final Set<String> tags;

        private InList(String... tags) {
            this.tags = new HashSet<>(Arrays.asList(tags));
        }

        @Override
        public boolean isAllowed(String childTag) {
            return tags.contains(childTag);
        }
    }

    private static final Map<String, Checker> CHECKERS = new HashMap<>();

    static {
        CHECKERS.put("TABLE", new InList("TR", "THEAD", "TBODY"));
        InList rows = new InList("TR");
        CHECKERS.put("THEAD", rows);
        CHECKERS.put("TR",   new InList("TD", "TH"));
    }
}
