package net.paulhertz.pixelaudio.example;

import processing.core.PApplet;
import processing.core.PGraphics;

import java.util.ArrayList;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;

import net.paulhertz.pixelaudio.*;
import net.paulhertz.pixelaudio.AffineTransformType;

//TODO window-resizing should stay in effect after switching in a new gen


/**
 * The LookupTables example shows how a central element of PixelAudio, the
 * lookup table, connects a 1D audio signal with a 2D bitmap, putting audio
 * samples and RGB pixels in one-to-one correspondence within a single
 * PixelMapGen instance such as a Hilbert, Moore, or DiagonalZigzag.
 * <p>
 * MultiGenLookupTables does the same thing for MultiGens, which connect
 * multiple PixelMapGens. Imagine the audio signal as line that visits every
 * pixel in a bitmap: the signal's lookup table is simply the list of pixels it
 * visits in the order it visits them. The pixel locations are indices for a
 * pixel array, which starts at (0,0) in the upper left corner of a bitmap, and
 * proceeds left to right, top to bottom. The bitmap has a corresponding lookup
 * table, where the array indices of the audio signal are stored. The lookup
 * tables may be generated by mathematical functions, such as the Hilbert curve,
 * but they only need to be calculated once. PixelAudio classes that implement
 * the abstract PixelMapGen abstract class generate the lookup tables. The "gen"
 * objects plug in to PixelAudioMapper, a class that handles mapping of audio
 * signals and RGB data using lookup tables.
 * </p><p>
 * This example also shows basic affine transforms of PixelMapGen objects, such
 * as rotation and reflection, how to change the PixelMapGen a PixelAudioMapper
 * instance is using, and basic animation using array rotation.
 * </p><p>
 * Signal path index numbers are small white numbers, bitmap index numbers are
 * big black numbers. Read the imageToSignalLUT values by following the pixel
 * index order and reading the white numbers. Read the signalToImageLUT values
 * by following the signal path order and reading the black numbers. Read the
 * imageToSignalLUT values by following the black pixel numbers in order and
 * reading the white numbers.
 * </p><p>
 * This example can be a good place to test your own MultiGen creation methods.  
 * </p>
 * <pre>
 * KEY COMMANDS
 * 
 * Press 'a' or 'A' to rotate the array of colors one step left or right.
 * Press 'g' to display a different generator.
 * Press 'n' to hide or show numbers.
 * Press 'l' to hide or show lines.
 * Press 'd' to print a description of the current generator to the console.
 * Press 'k' to print the imageToSignalLUT and the signalToImageLUT to the console.
 * Press 't' to print affine maps to the console.
 * Press 'r' to rotate 180 degress.
 * Press 'x' to flip x-coordinates (reflect on y-axis).
 * Press 'y' to flip y-coordinates (reflect on x-axis).
 * Press 'h' to show this help text in the console.
 * </pre>
 * 
 * TODO There are some annoying bugs associated with some of the transforms and window resizing. 
 * For the moment, I've commented out the key commands for the r90, r270, fx90, and fx270 
 * transforms that cause the problem. 
 * 
 */
public class MultiGenLookupTables extends PApplet {
	PixelAudio pixelaudio;     // our library
	PixelMapGen hilb6x4Loop;   // a 3x2 Hilbert MultiGen 
	PixelMapGen mixGen;        // a hilbertZigzagLoop6x4 curve generator
	PixelMapGen zz6x4Loop;     // diagonal zigzag generator
	PixelMapGen randoBou;      // Boustrophedon generator
	PixelMapGen gen;           // variable for the generator
	PixelAudioMapper mapper;   // PixelAUdioMapper, does stuff with pixels (here) and audio samples (elsewhere)
	int[] spectrum;            // an array of values for our mapper
	ArrayList<int[]> coords;   // local copy of generator coordinates
	int[] imageLUT;            // the imageToSignalLUT from mapper, also the sampleMap field from the generator
	int[] signalLUT;           // the signalToImageLUT from mapper, also the pixelMap field from the generator

