/*
 * Copyright (C) 2004  Mike Murray
 *
 *  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 General Public License for more details.
 *
 */

package net.sf.j3d.util.graphics;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

import net.sf.j3d.threeD.raytracer.graphics.ColorProducer;
import net.sf.j3d.threeD.run.Settings;

/**
 * An RGB object represents a color defined by three channels: red, green, and blue.
 * An RGB object stores these channels as double values between 0.0 (no color) and
 * 1.0 (strongest color).
 */
public class RGB implements Externalizable, Cloneable, ColorProducer {
  public static final long byteMask = 15;
  
  private double gamma = 0.75;
  private double r, g, b;

	/**
	  Constructs an RGB object with all channels set to 0.0.
	*/
	public RGB() {
		this.setRed(0.0);
		this.setGreen(0.0);
		this.setBlue(0.0);
	}
	
	/**
	  Constructs an RGB object with the specified red (r), green (g), and blue (b) channel values.
	  If any of the values are less than 0.0, the channel will be set to 0.0. If any value is greater than 1.0, the channel is set 1.0.
	*/
	public RGB(double r, double g, double b) {
		this.setRed(r);
		this.setGreen(g);
		this.setBlue(b);
	}

	public RGB(double nanom) {
		double f;

		if (nanom >= 350 && nanom <= 439) {
			this.r = (440.0 - nanom) / (440.0 - 350.0);
			this.b = 1.0;
		} else if(nanom >= 440 && nanom <= 489) {
			this.g = (nanom - 440) / (490 - 440);
			this.b = 1.0;
		} else if(nanom >= 490 && nanom <= 509) {
			this.g = 1.0;
			this.b = (510.0 - nanom) / (510.0 - 490.0);
		} else if(nanom >= 510 && nanom <= 579) {
			this.r = (nanom - 510.0) / (580.0 - 510.0);
			this.g = 1.0;
		} else if(nanom >= 580 && nanom <= 644) {
			this.r = 1.0;
			this.g = (645.0 - nanom) / (645.0 - 580.0);
		} else if(nanom >= 645 && nanom <= 780) {
			this.r = 1.0;
		}

		if (nanom >= 350 && nanom <= 419) {
			f = 0.3 + 0.7 * (nanom - 350.0) / (420.0 - 350.0);
		}else if(nanom >= 420 && nanom <= 700) {
			f = 1.0;
		}else if(nanom >= 701 && nanom <= 780) {
			f = 0.3 + 0.7 * (780.0 - nanom) / (780.0 - 700.0);
		} else {
			f = 0.0;
		}

		this.r = this.adjust(r, f);
		this.g = this.adjust(g, f);
		this.b = this.adjust(b, f);
	}

	private double adjust(double c, double f){
		if (c == 0.0)
			return 0;
		else
			return Math.pow(c * f, this.gamma);
	}

	/**
	  Sets the value of the red channel of this RGB object to the specified double value. If the value is less than 0.0, the channel is set to 0.0.
	  If the value is greater than 1.0, the channel is set to 1.0.
	*/
	public void setRed(double r) {
		if (r < 0.0) {
			this.r = 0.0;
		} else if (r > 1.0) {
			this.r = 1.0;
		} else {
			this.r = r;
		}
	}
	
	/**
	  Sets the value of the green channel of this RGB to the specified double value. If the value is less than 0.0, the channel is set to 0.0.
	  If the value is greater than 1.0, the channel is set to 1.0.
	*/
	public void setGreen(double g) {
		if (g < 0.0) {
			this.g = 0.0;
		} else if (g > 1.0) {
			this.g = 1.0;
		} else {
			this.g = g;
		}
	}
	
	/**
	  Sets the value of the blue channel of this RGB object to the specified double value. If the value is less than 0.0, the channel is set to 0.0.
	  If the value is greater than 1.0, the channel is set to 1.0.
	*/
	public void setBlue(double b) {
		if (b < 0.0) {
			this.b = 0.0;
		} else if (b > 1.0) {
			this.b = 1.0;
		} else {
			this.b = b;
		}
	}
	
	/**
	 * @return  The sum of each component of this RGB object.
	 */
	public double length() { return this.r + this.g + this.b; }
	
