/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.processor.pipeline;

import org.apache.log4j.Logger;
import org.orbeon.oxf.cache.OutputCacheKey;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.xml.XMLReceiver;
import org.orbeon.oxf.processor.*;
import org.orbeon.oxf.util.LoggerFactory;
import org.orbeon.oxf.xml.SAXStore;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.xml.sax.SAXException;

/**
 * This internal processor handles the tee-ing functionality of XPL, i.e. sending an XML infoset to multiple readers.
 */
public class TeeProcessor extends ProcessorImpl {

    private static final Logger logger = LoggerFactory.createLogger(TeeProcessor.class);
    private Exception creationException;
    private Exception resetException;
    private ProcessorKey resetProcessorKey;

    public TeeProcessor(LocationData locationData) {
        if (logger.isDebugEnabled()) {
            creationException = new ValidationException("", locationData);
        }
        addInputInfo(new ProcessorInputOutputInfo(INPUT_DATA));
        addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_DATA));
    }

    /**
     * Standard createOutput().
     */
    @Override
    public ProcessorOutput createOutput(String name) {
        return createOutput(name, false);
    }

    /**
     * Tee-specific createOutput().
     */
    public ProcessorOutput createOutput(String name, boolean isMultipleReads) {
        final ProcessorOutput output = new TeeProcessorOutputImpl(name, isMultipleReads);
        addOutput(name, output);
        return output;
    }

    public class TeeProcessorOutputImpl extends ProcessorOutputImpl {

        private boolean isMultipleReads;

        private TeeProcessorOutputImpl(String name, boolean isMultipleReads) {
            super(TeeProcessor.this, name);
            this.isMultipleReads = isMultipleReads;

        }

        @Override
        public void readImpl(PipelineContext context, XMLReceiver xmlReceiver) {
            try {
                final State state = (State) getState(context);
                if (state.store == null) {
                    // Tee hasn't been read yet

                    if (state.stateWasCleared) {
                        final ProcessorOutput output = getInputByName(INPUT_DATA).getOutput();
                        logger.error("Tee state was cleared and re-read for output: " + output.getName());
                    }

                    // Create SAXStore and read input through it
                    final ProcessorInput input = getInputByName(INPUT_DATA);
                    state.store = new SAXStore(xmlReceiver);
                    readInputAsSAX(context, input, state.store);
                } else {
                    state.store.replay(xmlReceiver);
                }

                // If this output can be read only once, increase read count
                if (!isMultipleReads) {
                    state.readCount++;
                }

                // If possible, free the SAXStore after the last read
                freeSAXStoreIfNeeded(state);

            } catch (SAXException e) {
                throw new OXFException(e);
            }
        }

        /**
         * This is called specifically by p:for-each once an input has been entirely read.
         */
        public void doneReading(PipelineContext context) {
            // This output can be read more than once, so increase read count only when needed
            final State state = (State) getState(context);
            state.readCount++;
            // If possible, free the SAXStore after the last read
            freeSAXStoreIfNeeded(state);
        }

        private void freeSAXStoreIfNeeded(State state) {
            if (state.readCount == getOutputCount()) {
                final SAXStore freedStore = state.store;
                state.store = null;
                state.stateWasCleared = true;

                final ProcessorOutput output = getInputByName(INPUT_DATA).getOutput();
                if (logger.isDebugEnabled()) {
                    final long saxStoreSize = freedStore.getApproximateSize();
                    logger.debug("Freed SAXStore for output id: " + output.getName() + "; approximate size: " + saxStoreSize + " bytes");
                }
            }
        }

        @Override
        public OutputCacheKey getKeyImpl(PipelineContext pipelineContext) {
            final State state;
            try {
                state = (State) getState(pipelineContext);
            } catch (OXFException e) {
                if (logger.isDebugEnabled()) {
                    logger.error("creation", creationException);
                    logger.error("reset", resetException);
                    logger.error("current processor key: " + getProcessorKey(pipelineContext));
                    logger.error("reset processor key: " + resetProcessorKey);
                }
                throw e;
            }
            if (state.outputCacheKey == null) {
                final ProcessorOutput output = getInputByName(INPUT_DATA).getOutput();
                state.outputCacheKey = output.getKey(pipelineContext);
            }
            return state.outputCacheKey;
        }

        @Override
        public Object getValidityImpl(PipelineContext pipelineContext) {
            final State state = (State) getState(pipelineContext);
            if (state.validity == null) {
                final ProcessorOutput output = getInputByName(INPUT_DATA).getOutput();
                state.validity = output.getValidity(pipelineContext);
            }
            return state.validity;
        }
    }

    @Override
    public void reset(PipelineContext context) {
        if (logger.isDebugEnabled()) {
            resetException = new Exception(Integer.toString(hashCode()));
            resetProcessorKey = getProcessorKey(context);
        }
        setState(context, new State());
    }

    private static class State {
        public SAXStore store;
        public int readCount;
        public OutputCacheKey outputCacheKey;
        public Object validity;

        public boolean stateWasCleared;
    }
}
