package cz.cuni.jagrlib.testing;

import cz.cuni.jagrlib.*;
import cz.cuni.jagrlib.iface.*;
import cz.cuni.jagrlib.reg.*;

/**
 * Color-reducing example - rounding with 3-3-2 color palette.
 * @version 0.24 $Rev: 142 $ $Date: 2005-12-06 22:57:07 +0100 (ut, 06 XII 2005) $ $Author: pepca $
 * @since   0.24
 * @see     <a href="../../../../../cz/cuni/jagrlib/testing/ColorReduce.java">ColorReduce.java</a>
 */
public class ColorReduceAndrejKrutak extends Piece implements Trigger
{
  //==========================================================================
  //  Data:

  protected int colorlight = 0; //0=>normal; 1=>lightness
  protected int colorsel = 2; //0=>box; 1=>color average; 2=>pixel average
  protected int rgbcorners=1;

  //==========================================================================
  //  Interface Trigger:
	
	protected int MAXHASH=65536;
	protected int TGTCOLORS=256;
	
	class mcrv {
		int[][] pal;
		int[][] ch;
		chash[] h;
		Integer noc;
	};
	class chashone {
		int num;
		int count;
		chashone next=null;
		
		chashone(int n, int c) {num=n; count=c;}
	};
	class chash {
		chashone data=null;
		int count=0;
		void add(int n, int v) {
			chashone c=data;
			boolean f=false;
			while (c!=null) {
				if (c.num==n) {
					c.count++;
					f=true;
					break;
				}
				c=c.next;
			}
			if (!f) {
				chashone tail=data;
				data=new chashone(n, v);
				data.next=tail;
				count++;
			}
		}
		void add(int n) {
			add(n, 1);
		}
		void set(int n, int v) {
			chashone c=data;
			while (c!=null) {
				if (c.num==n) {
					c.count=v;
					break;
				}
				c=c.next;
			}
		}
		int getval(int n) {
			chashone c=data;
			while (c!=null) {
				if (c.num==n) {
					return c.count;
				}
				c=c.next;
			}
			return -1;
		}
	};
	
	class box {
		int i/*starting index*/, n/*number of colors*/;
		int pn/*number of pixels*/;
	};
	
	void myqsort(int[][] ar, int st, int en, int what)
	{
		//System.out.println("qsort: "+st+"-"+en);
		int pivot=(ar[st][what]+ar[(st+en)/2][what] + ar[en][what])/3;
		int i=st, j=en;
		int[] t;
		
		do {
			while (ar[i][what]<pivot) i=i+1;
			while (ar[j][what]>pivot) j=j-1;
		
			if (i<j) {
				t=ar[i]; ar[i]=ar[j]; ar[j]=t;
				i=i+1; j=j-1;
			} else if (i==j) {
				i=i+1; j=j-1;
			}
		} while (i<=j);
		
		if (st<j) myqsort(ar, st, j, what);
		if (i<en) myqsort(ar, i, en, what);
	}
	
