/*
 * Copyright (C) 2004-05  Mike Murray
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License (version 2)
 *  as published by the Free Software Foundation.
 *
 *  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.threeD.raytracer.shaders;

import net.sf.j3d.threeD.raytracer.engine.Ray;
import net.sf.j3d.threeD.raytracer.engine.RayTracingEngine;
import net.sf.j3d.threeD.raytracer.engine.Surface;
import net.sf.j3d.threeD.raytracer.engine.Texture;
import net.sf.j3d.threeD.raytracer.graphics.ColorProducer;
import net.sf.j3d.threeD.raytracer.lighting.Light;
import net.sf.j3d.threeD.raytracer.util.Editable;
import net.sf.j3d.threeD.raytracer.util.Producer;
import net.sf.j3d.util.Vector;
import net.sf.j3d.util.graphics.RGB;

/**
 * A ReflectionShader object provides a shading method for reflective surfaces.
 * The ReflectionShader class uses a shading algorithm based on Shlick's
 * approximation to the Fresnel equations.
 * 
 * @author Mike Murray
 */
public class ReflectionShader extends ShaderSet implements Shader, Editable {
  public static int maxReflections = 4;
  
  private static final String propNames[] = {"Reflectivity", "Reflective Color",
  										"Blur factor", "Environment map"};
  private static final String propDesc[] = {"The reflectivity of the surface at a direct (normal) viewing angle, usually in the range [0,1].",
										"The base color of the reflection.", "Blur factor.",
										"Texture to use as an environment map."};
  private static final Class propTypes[] = {Double.class, ColorProducer.class, Double.class, Texture.class};
  
  private double reflectivity, blur;
  private ColorProducer reflectiveColor;
  private Texture eMap;

	/**
	 * Constructs a new ReflectionShader object with a reflectivity of 0.0
	 * and white as a reflective color.
	 */
	public ReflectionShader() {
		this.setReflectivity(0.0);
		this.setBlur(0.0);
		this.setReflectiveColor(new RGB(1.0, 1.0, 1.0));
	}
	
	/**
	 * Constructs a new ReflectionShader object with the specified reflectivity
	 * and reflective color.
	 */
	public ReflectionShader(double reflectivity, ColorProducer reflectiveColor) {
		this.setReflectivity(reflectivity);
		this.setReflectiveColor(reflectiveColor);
		this.setBlur(0.0);
	}
	
