/*

   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.

 */
package org.apache.tools.ant.taskdefs.optional;

// -- Ant classes ------------------------------------------------------------
import org.apache.tools.ant.Project;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.util.JAXPUtils;

// -- Batik classes ----------------------------------------------------------
import org.apache.batik.apps.rasterizer.SVGConverter;
import org.apache.batik.apps.rasterizer.DestinationType;
import org.apache.batik.apps.rasterizer.SVGConverterException;
import org.apache.batik.util.XMLResourceDescriptor;

// -- SAX classes ------------------------------------------------------------
import org.xml.sax.XMLReader;

// -- Java SDK classes -------------------------------------------------------
import java.awt.geom.Rectangle2D;
import java.awt.Color;
import java.io.File;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.ArrayList;
import java.util.List;



/**
 * This Ant task can be used to convert SVG images to raster images.
 *
 * <p>Possible result raster image formats are PNG, JPEG, TIFF, and PDF. 
 * Batik {@link SVGConverter} is used to execute the conversion. You need
 * <em>Batik</em> to produce the first three raster image types and 
 * <em>FOP</em> to produce PDF.</p>
 *
 * @see SVGConverter SVGConverter
 * @see org.apache.batik.apps.rasterizer.Main Main
 *
 * @author <a href="mailto:ruini@iki.fi">Henri Ruini</a>
 * @version $Id$
 */
public class RasterizerTask extends MatchingTask {

    // -- Constants ----------------------------------------------------------
    /**
     * Default quality value for JPEGs. This value is used when 
     * the user doesn't set the quality.
     */
    private static final float DEFAULT_QUALITY = 0.99f;
    /**
     * Magic string indicating that any JAXP conforming XML parser can 
     * be used.
     */
    private static final String JAXP_PARSER = "jaxp";

    // -- Variables ----------------------------------------------------------
    /** Result image type. The default is PNG. */
    protected DestinationType resultType = DestinationType.PNG;

    /** Output image height. */
    protected float height = Float.NaN;
    /** Output image width. */
    protected float width = Float.NaN;
    /** Maximum output image height. */
    protected float maxHeight = Float.NaN;
    /** Maximum output image width. */
    protected float maxWidth = Float.NaN;
    /** Output image quality. */
    protected float quality = Float.NaN;
    /** Output Area of Interest (AOI) area. */
    protected String area = null;
    /** Output background color. */
    protected String background = null;
    /** Media type of CSS file used to produce output images. */
    protected String mediaType = null;
    /** Output pixel size - dots per inch. */
    protected float dpi = Float.NaN;
    /** Output image language. */
    protected String language = null;
    /** XML parser class currently in use. */
    protected String readerClassName = XMLResourceDescriptor.getXMLParserClassName();


    /** Source image path. */
    protected File srcFile = null;
    /** Destination image path. */
    protected File destFile = null;
    /** Source directory of images. */
    protected File srcDir = null;
    /** Destination directory for output images. */
    protected File destDir = null;
    /** Contents of <code>fileset</code> elements. */
    protected Vector filesets = new Vector();

    /** Converter object used to convert SVG images to raster images. */
    protected SVGConverter converter;



    // -- Constructors -------------------------------------------------------
    /**
     * Initializes a new rasterizer task.
     */
    public RasterizerTask() {
        converter = new SVGConverter(new RasterizerTaskSVGConverterController(this));
    }



    // -- Methods required by Ant --------------------------------------------
    /**
     * Gets <code>result</code> attribute value. 
     *
     * <p>See the documentation for valid values.</p>
     *
     * @param type Attribute value.
     */
    public void setResult(ValidImageTypes type) {
        this.resultType = getResultType(type.getValue());
    }

    /**
     * Gets <code>height</code> attribute value.
     *
     * <p>The attribute is optional.</p>
     *
     * @param height Attribute value.
     */
    public void setHeight(float height) {
        this.height = height;
    }

    /**
     * Gets <code>width</code> attribute value.
     *
     * <p>The attribute is optional.</p>
     *
     * @param width Attribute value.
     */
    public void setWidth(float width) {
        this.width = width;
    }

