/*
 * 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: PreloaderEPS.java 750418 2009-03-05 11:03:54Z vhennebert $ */

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

import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.nio.ByteOrder;

import javax.imageio.stream.ImageInputStream;
import javax.xml.transform.Source;

import org.apache.xmlgraphics.image.loader.ImageContext;
import org.apache.xmlgraphics.image.loader.ImageInfo;
import org.apache.xmlgraphics.image.loader.ImageSize;
import org.apache.xmlgraphics.image.loader.util.ImageInputStreamAdapter;
import org.apache.xmlgraphics.image.loader.util.ImageUtil;
import org.apache.xmlgraphics.ps.DSCConstants;
import org.apache.xmlgraphics.ps.dsc.DSCException;
import org.apache.xmlgraphics.ps.dsc.DSCParser;
import org.apache.xmlgraphics.ps.dsc.DSCParserConstants;
import org.apache.xmlgraphics.ps.dsc.events.DSCComment;
import org.apache.xmlgraphics.ps.dsc.events.DSCCommentBoundingBox;
import org.apache.xmlgraphics.ps.dsc.events.DSCEvent;
import org.apache.xmlgraphics.util.MimeConstants;

/**
 * Image preloader for EPS images (Encapsulated PostScript).
 */
public class PreloaderEPS extends AbstractImagePreloader {

    /** Key for binary header object used in custom objects of the ImageInfo class. */
    public static final Object EPS_BINARY_HEADER = EPSBinaryFileHeader.class;
    /** Key for bounding box used in custom objects of the ImageInfo class. */
    public static final Object EPS_BOUNDING_BOX = Rectangle2D.class;

    /** {@inheritDoc} */
    public ImageInfo preloadImage(String uri, Source src, ImageContext context)
            throws IOException {
        if (!ImageUtil.hasImageInputStream(src)) {
            return null;
        }
        ImageInputStream in = ImageUtil.needImageInputStream(src);
        in.mark();
        ByteOrder originalByteOrder = in.getByteOrder();
        in.setByteOrder(ByteOrder.LITTLE_ENDIAN);
        EPSBinaryFileHeader binaryHeader = null;
        try {
            long magic = in.readUnsignedInt();
            magic &= 0xFFFFFFFFL; //Work-around for bug in Java 1.4.2
            // Check if binary header
            boolean supported = false;
            if (magic == 0xC6D3D0C5L) {
                supported = true; //binary EPS

                binaryHeader = readBinaryFileHeader(in);
                in.reset();
                in.mark(); //Mark start of file again
                in.seek(binaryHeader.psStart);

            } else if (magic == 0x53502125L) { //"%!PS" in little endian
                supported = true; //ascii EPS
                in.reset();
                in.mark(); //Mark start of file again
            } else {
                in.reset();
            }

            if (supported) {
                ImageInfo info = new ImageInfo(uri, MimeConstants.MIME_EPS);
                boolean success = determineSize(in, context, info);
                in.reset(); //Need to go back to start of file
                if (!success) {
                    //No BoundingBox found, so probably no EPS
                    return null;
                }
                if (in.getStreamPosition() != 0) {
                    throw new IllegalStateException("Need to be at the start of the file here");
                }
                if (binaryHeader != null) {
                    info.getCustomObjects().put(EPS_BINARY_HEADER, binaryHeader);
                }
                return info;
            } else {
                return null;
            }
        } finally {
            in.setByteOrder(originalByteOrder);
        }
    }

    private EPSBinaryFileHeader readBinaryFileHeader(ImageInputStream in) throws IOException {
        EPSBinaryFileHeader offsets = new EPSBinaryFileHeader();
        offsets.psStart = in.readUnsignedInt();
        offsets.psLength = in.readUnsignedInt();
        offsets.wmfStart = in.readUnsignedInt();
        offsets.wmfLength = in.readUnsignedInt();
        offsets.tiffStart = in.readUnsignedInt();
        offsets.tiffLength = in.readUnsignedInt();
        return offsets;
    }