	/**
	  Returns the value of the red channel of this RGB object as a double value.
	*/
	public double getRed() { return this.r; }
	
	/**
	  Returns the value of the green channel of this RGB object as a double value.
	*/
	public double getGreen() { return this.g; }
	
	/**
	  Returns the value of the blue channel of this RGB object as a double value.
	*/
	public double getBlue() { return this.b; }
	
	/**
	 * Returns the sum of the RGB value represented by this RGB object and that of the specified RGB object as an RGB object.
	 * Consider using the addTo method to avoid creating unnecessary new RGB objects.
	 */
	public RGB add(RGB rgb) {
		RGB sum = new RGB(this.getRed() + rgb.getRed(), this.getGreen() + rgb.getGreen(), this.getBlue() + rgb.getBlue());
		
		return sum;
	}
	
	/**
	 * Adds the specified RGB object to this RGB object.
	 * 
	 * @param rgb  Value to add.
	 */
	public void addTo(RGB rgb) {
	    this.r += rgb.r;
	    this.g += rgb.g;
	    this.b += rgb.b;
	}
	
	/**
	 * Returns the difference of the RGB value represented by this RGB object and that of the specified RGB object as an RGB object.
	 * Consider using the subtractFrom method to avoid creating unnecessary new RGB objects.
	 */
	public RGB subtract(RGB rgb) {
		RGB difference = new RGB(this.getRed() - rgb.getRed(), this.getGreen() - rgb.getGreen(), this.getBlue() - rgb.getBlue());
		
		return difference;
	}
	
	/**
	 * Subtracts the specified RGB object from this RGB object.
	 * 
	 * @param rgb  Value to subtract.
	 */
	public void subtractFrom(RGB rgb) {
	    this.r = this.r - rgb.r;
	    this.g = this.g - rgb.g;
	    this.b = this.b - rgb.b;
	}
	
	/**
	 * @return  The product of the RGB value represented by this RGB object and the specified double
	 * value as an RGB object. Consider using the multiplyBy method to avoid creating unnecessary
	 * new RGB objects.
	 */
	public RGB multiply(double value) {
		RGB product = new RGB(this.getRed() * value, this.getGreen() * value, this.getBlue() * value);
		
		return product;
	}
	
	/**
	 * Multiplies this RGB object by the specified double value.
	 * 
	 * @param value  Value to multiply by.
	 */
	public void multiplyBy(double value) {
	    this.r *= value;
	    this.g *= value;
	    this.b *= value;
	}
	
	/**
	 * Returns the product of the RGB value represented by this RGB object and that of the specified RGB object as an RGB object.
	 * Consider using the multiplyBy method to avoid creating unnecessary new RGB objects.
	 */
	public RGB multiply(RGB rgb) {
		RGB product = new RGB(this.getRed() * rgb.getRed(), this.getGreen() * rgb.getGreen(), this.getBlue() * rgb.getBlue());
		
		return product;
	}
	
	/**
	 * Multiplies this RGB object by the specified RGB object.
	 * 
	 * @param rgb  Value to multiply by.
	 */
	public void multiplyBy(RGB rgb) {
	    this.r *= rgb.r;
	    this.g *= rgb.g;
	    this.b *= rgb.b;
	}
	
	/**
	 * Returns the quotient of the division of the RGB value represented by this RGB object as an RGB object.
	 * Consider using the divideBy method to avoid creating unnecessary new RGB objects.
	 */
	public RGB divide(double value) {
		RGB quotient = new RGB(this.r / value, this.g / value, this.b / value);
		
		return quotient;
	}
	
	/**
	 * Divides this RGB object by the specified value.
	 * 
	 * @param value  Value to divide by.
	 */
	public void divideBy(double value) {
	    this.r = this.r / value;
	    this.g = this.g / value;
	    this.b = this.b / value;
	}
	
	/**
	 * Returns the quotient of the division of the RGB value represented by this RGB object and that of the specified RGB object as an RGB object.
	 * Consider using the divideBy method to avoid creating unnecessary new RGB objects.
	 */
	public RGB divide(RGB rgb) {
		RGB quotient = new RGB(this.getRed() / rgb.getRed(), this.getGreen() / rgb.getGreen(), this.getBlue() / rgb.getBlue());
		
		return quotient;
	}
	