    /**
     * Gets <code>maxheight</code> attribute value.
     *
     * <p>The attribute is optional.</p>
     *
     * @param height Attribute value.
     */
    public void setMaxheight(float height) {
        this.maxHeight = height;
    }

    /**
     * Gets <code>maxwidth</code> attribute value.
     *
     * <p>The attribute is optional.</p>
     *
     * @param width Attribute value.
     */
    public void setMaxwidth(float width) {
        this.maxWidth = width;
    }

    /**
     * Gets <code>quality</code> attribute value.
     *
     * <p>The value have to be a float between 0 and 1 excluded. 
     * The attribute is optional.</p>
     *
     * @param quality Attribute value.
     */
    public void setQuality(float quality) {
        this.quality = quality;
    }

    /**
     * Gets <code>area</code> attribute value.
     *
     * <p>The value have to be four integers separated with whitespaces or
     * commas.
     * The attribute is optional.</p>
     *
     * @param area Attribute value.
     */
    public void setArea(String area) {
        this.area = area;
    }

    /**
     * Gets <code>bg</code> attribute value.
     *
     * <p>The value have to be three or four integers separated with 
     * whitespaces or commas.
     * The attribute is optional.</p>
     *
     * @param bg Attribute value.
     */
    public void setBg(String bg) {
        this.background = bg;
    }

    /**
     * Gets <code>media</code> attribute value.
     *
     * <p>The value have to a media type defined in CSS2 specifications.
     * Only visual media group is supported.</p>
     *
     * @param media Attribute value.
     */
    public void setMedia(ValidMediaTypes media) {
        this.mediaType = media.getValue();
    }

    /**
     * Gets <code>dpi</code> attribute value.
     *
     * <p>The value have to be a positive float. 
     * The attribute is optional.</p>
     *
     * @param dpi Attribute value.
     */
    public void setDpi(float dpi) {
        this.dpi = dpi;
    }

    /**
     * Gets <code>lang</code> attribute value.
     *
     * <p>See SVG specification for valid values.
     * The attribute is optional.</p>
     *
     * @param lang Attribute value.
     */
    public void setLang(String language) {
        this.language = language;
    }

    /**
     * Sets classname of an XML parser.
     * The attribute is optional.
     *
     * @param value Java classname of an XML parser.
     */
    public void setClassname(String value) {
        this.readerClassName = value;
    }

    /**
     * Gets <code>src</code> attribute value.
     *
     * <p>One of the following have to have a value: this attribute, 
     * <code>srcdir</code> attribute or <code>fileset</code> element.</p>
     *
     * @param file Attribute value.
     */
    public void setSrc(File file) {
        this.srcFile = file;
    }

    /**
     * Gets <code>dest</code> attribute value.
     *
     * <p>The attribute have to have a value when 
     * <code>src</code> attribute has a value.</p>
     *
     * @param file Attribute value.
     */
    public void setDest(File file) {
        this.destFile = file;
    }

    /**
     * Gets <code>srcdir</code> attribute value.
     *
     * <p>If <code>srcfile</code> attribute doesn't have a value then 
     * either this attribute have to have a value or the element have
     * to contain <code>fileset</code> elements.</p>
     *
     * @param dir Attribute value.
     */
    public void setSrcdir(File dir) {
        this.srcDir = dir;
    }

    /**
     * Gets <code>destdir</code> attribute value.
     *
     * <p>This attribute have to have a value if either 
     * <code>srcdir</code> attribute or <code>fileset</code> elements 
     * exists.</p>
     *
     * @param dir Attribute value.
     */
    public void setDestdir(File dir) {
        this.destDir = dir;
    }

    /**
     * Reads <code>fileset</code> elements.
     *
     * <p><code>fileset</code> elements can be used when there are many files
     * to be rasterized.</p>
     *
     * @param set <code>fileset</code> elements
     */
    public void addFileset(FileSet set) {
        filesets.addElement(set);
    }