    private boolean determineSize(ImageInputStream in, ImageContext context, ImageInfo info)
            throws IOException {

        in.mark();
        try {
            Rectangle2D bbox = null;
            DSCParser parser;
            try {
                parser = new DSCParser(new ImageInputStreamAdapter(in));
                outerLoop:
                while (parser.hasNext()) {
                    DSCEvent event = parser.nextEvent();
                    switch (event.getEventType()) {
                    case DSCParserConstants.HEADER_COMMENT:
                    case DSCParserConstants.COMMENT:
                        //ignore
                        break;
                    case DSCParserConstants.DSC_COMMENT:
                        DSCComment comment = event.asDSCComment();
                        if (comment instanceof DSCCommentBoundingBox) {
                            DSCCommentBoundingBox bboxComment = (DSCCommentBoundingBox)comment;
                            if (DSCConstants.BBOX.equals(bboxComment.getName()) && bbox == null) {
                                bbox = (Rectangle2D)bboxComment.getBoundingBox().clone();
                                //BoundingBox is good but HiRes is better so continue
                            } else if (DSCConstants.HIRES_BBOX.equals(bboxComment.getName())) {
                                bbox = (Rectangle2D)bboxComment.getBoundingBox().clone();
                                //HiRefBBox is great so stop
                                break outerLoop;
                            }
                        }
                        break;
                    default:
                        //No more header so stop
                        break outerLoop;
                    }
                }
                if (bbox == null) {
                    return false;
                }
            } catch (DSCException e) {
                throw new IOException("Error while parsing EPS file: " + e.getMessage());
            }

            ImageSize size = new ImageSize();
            size.setSizeInMillipoints(
                    (int)Math.round(bbox.getWidth() * 1000),
                    (int)Math.round(bbox.getHeight() * 1000));
            size.setResolution(context.getSourceResolution());
            size.calcPixelsFromSize();
            info.setSize(size);
            info.getCustomObjects().put(EPS_BOUNDING_BOX, bbox);
            return true;
        } finally {
            in.reset();
        }
    }

    /**
     * Holder class for various pointers to the contents of the EPS file.
     */
    public static class EPSBinaryFileHeader {

        private long psStart = 0;
        private long psLength = 0;
        private long wmfStart = 0;
        private long wmfLength = 0;
        private long tiffStart = 0;
        private long tiffLength = 0;

        /**
         * Returns the start offset of the PostScript section.
         * @return the start offset
         */
        public long getPSStart() {
            return psStart;
        }

        /**
         * Returns the length of the PostScript section.
         * @return the length of the PostScript section (in bytes)
         */
        public long getPSLength() {
            return psLength;
        }

        /**
         * Indicates whether the EPS has a WMF preview.
         * @return true if there is a WMF preview
         */
        public boolean hasWMFPreview() {
            return (wmfStart != 0);
        }

        /**
         * Returns the start offset of the WMF preview.
         * @return the start offset (or 0 if there's no WMF preview)
         */
        public long getWMFStart() {
            return wmfStart;
        }

        /**
         * Returns the length of the WMF preview.
         * @return the length of the WMF preview (in bytes)
         */
        public long getWMFLength() {
            return wmfLength;
        }

        /**
         * Indicates whether the EPS has a TIFF preview.
         * @return true if there is a TIFF preview
         */
        public boolean hasTIFFPreview() {
            return (tiffStart != 0);
        }

        /**
         * Returns the start offset of the TIFF preview.
         * @return the start offset (or 0 if there's no TIFF preview)
         */
        public long getTIFFStart() {
            return tiffStart;
        }

        /**
         * Returns the length of the TIFF preview.
         * @return the length of the TIFF preview (in bytes)
         */
        public long getTIFFLength() {
            return tiffLength;
        }

    }

}
