/*
 *  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
 *
 *      https://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.image;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
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.types.Mapper;
import org.apache.tools.ant.types.optional.imageio.Draw;
import org.apache.tools.ant.types.optional.imageio.ImageOperation;
import org.apache.tools.ant.types.optional.imageio.Rotate;
import org.apache.tools.ant.types.optional.imageio.Scale;
import org.apache.tools.ant.types.optional.imageio.TransformOperation;
import org.apache.tools.ant.util.FileNameMapper;
import org.apache.tools.ant.util.IdentityMapper;
import org.apache.tools.ant.util.StringUtils;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * A MatchingTask which relies on Java ImageIO to read existing image files
 * and write the results of AWT image manipulation operations.
 * The operations are represented as ImageOperation DataType objects.
 * The task replaces a JAI-based Image task which no longer works with Java 9+.
 *
 * @see ImageOperation
 * @see org.apache.tools.ant.types.DataType
 */
public class ImageIOTask extends MatchingTask {
    private final List<ImageOperation> instructions = new ArrayList<>();
    private boolean overwrite = false;
    private final List<FileSet> filesets = new ArrayList<>();
    private File srcDir = null;
    private File destDir = null;

    private ImageFormat outputFormat;

    private boolean garbageCollect = false;

    private boolean failOnError = true;

    private Mapper mapperElement = null;

    /**
     * Add a set of files to be deleted.
     * @param set the FileSet to add.
     */
    public void addFileset(FileSet set) {
        filesets.add(set);
    }

    /**
     * Set whether to fail on error.
     * If false, note errors to the output but keep going.
     * @param flag true or false.
     */
    public void setFailOnError(boolean flag) {
        failOnError = flag;
    }

    /**
     * Set the source dir to find the image files.
     * @param srcDir the directory in which the image files reside.
     */
    public void setSrcdir(File srcDir) {
        this.srcDir = srcDir;
    }

    /**
     * Set the output image format.
     * @param format an ImageFormat.
     */
    public void setFormat(ImageFormat format) {
        outputFormat = format;
    }

    /**
     * Set whether to overwrite a file if there is a naming conflict.
     * @param overwrite whether to overwrite.
     */
    public void setOverwrite(boolean overwrite) {
        this.overwrite = overwrite;
    }

    /**
     * Set whether to invoke Garbage Collection after each image processed.
     * Defaults to false.
     * @param gc whether to invoke the garbage collector.
     */
    public void setGc(boolean gc) {
        garbageCollect = gc;
    }

    /**
     * Set the destination directory for manipulated images.
     * @param destDir The destination directory.
     */
    public void setDestDir(File destDir) {
        this.destDir = destDir;
    }

    /**
     * Add an ImageOperation to chain.
     * @param instr The ImageOperation to append to the chain.
     */
    public void addImageOperation(ImageOperation instr) {
        instructions.add(instr);
    }

    /**
     * Add a Rotate ImageOperation to the chain.
     * @param instr The Rotate operation to add to the chain.
     * @see Rotate
     */
    public void addRotate(Rotate instr) {
        instructions.add(instr);
    }

    /**
     * Add a Scale ImageOperation to the chain.
     * @param instr The Scale operation to add to the chain.
     * @see Scale
     */
    public void addScale(Scale instr) {
        instructions.add(instr);
    }

    /**
     * Add a Draw ImageOperation to the chain.  DrawOperation
     * DataType objects can be nested inside the Draw object.
     * @param instr The Draw operation to add to the chain.
     * @see Draw
     * @see org.apache.tools.ant.types.optional.image.DrawOperation
     */
    public void addDraw(Draw instr) {
        instructions.add(instr);
    }

    /**
    * Add an ImageOperation to chain.
    * @param instr The ImageOperation to append to the chain.
    * @since Ant 1.7
    */
    public void add(ImageOperation instr) {
        addImageOperation(instr);
    }

    /**
     * Defines the mapper to map source to destination files.
     * @return a mapper to be configured
     * @exception BuildException if more than one mapper is defined
     * @since Ant 1.8.0
     */
    public Mapper createMapper() throws BuildException {
        if (mapperElement != null) {
            throw new BuildException("Cannot define more than one mapper",
                                     getLocation());
        }
        mapperElement = new Mapper(getProject());
        return mapperElement;
    }

    /**
     * Add a nested filenamemapper.
     * @param fileNameMapper the mapper to add.
     * @since Ant 1.8.0
     */
    public void add(FileNameMapper fileNameMapper) {
        createMapper().add(fileNameMapper);
    }

