package net.returnvoid.io;

import java.io.File;
import java.util.ArrayList;

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

/*
 * 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.
 */

/**
 * Abstract class for DataLoaders. DataLoaders will handle to load files from a
 * specific type (e.g. images) from a specific folder. 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).
 * 
 * @author Diana Lange
 *
 */
public abstract class DataLoader {

	/**
	 * The key / keyCode for loading the previous file.
	 */
	private int prevKey = java.awt.event.KeyEvent.VK_LEFT;

	/**
	 * The key / keyCode for loading the next file.
	 */
	private int nextKey = java.awt.event.KeyEvent.VK_RIGHT;

	/**
	 * The key / keyCode for loading the first file of the folder.
	 */
	private int firstKey = java.awt.event.KeyEvent.VK_UP;

	/**
	 * The key / keyCode for loading the last file of the folder.
	 */
	private int lastKey = java.awt.event.KeyEvent.VK_DOWN;

	/**
	 * The key / keyCode for loading the next randomly chosen file.
	 */
	private int nextRandomKey = java.awt.event.KeyEvent.VK_TAB;

	/**
	 * The PApplet object (KeyEvents will be regulated by it).
	 */
	protected PApplet parent;

	/**
	 * True if the object should listen to KeyEvents.
	 */
	private boolean keysEnabled = true;

	/**
	 * Helper variable to calculate the time between the current key event and
	 * the last key event to prevent accidentally double key events.
	 */
	private long lastTimeKeyPressed = -1;

	/**
	 * Contains the absolut paths to files which will be loaded.
	 */
	protected String[] filePaths;

	/**
	 * The type of this object, e.g. IMAGE.
	 */
	protected int type;

	/**
	 * The index of the current loaded file.
	 */
	protected int fileIndex;

	/**
	 * File type: Image (.jpg, .gif, .png, ...)
	 */
	public final static int IMAGE = 0;

	/**
	 * File type: Text (.txt)
	 */
	public final static int TEXT = 1;

	/**
	 * File type: CSV (.csv)
	 */
	public final static int CSV = 2;

	/**
	 * File type: JSON (.json)
	 */
	public final static int JSON = 3;

	/**
	 * File type: Shape (.svg)
	 */
	public final static int SHAPE = 4;

	/**
	 * File type: XML (.xml, .htm, .html)
	 */
	public final static int XML = 5;

	/**
	 * File type: COLOR_PALETTE (.color.json)
	 */
	public final static int COLOR_PALETTE = 6;

	/**
	 * Name of the folder of which the files will be loaded. Default: \"data\"
	 */
	private String folderName;

	/**
	 * Absolute or relative path to the folder of which the files will be
	 * loaded. If non provided, i.e. folderpath is null, the folder with the
	 * name \"foldername\" will be searched within the current Processing sketch
	 * folder.
	 */
	private String folderPath;

	/**
	 * Builds the DataLoader and registers the KeyEvent to the parent (PApplet).
	 * 
	 * @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 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.
	 */
	protected DataLoader(PApplet parent, String folderPath, String folderName) {
		this.parent = parent;
		this.folderPath = folderPath;
		this.folderName = folderName;
		this.resetKeySettings();
		this.keysEnabled = true;
		this.fileIndex = 0;

		this.parent.registerMethod("keyEvent", this);
	}

	/**
	 * Returns the name of the folder of whom the files will be loaded.
	 * 
	 * @return The folder name (preset folder name: 'data').
	 */
	public String getFolder() {
		return this.folderName;
	}

	/**
	 * Returns the path to the loading folder.
	 * 
	 * @return The path to the folder containing the files which will be loaded.
	 */
	public String getPath() {
		// if folderPath is not set the folder will be looked within the sketch
		// path position
		return this.folderPath == null ? ProcessingTools.getSketchPath(parent) : this.folderPath;
	}

	/**
	 * Returns the path of the directory where all files of the given type will
	 * be loaded.
	 * 
	 * @return The absolute path to the loading directory.
	 */
	public String getLoadingDirectory() {
		return (new File(getPath() + "/" + getFolder())).getAbsolutePath();
	}