	int imageWidth = 1536;     // we are using a rectangular image and display window for MultiGen instances
	int imageHeight = 1024;    // imageWidth:imageHeight == 3:2
	int genW = 4;              // the width of generator: must be a power of 2 for Hilbert and Moore curves,
					           // 4 the minimum and probably is best for your MultiGens, but 8 might be worth a try. 
	int genH = 4;              // the height of generator: must be a power of 2 for Hilbert and Moore
	                           // and equal to genW (in this context, and always for Hilbert and Moore curves)
	int drawingScale = 1;      // scaling of drawing
	int offset = 0;            // offset of big text
	int bigTextSize = 64;      // big text size
	int smallTextSize = 32;    // small text size
	int coordsSize;            // size of the mapper coordinates array
	boolean isHideNumbers = false;    // show or hide the numbers, good idea when genW and genH are 16 or 32 or 64...
	boolean isHideLines = false;      // show or hide the lines, good idea when genW and genH greater than 128...
	PGraphics offscreen;

	// short names for transforms from
	// public enum AffineTransformType 
	public static AffineTransformType     r270      = AffineTransformType.R270;
	public static AffineTransformType     r90       = AffineTransformType.R90;
	public static AffineTransformType     r180      = AffineTransformType.R180;
	public static AffineTransformType     flipx     = AffineTransformType.FLIPX;
	public static AffineTransformType     fx270     = AffineTransformType.FX270;
	public static AffineTransformType     fx90      = AffineTransformType.FX90;
	public static AffineTransformType     flipy     = AffineTransformType.FLIPY;
	public static AffineTransformType     nada      = AffineTransformType.NADA;

	AffineTransformType currentTransform = nada;
	
	// display window sizes for resizing images to fit the screen, 
	// most are calculated by the setScaling() method
	boolean isOversize = false;			// if false, image is not too big to display
	boolean isFitToScreen = false;		// is the image currently fit to the screen? 
	int	maxWindowWidth;					// largest window width
	int maxWindowHeight;				// largest window height
	int scaledWindowWidth;				// scaled window width
	int scaledWindowHeight;				// scaled window height
	float windowScale = 1.0f; 			// scaling ratio, used to calculate scaled mouse location
	boolean isSecondScreen = false;		// for a two screen display
	int screen2x;						// second screen x-coord, will be set by setScaling()
	int screen2y;						// second window y-coord, will be set by setScaling()

	
	public static void main(String[] args) {
		PApplet.main(new String[] { MultiGenLookupTables.class.getName() });
	}

	public void settings() {
		size(imageWidth, imageHeight, JAVA2D);
		genW = constrain(genW, 2, 1024);
		genH = constrain(genH, 2, 1024);
	}

	public void setup() {
		windowResizable(true);
		offscreen = createGraphics(imageWidth, imageHeight);
		pixelaudio = new PixelAudio(this);
		listDisplays();
		setScaling(true);
		if (isOversize) {
			isFitToScreen = true;
			resizeWindow();
			println("Window is resized");
		}
		initGens();
		loadNewGen(hilb6x4Loop);		
		showHelp();
	}

	public void setDrawingVars() {
		drawingScale = imageWidth / gen.getWidth();
		offset = drawingScale / 2;
		if (genW > 8)
			isHideNumbers = true;
		if (genW > 128)
			isHideLines = true;
	}