	/**
	 * Divides this RGB object by the specified RGB object.
	 * 
	 * @param rgb  Value to divide by.
	 */
	public void divideBy(RGB rgb) {
	    this.r = this.r / rgb.r;
	    this.g = this.g / rgb.g;
	    this.b = this.b / rgb.b;
	}
	
	/**
	 * @return  this.
	 */
	public RGB evaluate(Object args[]) { return (RGB)this.clone(); }
	
	/**
	  Returns true if the color represented by this RGB object is the same as the color represented by the specified RGB object,
	  false otherwise.
	*/
	public boolean equals(RGB rgb) {
		if (this.r != rgb.getRed())
			return false;
		else if (this.g != rgb.getGreen())
			return false;
		else if (this.b != rgb.getBlue())
			return false;
		else
			return true;
	}
	
	/**
	  Returns an integer hash code for this RGB object.
	*/
	public int hashCode() {
		double d = this.r + this.g + this.b;
		return (int)d;
	}
	
	/**
	 * @return  An RGB object that represents the same color as this RGB object.
	 */
	public Object clone() { return new RGB(this.r, this.g, this.b); }
	
	/**
	 * @return  A String representation of this RGB object.
	 */
	public String toString() {
		String value = "[" + Settings.displayFormat.format(this.getRed())  +
						", " + Settings.displayFormat.format(this.getGreen()) +
						", " + Settings.displayFormat.format(this.getBlue()) + "]";
		
		return value;
	}
	
	/**
	 * Parses a string representation of an RGB object of the format
	 * produced by the toString method.
	 * 
	 * @param s  String representation of RGB.
	 * @return  An RGB object
	 */
	public static RGB parseRGB(String s) {
		int i = s.indexOf(",");
		
		double r = Double.parseDouble(s.substring(s.indexOf("[") + 1, i));
		s = s.substring(i + 1);
		i = s.indexOf(",");
		double g = Double.parseDouble(s.substring(0, i));
		s = s.substring(i + 1);
		i = s.indexOf("]");
		double b = Double.parseDouble(s.substring(0, i));
		
		return new RGB(r, g, b);
	}
	
	public char[] encode() {
		long lr = Double.doubleToRawLongBits(this.r);
		long lg = Double.doubleToRawLongBits(this.g);
		long lb = Double.doubleToRawLongBits(this.b);
		
		char data[] = new char[48];
		
		for (int i = 0; i < 16; i++)
			data[i] = (char) (((lr & (RGB.byteMask << (4 * i))) >> (4 * i)) + 32);
		for (int i = 0; i < 16; i++)
			data[16 + i] = (char) (((lg & (RGB.byteMask << (4 * i))) >> (4 * i)) + 32);
		for (int i = 0; i < 16; i++)
			data[32 + i] = (char) (((lb & (RGB.byteMask << (4 * i))) >> (4 * i)) + 32);
		
		return data;
	}
	
	public static RGB decode(char data[]) { return RGB.decode(data, 0); }
	
	public static RGB decode(char data[], int index) {
		long lr = 0, lg = 0, lb = 0;
		
		for (int i = 0; i < 16; i++) lr = lr + (((byte)data[index + i]) - 32) << (4 * i);
		for (int i = 0; i < 16; i++) lg = lg + (((byte)data[index + 16 + i]) - 32) << (4 * i);
		for (int i = 0; i < 16; i++) lb = lb + (((byte)data[index + 32 + i]) - 32) << (4 * i);
		
		double r = Double.longBitsToDouble(lr);
		double g = Double.longBitsToDouble(lg);
		double b = Double.longBitsToDouble(lb);
		
		return new RGB(r, g, b);
	}
	
	/**
	 * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput)
	 */
	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeDouble(this.r);
		out.writeDouble(this.g);
		out.writeDouble(this.b);
	}

	/**
	 * @see java.io.Externalizable#readExternal(java.io.ObjectInput)
	 */
	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		this.r = in.readDouble();
		this.g = in.readDouble();
		this.b = in.readDouble();
	}
}
