/*
 * 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.
 */

/* $Id: AFPStreamer.java 1681394 2015-05-23 22:51:40Z adelmelle $ */

package org.apache.fop.afp;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.xmlgraphics.io.TempResourceURIGenerator;

import org.apache.fop.afp.modca.ResourceGroup;
import org.apache.fop.afp.modca.StreamedResourceGroup;
import org.apache.fop.apps.io.InternalResourceResolver;

/**
 * Manages the streaming of the AFP output
 */
public class AFPStreamer implements Streamable {
    /** Static logging instance */
    private static final Log LOG = LogFactory.getLog(AFPStreamer.class);

    private static final String DEFAULT_EXTERNAL_RESOURCE_FILENAME = "resources.afp";

    private static final TempResourceURIGenerator TEMP_URI_GENERATOR
            = new TempResourceURIGenerator("AFPDataStream_");

    private final Factory factory;

    private final InternalResourceResolver resourceResolver;

    /** A mapping of external resource destinations to resource groups */
    private final Map<URI, ResourceGroup> pathResourceGroupMap = new HashMap<URI, ResourceGroup>();

    private StreamedResourceGroup printFileResourceGroup;

    /** Sets the default resource group file path */
    private URI defaultResourceGroupUri;

    private final URI tempUri;

    /** temporary document outputstream */
    private OutputStream tempOutputStream;

    /** the final outputstream */
    private OutputStream outputStream;

    private DataStream dataStream;

    /**
     * Main constructor
     *
     * @param factory a factory
     * @param resourceResolver resource resolver
     */
    public AFPStreamer(Factory factory, InternalResourceResolver resourceResolver) {
        this.factory = factory;
        this.resourceResolver = resourceResolver;
        this.tempUri = TEMP_URI_GENERATOR.generate();
        defaultResourceGroupUri = URI.create(DEFAULT_EXTERNAL_RESOURCE_FILENAME);

    }

    /**
     * Creates a new DataStream
     *
     * @param paintingState the AFP painting state
     * @return a new {@link DataStream}
     * @throws IOException thrown if an I/O exception of some sort has occurred
     */
    public DataStream createDataStream(AFPPaintingState paintingState) throws IOException {
        this.tempOutputStream = new BufferedOutputStream(resourceResolver.getOutputStream(tempUri));
        this.dataStream = factory.createDataStream(paintingState, tempOutputStream);
        return dataStream;
    }

    /**
     * Sets the default resource group URI.
     *
     * @param uri the default resource group URI
     */
    public void setDefaultResourceGroupUri(URI uri) {
        this.defaultResourceGroupUri = uri;
    }

    /**
     * Returns the resource group for a given resource info
     *
     * @param level a resource level
     * @return a resource group for the given resource info
     */
    public ResourceGroup getResourceGroup(AFPResourceLevel level) {
        ResourceGroup resourceGroup = null;
        if (level.isInline()) { // no resource group for inline level
            return null;
        }
        if (level.isExternal()) {
            URI uri = level.getExternalURI();
            if (uri == null) {
                LOG.warn("No file path provided for external resource, using default.");
                uri = defaultResourceGroupUri;
            }
            resourceGroup = pathResourceGroupMap.get(uri);
            if (resourceGroup == null) {
                OutputStream os = null;
                try {
                    os = new BufferedOutputStream(resourceResolver.getOutputStream(uri));
                } catch (IOException ioe) {
                    LOG.error("Failed to create/open external resource group for uri '"
                            + uri + "'");
                } finally {
                    if (os != null) {
                        resourceGroup = factory.createStreamedResourceGroup(os);
                        pathResourceGroupMap.put(uri, resourceGroup);
                    }
                }
            }
        } else if (level.isPrintFile()) {
            if (printFileResourceGroup == null) {
                // use final outputstream for print-file resource group
                printFileResourceGroup = factory.createStreamedResourceGroup(outputStream);
            }
            resourceGroup = printFileResourceGroup;
        } else {
            // resource group in afp document datastream
            resourceGroup = dataStream.getResourceGroup(level);
        }
        return resourceGroup;
    }

    /**
     * Closes off the AFP stream writing the document stream
     *
     * @throws IOException if an an I/O exception of some sort has occurred
     */
    // write out any external resource groups
    public void close() throws IOException {
        for (ResourceGroup resourceGroup : pathResourceGroupMap.values()) {
            // TODO - Why not a Map<URI, StreamedResourceGroup>, if all the elements are expected to be of that type?
            assert (resourceGroup instanceof StreamedResourceGroup);
            ((StreamedResourceGroup) resourceGroup).close();
        }
        // close any open print-file resource group
        if (printFileResourceGroup != null) {
            printFileResourceGroup.close();
        }
        // write out document
        writeToStream(outputStream);
        outputStream.close();
    }

    /**
     * Sets the final outputstream
     *
     * @param outputStream an outputstream
     */
    public void setOutputStream(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    /** {@inheritDoc} */
    public void writeToStream(OutputStream os) throws IOException {
        tempOutputStream.close();
        InputStream tempInputStream = resourceResolver.getResource(tempUri);
        IOUtils.copy(tempInputStream, os);
        //TODO this should notify the stream provider that it is safe to delete the temp data
        tempInputStream.close();
        os.flush();
    }
}