	/**
	 * This method creates a MultiGen consisting of a mix of zigzag and Hilbert curves
	 * in 6 columns and 4 rows arranged to provide a continuous loop. This method shows
	 * one approach to mixing different PixelMapGens in a MultiGen. We use "gen" as an
	 * abbreviated name for a PixelMapGen.
	 * 
	 * @param genW
	 * @param genH
	 * @return
	 */
	public MultiGen hilbertZigzagLoop6x4(int genW, int genH) {
	    // list of gens that create a path through an image using PixelAudioMapper
		ArrayList<PixelMapGen> genList = new ArrayList<PixelMapGen>(); 
		// list of x,y coordinates for placing gens from genList
		ArrayList<int[]> offsetList = new ArrayList<int[]>();
		// array of grid locations for each gen
		int[][] locs = {{0,0}, {0,1}, {0,2}, {0,3}, {1,3}, {1,2}, {2,2}, {2,3}, 
						{3,3}, {3,2}, {4,2}, {4,3}, {5,3}, {5,2}, {5,1}, {5,0},
						{4,0}, {4,1}, {3,1}, {3,0}, {2,0}, {2,1}, {1,1}, {1,0}};
		// array of the transforms applied to each gen
		AffineTransformType[] trans = {r270, r270, nada, r270, r90, fx270, nada, r270, 
				                       r90, r90, fx90, nada, r90, r90, r180, r90, 
				                       r270, fx90, r180, r90, r270, r270, fx270, r180};
		// array of characters that flag the type of each gen
		char[] cues = {'H','D','D','H','D','H','D','H', 
				       'H','D','H','D','H','D','D','H',
				       'D','H','D','H','H','D','H','D'}; 
		int i = 0;
		for (AffineTransformType att: trans) {
			int x = locs[i][0] * genW;
			int y = locs[i][1] * genH;
			offsetList.add(new int[] {x,y});
			// println("hzLoop locs: ", locs[i][0], locs[i][1]);
			if (cues[i] == 'H') {
				genList.add(new HilbertGen(genW, genH, att));		
			}
			else {
				genList.add(new DiagonalZigzagGen(genW, genH, att));		
			}
			i++;
		}
		MultiGen multi = new MultiGen(6 * genW, 4 * genH, offsetList, genList);
		// println("hzLoop coords size = "+ multi.coords.size());
		// println("hzLoop dimensions = "+ multi.getWidth() +", "+ multi.getHeight());
		return multi;
	}

	public void initGens() {
		// get a HIlbert curve generator
		hilb6x4Loop = HilbertGen.hilbertMultigenLoop(6, 4, genW/2);
		// get a diagonal zigzag generator and flip the x-coordinates (same as
		// reflecting it on the y-axis)
		zz6x4Loop = DiagonalZigzagGen.zigzagLoop6x4(genW, genH);
		// get a Moore curve generator
		mixGen = hilbertZigzagLoop6x4(genW, genH);
		// get a bou curve generator
		randoBou = BoustropheGen.boustrophRowRandom(3, 2, genW, genH);
	}

	public void initMapper(PixelMapGen gen) {
		this.mapper = new PixelAudioMapper(gen);
		this.coords = gen.getCoordinatesCopy();
		this.imageLUT = mapper.getImageToSignalLUT();  // gen.getSampleMapCopy();
		this.signalLUT = mapper.getSignalToImageLUT(); // gen.getPixelMapCopy();
		this.gen = gen;
	}

	public void updateMapper(PixelMapGen gen) {
		this.mapper.setGenerator(gen);
		this.coords = gen.getCoordinatesCopy();
		this.imageLUT = mapper.getImageToSignalLUT();  // gen.getSampleMapCopy();
		this.signalLUT = mapper.getSignalToImageLUT(); // gen.getPixelMapCopy();
		this.gen = gen;
	}
	
	public void updateWindow() {
		if (mapper.getWidth() * drawingScale != width || mapper.getHeight() * drawingScale != height) {
			int newWidth = mapper.getWidth() * drawingScale;
			int newHeight = mapper.getHeight() * drawingScale;
			println("New dimensions: ", newWidth, newHeight);
			windowResize(mapper.getWidth() * drawingScale, mapper.getHeight() * drawingScale);
			setScaling(false);
			offscreen = createGraphics(newWidth, newHeight);
		}
	}

	public void draw() {
		background(255);
		drawSquares();
		if (!isHideLines)
			drawLines();
		if (!isHideNumbers)
			drawNumbers();
		image(offscreen, 0, 0, width, height);
	}

