/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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
 *
 *      https://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.ivy.osgi.util;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.apache.ivy.util.Message;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.DTDHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

public class DelegatingHandler extends DefaultHandler implements DTDHandler, ContentHandler,
        ErrorHandler {

    private DelegatingHandler delegate = null;

    DelegatingHandler parent;

    private final Map<String, DelegatingHandler> saxHandlerMapping = new HashMap<>();

    private final Map<String, ChildElementHandler<?>> childHandlerMapping = new HashMap<>();

    private final String tagName;

    private boolean started = false;

    private boolean skip = false;

    private boolean skipOnError = false;

    private StringBuilder charBuffer = new StringBuilder();

    private boolean bufferingChar = false;

    private Locator locator;

    public DelegatingHandler(String name) {
        this.tagName = name;
        charBuffer.setLength(0);
    }

    protected <DH extends DelegatingHandler> void addChild(DH saxHandler,
            ChildElementHandler<DH> elementHandler) {
        saxHandlerMapping.put(saxHandler.getName(), saxHandler);
        childHandlerMapping.put(saxHandler.getName(), elementHandler);
        saxHandler.parent = this;
    }

    public String getName() {
        return tagName;
    }

    public DelegatingHandler getParent() {
        return parent;
    }

    public void setBufferingChar(boolean bufferingChar) {
        this.bufferingChar = bufferingChar;
    }

    public void setSkipOnError(boolean skipOnError) {
        this.skipOnError = skipOnError;
    }

    public boolean isBufferingChar() {
        return bufferingChar;
    }

    public String getBufferedChars() {
        return charBuffer.toString();
    }

    @Override
    public void setDocumentLocator(Locator locator) {
        this.locator = locator;
        for (DelegatingHandler subHandler : saxHandlerMapping.values()) {
            subHandler.setDocumentLocator(locator);
        }
    }

    public Locator getLocator() {
        return locator;
    }

    /**
     * Return an sort of identifier of the current element being parsed. It will only be used for
     * logging purpose.
     *
     * @return an empty string by default
     */
    protected String getCurrentElementIdentifier() {
        return "";
    }

    public void skip() {
        skip = true;
        for (DelegatingHandler subHandler : saxHandlerMapping.values()) {
            subHandler.stopDelegating();
        }
    }

    protected void stopDelegating() {
        parent.delegate = null;
        skip = false;
        started = false;
        for (DelegatingHandler subHandler : saxHandlerMapping.values()) {
            subHandler.stopDelegating();
        }
    }

    private interface SkipOnErrorCallback {
        void call() throws SAXException;
    }

    private void skipOnError(SkipOnErrorCallback callback) throws SAXException {
        try {
            callback.call();
        } catch (SAXException e) {
            if (skipOnError) {
                skip();
                log(Message.MSG_ERR, e.getMessage(), e);
            } else {
                throw e;
            }
        }
    }

    @Override
    public final void startDocument() throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.startDocument();
        } else {
            doStartDocument();
        }
    }

    /**
     * By default do nothing.
     *
     * @throws SAXException API told me so
     */
    protected void doStartDocument() throws SAXException {
    }

    @Override
    public final void endDocument() throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.endDocument();
        } else {
            doEndDocument();
        }
    }

    /**
     * By default do nothing.
     *
     * @throws SAXException API told me so
     */
    protected void doEndDocument() throws SAXException {
    }

    @Override
    public final void startElement(final String uri, final String localName, final String n,
            final Attributes atts) throws SAXException {
        // reset the char buffer
        charBuffer.setLength(0);
        if (delegate != null) {
            // we are already delegating, let's continue
            skipOnError(new SkipOnErrorCallback() {
                public void call() throws SAXException {
                    delegate.startElement(uri, localName, n, atts);
                }
            });
        } else {
            if (!started) { // first time called ?
                // just for the root, check the expected element name
                // not need to check the delegated as the mapping is already taking care of it
                if (parent == null && !localName.equals(tagName)) {
                    // we are at the root and the saxed element doesn't match
                    throw new SAXException("The root element of the parsed document '" + localName
                            + "' didn't matched the expected one: '" + tagName + "'");
                }
                skipOnError(new SkipOnErrorCallback() {
                    public void call() throws SAXException {
                        handleAttributes(atts);
                    }
                });
                started = true;
            } else {
                if (skip) {
                    // we con't care anymore about that part of the xml tree
                    return;
                }
                // time now to delegate for a new element
                delegate = saxHandlerMapping.get(localName);
                if (delegate != null) {
                    skipOnError(new SkipOnErrorCallback() {
                        public void call() throws SAXException {
                            delegate.startElement(uri, localName, n, atts);
                        }
                    });
                }
            }
        }
    }

    /**
     * Called when the expected node is achieved; nothing to do by default.
     *
     * @param atts
     *            the xml attributes attached to the expected node
     * @exception SAXException
     *                in case the parsing should be completely stopped
     */
    protected void handleAttributes(Attributes atts) throws SAXException {
    }

    /**
     * By default do nothing.
     *
     * @param uri String
     * @param localName String
     * @param name String
     * @param atts Attributes
     * @throws SAXException API told me so
     */
    protected void doStartElement(String uri, String localName, String name, Attributes atts)
            throws SAXException {
    }

    @Override
    public final void endElement(final String uri, final String localName, final String n)
            throws SAXException {
        if (delegate != null) {
            final DelegatingHandler savedDelegate = delegate;
            // we are already delegating, let's continue
            skipOnError(new SkipOnErrorCallback() {
                public void call() throws SAXException {
                    delegate.endElement(uri, localName, n);
                }
            });
            if (delegate == null) {
                // we just stopped delegating, it means that the child has ended
                final ChildElementHandler<?> childHandler = childHandlerMapping.get(localName);
                if (childHandler != null) {
                    skipOnError(new SkipOnErrorCallback() {
                        public void call() throws SAXException {
                            childHandler._childHandled(savedDelegate);
                        }
                    });
                }
            }
        } else {
            if (!skip) {
                doEndElement(uri, localName, n);
            }
            if (parent != null && tagName.equals(localName)) {
                // the current element is closed, let's tell the parent to stop delegating
                stopDelegating();
            }
        }
    }

    /**
     * By default do nothing.
     *
     * @param uri String
     * @param localName String
     * @param name String
     * @throws SAXException API told me so
     */
    protected void doEndElement(String uri, String localName, String name) throws SAXException {
    }

    public abstract static class ChildElementHandler<DH extends DelegatingHandler> {

        /**
         * @param child DH
         * @throws SAXParseException on failure
         * @deprecated because of renaming due spell check.
         */
        @Deprecated
        public void childHanlded(DH child) throws SAXParseException {
            childHandled(child);
        }

        public abstract void childHandled(DH child) throws SAXParseException;

        // because we know what we're doing
        @SuppressWarnings("unchecked")
        private void _childHandled(DelegatingHandler delegate) throws SAXParseException {
            childHandled((DH) delegate);
        }

    }

    @Override
    public final void characters(char[] ch, int start, int length) throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.characters(ch, start, length);
        } else {
            doCharacters(ch, start, length);
        }
    }

    /**
     * @param ch char[]
     * @param start int
     * @param length int
     * @throws SAXException if something goes wrong
     */
    protected void doCharacters(char[] ch, int start, int length) throws SAXException {
        if (bufferingChar) {
            charBuffer.append(ch, start, length);
        }
    }

    @Override
    public final void startPrefixMapping(String prefix, String uri) throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.startPrefixMapping(prefix, uri);
        } else {
            doStartPrefixMapping(prefix, uri);
        }
    }

    /**
     * By default do nothing.
     *
     * @param prefix String
     * @param uri String
     * @throws SAXException API told me so
     */
    protected void doStartPrefixMapping(String prefix, String uri) throws SAXException {
    }

    @Override
    public final void endPrefixMapping(String prefix) throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.endPrefixMapping(prefix);
        } else {
            doEndPrefixMapping(prefix);
        }
    }

    /**
     * By default do nothing.
     *
     * @param prefix String
     * @throws SAXException API told me so
     */
    protected void doEndPrefixMapping(String prefix) throws SAXException {
    }

    @Override
    public final void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.ignorableWhitespace(ch, start, length);
        } else {
            doIgnorableWhitespace(ch, start, length);
        }
    }

    /**
     * By default do nothing
     *
     * @param ch char[]
     * @param start int
     * @param length int
     * @throws SAXException API told me so
     */
    protected void doIgnorableWhitespace(char[] ch, int start, int length) throws SAXException {
    }

    @Override
    public final void notationDecl(String name, String publicId, String systemId)
            throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.notationDecl(name, publicId, systemId);
        } else {
            doNotationDecl(name, publicId, systemId);
        }
    }

    /**
     * By default do nothing.
     *
     * @param name String
     * @param publicId String
     * @param systemId String
     * @throws SAXException API told me so
     */
    protected void doNotationDecl(String name, String publicId, String systemId)
            throws SAXException {
    }

    @Override
    public final void processingInstruction(String target, String data) throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.processingInstruction(target, data);
        } else {
            doProcessingInstruction(target, data);
        }
    }

    /**
     * By default do nothing
     *
     * @param target String
     * @param data String
     * @throws SAXException API told me so
     */
    protected void doProcessingInstruction(String target, String data) throws SAXException {
    }

    @Override
    public final void skippedEntity(String name) throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.skippedEntity(name);
        } else {
            doSkippedEntity(name);
        }
    }

    /**
     * By default do nothing.
     *
     * @param name String
     * @throws SAXException API told me so
     */
    protected void doSkippedEntity(String name) throws SAXException {
    }

    @Override
    public final void unparsedEntityDecl(String name, String publicId, String systemId,
            String notationName) throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.unparsedEntityDecl(name, publicId, systemId, notationName);
        } else {
            doUnparsedEntityDecl(name, publicId, systemId, notationName);
        }
    }

    /**
     * By default do nothing.
     *
     * @param name String
     * @param publicId String
     * @param systemId String
     * @param notationName String
     * @throws SAXException API told me so
     */
    protected void doUnparsedEntityDecl(String name, String publicId, String systemId,
            String notationName) throws SAXException {
    }

    // ERROR HANDLING

    @Override
    public final void warning(SAXParseException exception) throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.warning(exception);
        } else {
            doWarning(exception);
        }
    }

    /**
     * By default do nothing.
     *
     * @param exception SAXParseException
     * @throws SAXException API told me so
     */
    protected void doWarning(SAXParseException exception) throws SAXException {
    }

    @Override
    public final void error(SAXParseException exception) throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.error(exception);
        } else {
            doError(exception);
        }
    }

    /**
     * By default do nothing.
     *
     * @param exception SAXParseException
     * @throws SAXException API told me so
     */
    protected void doError(SAXParseException exception) throws SAXException {
    }

    @Override
    public final void fatalError(SAXParseException exception) throws SAXException {
        if (skip) {
            return;
        }
        if (delegate != null) {
            delegate.fatalError(exception);
        } else {
            doFatalError(exception);
        }
    }

    /**
     * By default do nothing.
     *
     * @param exception SAXParseException
     * @throws SAXException API told me so
     */
    protected void doFatalError(SAXParseException exception) throws SAXException {
    }

    // //////////////////////
    // Functions related to error handling
    // //////////////////////

    protected void log(int logLevel, String message, Throwable t) {
        Message.debug(t);
        log(logLevel, message);
    }

    protected void log(int logLevel, String message) {
        Message.log(logLevel, getLocation(getLocator()) + message);
    }

    protected static String getLocation(Locator locator) {
        if (locator == null) {
            return "";
        }
        return "[line " + locator.getLineNumber() + " col. " + locator.getColumnNumber() + "] ";
    }

    @SuppressWarnings("unused")
    private void skipOnError(DelegatingHandler currentHandler,
            Class<? extends DelegatingHandler> handlerClassToSkip, String message) {
        DelegatingHandler handlerToSkip = currentHandler;
        while (!(handlerClassToSkip.isAssignableFrom(handlerToSkip.getClass()))) {
            handlerToSkip = handlerToSkip.getParent();
        }
        log(Message.MSG_ERR, message + ". The '" + handlerToSkip.getName() + "' element "
                + getCurrentElementIdentifier() + " is then ignored.");
        handlerToSkip.skip();
    }

    // //////////////////////
    // Helpers to parse the attributes
    // //////////////////////

    protected String getRequiredAttribute(Attributes atts, String name) throws SAXParseException {
        String value = atts.getValue(name);
        if (value == null) {
            throw new SAXParseException("Required attribute '" + name + "' not found", getLocator());
        }
        return value;
    }

    protected String getOptionalAttribute(Attributes atts, String name, String defaultValue) {
        String value = atts.getValue(name);
        if (value == null) {
            return defaultValue;
        }
        return value;
    }

    protected int getRequiredIntAttribute(Attributes atts, String name, Integer logLevel)
            throws SAXParseException {
        return parseInt(name, getRequiredAttribute(atts, name));
    }

    protected Integer getOptionalIntAttribute(Attributes atts, String name, Integer defaultValue)
            throws SAXParseException {
        String value = atts.getValue(name);
        if (value == null) {
            return defaultValue;
        }
        return parseInt(name, value);
    }

    private int parseInt(String name, String value) throws SAXParseException {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new SAXParseException("Attribute '" + name
                    + "' is expected to be an integer but was '" + value + "' (" + e.getMessage()
                    + ")", getLocator());
        }
    }

    protected long getRequiredLongAttribute(Attributes atts, String name) throws SAXParseException {
        return parseLong(name, getRequiredAttribute(atts, name));
    }

    protected Long getOptionalLongAttribute(Attributes atts, String name, Long defaultValue)
            throws SAXParseException {
        String value = atts.getValue(name);
        if (value == null) {
            return defaultValue;
        }
        return parseLong(name, value);
    }

    private long parseLong(String name, String value) throws SAXParseException {
        try {
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            throw new SAXParseException("Attribute '" + name
                    + "' is expected to be an long but was '" + value + "' (" + e.getMessage()
                    + ")", getLocator());
        }
    }

    protected boolean getRequiredBooleanAttribute(Attributes atts, String name)
            throws SAXParseException {
        return parseBoolean(name, getRequiredAttribute(atts, name));
    }

    protected Boolean getOptionalBooleanAttribute(Attributes atts, String name, Boolean defaultValue)
            throws SAXParseException {
        String value = atts.getValue(name);
        if (value == null) {
            return defaultValue;
        }
        return parseBoolean(name, value);
    }

    static final String TRUE = Boolean.TRUE.toString().toLowerCase(Locale.US);

    static final String FALSE = Boolean.FALSE.toString().toLowerCase(Locale.US);

    private boolean parseBoolean(String name, String value) throws SAXParseException {
        String lowerValue = value.toLowerCase(Locale.US);
        if (lowerValue.equals(TRUE)) {
            return true;
        }
        if (lowerValue.equals(FALSE)) {
            return false;
        }
        throw new SAXParseException("Attribute '" + name
                + "' is expected to be a boolean but was '" + value + "'", getLocator());
    }
}