	mcrv mediancut(RasterGraphics img, int ncolors)
	{
		int width = img.getWidth();
		int height = img.getHeight();
		int i, j, k;
		int[] pixel = new int[3];
		int[][] pal;
		int[][] ch;
		int noc;
		mcrv rv=new mcrv();

		pal=new int[ncolors][3];
		chash[] h=new chash[MAXHASH];

		if (rgbcorners==1) ncolors-=8;
		//calculate color histogram (1) make hash
		for (j=0; j<height; j++) {
			for (i=0; i<width; i++) {
				img.getRGB( i, j, pixel );
				int idx=pixel[0]*256+pixel[1];
				if (h[idx]==null) {
					h[idx]=new chash();
				}
				h[idx].add(pixel[2]);
			}
		}

		//calculate color histogram (2) now we have hash => make histogram
		noc=new Integer(0);
		for (i=0; i<MAXHASH; i++) {
			if (h[i]!=null)
				noc+=h[i].count;
		}
		//System.out.println("Unique colors: "+noc);
		
		ch=new int[noc][4];
		chashone cho;
		j=0; k=0;
		for (i=0; i<MAXHASH; i++) {
			if (h[i]!=null) {
				j+=h[i].count;
				
				cho=h[i].data;
				
				while (cho!=null) {
					ch[k][0]=i/256;
					ch[k][1]=i%256;
					ch[k][2]=cho.num;
					ch[k][3]=cho.count;
					cho=cho.next;
					k++;
				}
			}
		}

		//try to create ncolors boxes
		box[] b=new box[ncolors];
		int bn;
		for (i=0; i<ncolors; i++) b[i]=new box();
		b[0].i=0;
		b[0].n=j;
		b[0].pn=width*height;
		bn=1;

		while (bn < ncolors) {
			int bi;
			int xn, xi;
			int ci, cn, cpn;
			int mir, mig, mib, mar, mag, mab;
			xn=-1;
			xi=-1;
			for (bi=0; bi<bn; bi++) { //choose the biggest box
				if (b[bi].n>1 && b[bi].n>xn) {
					xn=b[bi].n;
					xi=bi;
				}
			}
			if (xn==-1) //there ain't anymore boxes
				break;
			bi=xi;
			ci=b[bi].i;
			cn=b[bi].n;
			cpn=b[bi].pn;
			
			//System.out.println(bn+" => " +xi+": "+xn + "=>"+ci+", "+cn);
			//find the size of box
			mir=ch[ci][0]; mar=ch[ci][0];
			mig=ch[ci][1]; mag=ch[ci][1];
			mib=ch[ci][2]; mab=ch[ci][2];

			for (i=1; i<cn; i++) {
				j=ch[ci+i][0]; if (j < mir) mir=j; else if (j > mar) mar=j;
				j=ch[ci+i][1]; if (j < mig) mig=j; else if (j > mag) mag=j;
				j=ch[ci+i][2]; if (j < mib) mib=j; else if (j > mab) mab=j;
			}

			//now split the box
			//choose the biggest difference in each axe of the RGB cube
			int rdi=(mar-mir);
			int gdi=(mag-mig);
			int bdi=(mab-mib);
			if (colorlight==1) { //stretch the cube according to yuv color model
				rdi=(int)(0.299*(double)rdi);
				gdi=(int)(0.587*(double)gdi);
				bdi=(int)(0.114*(double)bdi);
			}

			if (rdi>=gdi && rdi>=bdi)	myqsort(ch, ci, ci+cn-1, 0);
			else if (gdi >= bdi)		myqsort(ch, ci, ci+cn-1, 1);
			else 				myqsort(ch, ci, ci+cn-1, 2);

			//distribute count of pixels to both boxes
			int hs=cpn/2; //half number of pixels
			int xs=ch[ci][3]; //current no of pixels
			for (i=1; i<cn-1; i++) {
				if (xs>=hs) break;
				xs+=ch[ci+i][3];
			}
			b[bn].i=ci+i;
			b[bn].n=cn-i;
			b[bn].pn=b[bi].n-xs;

			b[bi].n=i;
			b[bi].pn=xs;
			bn++;
		}

		int cr, cg, cb, xi, xn, zi;
		//calculate colors for boxes
		for (i=0; i<bn; i++) {
			xi=b[i].i;
			xn=b[i].n;
			switch (colorsel) {
			case 0: //center box
			{
				int mir, mig, mib, mar, mag, mab;
				mir=ch[xi][0]; mar=ch[xi][0];
				mig=ch[xi][1]; mag=ch[xi][1];
				mib=ch[xi][2]; mab=ch[xi][2];
				for (zi=1; zi<xn; zi++) {
					j=ch[xi+zi][0]; if (j < mir) mir=j; else if (j > mar) mar=j;
					j=ch[xi+zi][1]; if (j < mig) mig=j; else if (j > mag) mag=j;
					j=ch[xi+zi][2]; if (j < mib) mib=j; else if (j > mab) mab=j;
				}
				pal[i][0]=(mir+mar)/2;
				pal[i][1]=(mig+mag)/2;
				pal[i][2]=(mib+mab)/2;
				break;
			}
			case 1: //clr average
			{
				cr=0; cg=0; cb=0;
				
				for (zi=0; zi<xn; zi++) {
					cr+=ch[xi+zi][0];
					cg+=ch[xi+zi][1];
					cb+=ch[xi+zi][2];
				}
				pal[i][0]=cr/xn;
				pal[i][1]=cg/xn;
				pal[i][2]=cb/xn;
				break;
			}
			case 2: //pixel average
			{
				int cs=0;
				long lcr=0, lcg=0, lcb=0;
				
				for (zi=0; zi<xn; zi++) {
					lcr+=ch[xi+zi][0]*ch[xi+zi][3];
					lcg+=ch[xi+zi][1]*ch[xi+zi][3];
					lcb+=ch[xi+zi][2]*ch[xi+zi][3];
					cs+=ch[xi+zi][3];
				}
				pal[i][0]=(int)(lcr/cs); if (pal[i][0]>255) pal[i][0]=255;
				pal[i][1]=(int)(lcg/cs); if (pal[i][1]>255) pal[i][1]=255;
				pal[i][2]=(int)(lcb/cs); if (pal[i][2]>255) pal[i][2]=255;
				break;
			}
			}
			//put the new indexes to the histogram
			for (zi=0; zi<xn; zi++) {
				h[ch[xi+zi][0]*256 + ch[xi+zi][1]].set(ch[xi+zi][2], i);
				//ch[xi+zi][3]=i;
			}
		}
		
		if (rgbcorners==1) {
			pal[248]=new int[]{0, 0, 0};
			pal[249]=new int[]{255, 0, 0};
			pal[250]=new int[]{0, 255, 0};
			pal[251]=new int[]{0, 0, 255};
			pal[252]=new int[]{255, 255, 0};
			pal[253]=new int[]{255, 0, 255};
			pal[254]=new int[]{0, 255, 255};
			pal[255]=new int[]{255, 255, 255};
		}
		
		rv.pal=pal;
		rv.ch=ch;
		rv.h=h;
		rv.noc=noc;
		return rv;
	}
	
