/*
 *  Copyright (c) 2024 - 2025 by Paul Hertz <ignotus@gmail.com>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

package net.paulhertz.pixelaudio;

import java.util.ArrayList;
import java.util.Arrays;


/**
 * @author Paul Hertz
 * <p>
 * Argosy provides tools for shifting pixel patterns along the signal path of an image.
 * One way to generate the pixel patterns is with the Lindenmayer class. 
 * The patterns are generated by arrays of numbers ("argosy patterns") and arrays of colors, 
 * with a gap in between each repeated pattern. The patterns could also be interpreted as
 * rhythmic values in music, and the colors as timbre or instrumentation. Both music and 
 * visual patterns can be generated by a Lindenmayer pattern or "L-system." 
 * </p><p>
 * The Argosy class create pixel patterns ordered by the signal path of a PixelAudioMapper.
 * Argosy patterns consist of an array of numbers (argosyArray), an array of colors (argosyColors), 
 * a gap between patterns (unitSize * argosyGapScale) and a color for the gap (argosyGapColor). 
 * Each number in the pattern array determines the length of a run of pixels in a color specified
 * by the color array. The Argosy pattern maker steps through the pattern, scaling it by unitSize,
 * and assigns it the current color in the argosyColors, which it also steps through. The arrays 
 * do not have to be the same size. 
 * </p><p>
 * TODO implement pixel offset
 * TODO interpolating version of Argosy patterns, with floating point pattern values
 * TODO dynamic functional version of patterns and other parameters
 * TODO perhaps filling should permit one less gap than repetitions of pattern -- the first and last pattern will abut
 *      more precisely, we should provide conditions for partial filling (but not in version 1).
 * </p>
 */
public class Argosy {
	/** PixelAudioMapper that provides values for several variables and maps bigArray to bitmaps or audio signals */
	PixelAudioMapper mapper;
	/** Array of color values used for animation by rotating the array left or right */
	int[] argosyArray;
	/** the number of pixels in an argosy unit */
	int argosyUnitSize;
	/** number of pixels in a shiftLeft animation Step */
	int argosyStep;
	/** subunit divisor for animStep */
	float animStepDivisor = 16.0f;
	/** background color, fills the argosy array before colors are added */
	int bgColor = PixelAudioMapper.composeColor(0, 0, 0, 0);
	/** default colors for argosy units */
	int[] argosyColors = {PixelAudioMapper.composeColor(255, 255, 255, 255), PixelAudioMapper.composeColor(0, 0, 0, 255) };
	/** scaling for number of units in gap between argosies */
	float argosyGapScale = 1.0f;
	/** number of pixels in the gap between argosy patterns */
	int argosyGap;
	/** color of pixels in the gap */
	int argosyGapColor = PixelAudioMapper.composeColor(127, 127, 127, 255);
	/** how many times to repeat the pattern, 0 to fill array */
	int argosyReps = 0;
	/** maximum number of repetitions, used internally */
	int maxReps;
	/** margin on either side of the argosy patterns */
	int argosyMargin;
	/** current pattern to fill bigArray  */
	int[] argosyPattern;
	/** sum of values in argosy pattern */
	int argosySize;
	/** center the argosy patterns in the big array */
	boolean isCentered = true;
	/** array of number of pixels in each element of the expanded argosy pattern */
	int[] argosyIntervals;
	/** the number of pixels the array has shifted from its initial state */
	int argosyPixelShift = 0;
	/** do we count the shift or not?  */
	boolean isCountShift = true;
    /** count the number of unit shifts */
    int argosyShiftStep = 0;
    /** pixel count by which to shift the argosy pixels  at initialization */
    int argosyOffset = 0;

    // an argosy pattern with 55 elements, 89 = (34 * 2 + 21 * 1) units long, derived from a Fibonacci L-system
	public static final int[] argosy55 = new int[]{ 2, 1, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 2,
                                           1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 2,
                                           1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 2 };

