package net.paulhertz.pixelaudio.example;

import processing.core.PApplet;

import java.util.ArrayList;

import net.paulhertz.pixelaudio.*;

/**
 * 
 * PixelAudio Core Concept: Mapping
 * 
 * The LookupTables example shows how to map a 1D audio signal onto a  
 * 2D bitmap, using lookup tables (LUTs) to create a one-to-one correspondence
 * between audio samples and RGB pixels.
 * 
 * Imagine the audio signal as path that visits every pixel in a bitmap: the
 * signal's lookup table for pixels is a list of the array indices of the pixels
 * it visits, in the order it visits them. By convention, pixel array
 * coordinates start at (0,0) in the upper left corner of a bitmap, and proceed
 * in "row major" order, left to right, top to bottom. The bitmap has a
 * corresponding lookup table, where the array indices of the audio signal are
 * stored in the order of the pixel coordinates. The signal and bitmap lookup
 * tables are inverses of each other, so that the signal can be mapped to the
 * bitmap and the bitmap can be mapped to the signal. PixelAudio classes that
 * implement the abstract PixelMapGen class just have to provide the coordinates
 * of the signal path, then PixelMapGen will generate the lookup tables for the
 * signal and the image. The coordinates may be generated by mathematical
 * functions, but they only need to be calculated once. 
 * 
 * In PixelAudio, I call a subclass of PixelMapGen a "gen" object. Gen objects
 * plug in to PixelAudioMapper. PixelAudioMapper handles mapping back and
 * forth between audio and images and can transcode audio and image data. It has
 * methods for handling arrays of audio samples and RGB pixels and working with
 * the RGB and HSB color spaces. 
 *
 * This example also shows 1) basic affine transforms of PixelMapGen objects,
 * such as rotation and reflection, 2) how to change the PixelMapGen instance a
 * PixelAudioMapper object is using, and 3) basic animation using array rotation.
 * 
 * Signal path index numbers are small white numbers. 
 * Bitmap index numbers are big black numbers. 
 * Read the signalToImageLUT values by following the white signal numbers 
 * and reading the black numbers. 
 * Read the imageToSignalLUT values by following the black pixel numbers 
 * and reading the white numbers.
 * 
 * KEY COMMANDS
 * 
 * Press 'a' to advance animation by one step.
 * Press 'A' to rewind animation by one step.
 * Press 'd' to print the current PixelMapGen's description String.
 * Press 'g' to swap in a new PixelMapGen.
 * Press 'n' to hide the numbers overlay.
 * Press 'l' to hide the lines overlay.
 * Press 'k' to print the imageToSignalLUT and the signalToImageLUT to the console.
 * Press 't' to print affine maps to the console.
 * Press 'f' to rotate current gen 90 degrees clockwise.
 * Press 'b' to rotate current gen 90 degrees counterclockwise.
 * Press 'r' to rotate current gen 180 degrees.
 * Press 'x' to flip x-coordinates (reflect on y-axis).
 * Press 'y' to flip y-coordinates (reflect on x-axis).
 * Press '1' to mirror current gen on the primary diagonal, upper left to lower right.
 * Press '2' to mirror current gen on the secondary diagonal, upper right to lower left.
 * Press 'h' to show this help text in the console.
 * 
 */
public class LookupTables extends PApplet {
	PixelAudio pixelaudio;     // our library
	HilbertGen hGen;           // a Hilbert curve generator
	MooreGen mGen;             // a Moore curve generator
	DiagonalZigzagGen zGen;    // diagonal zigzag generator
	BoustropheGen bGen;        // 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 = 1024;     // for Moore and Hilbert curves, use equal powers of 2
	int imageHeight = 1024;    // for imageWidth and imageHeight
	int genW = 4;              // the width of generator: 
	                           // - must be a power of 2 for Hilbert and Moore gens
                               // - genW and genH must be equal 
	int genH = genW;           // - try 8 as an alternate value
					           // - 1024, the image width, is the maximum value for genW
	// LAYOUT
	int drawingScale;          // scaling of drawing == imageWidth / gen.getWidth()
	int offset = 0;            // offset of big text
	int bigTextSize = 64;      // big text size
	int smallTextSize = 32;    // small text size
	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...

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

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