	int findinhash(int[] clr, mcrv x)
	{
		if (x.h[clr[0]*256+clr[1]]==null)
			return -1;
		return x.h[clr[0]*256+clr[1]].getval(clr[2]);
	}
	
	void setnewpalette(RasterGraphics input, RasterGraphics output, mcrv mc)
	{
		int width = input.getWidth();
		int height = input.getHeight();
		int[][][] fserr=new int[2][width+2][3];
		int[][] tmparr;
		int x, y, xn, i;
		boolean dir=true;
		int prec=1024;
		int[] pixel = new int[3];

		RandomJames rnd = new RandomJames();
		for (x=0; x<width+2; x++) { //initialize error for line 0 with some random seed
			fserr[0][x][0]=(int)((rnd.uniformNumber()*prec*2)-prec);
			fserr[0][x][1]=(int)((rnd.uniformNumber()*prec*2)-prec);
			fserr[0][x][2]=(int)((rnd.uniformNumber()*prec*2)-prec);
		}
		for (y=0; y<height; y++) {
			for (x=0; x<width+2; x++) { //clear next row errors
				fserr[1][x][0]=0;
				fserr[1][x][1]=0;
				fserr[1][x][2]=0;
			}
			if (dir) { //left->right
				x=0;
				xn=width;
			} else {
				x=width-1;
				xn=-1;
			}
			do {
				input.getRGB( x, y, pixel );
				
				pixel[0]+=fserr[0][x+1][0]/prec;
				if (pixel[0]<0) pixel[0]=0;
				else if (pixel[0]>255) pixel[0]=255;
				pixel[1]+=fserr[0][x+1][1]/prec;
				if (pixel[1]<0) pixel[1]=0;
				else if (pixel[1]>255) pixel[1]=255;
				pixel[2]+=fserr[0][x+1][2]/prec;
				if (pixel[2]<0) pixel[2]=0;
				else if (pixel[2]>255) pixel[2]=255;
				
				i=findinhash(pixel, mc);
				if (i==-1) { //didn't find the color in original image => add to hash
					int diff=0xfffffff;
					int ii=-1;
					
					//find the closest color
					for (i=0; i<TGTCOLORS; i++) {
						int d=(pixel[0]-mc.pal[i][0])*(pixel[0]-mc.pal[i][0]) +
						      (pixel[1]-mc.pal[i][1])*(pixel[1]-mc.pal[i][1]) +
						      (pixel[2]-mc.pal[i][2])*(pixel[2]-mc.pal[i][2]);
						if (d<diff) {
							ii=i;
							diff=d;
						}
					}
					if (mc.h[pixel[0]*256+pixel[1]]==null)
						mc.h[pixel[0]*256+pixel[1]]=new chash();
					mc.h[pixel[0]*256+pixel[1]].add(pixel[2], ii); //put it to hash
					i=ii;
				}
				int err;
				if (dir) { //distribute error
					err=(pixel[0]-mc.pal[i][0])*prec;
					fserr[0][x+2][0]+=(err*7)/16; fserr[1][x  ][0]+=(err*3)/16; fserr[1][x+1][0]+=(err*5)/16; fserr[1][x+2][0]+=(err  )/16;
					err=(pixel[1]-mc.pal[i][1])*prec;
					fserr[0][x+2][1]+=(err*7)/16; fserr[1][x  ][1]+=(err*3)/16; fserr[1][x+1][1]+=(err*5)/16; fserr[1][x+2][1]+=(err  )/16;
					err=(pixel[2]-mc.pal[i][2])*prec;
					fserr[0][x+2][2]+=(err*7)/16; fserr[1][x  ][2]+=(err*3)/16; fserr[1][x+1][2]+=(err*5)/16; fserr[1][x+2][2]+=(err  )/16;
				} else {
					err=(pixel[0]-mc.pal[i][0])*prec;
					fserr[0][x  ][0]+=(err*7)/16; fserr[1][x+2][0]+=(err*3)/16; fserr[1][x+1][0]+=(err*5)/16; fserr[1][x  ][0]+=(err  )/16;
					err=(pixel[1]-mc.pal[i][1])*prec;
					fserr[0][x  ][1]+=(err*7)/16; fserr[1][x+2][1]+=(err*3)/16; fserr[1][x+1][1]+=(err*5)/16; fserr[1][x  ][1]+=(err  )/16;
					err=(pixel[2]-mc.pal[i][2])*prec;
					fserr[0][x  ][2]+=(err*7)/16; fserr[1][x+2][2]+=(err*3)/16; fserr[1][x+1][2]+=(err*5)/16; fserr[1][x  ][2]+=(err  )/16;
				}
				
				output.putPixel(x, y, i);
				
				if (dir) x++;
				else x--;
			} while (x!=xn);
			
			tmparr=fserr[0]; fserr[0]=fserr[1]; fserr[1]=tmparr;
			dir=!dir;
		}
	}
  /**
   * Starts image remapping.
   * @param  type The action type (whatever it means..).
   * @return <code>true</code> if the action was successful.
   */
  public boolean fire ( int type )
  {
    RasterGraphics input = (RasterGraphics)getInterface("image1",IFACE+"RasterGraphics");
    RasterGraphics output = (RasterGraphics)getInterface("image2",IFACE+"RasterGraphics");
    ColormapStore colorMap = (ColormapStore)getInterface("palette",IFACE+"ColormapStore");
    int i, j;

      // output image:
    int width = input.getWidth();
    int height = input.getHeight();
    output.init( width, height, RasterGraphics.MODE_COLORMAP, 0 );

	mcrv mc=mediancut(input, TGTCOLORS);

    colorMap.setSize( 1 );
    colorMap.setColormap( 0, mc.pal);
    output.setColormapType( RasterGraphics.MODE_RGB, 255 );
    output.setColormap( mc.pal );

	setnewpalette(input, output, mc);
    return true;
  }

