package net.returnvoid.io;

import java.util.ArrayList;

import net.returnvoid.tools.RMath;
import processing.core.PApplet;
import processing.core.PImage;

/*
 * This code is copyright (c) Diana Lange 2017
 *
 * The library is published under the Creative Commons license NonCommercial 4.0.
 * Please check https://creativecommons.org/licenses/by-nc/4.0/ for more information.
 * 
 * This program is distributed in the hope that it will be useful, but without any warranty.
 */

/**
 * ImageLoader will handle to load files from type image (.jpg, .tif, .png,
 * .gif). Currently doesn't support the automatic loading of files within
 * sub-folders. The files are loaded "on demand" as they are tried to be read
 * with the getCurrent() or any other get method. The class will handle to run
 * through all files of the given type via methods (e.g. next()) or on a
 * KeyEvent (keyPressed with a pre-defined / defined key / keycode).<br>
 * <br>
 * 
 * On default the keyPressed event with following keys are associated with
 * following methods: <br>
 * Arrow left : previous() - previous image <br>
 * Arrow right : next() - next image<br>
 * Arrow up : first() - first image in directory<br>
 * Arrow down: last() - last image in directory
 * <br>
 * Tab : nextRandom() - random image in directory <br>
 * <br>
 * Use disableKeys() to disable these automatic set key events.<br>
 * <br>
 * You can add ImageUpdateObservers. These observers will be informed when the
 * current image has changed. (e.g. after every next() method call).<br>
 * 
 * 
 * @author Diana Lange
 *
 */

// to do: autoresize on the fly -> evtl. neues Array mit Ladestatus (-1 == not
// loaded, 0 == loaded w/o resize, 1 == loaded /w resize
// to do 2: buffering off, falls sehr viele Bilder geladen werden (z.B. bei
// frames einer Animation)
public class ImageLoader extends DataLoader {

	/**
	 * The images array with all the loaded images. The entries will be null, if
	 * the image wasn't loaded yet. Each image will be loaded on demand as they
	 * are tried to get with any of the get methods, e.g. getCurrent(),
	 * get(int).
	 */
	private PImage[] images;

	/**
	 * True, if the images should be resized to the parents width and height
	 * directly after the loading.
	 */
	private boolean autoResize = true;

	/**
	 * All objects that are interested in a change of the current image.
	 */
	private ArrayList<ImageUpdateObserver> observers;

	/**
	 * Builds the ImageLoader and registers the KeyEvent to the parent
	 * (PApplet). The files have to be located in a sub-folder named "data" in
	 * the current sketch folder. The "data" folder should at least contain one
	 * image file. Auto resize is set to false.
	 * 
	 * @param parent
	 *            A PApplet object.
	 */
	public ImageLoader(PApplet parent) {
		this(parent, null, "data", false);
	}

	/**
	 * Builds the ImageLoader and registers the KeyEvent to the parent
	 * (PApplet). The files have to be located in a sub-folder named "data" in
	 * the current sketch folder. The "data" folder should at least contain one
	 * image file.
	 * 
	 * @param parent
	 *            A PApplet object.
	 * @param autoResize
	 *            If true, all images will be resized to the parents width and
	 *            height directly after they are loaded.
	 */
	public ImageLoader(PApplet parent, boolean autoResize) {
		this(parent, null, "data", autoResize);
	}

	/**
	 * Builds the ImageLoader and registers the KeyEvent to the parent
	 * (PApplet). The given folder should at least contain one image file. Auto
	 * resize is set to false.
	 * 
	 * @param parent
	 *            A PApplet object.
	 * @param folderName
	 *            Folder containing files (not null). This folder should at
	 *            least contain one image file. The given folder should be a
	 *            sub-folder in the current sketch folder or a relative path to
	 *            the sketch folder. e.g. "..\\data" is a folder which is
	 *            located in the parent directory of the sketch folder. Make
	 *            sure to escape the backslashes in the path in the shown way.
	 */
	public ImageLoader(PApplet parent, String folderName) {
		this(parent, null, folderName, false);
	}