    /**
     * Executes all the chained ImageOperations on the files inside
     * the directory.
     * @param srcDir File
     * @param srcNames String[]
     * @param dstDir File
     * @param mapper FileNameMapper
     * @return int
     * @since Ant 1.8.0
     */
    public int processDir(final File srcDir, final String[] srcNames,
                          final File dstDir, final FileNameMapper mapper) {
        int writeCount = 0;

        for (final String srcName : srcNames) {
            final File srcFile = new File(srcDir, srcName).getAbsoluteFile();

            final String[] dstNames = mapper.mapFileName(srcName);
            if (dstNames == null) {
                log(srcFile + " skipped, don't know how to handle it",
                    Project.MSG_VERBOSE);
                continue;
            }

            for (String dstName : dstNames) {
                final File dstFile = new File(dstDir, dstName).getAbsoluteFile();

                if (dstFile.exists()) {
                    // avoid overwriting unless necessary
                    if (!overwrite
                       && srcFile.lastModified() <= dstFile.lastModified()) {

                        log(srcFile + " omitted as " + dstFile
                            + " is up to date.", Project.MSG_VERBOSE);

                        // don't overwrite the file
                        continue;
                    }

                    // avoid extra work while overwriting
                    if (!srcFile.equals(dstFile)) {
                        dstFile.delete();
                    }
                }
                processFile(srcFile, dstFile);
                ++writeCount;
            }
        }

        // run the garbage collector if wanted
        if (garbageCollect) {
            System.gc();
        }

        return writeCount;
    }

    /**
     * Executes all the chained ImageOperations on the file
     * specified.
     * @param file The file to be processed.
     * @deprecated this method isn't used anymore
     */
    @Deprecated
    public void processFile(File file) {
        processFile(file, new File(destDir == null
                                   ? srcDir : destDir, file.getName()));
    }

    /**
     * Executes all the chained ImageOperations on the file
     * specified.
     * @param file The file to be processed.
     * @param newFile The file to write to.
     * @since Ant 1.8.0
     */
    public void processFile(File file, File newFile) {
        log("Processing File: " + file.getAbsolutePath());

        try (ImageInputStream input = ImageIO.createImageInputStream(file)) {
            Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
            if (!readers.hasNext()) {
                log("No decoder available, skipping");
                return;
            }
            ImageReader reader = readers.next();
            if (outputFormat == null) {
                outputFormat = new ImageFormat(reader.getFormatName());
            }
            reader.setInput(input);

            BufferedImage image = reader.read(0);
            reader.dispose();

            for (ImageOperation instr : instructions) {
                if (instr instanceof TransformOperation) {
                    image = ((TransformOperation) instr).executeTransformOperation(image);
                } else {
                    log("Not a TransformOperation: " + instr);
                }
            }

            File dstParent = newFile.getParentFile();
            if (!dstParent.isDirectory()
                && !(dstParent.mkdirs() || dstParent.isDirectory())) {
                throw new BuildException("Failed to create parent directory %s",
                    dstParent);
            }

            if (overwrite && newFile.exists() && !newFile.equals(file)) {
                newFile.delete();
            }

            if (!ImageIO.write(image, outputFormat.getValue(), newFile)) {
                log("Failed to save the transformed file");
            }
        } catch (IOException | RuntimeException err) {
            if (!file.equals(newFile)) {
                newFile.delete();
            }
            if (!failOnError) {
                log("Error processing file:  " + err);
            } else {
                throw new BuildException(err);
            }
        }
    }

    /**
     * Executes the Task.
     * @throws BuildException on error.
     */
    @Override
    public void execute() throws BuildException {

        validateAttributes();

        try {
            File dest = (destDir != null) ? destDir : srcDir;

            int writeCount = 0;

            // build mapper
            final FileNameMapper mapper = mapperElement == null
                ? new IdentityMapper() : mapperElement.getImplementation();

            // deal with specified srcDir
            if (srcDir != null) {
                writeCount += processDir(srcDir,
                    super.getDirectoryScanner(srcDir).getIncludedFiles(), dest,
                    mapper);
            }
            // deal with the filesets
            for (FileSet fs : filesets) {
                writeCount += processDir(fs.getDir(),
                    fs.getDirectoryScanner().getIncludedFiles(),
                    dest, mapper);
            }

            if (writeCount > 0) {
                log("Processed " + writeCount + (writeCount == 1 ? " image." : " images."));
            }

        } catch (Exception err) {
            log(StringUtils.getStackTrace(err), Project.MSG_ERR);
            throw new BuildException(err.getMessage());
        }
    }

    /**
     * Ensure we have a consistent and legal set of attributes, and set
     * any internal flags necessary based on different combinations
     * of attributes.
     * @throws BuildException on error.
     */
    protected void validateAttributes() throws BuildException {
        if (srcDir == null && filesets.isEmpty()) {
            throw new BuildException(
                "Specify at least one source--a srcDir or a fileset.");
        }
        if (srcDir == null && destDir == null) {
            throw new BuildException("Specify the destDir, or the srcDir.");
        }
    }

    /**
     * defines acceptable image formats.
     */
    public static class ImageFormat extends EnumeratedAttribute {

        private static final String[] VALUES = ImageIO.getReaderFormatNames();

        /**
         * Constructor
         */
        public ImageFormat() {
        }

        /**
         * Constructor using a string.
         * @param value the value of the attribute
         */
        public ImageFormat(String value) {
            setValue(value);
        }

        /** {@inheritDoc}. */
        @Override
        public String[] getValues() {
            return VALUES;
        }
    }
}