	/**
	 * @param mapper		PixelAudioMapper
	 * @param unitSize      size of a unit of the argosy -- powers of 2 or 4 are a good fit for the Hilbert PixelMapGen
	 * @param reps          number of repetitions of the argosy pattern, pass in 0 for maximum that fit
	 * @param isCentered    true if argosy array should be centered in bigArray
	 */
	public Argosy(PixelAudioMapper mapper, int unitSize, int reps, boolean isCentered) {
		this.mapper = mapper;
		int size = mapper.getSize();
		this.argosyArray = new int[size];
		this.argosyUnitSize = unitSize;
		argosyPattern = new int[argosy55.length];
		for (int i = 0; i < argosy55.length; i++) {
			argosyPattern[i] = argosy55[i];
		}
		this.argosyStep = Math.round(this.argosyUnitSize / animStepDivisor);
		if (argosyStep == 0) argosyStep = 1;
		this.argosyGap = Math.round((this.argosyGapScale * this.argosyUnitSize));
		this.argosyReps = reps;
		this.isCentered = isCentered;
		this.initArgosy();
	}

	/**
	 * @param mapper		PixelAudioMapper
	 * @param unitSize      size of a unit of the argosy
	 * @param reps          number of repetitions of the argosy pattern, pass in 0 for maximum that fit
	 * @param isCentered    true if argosy array should be centered in bigArray
	 * @param colors        an array of colors for the argosy patterns
	 * @param gapColor      a color for the spaces between argosy patterns
	 * @param gapScale      scaling for number of units in gap between argosies
	 * @param pattern       a pattern of numbers, will be copied to argosyPattern
	 */
	public Argosy(PixelAudioMapper mapper, int unitSize, int reps, boolean isCentered, 
					   int[] colors, int gapColor, float gapScale, int[] pattern) {
		this.mapper = mapper;
		int size = mapper.getSize();
		this.argosyArray = new int[size];
		this.argosyUnitSize = unitSize;
		this.argosyPattern = new int[pattern.length];
		for (int i = 0; i < pattern.length; i++) {
			this.argosyPattern[i] = pattern[i];
		}
		this.argosyStep = Math.round(this.argosyUnitSize / animStepDivisor);
		if (argosyStep == 0) argosyStep = 1;
		this.argosyReps = reps;
		this.isCentered = isCentered;
		this.argosyColors = colors;
		this.argosyGapColor = gapColor;
		this.argosyGapScale = gapScale;
		this.argosyGap = Math.round((this.argosyGapScale * this.argosyUnitSize));
		this.initArgosy();
	}

	/**
	 * @param mapper		PixelAudioMapper
	 * @param pattern       a pattern of numbers, will be copied to argosyPattern
	 * @param unitSize      size of a unit of the argosy
	 * @param reps          number of repetitions of the argosy pattern, pass in 0 for maximum that fit
	 * @param isCentered    true if argosy array should be centered in bigArray
	 * @param colors        an array of colors for the argosy patterns
	 * @param gap           number of pixels in gap between pattern repeats (argosyGapScale * argosyUnitSize)
	 * @param gapColor      a color for the spaces between argosy patterns
	 * @param animStep		number of pixels on each animation step, when calling shiftLeft() or shiftRight()
	 */
	public Argosy(PixelAudioMapper mapper, int[] pattern, int unitSize, int reps, boolean isCentered, 
					   int[] colors, int gap, int gapColor, int animStep) {
		this.mapper = mapper;
		int size = mapper.getSize();
		this.argosyArray = new int[size];
		this.argosyPattern = new int[pattern.length];
		for (int i = 0; i < pattern.length; i++) {
			this.argosyPattern[i] = pattern[i];
		}
		this.argosyUnitSize = unitSize;
		this.argosyReps = reps;
		this.isCentered = isCentered;
		this.argosyColors = colors;
        this.argosyGap = gap;
		this.argosyGapScale = ((float)this.argosyGap)/this.argosyUnitSize;
		this.argosyGapColor = gapColor;
		this.argosyStep = animStep;
		this.initArgosy();
	}
	