	public void keyPressed() {
		switch (key) {
		case 'a':
			stepAnimation(1);
			break;
		case 'A':
			stepAnimation(-1);
			break;
		case 'd':
			println("\n" + mapper.getGeneratorDescription());
			println("Dimensions: "+ mapper.getWidth(), mapper.getHeight());
			break;
		case 'g':
			if (gen == zz6x4Loop) {
				loadNewGen(hilb6x4Loop);
				println("\nhilb6x4Loop");
			}
			else if (gen == hilb6x4Loop) {
				loadNewGen(mixGen);
				println("\nmixGen");
			}
			else if (gen == mixGen) {
				loadNewGen(randoBou);
				println("\nrandoBou");
			}
			else if (gen == randoBou) {
				loadNewGen(zz6x4Loop);
				println("\nzz6x4Loop");
			}
			loadNewGen(gen);
			break;
		case 'n':
			isHideNumbers = !isHideNumbers;
			break;
		case 'l':
			isHideLines = !isHideLines;
			break;
		case 'k':
		case 'K':
			printLUTs();
			break;
		case 't':
		case 'T':
			testAffineMap(genW, genH);
			break;
		case 'r':
			currentTransform = r180;
			gen.setTransformType(currentTransform);
			updateMapper(gen);
			break;
		case 'x':
			currentTransform = flipx;
			gen.setTransformType(currentTransform);
			updateMapper(gen);
			break;
		case 'y':
			currentTransform = flipy;
			gen.setTransformType(currentTransform);
			updateMapper(gen);
			break;

			// I omit transforms that exchange width and height
			// There's enough to deal with in this sketch
			// without adding coordinate swapping and window resizing. 
			// Feel free to uncomment and experiment. 
/*
 		case 'f':
			currentTransform = r270;
			gen.setTransformType(currentTransform);
			updateMapper(gen);
			updateWindow();
			break;
		case 'b':
			currentTransform = r90;
			gen.setTransformType(currentTransform);
			updateMapper(gen);
			updateWindow();
			break;
		case '1':
			currentTransform = fx90;
			gen.setTransformType(currentTransform);
			updateMapper(gen);
			updateWindow();
			break;
		case '2':
			currentTransform = fx270;
			gen.setTransformType(currentTransform);
			updateMapper(gen);
			updateWindow();
			break;
*/
		case 'w':
			// toggles display window to fit screen or display at size
			// resizing is easy when you draw to an offscreen image and resize it to the display
		    isFitToScreen = !isFitToScreen;
		    resizeWindow();
		    println("----->>> window width: "+ width +", window height: "+ height);
		    break;
		case 'e': 
			isFitToScreen = !isFitToScreen;
			resizeWindow();
			break;
		case 'h':
			showHelp();
			break;
		default:
			break;
		}
	}

	/**
	 * Load a new PixelMapGen
	 */
	public void loadNewGen(PixelMapGen newGen) {
		gen = newGen;
		initMapper(gen);
		spectrum = initColors();
		gen.setTransformType(currentTransform);
		setDrawingVars();
		updateWindow();
	}

	public int[] initColors() {
		int[] colorWheel = new int[mapper.getSize()];
		pushStyle();
		colorMode(HSB, colorWheel.length, 100, 100);
		int h = 0;
		for (int i = 0; i < colorWheel.length; i++) {
			colorWheel[i] = color(h, 66, 66);
			h++;
		}
		popStyle();
		return colorWheel;
	}

	public void printLUTs() {
		println("\n----- imageToSignalLUT -----");
		for (int i = 0; i < imageLUT.length; i++) {
			print(imageLUT[i] + "  ");
		}
		println();
		println("----- signaToImagelLUT -----");
		for (int i = 0; i < signalLUT.length; i++) {
			print(signalLUT[i] + "  ");
		}
		println();
		println("----- Coordinates -----");
		for (int[] xy : this.coords) {
			print("(" + xy[0] + ", " + xy[1] + ")  ");
		}
	}

	public void testAffineMap(int w, int h) {
		println("\n" + w + " x " + h + " bitmap index remapping\n");
		for (AffineTransformType type : AffineTransformType.values()) {
			println("------------- " + type.name() + " -------------");
			int[] newMap = BitmapTransform.getIndexMap(w, h, type);
			int i = 0;
			for (int n : newMap) {
				if (i < newMap.length - 1)
					print(n + ", ");
				else
					print(n + "\n ");
				i++;
			}
		}
	}