	public void setup() {
		// 1. initialize PixelAudio library
		pixelaudio = new PixelAudio(this);   
		println("---- generator size: " + genW + " * " + genH);
		// 2. create various gens
		initGens(); 
		// 3. create a PixelAudioMapper, mapper
		initMapper(hGen);
		// 4. get a color spectrum to write to the signal path
		spectrum = getColors(mapper.getSize());    
		// 5. set up the layout
		drawingScale = imageWidth / gen.getWidth();
		offset = drawingScale / 2;
		if (genW > 8)
			isHideNumbers = true;
		if (genW > 128)
			isHideLines = true;
		// 6. show help message
		showHelp();
	}

	/**
	 * Initialize a HilbertGen, a DiagonalZigzagGen, a MooreGen, and a BoustropheGen
	 * that fit the width and height of this sketch.
	 */
	public void initGens() {
		// get a Hilbert curve generator
		hGen = new HilbertGen(genW, genH);
		// get a diagonal zigzag generator and flip the x-coordinates (same as
		// reflecting it on the y-axis)
		zGen = new DiagonalZigzagGen(genW, genH, AffineTransformType.FLIPX);
		// get a Moore curve generator
		mGen = new MooreGen(genW, genH);
		// get a boustrophedon curve generator
		bGen = new BoustropheGen(genW, genH, AffineTransformType.R270);
	}