	/* --------------------------------------------------------------------------- */
	/*                                                                             */
	/*    Initialization: call initArgosy(() when you change other values that     */
	/*    affect the ordering of patterns and colors (just about everything).      */
	/*                                                                             */
	/* --------------------------------------------------------------------------- */


	/**
	 * Sets up the argosy array and fills it with colors using the argosy pattern.
	 */
	public void initArgosy() {
		this.argosyStep = Math.round(this.argosyUnitSize / animStepDivisor);
		if (argosyStep == 0) argosyStep = 1;
		// determine the number of units in a single argosy pattern
		this.argosySize = 0;
		for (int element : argosyPattern) {
			argosySize += element;
		}
		Arrays.fill(argosyArray, bgColor);
		int size = this.argosyArray.length;
		maxReps = Math.round(size / (argosySize * argosyUnitSize + argosyGap));
		// System.out.println("-- max reps: "+ maxReps);
		if (argosyReps != 0 && argosyReps < maxReps) {
			maxReps = argosyReps;
		}
		// System.out.println("-- max reps: "+ maxReps);
		if (isCentered) {
			// calculate how many repetitions of argosy + argosy gap fit into the array,
			// minding that there is one less gap than the number of argosies
			argosyMargin = size - (maxReps * (argosySize * argosyUnitSize) + maxReps * (argosyGap - 1));
			// margin on either side, to center the argosies in the array
			// TODO review, revise argosyMargin calculations
			argosyMargin /= 2;
		} 
		else {
			argosyMargin = 0;
		}
		// calculate the number of pixels in each argosy element
		argosyIntervals = new int[argosyPattern.length];
		for (int i = 0; i < argosyPattern.length; i++) {
			argosyIntervals[i] = argosyPattern[i] * argosyUnitSize;
		}
		this.argosyPixelShift = 0;
		argosyFill();
	}

	/**
	 * Fills <code>argosyArray</code> with colors from argosyColors following the argosy pattern
	 * stored in argosyIntervals. Tbis method is generally called from initArgosy(), which fills
	 * argosyArray with the bgColor pixels. 
	 */
	public void argosyFill() {
		int size = this.argosyArray.length;
		int reps = 0;
		int vi = 0; 	// argosyIntervals index
		int ci = 0; 	// argosyColors index
		int si = 0; 	// argosyArray index
		int i = 0; 		// local index
		if (isCentered) si += argosyMargin;
		while (si < size) {
			// fill in one color element
			for (i = si; i < si + argosyIntervals[vi]; i++) {
				if (i >= size)
					break;
				argosyArray[i] = argosyColors[ci];
			}
			// increment counter variables
			si = i;
			ci = (ci + 1) % argosyColors.length;
			vi = (vi + 1) % argosyIntervals.length;
			// fill in the argosyGapColor if we hit the end of the argosyIntervals array (vi == 0)
			if (vi == 0) {
				reps++;
				for (i = si; i < si + argosyGap; i++) {
					if (i >= size)
						break;
					argosyArray[i] = argosyGapColor;
				}
				si = i;
			}
			if (reps == maxReps)
				break;
		}
	}

	/* ----->>> ANIMATION <<<----- */

	/**
	 * Rotates bigArray left by d values. Uses efficient "Three Rotation" algorithm.
	 * 
	 * @param d number of elements to shift
	 */
	public void rotateLeft(int d) {
		int[] arr = this.argosyArray;
		if (d < 0) {
			d = arr.length - (-d % arr.length);
		} else {
			d = d % arr.length;
		}
		reverseArray(arr, 0, d - 1);
		reverseArray(arr, d, arr.length - 1);
		reverseArray(arr, 0, arr.length - 1);
		if (isCountShift) {
			argosyPixelShift += d;
			argosyPixelShift %= mapper.getSize();
		}
	}