	/**
	 * Draw background squares.
	 */
	public void drawSquares() {
		int x1 = 0;
		int y1 = 0;
		int x2 = 0;
		int y2 = 0;
		int pos = 0;
		offscreen.beginDraw();
		offscreen.pushStyle();
		for (int[] coordinate : coords) {
			offscreen.fill(spectrum[pos]);
			if (pos == 0) {
				x1 = coordinate[0] * drawingScale;
				y1 = coordinate[1] * drawingScale;
			} else {
				x2 = coordinate[0] * drawingScale;
				y2 = coordinate[1] * drawingScale;
				x1 = x2;
				y1 = y2;
			}
			offscreen.noStroke();
			offscreen.square(x1, y1, drawingScale);
			pos++;
		}
		offscreen.noStroke();
		offscreen.square(x2, y2, drawingScale);
		offscreen.popStyle();
		offscreen.endDraw();
	}

	/**
	 * Draw the signal path lines.
	 */
	public void drawLines() {
		int x1 = 0;
		int y1 = 0;
		int x2 = 0;
		int y2 = 0;
		int pos = 0;
		offscreen.beginDraw();
		offscreen.pushStyle();
		offscreen.strokeWeight(1.5f);
		offscreen.stroke(255, 160);
		for (int[] coordinate : coords) {
			if (pos == 0) {
				x1 = coordinate[0] * drawingScale + offset;
				y1 = coordinate[1] * drawingScale + offset;
			} else {
				x2 = coordinate[0] * drawingScale + offset;
				y2 = coordinate[1] * drawingScale + offset;
				offscreen.line(x1, y1, x2, y2);
				x1 = x2;
				y1 = y2;
			}
			pos++;
		}
		// line(x1, y1, x2, y2);
		offscreen.popStyle();
		offscreen.endDraw();
	}

	/**
	 * Draw the numbers for image path and signal path.
	 */
	public void drawNumbers() {
		int x1 = 0;
		int y1 = 0;
		int pos = 0;
		int drop = bigTextSize / 4;
		offscreen.beginDraw();
		offscreen.pushStyle();
		for (int[] coordinate : coords) { // coords follows the signal path
			x1 = coordinate[0] * drawingScale + offset; // x-coordinate along the signal path
			y1 = coordinate[1] * drawingScale + offset + drop; // y-coordinate along the signal path
			offscreen.textAlign(CENTER); // text for the center of each square
			offscreen.textSize(bigTextSize * 0.5f); // big font size
			offscreen.fill(0, 192); // dark color
			offscreen.text(signalLUT[pos], x1, y1); // show the bitmap pixel number in the signalToImageLUT
			offscreen.textAlign(LEFT); // small white text for the signal path index numbers
			offscreen.textSize(smallTextSize * 0.75f); // which we a flagging with the pos variable
			offscreen.fill(255, 192); // upper left corner
			offscreen.text(pos, x1 - offset + smallTextSize / 2, y1 - offset + smallTextSize / 2);
			pos++;
		}
		offscreen.popStyle();
		offscreen.endDraw();
	}

	public void stepAnimation(int step) {
		PixelAudioMapper.rotateLeft(spectrum, step);
	}

	public void showHelp() {
		println("\n----- HELP -----\n");
		println(" * Signal path index numbers are small white numbers, bitmap index numbers are big black numbers.");
		println(" * Read the imageToSignalLUT values by following the pixel index order and reading the white numbers.");
		println(" * Read the signalToImageLUT values by following the signal path order and reading the black numbers.");
		println(" * Read the imageToSignalLUT values by following the black pixel numbers in order and reading the white numbers.\n");
		println(" * Press 'a' or 'A' to rotate the array of colors one step left or right.");
		println(" * Press 'g' to display a different generator.");
		println(" * Press 'n' to hide or show numbers.");
		println(" * Press 'l' to hide or show lines.");
		println(" * Press 'd' to print a description of the current generator to the console.");
		if (genW <= 4)
			println(" * Press 'k' to print the imageToSignalLUT and the signalToImageLUT to the console.");
		if (genW <= 4)
			println(" * Press 't' to print affine maps to the console."); // omit for published version
//		println(" * Press 'f' to rotate 90 degress clockwise.");
//		println(" * Press 'b' to rotate 90 degress counterclockwise.");
		println(" * Press 'r' to rotate 180 degress.");
		println(" * Press 'x' to flip x-coordinates (reflect on y-axis).");
		println(" * Press 'y' to flip y-coordinates (reflect on x-axis).");
//		println(" * Press '1' to flip on primary diagonal.");
//		println(" * Press '2' to flip on secondary diagonal.");
		println(" * Press 'h' to show this help text in the console.");
	}
	