	/**
	 * Builds the ImageLoader and registers the KeyEvent to the parent
	 * (PApplet). The given folder should at least contain one image file.
	 * 
	 * @param parent
	 *            A PApplet object.
	 * @param folderName
	 *            Folder containing files (not null). This folder should at
	 *            least contain one image file. The given folder should be a
	 *            sub-folder in the current sketch folder or a relative path to
	 *            the sketch folder. e.g. "..\\data" is a folder which is
	 *            located in the parent directory of the sketch folder. Make
	 *            sure to escape the backslashes in the path in the shown way.
	 * @param autoResize
	 *            If true, all images will be resized to the parents width and
	 *            height directly after they are loaded.
	 */
	public ImageLoader(PApplet parent, String folderName, boolean autoResize) {
		this(parent, null, folderName, autoResize);
	}

	/**
	 * Builds the ImageLoader and registers the KeyEvent to the parent
	 * (PApplet). The given folder should at least contain one image file. Auto
	 * resize is set to false.
	 * 
	 * @param parent
	 *            A PApplet object.
	 * @param folderPath
	 *            A absolute path to the folder. Example:
	 *            "C:\\" is the folderPath and "data
	 *            " is the foldername then the files of the folder located at "
	 *            C:\\data\\" will be loaded. Make sure to escape the
	 *            backslashes in the path in the shown way. If folderPath is
	 *            null, the sketch location will be used to set this path.
	 * @param folderName
	 *            Folder containing files (not null). This folder should at
	 *            least contain one image file. The given folder should be a
	 *            sub-folder of folderpath or a relative path to folderPath.
	 *            e.g. "C:\\data" is the folderpath and "..\\temp" is the
	 *            foldername then "C:\\temp\\" will be the location where the
	 *            files will be loaded. Make sure to escape the backslashes in
	 *            the path in the shown way.
	 */
	public ImageLoader(PApplet parent, String folderPath, String folderName) {
		this(parent, folderPath, folderName, true);
	}