	/**
	 * Reverses an arbitrary subset of an array.
	 * 
	 * @param arr array to modify
	 * @param l   left bound of subset to reverse
	 * @param r   right bound of subset to reverse
	 */
	private void reverseArray(int[] arr, int l, int r) {
		int temp;
		while (l < r) {
			temp = arr[l];
			arr[l] = arr[r];
			arr[r] = temp;
			l++;
			r--;
		}
	}

	/**
	 * basic animation, rotate right by animStep pixels, decrement the step counter
	 * argosyShiftStep
	 */
	public void shiftRight() {
		rotateLeft(-this.argosyStep);
		argosyShiftStep--;
	}

	/**
	 * basic animation, rotate left by animStep pixels, increment the step counter
	 * argosyShiftStep
	 */
	public void shiftLeft() {
		rotateLeft(this.argosyStep);
		argosyShiftStep++;
	}


	/**
	 * @return the argosyPixelShift, save this if you want to reshift
	 */
	public int getArgosyPixelShift() {
		return argosyPixelShift;
	}
	
	public void setArgosyPixelShift(int newShift) {
		this.argosyPixelShift = newShift;
	}

	public void zeroArgosyPixelShift() {
		setArgosyPixelShift(0);
	}

	/**
	 * Shifts left by a specified number of pixels, summing them to argosyPixelShift
	 * if isCounted is true. This is the most flexible animation method with animation
	 * steps set externally. The other methods, shiftLeft() and shiftRight(), use
	 * this.animStep to determine pixel shift. 
	 *  
	 * @param pixelShift
	 * @param isCounted
	 */
	public void shift(int pixelShift, boolean isCounted) {
		boolean oldCountShift = isCountShift;
		isCountShift = isCounted;
		rotateLeft(pixelShift);
		isCountShift = oldCountShift;
	}


	/* ----->>> GETTERS AND SETTERS <<<----- */
	
	/*
	 * Where setting a value would change the argosy array, we call initArgosy().
	 * Of course, this could be left to the user, too, allowing several values to 
	 * be changes before the call to initArgosy(). 
	 * 
	 */

	/**
	 * @return argosyArray, not a copy
	 */
	public int[] getArgosyArray() {
		return argosyArray;
	}
	/**
	 * @return a copy of argosyArray
	 */
	public int[] getArgosyArrayCopy() {
		return Arrays.copyOf(argosyArray, argosyArray.length);
	}

	/**
	 * @param newArgosyArray 	the int[] array to set, must be same length as this.argosyArray.
	 * 
	 */
	public void setArgosyArray(int[] newArgosyArray) {
		if (newArgosyArray.length != this.argosyArray.length) {
			System.out.println("----->>> ERROR : new argosy array must be the same size as the old array!");
			return;
		}
		for (int i = 0; i < newArgosyArray.length; i++) {
			this.argosyArray[i] = newArgosyArray[i];
		}
	}
	
	public float[] getArgosySignal() {
		float[] signal = new float[argosyArray.length];
		float[] hsbPixel = new float[3];
		return PixelAudioMapper.pullPixelAsAudio(argosyArray, signal, PixelAudioMapper.ChannelNames.L, hsbPixel);
	}

	public float[] getArgosySignal(PixelAudioMapper.ChannelNames chan) {
		float[] signal = new float[argosyArray.length];
		float[] hsbPixel = new float[3];
		return PixelAudioMapper.pullPixelAsAudio(argosyArray, signal, chan, hsbPixel);
	}

	public float[] getArgosySignal(float scale) {
		float[] signal = new float[argosyArray.length];
		float[] hsbPixel = new float[3];
		PixelAudioMapper.pullPixelAsAudio(argosyArray, signal, PixelAudioMapper.ChannelNames.L, hsbPixel);
		for (int i = 0; i < signal.length; i++) {
			signal[i] *= scale;
		}
		return signal;
	}

	public float[] getArgosySignal(PixelAudioMapper.ChannelNames chan, float scale) {
		float[] signal = new float[argosyArray.length];
		float[] hsbPixel = new float[3];
		PixelAudioMapper.pullPixelAsAudio(argosyArray, signal, chan, hsbPixel);
		for (int i = 0; i < signal.length; i++) {
			signal[i] *= scale;
		}
		return signal;
	}