	/**
	 * Loads a file with the given index and returns it. Prevent out of bounds
	 * exception by checking the range with the size() method. Even if the files
	 * has been loaded before this will induce a load again. This might be
	 * useful if the content of the file has been changed in the meen time.
	 * 
	 * @param index
	 *            The index of the file which should be loaded.
	 * @return The loaded file.
	 */
	abstract public Object loadFile(int index);

	/**
	 * Loads all files (if not loaded yet) and returns them all.
	 * 
	 * @return The object of the file.
	 */
	abstract public Object[] loadAllFiles();

	/**
	 * Loads (if necessary) and returns the next random file of the given
	 * folder.
	 * 
	 * @return The object of the file.
	 */
	abstract public Object nextRandom();

	/**
	 * Loads (if necessary) and returns the previous file of the given folder.
	 * The order of the files is set by their file names. Previous will load the
	 * file previous to the current file concerning their names.
	 * 
	 * @return The object of the file.
	 */
	abstract public Object previous();

	/**
	 * Loads (if necessary) and returns the next file of the given folder. The
	 * order of the files is set by their file names. Previous will load the
	 * file previous to the current file concerning their names.
	 * 
	 * @return The object of the file.
	 */
	abstract public Object next();

	/**
	 * Loads (if necessary) and returns the first file of the given folder. The
	 * order of the files is set by their file names.
	 * 
	 * @return The object of the file.
	 */
	abstract public Object first();

	/**
	 * Loads (if necessary) and returns the last file of the given folder. The
	 * order of the files is set by their file names.
	 * 
	 * @return The object of the file.
	 */
	abstract public Object last();

	/**
	 * Loads (if necessary) and returns the current file.
	 * 
	 * @return The object of the file.
	 */
	abstract public Object getCurrent();

	/**
	 * Loads (if necessary) and returns the file with the given index. Prevent
	 * out of bounds exception by checking the range with the size() method.
	 * 
	 * @return The object of the file.
	 */
	abstract public Object get(int index);

	/**
	 * 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 DataLoader object.
	 */
	public DataLoader setIndex(int index) {
		this.fileIndex = RMath.constrain(index, 0, size());
		return this;
	}

	/**
	 * Sets the current element.
	 * 
	 * @param filename
	 *            The name of the file which should be the current element.
	 * @return The current DataLoader object.
	 */
	public DataLoader setIndex(String filename) {

		// TODO: Since the paths are sorted alphabetical this search should be
		// improved in future releases
		boolean found = false;

		for (int i = 0; i < this.filePaths.length; i++) {
			File file = new File(filePaths[i]);
			//System.out.println(file.getName());
			if (file.exists() && file.getName().equals(filename)) {
				found = true;
				this.setIndex(i);
				break;
			}
		}

		if (!found) {
			System.err.println("The file \"" + filename + "\" could not be found at " + this.getPath());
		}

		return this;
	}

	/**
	 * Resets the key settings for the KeyEvents to its default. The default is:
	 * <br>
	 * Arrow left = previous <br>
	 * Arrow right = next <br>
	 * Arrow up = first <br>
	 * Arrow down = last <br>
	 * Tabulator = next random <br>
	 * 
	 * @return The DataLoader object.
	 */
	public DataLoader resetKeySettings() {
		this.prevKey = java.awt.event.KeyEvent.VK_LEFT;
		this.nextKey = java.awt.event.KeyEvent.VK_RIGHT;
		this.firstKey = java.awt.event.KeyEvent.VK_UP;
		this.lastKey = java.awt.event.KeyEvent.VK_DOWN;
		this.nextRandomKey = java.awt.event.KeyEvent.VK_TAB;

		return this;
	}

	/**
	 * Returns the number of files within the given folder.
	 * 
	 * @return The number of accessible files.
	 */
	public int size() {
		return filePaths.length;
	}

	/**
	 * A function for handling the KeyEvent. Is controlled by the parent
	 * (PApplet).
	 * 
	 * @param evt
	 *            The object containing the information about the KeyEvent.
	 */
	public void keyEvent(processing.event.KeyEvent evt) {
		if (this.keysEnabled && (lastTimeKeyPressed == -1 || System.currentTimeMillis() - lastTimeKeyPressed > 300)) {
			if (evt.getKeyCode() == prevKey || evt.getKey() == prevKey) {
				previous();
			} else if (evt.getKeyCode() == nextKey || evt.getKey() == nextKey) {
				next();
			} else if (evt.getKeyCode() == nextRandomKey || evt.getKey() == nextRandomKey) {
				nextRandom();
			} else if (evt.getKeyCode() == lastKey || evt.getKey() == lastKey) {
				last();
			} else if (evt.getKeyCode() == firstKey || evt.getKey() == firstKey) {
				first();
			}

			this.lastTimeKeyPressed = System.currentTimeMillis();
		}
	}