	/**
	 * Initialize a PixelAudioMapper, mapper, with a supplied gen, and request
	 * some useful data structures from it. 
	 * 
	 * @param gen    a PixelMapGen instance
	 */
	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;
	}

	/**
	 * Swaps in a new gen to a previously created PixelAudioMapper instance, and 
	 * reloads relevant data structures.
	 * 
	 * @param gen    a PixelMapGen instance
	 */
	public void updateMapper(PixelMapGen gen) {
		this.mapper.setGenerator(gen);
		this.coords = gen.getCoordinatesCopy();
		this.imageLUT = mapper.getImageToSignalLUT(); 
		this.signalLUT = mapper.getSignalToImageLUT();
		this.gen = gen;
	}

	public void draw() {
		background(255);
		drawSquares();
		if (!isHideLines)
			drawLines();
		if (!isHideNumbers)
			drawNumbers();
	}

	public void keyPressed() {
		switch (key) {
		case 'a': // advance animation by one step
			stepAnimation(1);
			break;
		case 'A': // rewind animation by one step
			stepAnimation(-1);
			break;
		case 'd': // print the current PixelMapGen's description String
			println("\n" + mapper.getGeneratorDescription());
			break;
		case 'g': // swap in a new PixelMapGen
			if (gen == zGen) {
				gen = hGen;
				println("\nHilbert");
				initMapper(gen);
				break;
			}
			if (gen == hGen) {
				gen = mGen;
				println("\nMoore");
				initMapper(gen);
				break;
			}
			if (gen == mGen) {
				gen = bGen;
				println("\nBoustrophedon, rotated 90 degrees clockwise");
				initMapper(gen);
				break;
			}
			if (gen == bGen) {
				gen = zGen;
				println("\nZigzag path");
				initMapper(gen);
				break;
			}
			break;
		case 'n': // hide the numbers overlay
			isHideNumbers = !isHideNumbers;
			break;
		case 'l': // hide the lines overlay
			isHideLines = !isHideLines;
			break;
		case 'k': // print the Lookup Tables and coordinates for the current PixelMapGen
		case 'K':
			printLUTs();
			break;
		case 't': // print affine map geometric transforms
		case 'T':
			testAffineMap(genW, genH);
			break;
		case 'f': // rotate current gen 90 degrees clockwise
			gen.setTransformType(AffineTransformType.R270);
			updateMapper(gen);
			break;
		case 'b': // rotate current gen 90 degrees counterclockwise
			gen.setTransformType(AffineTransformType.R90);
			updateMapper(gen);
			break;
		case 'r': // rotate current gen 180 degrees
			gen.setTransformType(AffineTransformType.R180);
			updateMapper(gen);
			break;
		case 'x': // mirror current gen on the x-axis
			gen.setTransformType(AffineTransformType.FLIPX);
			updateMapper(gen);
			break;
		case 'y': // mirror current gen on the y-axis
			gen.setTransformType(AffineTransformType.FLIPY);
			updateMapper(gen);
			break;
		case '1': // mirror current gen on the primary diagonal, upper left to lower right
			gen.setTransformType(AffineTransformType.FX90);
			updateMapper(gen);
			break;
		case '2': // mirror current gen on the secondary diagonal, upper right to lower left
			gen.setTransformType(AffineTransformType.FX270);
			updateMapper(gen);
			break;
		case 'h': // print help message to the console
			showHelp();
			break;
		default:
			break;
		}
	}

	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.\n");
		println(" * Press 'a' to advance animation by one step.");
		println(" * Press 'A' to rewind animation by one step.");
		println(" * Press 'd' to print the current PixelMapGen's description String.");
		println(" * Press 'g' to swap in a new PixelMapGen.");
		println(" * Press 'n' to hide the numbers overlay.");
		println(" * Press 'l' to hide the lines overlay.");
		// println(" * Press 'k'or 'K'  to print the Lookup Tables and coordinates for the current PixelMapGen.");
		// println(" * Press 't' or 'T' to print affine map geometric transforms.");
		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 current gen 90 degrees clockwise.");
		println(" * Press 'b' to rotate current gen 90 degrees counterclockwise.");
		println(" * Press 'r' to rotate current gen 180 degrees.");
		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 mirror current gen on the primary diagonal, upper left to lower right.");
		println(" * Press '2' to mirror current gen on the secondary diagonal, upper right to lower left.");
		println(" * Press 'h' to show this help text in the console.");
	}

	/**
	 * Creates an array of colors in rainbow (spectrum) order.
	 * @param size    size of the desired array
	 * @return an array of colors in rainbow (spectrum) order
	 */
	public int[] getColors(int size) {
	  int[] colorWheel = new int[size];
	  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;
	}

	/**
	 * Print the LUTs and gen coordinates to the console.
	 */
	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] + ")  ");
		}
	}

	/**
	 * A test of the affine transforms in the BitmapTransform class, 
	 * which uses indexing to perform bitmap rotation and reflection.
	 * 
	 * @param w    width of a hypothetical bitmap
	 * @param h    height of a hypothetical bitmap 
	 */
	public void testAffineMap(int w, int h) {
		println("\n\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 color squares to the display.
	 */
	public void drawSquares() {
		int x1 = 0;
		int y1 = 0;
		int x2 = 0;
		int y2 = 0;
		int pos = 0;
		pushStyle();
		for (int[] coordinate : coords) {
			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;
			}
			noStroke();
			square(x1, y1, drawingScale);
			pos++;
		}
		noStroke();
		square(x2, y2, drawingScale);
		popStyle();
	}

	/**
	 * Draw lines to the display.
	 */
	public void drawLines() {
		int x1 = 0;
		int y1 = 0;
		int x2 = 0;
		int y2 = 0;
		int pos = 0;
		pushStyle();
		strokeWeight(1);
		stroke(255, 216);
		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;
				line(x1, y1, x2, y2);
				x1 = x2;
				y1 = y2;
			}
			pos++;
		}
		// line(x1, y1, x2, y2);
		popStyle();
	}

	/**
	 * Draw numbers to the display.
	 */
	public void drawNumbers() {
		int x1 = 0;
		int y1 = 0;
		int pos = 0;
		int drop = bigTextSize / 4;
		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
			textAlign(CENTER); // text for the center of each square
			textSize(bigTextSize); // big font size
			fill(0, 192); // dark color
			text(signalLUT[pos], x1, y1); // show the bitmap pixel number in the signalToImageLUT
			textAlign(LEFT); // small white text for the signal path index numbers
			textSize(smallTextSize); // which we a flagging with the pos variable
			fill(255, 192); // upper left corner
			text(pos, x1 - offset + smallTextSize / 2, y1 - offset + smallTextSize / 2);
			pos++;
		}
		popStyle();
	}

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

}