	/**
	 * @return argosySize, the number of argsoyUnits in argosyPattern
	 */
	public int getArgosySize() {
		return argosySize;
	}

	/**
	 * @return the unitSize
	 */
	public int getUnitSize() {
		return argosyUnitSize;
	}
	/**
	 * Sets unitSize and triggers a call to initArgosy() to reset the pattern in argosyArray.
	 * @param unitSize the new unitSize
	 */
	public void setUnitSize(int unitSize) {
		this.argosyUnitSize = unitSize;
		this.argosyStep = Math.round(this.argosyUnitSize / animStepDivisor);
		// System.out.println("--->> argosyStep = "+ argosyStep);
		if (argosyStep == 0) argosyStep = 1;
		initArgosy();
	}


	/**
	 * @return the animStep, number of pixels to shift in an animation
	 */
	public int getArgosyStep() {
		return argosyStep;
	}
	/**
	 * Set the animStep, with no side effects (but animation calls will use the new value)
	 * @param argoStep
	 */
	public void setArgosyStep(int argoStep) {
		this.argosyStep = argoStep;
	}


	/**
	 * @return the array of colors for the argosy pattern elements
	 */
	public int[] getArgosyColors() {
		return argosyColors;
	}
	/**
	 * Sets new argosyColors and triggers a call to initArgosy() to reset the pattern in argosyArray.
	 * @param argosyColors
	 */
	public void setArgosyColors(int[] argosyColors) {
		this.argosyColors = argosyColors;
		initArgosy();
	}


	/**
	 * @return the argosyGapScale
	 */
	public float getArgosyGapScale() {
		return argosyGapScale;
	}
	/**
	 * Sets argosyGapScale and triggers a call to initArgosy() to reset the pattern in argosyArray.
	 * @param argosyGapScale
	 */
	public void setArgosyGapScale(float argosyGapScale) {
		this.argosyGapScale = argosyGapScale;
		this.argosyGap = Math.round((this.argosyGapScale * this.argosyUnitSize));
		initArgosy();
	}


	/**
	 * @return the argosyGap, number of pixels between iterations of the argosy pattern
	 */
	public int getArgosyGap() {
		return argosyGap;
	}
	/**
	 * Sets argosyGap and triggers a call to initArgosy() to reset the pattern in argosyArray.
	 * Usually it's better to set the argosyGapScale, but if you want a gap that isn't a
	 * multiple of unitSize, this is the way to do it.
	 * @param argosyGap
	 */
	public void setArgosyGap(int argosyGap) {
		this.argosyGap = argosyGap;
		this.argosyGapScale = ((float)this.argosyGap)/this.argosyUnitSize;
		initArgosy();
	}


	/**
	 * @return the argosyGapColor
	 */
	public int getArgosyGapColor() {
		return argosyGapColor;
	}
	/**
	 * Sets argosyGapColor and triggers a call to argosyFill() to reset the pattern in bigArray.
	 * @param argosyGapColor
	 */
	public void setArgosyGapColor(int argosyGapColor) {
		this.argosyGapColor = argosyGapColor;
		initArgosy();
	}


	/**
	 * @return argosyReps, the number of repetitions of the argosy pattern in the array,
	 * set by argosyFill().
	 */
	public int getArgosyReps() {
		return argosyReps;
	}
	public void setArgosyReps(int newReps) {
		this.argosyReps = newReps;
		initArgosy();
	}

	

	/**
	 * @return the PixelAudioMapper associated with this Argosy
	 */
	public PixelAudioMapper getMapper() {
		return mapper;
	}

	/**
	 * @return argosyMargin, the left and right margin to argosy patterns in the array,
	 * set by argosyFill().
	 */
	public int getArgosyMargin() {
		return argosyMargin;
	}
	
	public int getArgosyOffset() {
		return argosyOffset;
	}

