/**
 * 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
 *
 *      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.camel.converter.jaxp;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.ContentHandler;
import org.xml.sax.DTDHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;

/**
 * Manages a pool of XMLReader (and associated SAXParser) instances for reuse.
 */
public class XMLReaderPool {
    private final Queue<WeakReference<XMLReader>> pool = new ConcurrentLinkedQueue<>();
    private final SAXParserFactory saxParserFactory;

    /**
     * Creates a new instance.
     *
     * @param saxParserFactory
     *            the SAXParserFactory used to create new SAXParser instances
     */
    public XMLReaderPool(SAXParserFactory saxParserFactory) {
        this.saxParserFactory = saxParserFactory;
    }

    /**
     * Returns an XMLReader that can be used exactly once. Calling one of the
     * {@code parse} methods returns the reader to the pool. This is useful
     * for e.g. SAXSource which bundles an XMLReader with an InputSource that
     * can also be consumed just once.
     *
     * @return the XMLReader
     * @throws SAXException
     *             see {@link SAXParserFactory#newSAXParser()}
     * @throws ParserConfigurationException
     *             see {@link SAXParserFactory#newSAXParser()}
     */
    public XMLReader createXMLReader() throws SAXException, ParserConfigurationException {
        XMLReader xmlReader = null;
        WeakReference<XMLReader> ref;
        while ((ref = pool.poll()) != null) {
            if ((xmlReader = ref.get()) != null) {
                break;
            }
        }

        if (xmlReader == null) {
            xmlReader = saxParserFactory.newSAXParser().getXMLReader();
        }

        return new OneTimeXMLReader(xmlReader);
    }

    /**
     * Wraps another XMLReader for single use only.
     */
    private final class OneTimeXMLReader implements XMLReader {
        private final XMLReader xmlReader;
        private final Map<String, Boolean> initFeatures = new HashMap<>();
        private final Map<String, Object> initProperties = new HashMap<>();
        private final ContentHandler initContentHandler;
        private final DTDHandler initDtdHandler;
        private final EntityResolver initEntityResolver;
        private final ErrorHandler initErrorHandler;
        private boolean readerInvalid;

        private OneTimeXMLReader(XMLReader xmlReader) {
            this.xmlReader = xmlReader;
            this.initContentHandler = xmlReader.getContentHandler();
            this.initDtdHandler = xmlReader.getDTDHandler();
            this.initEntityResolver = xmlReader.getEntityResolver();
            this.initErrorHandler = xmlReader.getErrorHandler();
        }

        private void release() {
            try {
                // reset XMLReader to its initial state
                for (Map.Entry<String, Boolean> feature : initFeatures.entrySet()) {
                    try {
                        xmlReader.setFeature(feature.getKey(), feature.getValue().booleanValue());
                    } catch (Exception e) {
                        // ignore
                    }
                }
                for (Map.Entry<String, Object> property : initProperties.entrySet()) {
                    try {
                        xmlReader.setProperty(property.getKey(), property.getValue());
                    } catch (Exception e) {
                        // ignore
                    }
                }
                xmlReader.setContentHandler(initContentHandler);
                xmlReader.setDTDHandler(initDtdHandler);
                xmlReader.setEntityResolver(initEntityResolver);
                xmlReader.setErrorHandler(initErrorHandler);

                // return the wrapped instance to the pool
                pool.offer(new WeakReference<>(xmlReader));
            } finally {
                readerInvalid = true;
            }
        }

        @Override
        public boolean getFeature(String name) throws SAXNotRecognizedException, SAXNotSupportedException {
            return xmlReader.getFeature(name);
        }

        @Override
        public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
            if (!readerInvalid) {
                if (!initFeatures.containsKey(name)) {
                    initFeatures.put(name, Boolean.valueOf(xmlReader.getFeature(name)));
                }
                xmlReader.setFeature(name, value);
            }
        }

        @Override
        public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException {
            return xmlReader.getProperty(name);
        }

        @Override
        public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException {
            if (!readerInvalid) {
                if (!initProperties.containsKey(name)) {
                    initProperties.put(name, xmlReader.getProperty(name));
                }
                xmlReader.setProperty(name, value);
            }
        }

        @Override
        public ContentHandler getContentHandler() {
            return xmlReader.getContentHandler();
        }

        @Override
        public void setContentHandler(ContentHandler handler) {
            if (!readerInvalid) {
                xmlReader.setContentHandler(handler);
            }
        }

        @Override
        public DTDHandler getDTDHandler() {
            return xmlReader.getDTDHandler();
        }

        @Override
        public void setDTDHandler(DTDHandler handler) {
            if (!readerInvalid) {
                xmlReader.setDTDHandler(handler);
            }
        }

        @Override
        public EntityResolver getEntityResolver() {
            return xmlReader.getEntityResolver();
        }

        @Override
        public void setEntityResolver(EntityResolver resolver) {
            if (!readerInvalid) {
                xmlReader.setEntityResolver(resolver);
            }
        }

        @Override
        public ErrorHandler getErrorHandler() {
            return xmlReader.getErrorHandler();
        }

        @Override
        public void setErrorHandler(ErrorHandler handler) {
            if (!readerInvalid) {
                xmlReader.setErrorHandler(handler);
            }
        }

        @Override
        public synchronized void parse(InputSource input) throws IOException, SAXException {
            checkValid();
            try {
                xmlReader.parse(input);
            } finally {
                release();
            }
        }

        @Override
        public synchronized void parse(String systemId) throws IOException, SAXException {
            checkValid();
            try {
                xmlReader.parse(systemId);
            } finally {
                release();
            }
        }
        
        private void checkValid() {
            if (readerInvalid) {
                throw new IllegalStateException("OneTimeXMLReader can only be used once!");
            }
        }
    }
}