    /**
     * Validates and sets input values and files, then starts the conversion 
     * process.
     *
     * <p>See Ant documentation to find out more about the meaning of
     * <code>execute</code> in Ant task.</p>
     *
     * @throws BuildException Parameters are not set correctly or the conversion fails.
     */
    public void execute() throws BuildException {

        String[] sources;        // Array of input files.

        // Store default XML parser information and set user class.
        String defaultParser = XMLResourceDescriptor.getXMLParserClassName();
        // Throws BuildException.
        XMLResourceDescriptor.setXMLParserClassName(getParserClassName(readerClassName));

        try {
            // Check file and directory values.
            if(this.srcFile != null) {
                if(this.destFile == null) {
                    throw new BuildException("dest attribute is not set.");
                }
            } else {
                if((this.srcDir == null) && (filesets.size() == 0)) {
                    throw new BuildException("No input files! Either srcdir or fileset have to be set.");
                }
                if(this.destDir == null) {
                    throw new BuildException("destdir attribute is not set!");
                }
            }

            // Throws BuildException.
            setRasterizingParameters();

            // Get and set source(s).
            sources = getSourceFiles();
            converter.setSources(sources);

            // Set destination.
            if(this.srcFile != null) {
                converter.setDst(this.destFile);
            } else {
                converter.setDst(this.destDir);
            }

            // Input filenames are stored in the converter and 
            // everything is ready for the conversion.

            log("Rasterizing " + sources.length + 
                (sources.length == 1 ? " image " : " images ") + 
                "from SVG to " + this.resultType.toString() + ".");

            try {
                converter.execute();
            } catch(SVGConverterException sce) {
                throw new BuildException(sce.getMessage());
            }
        } finally {
            // Restore default XML parser for the next execute.
            XMLResourceDescriptor.setXMLParserClassName(defaultParser);
        }
    }



    // -- Internal methods ---------------------------------------------------
    /**
     * Checks and sets parameter values to the converter.
     *
     * <p>Some invalid values are just swallowed and default values are 
     * used instead. This is done when the invalid value cannot
     * be correctly differentiated from the default value and the default
     * value doesn't cause any harm. <code>BuildException</code> is thrown
     * if the invalid value is clearly recognized.</p>
     * 
     * @throws BuildException Invalid parameter value.
     */
    protected void setRasterizingParameters() 
        throws BuildException {
        if(this.resultType != null) {
            converter.setDestinationType(this.resultType);
        } else {
            throw new BuildException("Unknown value in result parameter.");
        }
        // Set size values.
        if(!Float.isNaN(this.width)) {
            if(this.width < 0) {
                throw new BuildException("Value of width parameter must positive.");
            }
            converter.setWidth(this.width);
        }
        if(!Float.isNaN(this.height)) {
            if(this.height < 0) {
                throw new BuildException("Value of height parameter must positive.");
            }
            converter.setHeight(this.height);
        }
        // Set maximum size values.
        if(!Float.isNaN(this.maxWidth)) {
            if(this.maxWidth < 0) {
                throw new BuildException("Value of maxwidth parameter must positive.");
            }
            converter.setMaxWidth(this.maxWidth);
        }
        if(!Float.isNaN(this.maxHeight)) {
            if(this.maxHeight < 0) {
                throw new BuildException("Value of maxheight parameter must positive.");
            }
            converter.setMaxHeight(this.maxHeight);
        }
        // The quality is just swallowed if the result type is not correct.
        if(allowedToSetQuality(resultType)) {
            if(!Float.isNaN(this.quality)) {
                // Throws BuildException.
                converter.setQuality(getQuality(this.quality));
            } else {
                // Set something to quiet irritating error 
                // from JPEGTranscoder.
                converter.setQuality(DEFAULT_QUALITY);
            }
        }
        if(this.area != null) {
            // Throws BuildException.
            converter.setArea(getAreaOfInterest(this.area));
        }
        if(this.background != null) {
            // Throws BuildException.
            converter.setBackgroundColor(getBackgroundColor(this.background));
        }
        if(this.mediaType != null) {
            // Ant takes care of the correct media type values.
            converter.setMediaType(this.mediaType);
        }
        if(!Float.isNaN(this.dpi)) {
            if(this.dpi < 0) {
                throw new BuildException("Value of dpi parameter must positive.");
            }
            // The calculation is the same as 2.54/dpi*10 where
            converter.setPixelUnitToMillimeter(25.4f/this.dpi);
        }
        if(this.language != null) {
            converter.setLanguage(this.language);
        }
    }