	/**
	 * Method specified by the Shader interface.
	 */
	public RGB shade(ShaderParameters p) {
		if (p.getReflectionCount() > ReflectionShader.maxReflections)
			return this.reflectiveColor.evaluate(
									new Object[] {p}).multiply(p.getSurface().getColorAt(p.getPoint()));
		
		p.addReflection();
		
		Surface allSurfaces[] = new Surface[p.getOtherSurfaces().length + 1];
		for (int i = 0; i < p.getOtherSurfaces().length; i++) { allSurfaces[i] = p.getOtherSurfaces()[i]; }
		allSurfaces[allSurfaces.length - 1] = p.getSurface();
		
		Light allLights[] = new Light[p.getOtherLights().length + 1];
		for (int i = 0; i < p.getOtherLights().length; i++) { allLights[i] = p.getOtherLights()[i]; }
		allLights[allLights.length - 1] = p.getLight();
		
		RGB lightColor = p.getLight().getColorAt(p.getPoint());
		
		Vector n = p.getSurface().getNormalAt(p.getPoint());
		RGB surfaceColor = p.getSurface().getColorAt(p.getPoint());
		
		RGB totalColor = new RGB(0.0, 0.0, 0.0);
		
		RGB r = this.getReflectiveColor().evaluate(new Object[] {p});
		if (super.size() > 0) r.multiplyBy(super.shade(p));
		
		if (r.getRed() + r.getGreen() + r.getBlue() < 0.001) return new RGB(0.0, 0.0, 0.0);
		
		f: if (p.getSurface().getShadeFront()) {
			Vector ref = RayTracingEngine.reflect(p.getViewerDirection(), n);
			
			if (this.blur != 0.0) {
				double a = this.blur * (-0.5 + Math.random());
				double b = this.blur * (-0.5 + Math.random());
				
				Vector u, v, w = (Vector)n.clone();
				
				Vector t = (Vector)n.clone();
				
				if (t.getX() < t.getY() && t.getY() < t.getZ()) {
					t.setX(1.0);
				} else if (t.getY() < t.getX() && t.getY() < t.getZ()) {
					t.setY(1.0);
				} else {
					t.setZ(1.0);
				}
				
				w.divideBy(w.length());
				
				u = t.crossProduct(w);
				u.divideBy(u.length());
				
				v = w.crossProduct(u);
				
				ref.addTo(u.multiply(a));
				ref.addTo(v.multiply(b));
				ref.divideBy(ref.length());
			}
			
			Ray reflectedRay = new Ray(p.getPoint(), ref);
			
			RGB color = RayTracingEngine.lightingCalculation(reflectedRay, allSurfaces, allLights,
														p.fogColor, p.fogDensity, p.fogRatio, p);
			
			if (color == null) {
				if (this.eMap == null)
					break f;
				else
					color = this.eMap.getColorAt(ref);
			}
			
			double c = 1 - p.getViewerDirection().minus().dotProduct(n) / (p.getViewerDirection().minus().length() * n.length());
			double reflectivity = this.reflectivity + (1 - this.reflectivity) * Math.pow(c, 5.0);
			color.multiplyBy(r.multiply(reflectivity));
			
			totalColor.addTo(color);
		}
		
		b: if (p.getSurface().getShadeBack()) {
			n = n.minus();
			
			Vector ref = RayTracingEngine.reflect(p.getViewerDirection(), n);
			
			if (this.blur != 0.0) {
				double a = this.blur * (-0.5 + Math.random());
				double b = this.blur * (-0.5 + Math.random());
				
				Vector u, v, w = (Vector)n.clone();
				
				Vector t = (Vector)n.clone();
				
				if (t.getX() < t.getY() && t.getY() < t.getZ()) {
					t.setX(1.0);
				} else if (t.getY() < t.getX() && t.getY() < t.getZ()) {
					t.setY(1.0);
				} else {
					t.setZ(1.0);
				}
				
				w.divideBy(w.length());
				
				u = t.crossProduct(w);
				u.divideBy(u.length());
				
				v = w.crossProduct(u);
				
				ref.addTo(u.multiply(a));
				ref.addTo(v.multiply(b));
				ref.divideBy(ref.length());
			}
			
			Ray reflectedRay = new Ray(p.getPoint(), ref);
			
			RGB color = RayTracingEngine.lightingCalculation(reflectedRay, allSurfaces, allLights, 
												p.fogColor, p.fogDensity, p.fogRatio, p);
			
			if (color == null) {
				if (this.eMap == null)
					break b;
				else
					color = this.eMap.getColorAt(ref);
			}
			
			double c = 1 - p.getViewerDirection().minus().dotProduct(n) / (p.getViewerDirection().minus().length() * n.length());
			double reflectivity = this.reflectivity + (1 - this.reflectivity) * Math.pow(c, 5.0);
			color.multiplyBy(r.multiply(reflectivity));
			
			totalColor.addTo(color);
		}
		
		totalColor.multiplyBy(lightColor);
		
		return totalColor;
	}
	
	/**
	 * @throws IllegalArgumentException  If args[0] is not a ShaderParameters object.
	 * @return  this.shade(args[0]).
	 */
	public RGB evaluate(Object args[]) {
	    if (args[0] instanceof ShaderParameters) {
	        return this.shade((ShaderParameters)args[0]);
	    } else {
	        throw new IllegalArgumentException("Illegal argument: " + args[0]);
	    }
	}
	
	/**
	 * Sets the reflectivity value used by this ReflectionShader object.
	 */
	public void setReflectivity(double reflectivity) { this.reflectivity = reflectivity; }
	
	/**
	 * Sets the blur factor used by this ReflectionShader object.
	 * 
	 * @param blur  Blur factor to use.
	 */
	public void setBlur(double blur) { this.blur = blur; }
	
	/**
	 * Sets the reflective color used by this ReflectionShader object
	 * to the color represented by the specified ColorProducer object.
	 */
	public void setReflectiveColor(ColorProducer color) { this.reflectiveColor = color; }
	