	/**
	 * Disable the automatic key events of this object.
	 * 
	 * @return The DataLoader object.
	 */
	public DataLoader disableKeys() {
		keysEnabled = false;
		return this;
	}

	/**
	 * Enables the automatic key events of this object.
	 * 
	 * @return The DataLoader object.
	 */
	public DataLoader enableKeys() {
		keysEnabled = true;
		return this;
	}

	/**
	 * Links a certain method call (e.g. next()) with a certain keyCode (e.g.
	 * KeyEvent.VK_RIGHT).
	 * 
	 * @param which
	 *            A String representing the method which should be linked to the
	 *            keyCode. Possible values: \ "previous\", \"next\", \"first\",
	 *            \"last\", \"random\".
	 * @param keyCode
	 *            The keyCode for the key which should be used for the given
	 *            function.
	 * @return The DataLoader object.
	 */
	public DataLoader setKey(String which, int keyCode) {
		// int keyCode = KeyStroke.getKeyStroke(key, 0).getKeyCode();

		if (which.toLowerCase().contains("pre")) {
			prevKey = keyCode;
		} else if (which.toLowerCase().contains("nex")) {
			nextKey = keyCode;
		} else if (which.toLowerCase().contains("fir")) {
			firstKey = keyCode;
		} else if (which.toLowerCase().contains("las")) {
			lastKey = keyCode;
		} else if (which.toLowerCase().contains("ran")) {
			nextRandomKey = keyCode;
		}

		return this;
	}

	/**
	 * Links a certain method call (e.g. next()) with a certain key (e.g.
	 * \'n\').
	 * 
	 * @param which
	 *            A String representing the method which should be linked to the
	 *            keyCode. Possible values: \ "previous\", \"next\", \"first\",
	 *            \"last\", \"random\".
	 * @param key
	 *            The char containing the key which should be used for the given
	 *            function.
	 * @return The DataLoader object.
	 */
	public DataLoader setKey(String which, char key) {

		// int keyCode = KeyStroke.getKeyStroke(key, 0).getKeyCode();
		int keyCode = (int) key;

		if (which.toLowerCase().contains("pre")) {
			prevKey = keyCode;
		} else if (which.toLowerCase().contains("nex")) {
			nextKey = keyCode;
		} else if (which.toLowerCase().contains("fir")) {
			firstKey = keyCode;
		} else if (which.toLowerCase().contains("las")) {
			lastKey = keyCode;
		} else if (which.toLowerCase().contains("ran")) {
			nextRandomKey = keyCode;
		}

		return this;
	}

	/**
	 * Returns the file extensions of the specific given type; i.e. maps type
	 * IMAGE to [.jpg, .png, ...]
	 * 
	 * @param type
	 *            A file type (DataLoader.IMAGE, DataLoader.TEXT, ...)
	 * @return A array containing valid file extensions for the given data type.
	 */
	private String[] getFileExtensions(int type) {

		if (type == DataLoader.TEXT) {
			return new String[] { ".txt", ".TXT" };
		} else if (type == DataLoader.CSV) {
			return new String[] { ".csv", ".CSV" };
		} else if (type == DataLoader.JSON) {
			return new String[] { ".json", ".JSON" };
		} else if (type == DataLoader.XML) {
			return new String[] { ".xml, .html, .htm", ".XML", ".HTML", ".HTM" };
		} else if (type == DataLoader.SHAPE) {
			return new String[] { ".csv", ".CSV" };
		} else if (type == DataLoader.COLOR_PALETTE) {
			return new String[] { ".color.json" };
		} else {
			return new String[] { ".png", ".jpg", ".tif", ".jpeg", ".gif", ".PNG", ".JPG", ".TIF", ".JPEG", ".GIF" };
		}
	}