	/**
	 * Builds the ImageLoader and registers the KeyEvent to the parent
	 * (PApplet). The given folder should at least contain one image file.
	 * 
	 * @param parent
	 *            A PApplet object.
	 * @param folderPath
	 *            A absolute path to the folder. Example:
	 *            "C:\\" is the folderPath and "data
	 *            " is the foldername then the files of the folder located at "
	 *            C:\\data\\" will be loaded. Make sure to escape the
	 *            backslashes in the path in the shown way.
	 * @param folderName
	 *            Folder containing files (not null). This folder should at
	 *            least contain one image file. The given folder should be a
	 *            sub-folder of folderpath or a relative path to folderPath.
	 *            e.g. "C:\\data" is the folderpath and "..\\temp" is the
	 *            foldername then "C:\\temp\\" will be the location where the
	 *            files will be loaded. Make sure to escape the backslashes in
	 *            the path in the shown way.
	 * @param autoResize
	 *            If true, all images will be resized to the parents width and
	 *            height directly after they are loaded.
	 */
	public ImageLoader(PApplet parent, String folderPath, String folderName, boolean autoResize) {
		super(parent, folderPath, folderName);
		observers = new ArrayList<ImageUpdateObserver>();
		this.autoResize = autoResize;
		this.type = DataLoader.IMAGE;
		this.filePaths = loadPaths(this.type);
		this.images = new PImage[filePaths.length];
		// this.loadFile(fileIndex);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#update()
	 */
	public ImageLoader update() {
		super.update();
		this.fileIndex = 0;
		this.filePaths = loadPaths(this.type);
		this.images = new PImage[filePaths.length];
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#resetKeySettings()
	 */
	public ImageLoader resetKeySettings() {
		return (ImageLoader) super.resetKeySettings();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#disableKeys()
	 */
	public ImageLoader disableKeys() {
		return (ImageLoader) super.disableKeys();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#enableKeys()
	 */
	public ImageLoader enableKeys() {
		return (ImageLoader) super.enableKeys();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#setKey(java.lang.String, int)
	 */
	public DataLoader setKey(String which, int keyCode) {
		return (ImageLoader) super.setKey(which, keyCode);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#setKey(java.lang.String, char)
	 */
	public ImageLoader setKey(String which, char key) {
		return (ImageLoader) super.setKey(which, key);
	}

	/**
	 * Enable auto resizing of images. If this is actived, the images will be
	 * resized to the width and height of the parent object.
	 * 
	 * @return The current ImageLoader object.
	 */
	public ImageLoader enableAutoResize() {
		this.autoResize = true;
		return this;
	}

	/**
	 * Disable auto resizing of images. Images will be loaded with their actual size.
	 * 
	 * @return The current ImageLoader object.
	 */
	public ImageLoader disableAutoResize() {
		this.autoResize = false;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#loadFile(int)
	 */
	@Override
	public PImage loadFile(int index) {
		index = RMath.constrain(index, 0, filePaths.length);

		PImage img = parent.loadImage(filePaths[index]);

		if (autoResize) {
			img.resize(parent.width, parent.height);
		}
		img.loadPixels();

		images[index] = img;

		return img;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#loadAllFiles()
	 */
	@Override
	public PImage[] loadAllFiles() {

		for (int i = 0; i < images.length; i++) {

			if (images[i] == null) {
				loadFile(i);
			}

		}

		return images;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#nextRandom()
	 */
	@Override
	public PImage nextRandom() {

		if (filePaths.length == 0) {
			return null;
		} else {
			fileIndex = RMath.random(0, filePaths.length);
			if (images[fileIndex] == null) {
				loadFile(fileIndex);
			}

			informObservers();

			return images[fileIndex];
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#previous()
	 */
	@Override
	public PImage previous() {
		if (filePaths.length == 0) {
			return null;
		} else {
			fileIndex = --fileIndex >= 0 ? fileIndex : filePaths.length - 1;
			if (images[fileIndex] == null) {
				loadFile(fileIndex);
			}

			informObservers();

			return images[fileIndex];
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#next()
	 */
	@Override
	public PImage next() {
		if (filePaths.length == 0) {
			return null;
		} else {
			fileIndex = ++fileIndex < filePaths.length ? fileIndex : 0;
			if (images[fileIndex] == null) {
				loadFile(fileIndex);
			}

			informObservers();

			return images[fileIndex];
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#first()
	 */
	@Override
	public PImage first() {
		if (filePaths.length == 0) {
			return null;
		} else {
			fileIndex = 0;
			if (images[fileIndex] == null) {
				loadFile(fileIndex);
			}

			informObservers();

			return images[fileIndex];
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#last()
	 */
	@Override
	public PImage last() {
		if (filePaths.length == 0) {
			return null;
		} else {
			fileIndex = filePaths.length - 1;
			if (images[fileIndex] == null) {
				loadFile(fileIndex);
			}

			informObservers();

			return images[fileIndex];
		}
	}
	
	/**
	 * Sets the current element.
	 * 
	 * @param index
	 *            The index of the element which should be the current element.
	 *            Index is in range of [0, size()).
	 * @return The current ImageLoader object.
	 */
	@Override
	public ImageLoader setIndex(int index) {
		super.setIndex(index);
		this.informObservers();
		return this;
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#last()
	 */
	@Override
	public ImageLoader setIndex(String filename) {
		super.setIndex(filename);
		this.informObservers();
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#getCurrent()
	 */
	@Override
	public PImage getCurrent() {
		if (images[fileIndex] == null) {
			loadFile(fileIndex);
		}
		return images[fileIndex];
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.io.DataLoader#get(int)
	 */
	@Override
	public PImage get(int index) {
		index = RMath.constrain(index, 0, filePaths.length);

		if (images[index] == null) {
			loadFile(index);
		}
		return images[index];
	}

	/**
	 * Add an object that wants to be informed when there is a change of the
	 * current image.
	 * 
	 * @param observer
	 *            An object that will be informed at a change of the current
	 *            color palette.
	 * @return The ImageLoader object.
	 */
	public ImageLoader addObserver(ImageUpdateObserver observer) {
		observers.add(observer);
		return this;
	}

	/**
	 * Remove an observer from this loader.
	 * 
	 * @param observer
	 *            An object that was formerly an observer and which should be
	 *            removed.
	 * @return The ImageLoader object.
	 */
	public ImageLoader removeObserver(ImageUpdateObserver observer) {
		observers.remove(observer);
		return this;
	}

	/**
	 * Remove all observers from this loader.
	 * 
	 * @return The ImageLoader object.
	 */
	public ImageLoader clearObservers() {
		observers.clear();
		return this;
	}

	/**
	 * Inform all observers when there is a change of the current color palette.
	 * This method should be called after fileIndex has changed and the file has
	 * been loaded successfully.
	 */
	private void informObservers() {
		for (ImageUpdateObserver o : observers) {
			o.setImage(getCurrent());
		}
	}

}