	/**
	 * Sets the Texture object used as an environment map for this ReflectionShader object.
	 * 
	 * @param map  The Texture object to use.
	 */
	public void setEnvironmentMap(Texture map) { this.eMap = map; }
	
	/**
	 * @return  The reflectivity value used by this ReflectionShader object.
	 */
	public double getReflectivity() { return this.reflectivity; }
	
	/**
	 * @return  The blur factor used by this ReflectionShader object.
	 */
	public double getBlur() { return this.blur; }
	
	/**
	 * Returns the reflective color used by this ReflectionShader object
	 * as ColorProducer object.
	 */
	public ColorProducer getReflectiveColor() { return this.reflectiveColor; }
	
	/**
	 * @return  The Texture object used as an environment map for this ReflectionShader object.
	 */
	public Texture getEnvironmentMap() { return this.eMap; }
	
	/**
	 * Returns an array of String objects with names for each editable property of this ReflectionShader object.
	 */
	public String[] getPropertyNames() { return ReflectionShader.propNames; }
	
	/**
	 * Returns an array of String objects with descriptions for each editable property of this ReflectionShader object.
	 */
	public String[] getPropertyDescriptions() { return ReflectionShader.propDesc; }
	
	/**
	 * Returns an array of Class objects representing the class types of each editable property of this ReflectionShader object.
	 */
	public Class[] getPropertyTypes() { return ReflectionShader.propTypes; }
	
	/**
	 * Returns the values of the properties of this ReflectionShader object as an Object array.
	 */
	public Object[] getPropertyValues() {
		return new Object[] {new Double(this.reflectivity), this.reflectiveColor, new Double(this.blur), this.eMap};
	}
	
	/**
	 * Sets the value of the property of this ReflectionShader object at the specified index to the specified value.
	 * 
	 * @throws IllegalArgumentException  If the object specified is not of the correct type.
	 * @throws IndexOutOfBoundsException  If the index specified does not correspond to an editable property of this 
	 *                                    ReflectionShader object.
	 */
	public void setPropertyValue(Object value, int index) {
		if (index == 0) {
			if (value instanceof Double)
				this.setReflectivity(((Double)value).doubleValue());
			else
				throw new IllegalArgumentException("Illegal argument: " + value);
		} else if (index == 1) {
			if (value instanceof ColorProducer)
				this.setReflectiveColor((ColorProducer)value);
			else
				throw new IllegalArgumentException("Illegal argument: " + value);
		} else if (index == 2) {
			if (value instanceof Double)
				this.setBlur(((Double)value).doubleValue());
			else
				throw new IllegalArgumentException("Illegal argument: " + value);
		} else if (index == 3) {
			if (value instanceof Texture)
				this.setEnvironmentMap((Texture)value);
			else
				throw new IllegalArgumentException("Illegal argument: " + value);
		} else {
			throw new IndexOutOfBoundsException("Index out of bounds: " + index);
		}
	}
	
	/**
	 * Sets the values of editable properties of this ReflectionShader object to those specified.
	 * 
	 * @throws IllegalArgumentException  If one of the objects specified is not of the correct type.
     *                                       (Note: none of the values after the erroneous value will be set)
	 * @throws IndexOutOfBoundsException  If the length of the specified array is longer than permitted.
	 */
	public void setPropertyValues(Object values[]) {
		for (int i = 0; i < values.length; i++)
			this.setPropertyValue(values[i], i);
	}
	
	/**
	 * @return  {reflective color}.
	 */
	public Producer[] getInputPropertyValues() { return new Producer[] {this.reflectiveColor}; }
	
	/**
	 * Sets the values of properties of this HighlightShader object to those specified.
	 * 
	 * @throws IllegalArgumentException  If the Producer object specified is not of the correct type.
	 * @throws IndexOutOfBoundsException  If the lindex != 0;
	 */
	public void setInputPropertyValue(int index, Producer p) {
		if (index == 0)
			this.setPropertyValue(p, 1);
		else
			throw new IndexOutOfBoundsException("Index out of bounds: " + index);
	}
	
	/**
	 * Returns "Reflection Shader".
	 */
	public String toString() { return "Reflection Shader"; }
}