    /**
     * Gets source files from the task parameters and child elements, 
     * combines those to a one list, and returns the list.
     *
     * @return Array of source filename strings.
     */
    protected String[] getSourceFiles() {

        List inputFiles = new ArrayList(); // Input files in temp list.

        if(this.srcFile != null) {
            // Only one source and destination file have been set.
            inputFiles.add(this.srcFile.getAbsolutePath());
        } else {
            // Unknown number of files have to be converted. destdir 
            // attribute and either srcdir attribute or fileset element 
            // have been set.

            // Read source files from the child patterns.
            // The value of srcdir attribute overrides the dir attribute in
            // fileset element.
            if(this.srcDir != null) {
                // fileset is declared in the super class.
                // Scan to get all the files in srcdir directory that 
                // should be in input files.
                fileset.setDir(this.srcDir);
                DirectoryScanner ds = fileset.getDirectoryScanner(project);
                String[] includedFiles = ds.getIncludedFiles();
                // Add file and its path to the input file vector.
                for (int j = 0 ; j < includedFiles.length ; j++) {
                    File newFile = new File(srcDir.getPath(), includedFiles[j]);
                    inputFiles.add(newFile.getAbsolutePath());
                }
            }
            // Read source files from child filesets.
            for (int i = 0 ; i < filesets.size() ; i++) {
                // Scan to get all the files in this fileset that 
                // should be in input files.
                FileSet fs = (FileSet) filesets.elementAt(i);
                DirectoryScanner ds = fs.getDirectoryScanner(project);
                String[] includedFiles = ds.getIncludedFiles();
                // Add file and its path to the input file vector.
                for (int j = 0 ; j < includedFiles.length ; j++) {
                    File newFile = new File(fs.getDir(project).getPath(), includedFiles[j]);
                    inputFiles.add(newFile.getAbsolutePath());
                }
            }
        }

        // Convert List to array and return the array.
        return (String[])inputFiles.toArray(new String[0]);
    }

    /**
     * Returns the correct result image type object.
     *
     * @param type Result image type as a string.
     *
     * @return Result image type as an object or <code>null</code> if the parameter doesn't have corresponding object.
     */
    protected DestinationType getResultType(String type) {
        if(type.equals(DestinationType.PNG_STR)) {
            return DestinationType.PNG;
        } else if(type.equals(DestinationType.JPEG_STR)) {
            return DestinationType.JPEG;
        } else if(type.equals(DestinationType.TIFF_STR)) {
            return DestinationType.TIFF;
        } else if(type.equals(DestinationType.PDF_STR)) {
            return DestinationType.PDF;
        }
        return null;
    }

    /**
     * Checks if the quality value can be set. Only result image type
     * is checked.
     *
     * @param type Result image type.
     *
     * @return <code>true</code> if the quality can be set and <code>false</code> otherwise.
     */
    protected boolean allowedToSetQuality(DestinationType type) {
        if(!type.toString().equals(DestinationType.JPEG_STR)) {
            return false;
        }
        return true;
    }

    /**
     * Returns a valid quality value.
     *
     * @param quality Input quality value to be tested.
     *
     * @return Valid quality value.
     *
     * @throws BuildException Input quality value is not valid.
     */
    protected float getQuality(float quality) 
        throws BuildException {
        if((quality <= 0) || (quality >= 1)) {
            throw new BuildException("quality parameter value have to be between 0 and 1.");
        }
        return quality;
    }

    /**
     * Returns a valid Area of Interest (AOI) as a Rectangle2D object.
     *
     * @param area AOI input area.
     *
     * @return A valid AOI rectangle.
     *
     * @throws BuildException AOI area is invalid.
     */
    protected Rectangle2D getAreaOfInterest(String area) 
        throws BuildException {

        float x;            // Upper left x point value of the area.
        float y;            // Upper left y point value of the area.
        float width;        // Area width value.
        float height;       // Area height value.
        String token;       // A token from the input string.
        StringTokenizer tokenizer = new StringTokenizer(area, ", \t\n\r\f");
                            // Input string tokenizer.

        if(tokenizer.countTokens() != 4) {
            throw new BuildException("There must be four numbers in the area parameter: x, y, width, and height.");
        }
        try {
            x = Float.parseFloat(tokenizer.nextToken());
            y = Float.parseFloat(tokenizer.nextToken());
            width = Float.parseFloat(tokenizer.nextToken());
            height = Float.parseFloat(tokenizer.nextToken());
        } catch(NumberFormatException nfe) {
            throw new BuildException("Invalid area parameter value: " + nfe.toString());
        }

        // Negative values are not allowed.
        if((x < 0) || (y < 0) || (width < 0) || (height < 0)) {
            throw new BuildException("Negative values are not allowed in area parameter.");
        }

        return new Rectangle2D.Float(x, y, width, height);
    }

