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

package org.apache.xmlgraphics.image.loader.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.apache.commons.io.IOUtils;

import org.apache.xmlgraphics.image.loader.ImageFlavor;
import org.apache.xmlgraphics.image.loader.ImageInfo;
import org.apache.xmlgraphics.image.loader.MimeEnabledImageFlavor;

/**
 * This class is an implementation of the Image interface exposing an InputStream for loading the
 * raw/undecoded image.
 */
public class ImageRawStream extends AbstractImage {

    private ImageFlavor flavor;
    private InputStreamFactory streamFactory;

    /**
     * Main constructor.
     * @param info the image info object
     * @param flavor the image flavor for the raw image
     * @param streamFactory the InputStreamFactory that is used to create InputStream instances
     */
    public ImageRawStream(ImageInfo info, ImageFlavor flavor, InputStreamFactory streamFactory) {
        super(info);
        this.flavor = flavor;
        setInputStreamFactory(streamFactory);
    }

    /**
     * Constructor for a simple InputStream as parameter.
     * @param info the image info object
     * @param flavor the image flavor for the raw image
     * @param in the InputStream with the raw content
     */
    public ImageRawStream(ImageInfo info, ImageFlavor flavor, InputStream in) {
        this(info, flavor, new SingleStreamFactory(in));
    }

    /** {@inheritDoc} */
    public ImageFlavor getFlavor() {
        return this.flavor;
    }

    /**
     * Returns the MIME type of the stream data.
     * @return the MIME type
     */
    public String getMimeType() {
        if (getFlavor() instanceof MimeEnabledImageFlavor) {
            return getFlavor().getMimeType();
        } else {
            //Undetermined
            return "application/octet-stream";
        }
    }

    /** {@inheritDoc} */
    public boolean isCacheable() {
        return !this.streamFactory.isUsedOnceOnly();
    }

    /**
     * Sets the InputStreamFactory to be used by this image. This method allows to replace the
     * original factory.
     * @param factory the new InputStreamFactory
     */
    public void setInputStreamFactory(InputStreamFactory factory) {
        if (this.streamFactory != null) {
            this.streamFactory.close();
        }
        this.streamFactory = factory;
    }

    /**
     * Returns a new InputStream to access the raw image.
     * @return the InputStream
     */
    public InputStream createInputStream() {
        return this.streamFactory.createInputStream();
    }

    /**
     * Writes the content of the image to an OutputStream. The OutputStream in NOT closed at the
     * end.
     * @param out the OutputStream
     * @throws IOException if an I/O error occurs
     */
    public void writeTo(OutputStream out) throws IOException {
        InputStream in = createInputStream();
        try {
            IOUtils.copy(in, out);
        } finally {
            IOUtils.closeQuietly(in);
        }
    }

    /**
     * Represents a factory for InputStream objects. Make sure the class is thread-safe!
     */
    public interface InputStreamFactory {

        /**
         * Indicates whether this factory is only usable once or many times.
         * @return true if the factory can only be used once
         */
        boolean isUsedOnceOnly();

        /**
         * Creates and returns a new InputStream.
         * @return the new InputStream
         */
        InputStream createInputStream();

        /**
         * Closes the factory and releases any resources held open during the lifetime of this
         * object.
         */
        void close();

    }

    /**
     * InputStream factory that can return a pre-constructed InputStream exactly once.
     */
    private static class SingleStreamFactory implements InputStreamFactory {

        private InputStream in;

        public SingleStreamFactory(InputStream in) {
            this.in = in;
        }

        public synchronized InputStream createInputStream() {
            if (this.in != null) {
                InputStream tempin = this.in;
                this.in = null; //Don't close, just remove the reference
                return tempin;
            } else {
                throw new IllegalStateException("Can only create an InputStream once!");
            }
        }

        public synchronized void close() {
            IOUtils.closeQuietly(this.in);
            this.in = null;
        }

        public boolean isUsedOnceOnly() {
            return true;
        }

        /** {@inheritDoc} */
        protected void finalize() {
            close();
        }

    }

    /**
     * InputStream factory that wraps a byte array.
     */
    public static class ByteArrayStreamFactory implements InputStreamFactory {

        private byte[] data;

        /**
         * Main constructor.
         * @param data the byte array
         */
        public ByteArrayStreamFactory(byte[] data) {
            this.data = data;
        }

        /** {@inheritDoc} */
        public InputStream createInputStream() {
            return new ByteArrayInputStream(data);
        }

        /** {@inheritDoc} */
        public void close() {
            //nop
        }

        /** {@inheritDoc} */
        public boolean isUsedOnceOnly() {
            return false;
        }

    }

}