  //==========================================================================
  //  Interface Property:

  protected final static String LIGHTNESS = "Lightness";
  protected final static String COLSEL = "ColorSelect";
  protected final static String RGBCORNERS = "RGBCorners";

  /**
   * Sets the given property.
   * @param  key Key string.
   * @param  value The new value.
   */
  public void set ( String key, Object value )
  {
    if ( key == null || value == null ) return;
      // RANDOMIZE:
    if ( key.compareTo(LIGHTNESS) == 0 )
    {
      if ( value instanceof Integer )
        colorlight = ((Integer)value).intValue();
      return;
    } else if ( key.compareTo(COLSEL) == 0 )
    {
      if ( value instanceof Integer )
        colorsel = ((Integer)value).intValue();
      return;
    } else if ( key.compareTo(RGBCORNERS) == 0 )
    {
      if ( value instanceof Integer )
        rgbcorners = ((Integer)value).intValue();
      return;
    }
  }

  /**
   * Gets the given property.
   * @param  key Key string.
   * @return The actual value or <code>null</code>.
   */
  public Object get ( String key )
  {
    if ( key == null ) return null;
    if ( key.compareTo(LIGHTNESS) == 0 )
      return new Integer(colorlight);
    else if ( key.compareTo(COLSEL) == 0 )
      return new Integer(colorsel);
    else if ( key.compareTo(RGBCORNERS) == 0 )
      return new Integer(rgbcorners);
    return null;
  }