    /**
     * Returns a valid background color object.
     *
     * @param argb String containing color channel values.
     *
     * @return A valid background color.
     *
     * @throws BuildException Input value is invalid.
     */
    protected Color getBackgroundColor(String argb) 
        throws BuildException {

        int a;              // Value of the alpha channel.
        int r;              // Value of the red channel.
        int g;              // Value of the green channel.
        int b;              // Value of the blue channel.
        String token;       // A token from the input string.
        StringTokenizer tokenizer = new StringTokenizer(argb, ", \t\n\r\f");
                            // Input string tokenizer.

        try {
            if(tokenizer.countTokens() == 3) {
                // Default alpha channel is opaque.
                a = 255;
            } else if(tokenizer.countTokens() == 4) {
                a = Integer.parseInt(tokenizer.nextToken());
            } else {
                throw new BuildException("There must be either three or four numbers in bg parameter: (alpha,) red, green, and blue.");
            }
            r = Integer.parseInt(tokenizer.nextToken());
            g = Integer.parseInt(tokenizer.nextToken());
            b = Integer.parseInt(tokenizer.nextToken());
        } catch(NumberFormatException nfe) {
            throw new BuildException("Invalid bg parameter value: " + nfe.toString());
        }

        // Check that the values are valid.
        if((a < 0) ||(a > 255) || (r < 0) ||(r > 255) || 
           (g < 0) ||(g > 255) || (b < 0) ||(b > 255)) {
            throw new BuildException("bg parameter value is invalid. Numbers have to be between 0 and 255.");
        }

        return new Color(r, g, b, a);
    }

    /**
     * Returns name of an XML parser.
     * Magic string {@link #JAXP_PARSER} is also accepted.
     *
     * @param className Name of the XML parser class or a magic string.
     *
     * @return Name of an XML parser.
     *
     * @throws BuildException Unable to get the name of JAXP parser.
     */
    private String getParserClassName(final String className) {
        String name = className;
        if ((className == null) || className.equals(JAXP_PARSER)) {
            // Set first JAXP parser.
            // Throws BuildException.
            XMLReader reader = JAXPUtils.getXMLReader();
            name = reader.getClass().getName();
        }

        log("Using class '" + name + "' to parse SVG documents.", Project.MSG_VERBOSE);
        return name;
    }



    // -----------------------------------------------------------------------
    //   Inner classes
    // -----------------------------------------------------------------------

    /**
     * Defines the valid attribute values for <code>result</code> parameter.
     *
     * <p>See the Ant documentation for more information.</p>
     *
     * @author <a href="mailto:ruini@iki.fi">Henri Ruini</a>
     * @version $Id$
     */
    public static class ValidImageTypes extends EnumeratedAttribute {

        /**
         * Defines valid image types.
         *
         * @return Array of valid values as strings.
         */
        public String[] getValues() {
            return new String[] 
                {DestinationType.PNG_STR, 
                DestinationType.JPEG_STR, 
                DestinationType.TIFF_STR, 
                DestinationType.PDF_STR};
        }
    }

    /**
     * Defines the valid attribute values for a media parameter.
     *
     * <p>See the Ant documentation for more information.</p>
     *
     * @author <a href="mailto:ruini@iki.fi">Henri Ruini</a>
     * @version $Id$
     */
    public static class ValidMediaTypes extends EnumeratedAttribute {

        /**
         * Defines valid media types.
         *
         * <p>The types are defined in CSS2 specification.
         * Only visual media group is supported.</p>
         *
         * @return Array of valid values as strings.
         */
        public String[] getValues() {
            return new String[] {"all", "handheld", "print", 
                "projection", "screen", "tty", "tv"};
        }
    }

}
