/*
 * 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.dsh105.holoapi.HoloAPI;
import com.dsh105.holoapi.config.Settings;
import org.bukkit.ChatColor;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URI;

/**
 * Represents a generator used to produce images
 * <p>
 * Original code by @bobacadodl
 * https://forums.bukkit.org/threads/204902/
 */

public class ImageGenerator implements Generator {

    private String lines[];

    private String imageKey;

    private String imageUrl;
    private int imageHeight;
    private ImageChar imageChar = ImageChar.BLOCK;
    private boolean requiresBorder;
    private boolean hasLoaded;

    protected ImageGenerator(BufferedImage image, int height, ImageChar imgChar) {
        this.imageHeight = height;
        this.imageChar = imgChar;
        this.lines = this.generate(generateColours(image, height), imgChar.getImageChar(), false);
    }

    /**
     * Constructs an ImageGenerator for use in a Hologram
     *
     * @param image          {@link java.awt.image.BufferedImage} used to generate the image
     * @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
     */
    public ImageGenerator(BufferedImage image, int height, ImageChar imgChar, boolean requiresBorder) {
        this.imageHeight = height;
        this.imageChar = imgChar;
        this.requiresBorder = requiresBorder;
        this.lines = this.generate(generateColours(image, height), imgChar.getImageChar(), requiresBorder);
    }

    /**
     * Constructs an ImageGenerator for use in a Hologram
     *
     * @param imageUrl       URL to search the image for
     * @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
     */
    public ImageGenerator(String imageUrl, int height, ImageChar imgChar, boolean requiresBorder) {
        this(imageUrl, height, imgChar, true, requiresBorder);
    }

    /**
     * Constructs an ImageGenerator for use in a Hologram
     *
     * @param imageUrl       URL to search the image for
     * @param height         height of the display
     * @param imgChar        {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @param loadUrl        whether to automatically load the image from the specified URL
     * @param requiresBorder whether the display requires a border
     */
    public ImageGenerator(String imageUrl, int height, ImageChar imgChar, boolean loadUrl, boolean requiresBorder) {
        this.imageUrl = imageUrl;
        this.imageHeight = height;
        this.imageChar = imgChar;
        this.requiresBorder = requiresBorder;
        if (loadUrl) {
            this.loadUrlImage();
        }
    }

    /**
     * Constructs an ImageGenerator for use in a Hologram
     *
     * @param imageFile      file used to generate the image
     * @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 the image cannot be read
     */
    public ImageGenerator(File imageFile, int height, ImageChar imgChar, boolean requiresBorder) {
        this.imageHeight = height;
        this.imageChar = imgChar;
        this.requiresBorder = requiresBorder;
        BufferedImage image;
        try {
            image = ImageIO.read(imageFile);
        } catch (IOException e) {
            throw new RuntimeException("Cannot read image " + imageFile.getPath(), e);
        }
        this.lines = this.generate(generateColours(image, height), imgChar.getImageChar(), requiresBorder);
    }

    /**
     * Constructs an ImageGenerator for use in a Hologram
     *
     * @param imageKey       key to store the generator under. Only used for recognition of stored image generators
     * @param image          {@link java.awt.image.BufferedImage} used to generate the image
     * @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
     */
    public ImageGenerator(String imageKey, BufferedImage image, int height, ImageChar imgChar, boolean requiresBorder) {
        this(image, height, imgChar, requiresBorder);
        this.imageKey = imageKey;
    }

    /**
     * Constructs an ImageGenerator for use in a Hologram
     *
     * @param imageKey       key to store the generator under. Only used for recognition of stored image generators
     * @param imageUrl       URL to search the image for
     * @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
     */
    public ImageGenerator(String imageKey, String imageUrl, int height, ImageChar imgChar, boolean requiresBorder) {
        this(imageKey, imageUrl, height, imgChar, true, requiresBorder);
    }

    /**
     * Constructs an ImageGenerator for use in a Hologram
     *
     * @param imageKey       key to store the generator under. Only used for recognition of stored image generators
     * @param imageUrl       URL to search the image for
     * @param height         height of the display
     * @param imgChar        {@link com.dsh105.holoapi.image.ImageChar} of the display
     * @param loadUrl        whether to automatically load the image from the specified URL
     * @param requiresBorder whether the display requires a border
     */
    public ImageGenerator(String imageKey, String imageUrl, int height, ImageChar imgChar, boolean loadUrl, boolean requiresBorder) {
        this(imageUrl, height, imgChar, loadUrl, requiresBorder);
        this.imageKey = imageKey;
    }

    /**
     * Constructs an ImageGenerator for use in a Hologram
     *
     * @param imageKey       key to store the generator under. Only used for recognition of stored image generators
     * @param imageFile      file used to generate the image
     * @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 the image cannot be read
     */
    public ImageGenerator(String imageKey, File imageFile, int height, ImageChar imgChar, boolean requiresBorder) {
        this(imageFile, height, imgChar, requiresBorder);
        this.imageKey = imageKey;
    }

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

    /**
     * Loads the stored URL data to form a generated image
     *
     * @throws java.lang.IllegalArgumentException if the URL is not initiated
     * @throws java.lang.RuntimeException         if the image cannot be read
     */
    public void loadUrlImage() {
        if (this.hasLoaded) {
            return;
        }
        if (this.imageUrl == null) {
            throw new IllegalArgumentException("URL not initiated for ImageGenerator.");
        }
        URI uri = URI.create(this.imageUrl);
        BufferedImage image;
        try {
            image = ImageIO.read(uri.toURL());
        } catch (IOException e) {
            throw new RuntimeException("Cannot read image " + uri, e);
        }
        this.lines = this.generate(generateColours(image, this.imageHeight), this.imageChar.getImageChar(), requiresBorder);
        this.hasLoaded = true;
    }

    /**
     * Gets the generated lines
     *
     * @return generated lines
     */
    public String[] getLines() {
        return lines;
    }

    private String[] generate(ChatColor[][] colors, char imgchar, boolean border) {
        String[] lines = new String[colors[0].length];
        for (int y = 0; y < colors[0].length; y++) {
            String line = "";
            for (ChatColor[] color : colors) {
                ChatColor colour = color[y];
                if (colour == null) {
                    line += border ? Settings.TRANSPARENCY_WITH_BORDER.getValue() : Settings.TRANSPARENCY_WITHOUT_BORDER.getValue();
                } else {
                    line += colour.toString() + imgchar + ChatColor.RESET;
                }
            }

            if (!border) {
                line = line.trim();
            }

            lines[y] = line + ChatColor.RESET;
        }
        return lines;
    }

    private ChatColor[][] generateColours(BufferedImage image, int height) {
        double ratio = (double) image.getHeight() / image.getWidth();
        BufferedImage resized = resize(image, (int) (height / ratio), height);

        ChatColor[][] chatImg = new ChatColor[resized.getWidth()][resized.getHeight()];
        for (int x = 0; x < resized.getWidth(); x++) {
            for (int y = 0; y < resized.getHeight(); y++) {
                int rgb = resized.getRGB(x, y);
                ChatColor closest = ColourMap.getClosest(new Color(rgb, true));
                chatImg[x][y] = closest;
            }
        }
        return chatImg;
    }

    private BufferedImage resize(BufferedImage originalImage, int width, int height) {
        AffineTransform af = new AffineTransform();
        af.scale(width / (double) originalImage.getWidth(), height / (double) originalImage.getHeight());
        AffineTransformOp operation = new AffineTransformOp(af, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
        return operation.filter(originalImage, null);
    }

}