/*
 * This file is part of HoloAPI.
 *
 * HoloAPI is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * HoloAPI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with HoloAPI.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.dsh105.holoapi.image;

import com.google.common.collect.ImmutableList;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;

/**
 * Represents a generator used to produce animated image frames from either a GIF or set of images
 */

public class AnimatedImageGenerator implements Generator {

    // https://github.com/aadnk/DisplayFloatingImages/blob/master/DisplayFloatingImage/src/main/java/com/comphenix/example/nametags/GifImageMessage.java

    protected ImmutableList<GIFFrame> frames;
    private String key;
    private int maxHeight;
    private GIFFrame largestFrame;

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param frameRate   frame rate of the hologram display
     * @param imageFrames frames to use in the animated hologram
     */
    public AnimatedImageGenerator(int frameRate, ImageGenerator... imageFrames) {
        for (ImageGenerator generator : imageFrames) {
            frames.add(new GIFFrame(generator, frameRate));
        }
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param gifFile   GIF file used to generate the display
     * @param frameRate frame rate of the hologram display
     * @param height    height of the display
     * @param imgChar   {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(File gifFile, int frameRate, int height, ImageChar imgChar) {
        this(gifFile, frameRate, height, imgChar, false);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param gifFile        GIF file used to generate the display
     * @param frameRate      frame rate of the hologram display
     * @param height         height of the display
     * @param imgChar        {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @param requiresBorder whether the display requires a border
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(File gifFile, int frameRate, int height, ImageChar imgChar, boolean requiresBorder) {
        try {
            this.frames = this.readGif(gifFile);
        } catch (IOException e) {
            throw new RuntimeException("Cannot read file " + gifFile.getPath(), e);
        }
        this.prepare(height, imgChar, requiresBorder);
        this.prepareFrameRate(frameRate);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param gifFile GIF file used to generate the display
     * @param height  height of the display
     * @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(File gifFile, int height, ImageChar imgChar) {
        this(gifFile, height, imgChar, false);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param gifFile        GIF file used to generate the display
     * @param height         height of the display
     * @param imgChar        {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @param requiresBorder whether the display requires a border
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(File gifFile, int height, ImageChar imgChar, boolean requiresBorder) {
        try {
            this.frames = this.readGif(gifFile);
        } catch (IOException e) {
            throw new RuntimeException("Cannot read file " + gifFile.getPath(), e);
        }
        this.prepare(height, imgChar, requiresBorder);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param input     {@link java.io.InputStream} used to generate the display
     * @param frameRate frame rate of the hologram display
     * @param height    height of the display
     * @param imgChar   {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(InputStream input, int frameRate, int height, ImageChar imgChar) {
        this(input, frameRate, height, imgChar, false);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param input          {@link java.io.InputStream} used to generate the display
     * @param frameRate      frame rate of the hologram display
     * @param height         height of the display
     * @param imgChar        {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @param requiresBorder whether the display requires a border
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(InputStream input, int frameRate, int height, ImageChar imgChar, boolean requiresBorder) {
        try {
            this.frames = this.readGif(input);
        } catch (IOException e) {
            throw new RuntimeException("Cannot read input", e);
        }
        this.prepare(height, imgChar, requiresBorder);
        this.prepareFrameRate(frameRate);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param input   {@link java.io.InputStream} used to generate the display
     * @param height  height of the display
     * @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(InputStream input, int height, ImageChar imgChar) {
        this(input, height, imgChar, false);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param input          {@link java.io.InputStream} used to generate the display
     * @param height         height of the display
     * @param imgChar        {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @param requiresBorder whether the display requires a border
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(InputStream input, int height, ImageChar imgChar, boolean requiresBorder) {
        try {
            this.frames = this.readGif(input);
        } catch (IOException e) {
            throw new RuntimeException("Cannot read input", e);
        }
        this.prepare(height, imgChar, requiresBorder);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param key         key to store the generator under. Only used for recognition of stored animation generators
     * @param frameRate   frame rate of the hologram display
     * @param imageFrames frames to use in the animated hologram
     */
    public AnimatedImageGenerator(String key, int frameRate, ImageGenerator... imageFrames) {
        this(frameRate, imageFrames);
        this.key = key;
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param key       key to store the generator under. Only used for recognition of stored animation generators
     * @param gifFile   GIF file used to generate the display
     * @param frameRate frame rate of the hologram display
     * @param height    height of the display
     * @param imgChar   {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(String key, File gifFile, int frameRate, int height, ImageChar imgChar) {
        this(key, gifFile, frameRate, height, imgChar, false);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param key            key to store the generator under. Only used for recognition of stored animation generators
     * @param gifFile        GIF file used to generate the display
     * @param frameRate      frame rate of the hologram display
     * @param height         height of the display
     * @param imgChar        {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @param requiresBorder whether the display requires a border
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(String key, File gifFile, int frameRate, int height, ImageChar imgChar, boolean requiresBorder) {
        this(gifFile, frameRate, height, imgChar, requiresBorder);
        this.key = key;
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param key     key to store the generator under. Only used for recognition of stored animation generators
     * @param gifFile GIF file used to generate the display
     * @param height  height of the display
     * @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @throws java.io.IOException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(String key, File gifFile, int height, ImageChar imgChar) throws IOException {
        this(key, gifFile, height, imgChar, false);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param key            key to store the generator under. Only used for recognition of stored animation generators
     * @param gifFile        GIF file used to generate the display
     * @param height         height of the display
     * @param imgChar        {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @param requiresBorder whether the display requires a border
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(String key, File gifFile, int height, ImageChar imgChar, boolean requiresBorder) {
        this(gifFile, height, imgChar, requiresBorder);
        this.key = key;
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param key       key to store the generator under. Only used for recognition of stored animation generators
     * @param input     {@link java.io.InputStream} used to generate the display
     * @param frameRate frame rate of the hologram display
     * @param height    height of the display
     * @param imgChar   {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(String key, InputStream input, int frameRate, int height, ImageChar imgChar) {
        this(key, input, frameRate, height, imgChar, false);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param key            key to store the generator under. Only used for recognition of stored animation generators
     * @param input          {@link java.io.InputStream} used to generate the display
     * @param frameRate      frame rate of the hologram display
     * @param height         height of the display
     * @param imgChar        {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @param requiresBorder whether the display requires a border
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(String key, InputStream input, int frameRate, int height, ImageChar imgChar, boolean requiresBorder) {
        this(input, frameRate, height, imgChar, requiresBorder);
        this.key = key;
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param key     key to store the generator under. Only used for recognition of stored animation generators
     * @param input   {@link java.io.InputStream} used to generate the display
     * @param height  height of the display
     * @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(String key, InputStream input, int height, ImageChar imgChar) {
        this(key, input, height, imgChar, false);
    }

    /**
     * Constructs an AnimatedImageGenerator for use in an AnimatedHologram
     *
     * @param key            key to store the generator under. Only used for recognition of stored animation generators
     * @param input          {@link java.io.InputStream} used to generate the display
     * @param height         height of the display
     * @param imgChar        {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @param requiresBorder whether the display requires a border
     * @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
     */
    public AnimatedImageGenerator(String key, InputStream input, int height, ImageChar imgChar, boolean requiresBorder) {
        this(input, height, imgChar, requiresBorder);
        this.key = key;
    }

    protected AnimatedImageGenerator(String key) {
        this.key = key;
    }

    protected void prepare(int height, ImageChar imgChar) {
        this.prepare(height, imgChar, false);
    }

    protected void prepare(int height, ImageChar imgChar, boolean requiresBorder) {
        this.calculateMaxHeight();
        for (GIFFrame frame : frames) {
            int imageHeight = (int) ((frame.image.getHeight() / (double) this.maxHeight) * height);
            frame.imageGenerator = new ImageGenerator(frame.image, imageHeight, imgChar, requiresBorder);
        }
    }

    protected void prepareFrameRate(int frameRate) {
        for (GIFFrame frame : this.frames) {
            frame.delay = frameRate;
        }
    }

    protected void calculateMaxHeight() {
        int maxHeight = 0;
        GIFFrame largestFrame = this.frames.get(0);
        for (GIFFrame frame : frames) {
            if (frame.image.getHeight() > maxHeight) {
                maxHeight = frame.image.getHeight();
                largestFrame = frame;
            }
        }
        this.maxHeight = maxHeight;
        this.largestFrame = largestFrame;
    }

    protected ImmutableList<GIFFrame> readGif(InputStream input) throws IOException {
        GIFFrame[] frames;

        ImageReader reader = ImageIO.getImageReadersBySuffix("GIF").next();
        reader.setInput(ImageIO.createImageInputStream(input));
        frames = readGIF(reader);

        return ImmutableList.copyOf(frames);
    }

    private ImmutableList<GIFFrame> readGif(File inputFile) throws IOException {
        GIFFrame[] frames;

        ImageReader reader = ImageIO.getImageReadersBySuffix("GIF").next();
        reader.setInput(ImageIO.createImageInputStream(inputFile));
        frames = readGIF(reader);

        return ImmutableList.copyOf(frames);
    }

    // Source: https://stackoverflow.com/questions/8933893/convert-animated-gif-frames-to-separate-bufferedimages-java
    private GIFFrame[] readGIF(ImageReader reader) throws IOException {
        ArrayList<GIFFrame> frames = new ArrayList<>(2);

        int width = -1;
        int height = -1;

        IIOMetadata metadata = reader.getStreamMetadata();
        if (metadata != null) {
            IIOMetadataNode globalRoot = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());

            NodeList globalScreenDescriptor = globalRoot.getElementsByTagName("LogicalScreenDescriptor");

            if (globalScreenDescriptor != null && globalScreenDescriptor.getLength() > 0) {
                IIOMetadataNode screenDescriptor = (IIOMetadataNode) globalScreenDescriptor.item(0);

                if (screenDescriptor != null) {
                    width = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenWidth"));
                    height = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenHeight"));
                }
            }
        }

        BufferedImage master = null;
        Graphics2D masterGraphics = null;

        for (int frameIndex = 0; ; frameIndex++) {
            BufferedImage image;
            try {
                image = reader.read(frameIndex);
            } catch (IndexOutOfBoundsException io) {
                break;
            }

            if (width == -1 || height == -1) {
                width = image.getWidth();
                height = image.getHeight();
            }

            IIOMetadataNode root = (IIOMetadataNode) reader.getImageMetadata(frameIndex).getAsTree("javax_imageio_gif_image_1.0");
            IIOMetadataNode gce = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
            int delay = Integer.valueOf(gce.getAttribute("delayTime"));
            String disposal = gce.getAttribute("disposalMethod");

            int x = 0;
            int y = 0;

            if (master == null) {
                master = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
                masterGraphics = master.createGraphics();
                masterGraphics.setBackground(new Color(0, 0, 0, 0));
            } else {
                NodeList children = root.getChildNodes();
                for (int nodeIndex = 0; nodeIndex < children.getLength(); nodeIndex++) {
                    Node nodeItem = children.item(nodeIndex);
                    if (nodeItem.getNodeName().equals("ImageDescriptor")) {
                        NamedNodeMap map = nodeItem.getAttributes();
                        x = Integer.valueOf(map.getNamedItem("imageLeftPosition").getNodeValue());
                        y = Integer.valueOf(map.getNamedItem("imageTopPosition").getNodeValue());
                    }
                }
            }
            masterGraphics.drawImage(image, x, y, null);

            BufferedImage copy = new BufferedImage(master.getColorModel(), master.copyData(null), master.isAlphaPremultiplied(), null);
            frames.add(new GIFFrame(copy, (int) Math.ceil(delay / 2.5D), disposal));

            if (disposal.equals("restoreToPrevious")) {
                BufferedImage from = null;
                for (int i = frameIndex - 1; i >= 0; i--) {
                    if (!frames.get(i).disposal.equals("restoreToPrevious") || frameIndex == 0) {
                        from = frames.get(i).image;
                        break;
                    }
                }

                master = new BufferedImage(from.getColorModel(), from.copyData(null), from.isAlphaPremultiplied(), null);
                masterGraphics = master.createGraphics();
                masterGraphics.setBackground(new Color(0, 0, 0, 0));
            } else if (disposal.equals("restoreToBackgroundColor")) {
                masterGraphics.clearRect(x, y, image.getWidth(), image.getHeight());
            }
        }
        reader.dispose();

        return frames.toArray(new GIFFrame[frames.size()]);
    }

    @Override
    public String getKey() {
        return key;
    }

    /**
     * Gets the frames of the generator
     *
     * @return frames of the generator
     */
    public ImmutableList<GIFFrame> getFrames() {
        return this.frames;
    }

    /**
     * Gets the maximum frame height
     *
     * @return maximum frame height
     */
    public int getMaxHeight() {
        return maxHeight;
    }

    /**
     * Gets the largest frame of the generator
     *
     * @return largest frame of the generator
     */
    public GIFFrame getLargestFrame() {
        return largestFrame;
    }
}
