package imseProc.core;

import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

import otherSupport.RandomManager;


/** Core image storage class, which isn't called 'Image' because of the SWT image class 
 *
 * When modifing the image data (either underlying or through setPixelValue(), call 
 * startWriting() and endWriting().
 * 
 * When reading the data, startReading() and endReading() can be called to ensure it
 * won't be written to at the same time. 
 */
public abstract class Img {
	
	protected static Random randIDGen = new Random();
	
	/** Random number reset every time the image is changed
	 * so that sinks can tell what has actually been modified */
	protected int changeID = randIDGen.nextInt();

	/** Size of image */
	protected int width, height;
	
	/** for Locking: Number of things reading this image */
	private int reading = 0;
	/** for Locking: Is something currently writing the image */
	protected boolean writing = false;
	
	public enum RangeValidity {
		invalid, inCalc, valid
	};	
	protected RangeValidity rangeValid = RangeValidity.invalid;
	
	/** ImgSource from whence this came, for the purposes of notifying the source that the image
	 * has been changed by something (which really should only be the source, err... nvm)
	 * so it can notifyt he sinks. Oh ffs.
	 */
	protected ImgSource source;
	protected int sourceIndex;

	protected double min, max, sum;

	protected boolean destroyed = false;
		
	public Img(ImgSource source, int sourceIndex) {
		this.source = source;
		this.sourceIndex = sourceIndex;
	}
	
	/** Call to say the image data has been internally modified, and that is now complete.
	 * @param fast If true, range calculation is done in a separate thread.
	 */
	protected void imageChanged(boolean fast) {
		int r;
		do{ 
			r = randIDGen.nextInt();
		}while(r == changeID);
		changeID = r;
		
		if(!fast){
			calcRanges();				
		}
		
		IMSEProc.ensureFinalUpdate(this, new Runnable() {
			@Override
			public void run() {
				calcRanges();				
				source.imageRangesDone(sourceIndex);			
			}
		});	
	}	
	
	/** Returns the theoretical max value per pixel (e.g. 255 for a 8bpp image) */
	public abstract double getMaxPossibleValue();
	
	public abstract double getPixelValue(int x, int y);
	public int getSourceIndex(){ return sourceIndex; }

	/** Returns the max pixel value actually in the image 
	 * This is usually called from imageChanged() from the worker queue. 
	 * but can be called explicitly if the caller has time to spare */
	public void calcRanges(){
		if(rangeValid == RangeValidity.inCalc)
			System.out.println("calcRanges() called during range calc");
		else if(rangeValid == RangeValidity.valid){
			//System.out.println("calcRanges() called when ranges already valid");
			return;
		}
		if(destroyed){
			min = Double.NaN; 
			max = Double.NaN;
			sum = Double.NaN;
			rangeValid = RangeValidity.invalid;
			return;
		}
		rangeValid = RangeValidity.inCalc;
		double min = Double.POSITIVE_INFINITY;
		double max = Double.NEGATIVE_INFINITY;
		double sum = 0;
		for(int iY = 0; iY < height; iY++){
			for(int iX = 0; iX < width; iX++){
				double val = getPixelValue(iX, iY);
				if(val > max) max = val;
				if(val < min) min = val;
				sum += val;
			}	
		}
		this.min = min;
		this.max = max;
		this.sum = sum;
		if(rangeValid == RangeValidity.inCalc) //if it hasn't been overwritten mid-calc
			rangeValid = RangeValidity.valid;
		else
			System.out.println("["+toShortString()+": Image changed during range calc.");
	}
	
	public int getWidth() {	return width; }
	public int getHeight() { return height; }

	/** Whether or not range values represent the current image data */
	public final boolean isRangeValid(){ return (rangeValid == RangeValidity.valid); }
	/** Ranges might be non-NaN though not valid. */
	public final double getMin() { return min; }
	public final double getMax() { return max; }
	public final double getSum() { return sum; }
	public final double getMean() { return (sum / (width * height)); }
	
	@Override
	public String toString() {
		return getClass().getCanonicalName() + "[" + Integer.toHexString(hashCode()) + "]" + " " + width + " x " + height + 
				"from " + (source == null ? "Null" : source.toShortString()) + "#" + sourceIndex;
	}
	
	public final String getShortID(){
		String hhc = Integer.toHexString(hashCode());
		return hhc.substring(hhc.length()-3, hhc.length());
	}
	
	public String toShortString() {
		return getClass().getSimpleName() + "[" + getShortID() + "]"; 
	}
	
	/** Free any allocated data then report image as changed */
	public void destroy(){ 
		destroyed = true;
		imageChanged(false);
	}
	
	public boolean isDestroyed(){ return destroyed; }

	public void startReading() throws InterruptedException {
		while(true){
			synchronized (this) {
				if(destroyed)
					throw new RuntimeException("Attempted to get read lock on a destroyed image");
				if(!writing){
					reading++;
					return;
				}
			}
			Thread.sleep(3);
		}
	}

	public void endReading(){
		synchronized (this) {
			reading--;
		}
	}
	
	public void startWriting() throws InterruptedException{		
		while(true){
			synchronized (this) {
				if(destroyed)
					throw new RuntimeException("Attempted to get write lock on a destroyed image");
				if(!writing && reading == 0){
					writing = true;
					if(rangeValid == RangeValidity.inCalc)
						System.out.println("startWriting() called during range calc");
					rangeValid = RangeValidity.invalid;
					return;
				}
			}
			Thread.sleep(3);
		}
	}

	public void endWriting(){
		synchronized (this) {
			writing = false;
			rangeValid = RangeValidity.invalid;
			if(rangeValid == RangeValidity.inCalc)
				System.out.println("endWriting() called during range calc");
		}
	}
	
	public void invalidate(){
		rangeValid = RangeValidity.invalid;
	}
	
	public int getChangeID(){ return changeID; }
	
	public abstract boolean isMemoryCompatible(Img img);
}
 