	/********************************************************************/
	/* ----->>>             DISPLAY SCALING METHODS            <<<----- */
	/********************************************************************/

	/**
	 * Get a list of available displays and output information about them to the console.
	 * Sets screen2x, screen2y, displayWidth and displayHeight from dimensions of a second display.
	 */
	void listDisplays() {
	    // Get the local graphics environment
	    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
	    // Get the array of graphics devices (one for each display)
	    GraphicsDevice[] devices = ge.getScreenDevices();
	    this.isSecondScreen = (devices.length > 1);
	    println("Detected displays:");
	    for (int i = 0; i < devices.length; i++) {
	        GraphicsDevice device = devices[i];
	        // Get the display's configuration
	        GraphicsConfiguration config = device.getDefaultConfiguration();
	        Rectangle bounds = config.getBounds(); // Screen dimensions and position
	        println("Display " + (i + 1) + ":");
	        println("  Dimensions: " + bounds.width + " x " + bounds.height);
	        println("  Position: " + bounds.x + ", " + bounds.y);
	        if (i == 1) {
	            // second screen details
	            this.screen2x = bounds.x + 8;
	            this.screen2y = bounds.y + 8;
	            this.displayWidth = bounds.width;
	            this.displayHeight = bounds.height;
	        }
	    }
	}
	      
	/**
	 * Calculates window sizes for displaying mapImage at actual size and at full screen. 
	 * Press the 'r' key to resize the display window.
	 * This method will result in display on a second screen, if one is available. 
	 * If mapImage is smaller than the screen, mapImage is displayed at size on startup 
	 * and resizing zooms the image. 
	 * If mapImage is bigger than the display, mapImage is fit to the screen on startup
	 * and resizing shows it at full size, partially filling the window. 
	 * 
	 * 
	 * @param isVerbose    if true, post comments to the console
	 */
	public void setScaling(boolean isVerbose) {
	    // max window width is a little less than the screen width of the screen
	    maxWindowWidth = displayWidth - 80;
	    // leave window height some room for title bar, etc.
	    maxWindowHeight = displayHeight - 80;
	    float sc = maxWindowWidth / (float) width;
	    scaledWindowHeight = Math.round(height * sc);
	    if (scaledWindowHeight > maxWindowHeight) {
	        sc = maxWindowHeight / (float) height;
	        scaledWindowHeight = Math.round(height * sc);
	        scaledWindowWidth = Math.round(width * sc);
	    } 
	    else {
	        scaledWindowWidth = Math.round(width * sc);
	    }
	    // even width and height allow ffmpeg to save to video
	    scaledWindowWidth += (scaledWindowWidth % 2 != 0) ? 1 : 0;
	    scaledWindowHeight += (scaledWindowHeight % 2 != 0) ? 1 : 0;
	    isOversize = (width > scaledWindowWidth || height > scaledWindowHeight);
	    windowScale = (1.0f * width) / scaledWindowWidth;
	    if (isVerbose) {
		    println("maxWindowWidth " + maxWindowWidth + ", maxWindowHeight " + maxWindowHeight);
		    println("image width " + width + ", image height " + height);
		    println("scaled width " + scaledWindowWidth + ", scaled height " + scaledWindowHeight + ", "
		            + "oversize image is " + isOversize);
	    }
	}
	
	public void resizeWindow() {
		if (offscreen.width > offscreen.height) {
		    if (isFitToScreen) {
		        surface.setSize(scaledWindowWidth, scaledWindowHeight);
		    } 
		    else {
		        surface.setSize(imageWidth, imageHeight);
		    }
		}
		else {
		    if (isFitToScreen) {
		        surface.setSize(scaledWindowHeight, scaledWindowWidth);
		    } 
		    else {
		        surface.setSize(imageHeight, imageWidth);
		    }
			
		}
	}
	
	// ------------- END DISPLAY SCALING METHODS ------------- //


}