  //==========================================================================
  //  Module registration:

  /** Object name. */
  private final static String NAME = "ColorReduce";

  /** Object template identifier. */
  protected final static String TEMPLATE_NAME = "TriggerToRasterImageAndColormapStoreAndRasterImage";

  /** Object description. */
  private final static String DESCRIPTION = "Image remapping, using 3-3-2 palette and simple rounding.";

  /** Object category. */
  protected final static String CATEGORY = C_2D + "." + C_RASTER + "." + C_FILTER;

  /**
   * General-purpose registration routine.
   * Sets all plugs, strings, etc. to the given Template.
   */
  public static int setTemplate ( Template t, int ord )
  {
    if ( t != null && ord <= 0 )
    {
      t.setRegStrings(NAME,TEMPLATE_NAME,CATEGORY,DESCRIPTION);
        // Plugs:
      t.newInputPlug(PL_INPUT,IFACE+"Trigger");
      t.newOutputPlug("image1",IFACE+"RasterGraphics");
      t.newOutputPlug("image2",IFACE+"RasterGraphics");
      t.newOutputPlug("palette",IFACE+"ColormapStore");
        // Property:
      t.propBegin(LIGHTNESS,TYPE_INTEGER,"Color lightness",true);
        t.propDefault( "0" );
        t.propManipulator(MANIPULATOR_COMBO);
        t.propEnum( "Average", "0", "");
        t.propEnum( "Lightness", "1", "");
      t.propEnd();
      t.propBegin(COLSEL,TYPE_INTEGER,"Color select",true);
        t.propDefault("2");
        t.propManipulator(MANIPULATOR_COMBO);
        t.propEnum("Box", "0", "");
	t.propEnum("ColAverage", "1", "");
	t.propEnum("PixelAverage", "2", "");
      t.propEnd();
      t.propBegin(RGBCORNERS,TYPE_INTEGER,"Add RGB cube corners to palette",true);
        t.propDefault( "0" );
        t.propBounds( "0", "1" );
      t.propEnd();
    }
    return 1;
  }

  /**
   * Static registration instance for this class.
   * Automatically initialized in class-loading time.
   */
  public static final RegPiece reg;
  static
  {
    reg = new RegPiece();
    setTemplate(reg,0);
  }

}

