package de.is24.deadcode4j.analyzer.webxml;

import de.is24.deadcode4j.AnalysisContext;
import de.is24.deadcode4j.analyzer.XmlAnalyzer;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;

import javax.annotation.Nonnull;
import java.util.*;

import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.collect.Iterables.elementsEqual;
import static java.util.Arrays.asList;

/**
 * Parses {@code web.xml} and translates XML events into {@code web.xml}
 * specific events that can be consumed by a {@link WebXmlHandler}. It only
 * creates events for {@code web.xml} nodes that are needed by existing
 * {@link de.is24.deadcode4j.Analyzer}s. Please add further events if needed.
 *
 * @since 2.1.0
 */
public abstract class BaseWebXmlAnalyzer extends XmlAnalyzer {
    protected BaseWebXmlAnalyzer() {
        super("web.xml");
    }

    /**
     * This method is called to provide a <code>WebXmlHandler</code> for each file being processed.
     */
    @Nonnull
    protected abstract WebXmlHandler createWebXmlHandlerFor(@Nonnull AnalysisContext analysisContext);

    @Nonnull
    @Override
    protected DefaultHandler createHandlerFor(@Nonnull AnalysisContext analysisContext) {
        WebXmlHandler webXmlHandler = createWebXmlHandlerFor(analysisContext);
        return new WebXmlAdapter(webXmlHandler);
    }

    // Translates XML events into web.xml events that can be consumed by a WebXmlHandler
    private static class WebXmlAdapter extends DefaultHandler {
        @SuppressWarnings("unchecked")
        static final Collection<List<String>> NODES_WITH_TEXT = asList(
                asList("web-app", "context-param", "param-name"),
                asList("web-app", "context-param", "param-value"),
                asList("web-app", "filter", "filter-class"),
                asList("web-app", "filter", "init-param", "param-name"),
                asList("web-app", "filter", "init-param", "param-value"),
                asList("web-app", "listener", "listener-class"),
                asList("web-app", "servlet", "servlet-class"),
                asList("web-app", "servlet", "init-param", "param-name"),
                asList("web-app", "servlet", "init-param", "param-value"));
        static final Collection<String> CONTEXT_PARAM_PATH = asList("web-app", "context-param");
        static final Collection<String> FILTER_PATH = asList("web-app", "filter");
        static final Collection<String> FILTER_INIT_PARAM_PATH = asList("web-app", "filter", "init-param");
        static final Collection<String> LISTENER_PATH = asList("web-app", "listener");
        static final Collection<String> SERVLET_INIT_PARAM_PATH = asList("web-app", "servlet", "init-param");
        static final Collection<String> SERVLET_PATH = asList("web-app", "servlet");

        final Deque<String> deque = new ArrayDeque<String>();
        final List<Param> initParams = new ArrayList<Param>();
        final Map<String, String> texts = new HashMap<String, String>();
        StringBuilder buffer;
        final WebXmlHandler webXmlHandler;

        WebXmlAdapter(WebXmlHandler webXmlHandler) {
            this.webXmlHandler = webXmlHandler;
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) {
            deque.add(localName);
            if (isNodeWithText()) {
                buffer = new StringBuilder(128);
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) {
            if (isNodeWithText()) {
                buffer.append(new String(ch, start, length).trim());
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) {
            if (isNodeWithText()) {
                storeCharacters(localName);
            } else if (matchesPath(CONTEXT_PARAM_PATH)) {
                reportContextParam();
            } else if (matchesPath(FILTER_INIT_PARAM_PATH)) {
                storeInitParam();
            } else if (matchesPath(FILTER_PATH)) {
                reportFilter();
            } else if (matchesPath(LISTENER_PATH)) {
                reportListener();
            } else if (matchesPath(SERVLET_INIT_PARAM_PATH)) {
                storeInitParam();
            } else if (matchesPath(SERVLET_PATH)) {
                reportServlet();
            }
            deque.removeLast();
        }

        boolean isNodeWithText() {
            for (List<String> candidate : NODES_WITH_TEXT) {
                if (matchesPath(candidate)) {
                    return true;
                }
            }
            return false;
        }

        void reportContextParam() {
            webXmlHandler.contextParam(createParam());
        }

        void storeInitParam() {
            initParams.add(createParam());
        }

        Param createParam() {
            return new Param(getText("param-name"), getText("param-value"));
        }

        void reportFilter() {
            webXmlHandler.filter(
                    getText("filter-class"),
                    new ArrayList<Param>(initParams));
            initParams.clear();
        }

        void reportListener() {
            webXmlHandler.listener(getText("listener-class"));
        }

        void reportServlet() {
            webXmlHandler.servlet(
                    getText("servlet-class"),
                    new ArrayList<Param>(initParams));
            initParams.clear();
        }

        boolean matchesPath(Collection<String> path) {
            return path.size() == deque.size() && elementsEqual(path, deque);
        }

        void storeCharacters(String localName) {
            texts.put(localName, buffer.toString());
        }

        String getText(String localName) {
            String text = texts.remove(localName);
            return nullToEmpty(text);
        }
    }
}

@@ -3021,24 +3021,36 @@
 ();%0A        
+final Deque%3C
 StringBuilde
@@ -3050,23 +3050,63 @@
 gBuilder
- buffer
+%3E textBuffers = new ArrayDeque%3CStringBuilder%3E()
 ;%0A      
@@ -3465,17 +3465,28 @@
     
-buffer = 
+textBuffers.addLast(
 new 
@@ -3503,16 +3503,17 @@
 der(128)
+)
 ;%0A      
@@ -3668,22 +3668,37 @@
         
-buffer
+textBuffers.getLast()
 .append(
@@ -5840,14 +5840,32 @@
 me, 
-buffer
+textBuffers.removeLast()
 .toS

fix a potential bug in text handling