/*
 * Copyright 2011, 2012 Odysseus Software GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.synapse.commons.staxon.core.json.util;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import javax.xml.XMLConstants;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.stream.events.ProcessingInstruction;

import org.apache.synapse.commons.staxon.core.event.SimpleXMLEventFactory;
import org.apache.synapse.commons.staxon.core.json.JsonXMLStreamConstants;

/**
 * Package-private helper used by {@link XMLMultipleStreamWriter} and {@link XMLMultipleEventWriter}
 * to handle path matching and insert <code>&lt;xml-multiple&gt;</code> processing instruction events.
 */
class XMLMultipleProcessingInstructionHandler {
    private static final ProcessingInstruction MULTIPLE_PI =
            new SimpleXMLEventFactory().createProcessingInstruction(JsonXMLStreamConstants.MULTIPLE_PI_TARGET, null);

    /**
     * Processing instruction writer
     */
    private static abstract class ProcessingInstructionWriter {
        abstract void add(ProcessingInstruction pi) throws XMLStreamException;
    }

    private final StringBuilder path = new StringBuilder();
    private final Set<String> absoluteMultiplePaths = new HashSet<String>();
    private final List<String> relativeMultiplePaths = new ArrayList<String>();
    private final String[] names = new String[64];
    private final boolean matchRoot;
    private final boolean matchPrefixes;

    private final Pattern pathPattern;
    private final ProcessingInstructionWriter writer;

    private String previousSiblingName = null;
    private int depth = 0;

    XMLMultipleProcessingInstructionHandler(final XMLStreamWriter writer, boolean matchRoot, boolean matchPrefixes) {
        this(new ProcessingInstructionWriter() {
            @Override
            void add(ProcessingInstruction pi) throws XMLStreamException {
                if (pi.getData() == null) {
                    writer.writeProcessingInstruction(pi.getTarget());
                } else {
                    writer.writeProcessingInstruction(pi.getTarget(), pi.getData());
                }
            }
        }, matchRoot, matchPrefixes);
    }

    XMLMultipleProcessingInstructionHandler(final XMLEventWriter writer, boolean matchRoot, boolean matchPrefixes) {
        this(new ProcessingInstructionWriter() {
            @Override
            void add(ProcessingInstruction pi) throws XMLStreamException {
                writer.add(pi);
            }
        }, matchRoot, matchPrefixes);
    }

    private XMLMultipleProcessingInstructionHandler(ProcessingInstructionWriter writer, boolean matchRoot, boolean matchPrefixes) {
        this.matchRoot = matchRoot;
        this.matchPrefixes = matchPrefixes;
        this.writer = writer;

		/*
         * determine path pattern
		 */
        String identifier = "\\w(-?\\w)*";
        String name = matchPrefixes ? "(" + identifier + ":)?" + identifier : identifier;
        pathPattern = Pattern.compile("/?" + name + "(/" + name + ")*");
    }

    private boolean matches() {
        if (absoluteMultiplePaths.contains(path.toString())) {
            return true;
        }
        if (!relativeMultiplePaths.isEmpty()) {
            for (String suffix : relativeMultiplePaths) {
                if (path.toString().endsWith(suffix) && path.charAt(path.length() - suffix.length() - 1) == '/') {
                    return true;
                }
            }
        }
        return false;
    }

    private void push(String name) throws XMLStreamException {
        if (matchRoot || depth > 0) {
            path.append('/').append(name);
        }

        if (!name.equals(previousSiblingName) && matches()) {
            writer.add(MULTIPLE_PI);
        }

        names[depth] = name;
        previousSiblingName = null;
        depth++;
    }

    private void pop() {
        depth--;
        previousSiblingName = names[depth];
        names[depth] = null;

        if (matchRoot || depth > 0) {
            path.setLength(path.length() - previousSiblingName.length() - 1);
        }
    }

    /**
     * Add path to trigger <code>&lt;?xml-multiple?></code> PI.
     * The path may start with <code>'/'</code> and contain element
     * names, separated by <code>'/'</code>, e.g
     * <code>"/foo/bar"</code>, <code>"foo/bar"</code> or <code>"bar"</code>.
     * If <code>matchPrefixes</code> isv set to <code>true</code>, element
     * names may be prefixed, e.g. <code>"p:foo/bar"</code>.
     *
     * @param path
     */
    void addMultiplePath(String path) throws XMLStreamException {
        if (!pathPattern.matcher(path).matches()) {
            throw new XMLStreamException("multiple path does not match " + pathPattern.pattern());
        }
        if (path.charAt(0) == '/') {
            absoluteMultiplePaths.add(path);
        } else {
            relativeMultiplePaths.add(path);
        }
    }

    void preStartElement(String prefix, String localPart) throws XMLStreamException {
        if (matchPrefixes) {
            push(XMLConstants.DEFAULT_NS_PREFIX.equals(prefix) ? localPart : prefix + ':' + localPart);
        } else {
            push(localPart);
        }
    }

    void postStartElement() throws XMLStreamException {
        // do nothing
    }

    void preEndElement() throws XMLStreamException {
        // do nothing
    }

    void postEndElement() throws XMLStreamException {
        pop();
    }

    void preEmptyElement(String prefix, String localPart) throws XMLStreamException {
        preStartElement(prefix, localPart);
    }

    void postEmptyElement() throws XMLStreamException {
        postStartElement();
        preEndElement();
        postEndElement();
    }
}
