/**
 * 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.component.stax;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;

import org.apache.camel.Exchange;
import org.apache.camel.InvalidPayloadException;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.support.ExpressionAdapter;
import org.apache.camel.util.LRUSoftCache;
import org.apache.camel.util.ObjectHelper;

import static org.apache.camel.component.stax.StAXUtil.getTagName;

/**
 * {@link org.apache.camel.Expression} to walk a {@link org.apache.camel.Message} body
 * using an {@link Iterator}, which uses StAX to walk in streaming mode.
 * The elements returned is a POJO which is bound using JAXB annotations.
 * <p/>
 * The message body must be able to convert to {@link XMLEventReader} type which is used as stream
 * to access the message body. And there must be a JAXB annotated class to use as binding.
 */
public class StAXJAXBIteratorExpression<T> extends ExpressionAdapter {
    private static final Map<Class<?>, JAXBContext> JAX_CONTEXTS = new LRUSoftCache<Class<?>, JAXBContext>(1000);

    private final Class<T> handled;
    private final String handledName;
    private final boolean isNamespaceAware;

    /**
     * Creates this expression.
     *
     * @param handled the class which has JAXB annotations to bind POJO.
     */
    public StAXJAXBIteratorExpression(Class<T> handled) {
        this(handled, true);
    }

    /**
     * Creates this expression.
     *
     * @param handled          the class which has JAXB annotations to bind POJO.
     * @param isNamespaceAware sets the namespace awareness of the xml reader
     */
    public StAXJAXBIteratorExpression(Class<T> handled, boolean isNamespaceAware) {
        ObjectHelper.notNull(handled, "handled");
        this.handled = handled;
        this.handledName = null;
        this.isNamespaceAware = isNamespaceAware;
    }

    /**
     * Creates this expression.
     *
     * @param handledName the FQN name of the class which has JAXB annotations to bind POJO.
     */
    public StAXJAXBIteratorExpression(String handledName) {
        this(handledName, true);
    }

    /**
     * Creates this expression.
     *
     * @param handledName      the FQN name of the class which has JAXB annotations to bind POJO.
     * @param isNamespaceAware sets the namespace awareness of the xml reader
     */
    public StAXJAXBIteratorExpression(String handledName, boolean isNamespaceAware) {
        ObjectHelper.notNull(handledName, "handledName");
        this.handled = null;
        this.handledName = handledName;
        this.isNamespaceAware = isNamespaceAware;
    }

    private static JAXBContext jaxbContext(Class<?> handled) throws JAXBException {
        if (JAX_CONTEXTS.containsKey(handled)) {
            return JAX_CONTEXTS.get(handled);
        }

        JAXBContext context;
        synchronized (JAX_CONTEXTS) {
            context = JAXBContext.newInstance(handled);
            JAX_CONTEXTS.put(handled, context);
        }
        return context;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object evaluate(Exchange exchange) {
        try {
            XMLEventReader reader;
            if (isNamespaceAware) {
                reader = exchange.getIn().getMandatoryBody(XMLEventReader.class);
            } else {
                InputStream inputStream = exchange.getIn().getMandatoryBody(InputStream.class);
                XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
                xmlInputFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, false);
                reader = xmlInputFactory.createXMLEventReader(inputStream);
            }

            Class<T> clazz = handled;
            if (clazz == null && handledName != null) {
                clazz = (Class<T>) exchange.getContext().getClassResolver().resolveMandatoryClass(handledName);
            }
            return createIterator(reader, clazz);
        } catch (InvalidPayloadException e) {
            exchange.setException(e);
            return null;
        } catch (JAXBException e) {
            exchange.setException(e);
            return null;
        } catch (ClassNotFoundException e) {
            exchange.setException(e);
            return null;
        } catch (XMLStreamException e) {
            exchange.setException(e);
            return null;
        }
    }

    private Iterator<T> createIterator(XMLEventReader reader, Class<T> clazz) throws JAXBException {
        return new StAXJAXBIterator<T>(clazz, reader);
    }

    /**
     * Iterator to walk the XML reader
     */
    static class StAXJAXBIterator<T> implements Iterator<T>, Closeable {

        private final XMLEventReader reader;
        private final Class<T> clazz;
        private final String name;
        private final Unmarshaller unmarshaller;
        private T element;

        StAXJAXBIterator(Class<T> clazz, XMLEventReader reader) throws JAXBException {
            this.clazz = clazz;
            this.reader = reader;

            name = getTagName(clazz);
            JAXBContext jaxb = jaxbContext(clazz);
            // unmarshaller is not thread safe so we need to create a new instance per iterator
            unmarshaller = jaxb.createUnmarshaller();
        }

        @Override
        public boolean hasNext() {
            if (element == null) {
                element = getNextElement();
            }
            return element != null;
        }

        @Override
        public T next() {
            if (element == null) {
                element = getNextElement();
            }

            T answer = element;
            element = null;
            return answer;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

        T getNextElement() {
            XMLEvent xmlEvent;
            boolean found = false;
            while (!found && reader.hasNext()) {
                try {
                    xmlEvent = reader.peek();
                    if (xmlEvent != null && xmlEvent.isStartElement() && name.equals(xmlEvent.asStartElement().getName().getLocalPart())) {
                        found = true;
                    } else {
                        reader.nextEvent();
                    }
                } catch (XMLStreamException e) {
                    throw new RuntimeCamelException(e);
                }
            }

            if (!found) {
                return null;
            }

            try {
                return unmarshaller.unmarshal(reader, clazz).getValue();
            } catch (JAXBException e) {
                throw new RuntimeCamelException(e);
            }
        }

        @Override
        public void close() throws IOException {
            try {
                reader.close();
            } catch (XMLStreamException e) {
                throw new IOException(e);
            }
        }
    }

}
