/*
 * 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: PreloaderSVG.java 1664570 2015-03-06 09:41:07Z lbernardo $ */

package org.apache.fop.image.loader.batik;

import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.io.InputStream;

import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;

import org.w3c.dom.Element;
import org.w3c.dom.svg.SVGDocument;

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

import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.anim.dom.SVGOMDocument;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.DefaultFontFamilyResolver;
import org.apache.batik.bridge.UnitProcessor;
import org.apache.batik.bridge.UserAgent;

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.impl.AbstractImagePreloader;
import org.apache.xmlgraphics.image.loader.impl.ImageXMLDOM;
import org.apache.xmlgraphics.image.loader.util.ImageUtil;
import org.apache.xmlgraphics.io.XmlSourceUtil;
import org.apache.xmlgraphics.util.MimeConstants;
import org.apache.xmlgraphics.util.UnitConv;

import org.apache.fop.svg.SimpleSVGUserAgent;
import org.apache.fop.util.UnclosableInputStream;

/**
 * Image preloader for SVG images.
 */
public class PreloaderSVG extends AbstractImagePreloader {

    /** Logger instance */
    private static Log log = LogFactory.getLog(PreloaderSVG.class);

    private boolean batikAvailable = true;

    /** {@inheritDoc} */
    public ImageInfo preloadImage(String uri, Source src, ImageContext context)
            throws IOException {
        ImageInfo info = null;
        if (batikAvailable) {
            try {
                Loader loader = new Loader();
                if (!loader.isSupportedSource(src)) {
                    return null;
                }
                info = loader.getImage(uri, src, context);
            } catch (NoClassDefFoundError e) {
                batikAvailable = false;
                log.warn("Batik not in class path", e);
                return null;
            }
        }
        if (info != null) {
            XmlSourceUtil.closeQuietly(src); //Image is fully read
        }
        return info;
    }

    /**
     * Returns the fully qualified classname of an XML parser for
     * Batik classes that apparently need it (error messages, perhaps)
     * @return an XML parser classname
     */
    public static String getParserName() {
        try {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            return factory.newSAXParser().getXMLReader().getClass().getName();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * This method is put in another class so that the class loader does not
     * attempt to load Batik related classes when constructing the SVGPreloader
     * class.
     */
    private final class Loader {

        private Loader() {
        }

        private ImageInfo getImage(String uri, Source src,
                ImageContext context) {
            // parse document and get the size attributes of the svg element

            InputStream in = null;
            try {
                SVGDocument doc;
                if (src instanceof DOMSource) {
                    DOMSource domSrc = (DOMSource)src;
                    doc = (SVGDocument)domSrc.getNode();
                } else {
                    in = new UnclosableInputStream(XmlSourceUtil.needInputStream(src));
                    int length = in.available();
                    in.mark(length + 1);
                    SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(
                            getParserName());
                    doc = factory.createSVGDocument(src.getSystemId(), in);
                }
                ImageInfo info = createImageInfo(uri, context, doc);

                return info;
            } catch (NoClassDefFoundError ncdfe) {
                if (in != null) {
                    try {
                        in.reset();
                    } catch (IOException ioe) {
                        // we're more interested in the original exception
                    }
                }
                batikAvailable = false;
                log.warn("Batik not in class path", ncdfe);
                return null;
            } catch (IOException e) {
                // If the svg is invalid then it throws an IOException
                // so there is no way of knowing if it is an svg document

                log.debug("Error while trying to load stream as an SVG file: "
                                       + e.getMessage());
                // assuming any exception means this document is not svg
                // or could not be loaded for some reason
                try {
                    in.reset();
                } catch (IOException ioe) {
                    // we're more interested in the original exception
                }
                return null;
            }
        }

        private ImageInfo createImageInfo(String uri, ImageContext context, SVGDocument doc) {
            Element e = doc.getRootElement();
            float pxUnitToMillimeter = UnitConv.IN2MM / context.getSourceResolution();
            UserAgent userAg = new SimpleSVGUserAgent(pxUnitToMillimeter,
                        new AffineTransform(), DefaultFontFamilyResolver.SINGLETON) {

                /** {@inheritDoc} */
                public void displayMessage(String message) {
                    log.debug(message);
                }

            };
            BridgeContext ctx = new BridgeContext(userAg);
            UnitProcessor.Context uctx = UnitProcessor.createContext(ctx, e);

            String s;
            // 'width' attribute - default is 100%
            s = e.getAttributeNS(null, SVGOMDocument.SVG_WIDTH_ATTRIBUTE);
            if (s.length() == 0) {
                s = SVGOMDocument.SVG_SVG_WIDTH_DEFAULT_VALUE;
            }
            float width = UnitProcessor.svgHorizontalLengthToUserSpace(
                    s, SVGOMDocument.SVG_WIDTH_ATTRIBUTE, uctx);

            // 'height' attribute - default is 100%
            s = e.getAttributeNS(null, SVGOMDocument.SVG_HEIGHT_ATTRIBUTE);
            if (s.length() == 0) {
                s = SVGOMDocument.SVG_SVG_HEIGHT_DEFAULT_VALUE;
            }
            float height = UnitProcessor.svgVerticalLengthToUserSpace(
                    s, SVGOMDocument.SVG_HEIGHT_ATTRIBUTE, uctx);

            int widthMpt = (int)Math.round(px2mpt(width, context.getSourceResolution()));
            int heightMpt = (int)Math.round(px2mpt(height, context.getSourceResolution()));

            ImageInfo info = new ImageInfo(uri, MimeConstants.MIME_SVG);
            ImageSize size = new ImageSize();
            size.setSizeInMillipoints(widthMpt, heightMpt);
            //Set the resolution to that of the FOUserAgent
            size.setResolution(context.getSourceResolution());
            size.calcPixelsFromSize();
            info.setSize(size);

            //The whole image had to be loaded for this, so keep it
            ImageXMLDOM xmlImage = new ImageXMLDOM(info,
                    doc, BatikImageFlavors.SVG_DOM);
            info.getCustomObjects().put(ImageInfo.ORIGINAL_IMAGE, xmlImage);
            return info;
        }

        private boolean isSupportedSource(Source src) {
            if (src instanceof DOMSource) {
                DOMSource domSrc = (DOMSource)src;
                return (domSrc.getNode() instanceof SVGDocument);
            } else {
                return ImageUtil.hasInputStream(src);
            }
        }

    }

    private static double px2mpt(double px, double resolution) {
        return px * 1000 * UnitConv.IN2PT / resolution;
    }

}