	public void setArgosyOffset(int argosyOffset) {
		this.argosyOffset = argosyOffset;
		this.shift(argosyOffset, false);
	}

	public int getMaxReps() {
		return maxReps;
	}

	/**
	 * @return the argosyPattern, an array with a numeric pattern.
	 */
	public int[] getArgosyPattern() {
		return argosyPattern;
	}
	/**
	 * Sets a new argosy pattern and triggers a call to argosyFill() to reset the pattern in bigArray.
	 * @param pattern
	 */
	public void setArgosyPattern(int[] pattern) {
		this.argosyPattern = new int[pattern.length];
		for (int i = 0; i < pattern.length; i++) {
			this.argosyPattern[i] = pattern[i];
		}
		initArgosy();
	}

	/**
	 * Sets new argosy colors and argosy gap color, triggers a call to argosyFill().
	 * @param argosyColors
	 * @param argosyGapColor
	 */
	public void setNewColors(int[] argosyColors, int argosyGapColor) {
		this.argosyColors = argosyColors;
		this.argosyGapColor = argosyGapColor;
		initArgosy();
	}

	public float getAnimStepDivisor() {
		return animStepDivisor;
	}

	public void setAnimStepDivisor(float animStepDivisor) {
		this.animStepDivisor = animStepDivisor;
	}

	public int getBgColor() {
		return bgColor;
	}

	public void setBgColor(int bgColor) {
		this.bgColor = bgColor;
	}
	

	/* --------------------------------------------------------------------------- */
	/*                                                                             */
	/*             Two utility methods for generating argosy patterns.             */
	/*                                                                             */
	/* --------------------------------------------------------------------------- */


	/**
	 * An L-System generator for Fibonacci trees represented as a sequence of 0s and 1s. 
	 * There are two generation rules: 0 -> 1; 1 -> 01. The initial state is 0. 
	 *
	 * @param depth     depth of iteration of the L-System. A depth of 8 gets you an ArrayList with 34 elements.
	 * @param verbose   Keep me informed. Or not.
	 * @return          an ArrayList of String values "1" and "0".
	 */
	public static ArrayList<String> fibo(int depth, boolean verbose) {
		Lindenmayer lind = new Lindenmayer();
		lind.put("0", "1");
		lind.put("1", "01");
		ArrayList<String> buf = new ArrayList<>();
		ArrayList<String> seed = new ArrayList<>();
		seed.add("0");
		lind.expandString(seed, depth, buf);
		if (verbose) {
			System.out.println("Fibonacci L-system at depth "+ depth +"\n");
			for (String element : buf) {
				System.out.print(element);
			}
		}
		return buf;
	}

	/**
	 * Generates an argosy pattern based on a Fibonacci tree. Depth 8 gets you a 34 element
	 * sequence, and so Fibonacci forth. For example:
	 *     int[] testPattern = argosyGen(8, 5, 8, true);
	 *
	 * @param depth     depth of iteration of the L-System. A depth of 8 gets you an array with 34 elements.
	 * @param v1        value to substitute for a "0" in the ArrayList returned by fibo()
	 * @param v2        value to substitute for a "1" in the ArrayList return by fibo()
	 * @param verbose   if true, tells the console what's up
	 * @return          an array of ints determined by a Fibonacci tree generator and your inputs v1 and v2
	 */
	public static int[] fibonacciPattern(int depth, int v1, int v2, boolean verbose) {
		ArrayList<String> buf = fibo(depth, verbose);
		int[] argo = new int[buf.size()];
		for (int i = 0; i < argo.length; i++) {
			int gen = Integer.valueOf(buf.get(i));
			argo[i] = (gen == 0) ? v1 : v2;
		}
		if (verbose) {
			System.out.println("\n----- argosy pattern: ");
			int i;
			for (i = 0; i < argo.length - 1; i++) {
				System.out.print(argo[i] +", ");
			}
			System.out.println(argo[i]);
		}
		return argo;
	}




}