	/**
	 * Gets the file name of the file with the given index. Prevent out of
	 * bounds exception by checking the range with the size() method. Removes
	 * the file extension if wanted, i.e. returns \ "myimage\" instead of
	 * \"myimage.jpg\".
	 * 
	 * @param index
	 *            The index of the file of which the name should be returned.
	 * @param removeFileType
	 *            True if the file extension should be removed.
	 * @return The file's name.
	 */
	public String getFileName(int index, boolean removeFileType) {
		index = RMath.constrain(index, 0, filePaths.length - 1);
		String fullPath = filePaths[index];
		String[] split = fullPath.split("\\\\");

		return !removeFileType ? split[split.length - 1]
				: split[split.length - 1].substring(0, split[split.length - 1].indexOf("."));
	}

	/**
	 * Gets the file name of the file with the given index. Prevent out of
	 * bounds exception by checking the range with the size() method.
	 * 
	 * @param index
	 *            The index of the file of which the name should be returned.
	 * @return The file's name.
	 */
	public String getFileName(int index) {
		return getFileName(index, false);
	}

	/**
	 * Gets the file name of the current file.
	 * 
	 * @return The file's name.
	 */
	public String getFileName() {
		return getFileName(fileIndex, false);
	}

	/**
	 * Gets the file name of the current file. Removes the file extension if
	 * wanted, i.e. returns \ "myimage\" instead of \"myimage.jpg\".
	 * 
	 * @param removeFileType
	 *            True if the file extension should be removed.
	 * @return The file's name.
	 */
	public String getFileName(boolean removeFileType) {
		return getFileName(fileIndex, removeFileType);
	}

	/**
	 * Re-Loads all the absolute paths for all valid files defined by the given
	 * type in the set folder. All already loaded (and buffered) files will have
	 * to be reloaded afterwards. Update might get called after a change in the
	 * directory has been made (e.g. a file has been removed). The current
	 * object will still be the same after update() if the file still exits in
	 * the directory.
	 * 
	 * @return The DataLoader object.
	 */
	public DataLoader update() {
		String oldPath = this.filePaths[fileIndex];
		this.fileIndex = 0;
		this.filePaths = loadPaths(this.type);

		// look of the the current file previous to the update is still in the
		// working directory. If so set the fileIndex counter to that index.
		for (int i = 0; i < filePaths.length; i++) {
			if (filePaths[i].equals(oldPath)) {
				this.fileIndex = i;
				break;
			}
		}
		return this;
	}

	/**
	 * Loads all the absolute paths for all valid files defined by the given
	 * type in the set folder.
	 * 
	 * @param type
	 *            A file type (DataLoader.IMAGE, DataLoader.Text, ...)
	 * @return The absolute paths to the files.
	 */
	protected String[] loadPaths(int type) {
		return loadPaths(getFileExtensions(type));
	}

	/**
	 * Loads all the absolute paths for all valid files defined by the given
	 * dataTypes array in the set folder.
	 * 
	 * @param dataTypes
	 *            An array containing the valid extension types in form of e.g.
	 *            \".jpg\"
	 * @return The absolute paths to the files.
	 */
	protected String[] loadPaths(String[] dataTypes) {

		ArrayList<String> paths = new ArrayList<String>();

		File directory = new File(getLoadingDirectory());

		if (directory != null) {

			/*
			 * check if directory exists and that is is actually a folder
			 */
			if (directory.exists() && directory.isDirectory()) {

				/*
				 * names of the files
				 */
				String[] imagPath = directory.list();

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

					/*
					 * from name to file to create the absolute path and
					 * checking validity
					 */
					File file = new File(directory, imagPath[i]);

					// checking of that name is a file and not a directory
					if (file.isFile()) {

						/* get the absolut path */
						String absolutePath = file.getAbsolutePath();

						/*
						 * check if file has valid extension and store it if it
						 * is valid.
						 */

						for (int j = 0; j < dataTypes.length; j++) {
							if (absolutePath.indexOf(dataTypes[j]) != -1) {
								paths.add(absolutePath);
								break;
							}
						}

					}
				}
			} else {
				System.err.println("couldn't find directory");
			}
		}

		if (paths.size() == 0) {
			System.err.println("no matching file types found at directory " + directory.getAbsolutePath());
		}

		this.fileIndex = 0;

		/* convert to array and return */
		return paths.toArray(new String[paths.size()]);
	}
}
