/*
Copyright 2006 Jerry Huxtable

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.jhlabs.image;

import com.jhlabs.math.Function2D;
import com.jhlabs.math.ImageFunction2D;
import com.jhlabs.vecmath.Color4f;
import com.jhlabs.vecmath.Vector3f;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.Kernel;

public class ShadeFilter extends WholeImageFilter {
	
	public final static int COLORS_FROM_IMAGE = 0;
	public final static int COLORS_CONSTANT = 1;

	public final static int BUMPS_FROM_IMAGE = 0;
	public final static int BUMPS_FROM_IMAGE_ALPHA = 1;
	public final static int BUMPS_FROM_MAP = 2;
	public final static int BUMPS_FROM_BEVEL = 3;
    protected static final float r255 = 1.0f/255.0f;

	private float bumpHeight;
	private float bumpSoftness;
	private final float viewDistance = 10000.0f;
	private final int colorSource = COLORS_FROM_IMAGE;
	private int bumpSource = BUMPS_FROM_IMAGE;
	private Function2D bumpFunction;
	private BufferedImage environmentMap;
	private int[] envPixels;
	private int envWidth = 1, envHeight = 1;
	private final Vector3f l;
	private final Vector3f v;
	private final Vector3f n;
	private final Color4f shadedColor;
	private final Color4f diffuse_color;
	private final Color4f specular_color;
	private final Vector3f tmpv, tmpv2;

	public ShadeFilter() {
		bumpHeight = 1.0f;
		bumpSoftness = 5.0f;
		l = new Vector3f();
		v = new Vector3f();
		n = new Vector3f();
		shadedColor = new Color4f();
		diffuse_color = new Color4f();
		specular_color = new Color4f();
		tmpv = new Vector3f();
		tmpv2 = new Vector3f();
	}

	public void setBumpFunction(Function2D bumpFunction) {
		this.bumpFunction = bumpFunction;
	}

	public Function2D getBumpFunction() {
		return bumpFunction;
	}

	public void setBumpHeight(float bumpHeight) {
		this.bumpHeight = bumpHeight;
	}

	public float getBumpHeight() {
		return bumpHeight;
	}

	public void setBumpSoftness(float bumpSoftness) {
		this.bumpSoftness = bumpSoftness;
	}

	public float getBumpSoftness() {
		return bumpSoftness;
	}

	public void setEnvironmentMap(BufferedImage environmentMap) {
		this.environmentMap = environmentMap;
		if (environmentMap != null) {
			envWidth = environmentMap.getWidth();
			envHeight = environmentMap.getHeight();
			envPixels = getRGB( environmentMap, 0, 0, envWidth, envHeight, null );
		} else {
			envWidth = envHeight = 1;
			envPixels = null;
		}
	}

	public BufferedImage getEnvironmentMap() {
		return environmentMap;
	}

	public void setBumpSource(int bumpSource) {
		this.bumpSource = bumpSource;
	}

	public int getBumpSource() {
		return bumpSource;
	}


	protected void setFromRGB( Color4f c, int argb ) {
		c.set( ((argb >> 16) & 0xff) * r255, ((argb >> 8) & 0xff) * r255, (argb & 0xff) * r255, ((argb >> 24) & 0xff) * r255 );
	}
	
        @Override
	protected int[] filterPixels( int width, int height, int[] inPixels, Rectangle transformedSpace ) {
		int index = 0;
		int[] outPixels = new int[width * height];
		float width45 = Math.abs(6.0f * bumpHeight);
		boolean invertBumps = bumpHeight < 0;
		Vector3f position = new Vector3f(0.0f, 0.0f, 0.0f);
		Vector3f viewpoint = new Vector3f(width / 2.0f, height / 2.0f, viewDistance);
		Vector3f normal = new Vector3f();
		Color4f c = new Color4f();
		Function2D bump = bumpFunction;

		if (bumpSource == BUMPS_FROM_IMAGE || bumpSource == BUMPS_FROM_IMAGE_ALPHA || bumpSource == BUMPS_FROM_MAP || bump == null) {
			if ( bumpSoftness != 0 ) {
				int bumpWidth = width;
				int bumpHeight = height;
				int[] bumpPixels = inPixels;
				if ( bumpSource == BUMPS_FROM_MAP && bumpFunction instanceof ImageFunction2D ) {
					ImageFunction2D if2d = (ImageFunction2D)bumpFunction;
					bumpWidth = if2d.getWidth();
					bumpHeight = if2d.getHeight();
					bumpPixels = if2d.getPixels();
				}
				Kernel kernel = GaussianFilter.makeKernel( bumpSoftness );
				int [] tmpPixels = new int[bumpWidth * bumpHeight];
				int [] softPixels = new int[bumpWidth * bumpHeight];
				GaussianFilter.convolveAndTranspose( kernel, bumpPixels, tmpPixels, bumpWidth, bumpHeight, true, false, false, ConvolveFilter.CLAMP_EDGES);
				GaussianFilter.convolveAndTranspose( kernel, tmpPixels, softPixels, bumpHeight, bumpWidth, true, false, false, ConvolveFilter.CLAMP_EDGES);
				bump = new ImageFunction2D(softPixels, bumpWidth, bumpHeight, ImageFunction2D.CLAMP, bumpSource == BUMPS_FROM_IMAGE_ALPHA);
			} else {
                            bump = new ImageFunction2D(inPixels, width, height, ImageFunction2D.CLAMP, bumpSource == BUMPS_FROM_IMAGE_ALPHA);
                        }
		}

		Vector3f v1 = new Vector3f();
		Vector3f v2 = new Vector3f();
		Vector3f n = new Vector3f();

		// Loop through each source pixel
		for (int y = 0; y < height; y++) {
			float ny = y;
			position.y = y;
			for (int x = 0; x < width; x++) {
				float nx = x;
				
				// Calculate the normal at this point
				if (bumpSource != BUMPS_FROM_BEVEL) {
					// Complicated and slower method
					// Calculate four normals using the gradients in +/- X/Y directions
					int count = 0;
					normal.x = normal.y = normal.z = 0;
					float m0 = width45*bump.evaluate(nx, ny);
					float m1 = x > 0 ? width45*bump.evaluate(nx - 1.0f, ny)-m0 : -2;
					float m2 = y > 0 ? width45*bump.evaluate(nx, ny - 1.0f)-m0 : -2;
					float m3 = x < width-1 ? width45*bump.evaluate(nx + 1.0f, ny)-m0 : -2;
					float m4 = y < height-1 ? width45*bump.evaluate(nx, ny + 1.0f)-m0 : -2;
					
					if (m1 != -2 && m4 != -2) {
						v1.x = -1.0f; v1.y = 0.0f; v1.z = m1;
						v2.x = 0.0f; v2.y = 1.0f; v2.z = m4;
						n.cross(v1, v2);
						n.normalize();
						if (n.z < 0.0) {
                                                    n.z = -n.z;
                                                }
						normal.add(n);
						count++;
					}

					if (m1 != -2 && m2 != -2) {
						v1.x = -1.0f; v1.y = 0.0f; v1.z = m1;
						v2.x = 0.0f; v2.y = -1.0f; v2.z = m2;
						n.cross(v1, v2);
						n.normalize();
						if (n.z < 0.0) {
                                                    n.z = -n.z;
                                                }
						normal.add(n);
						count++;
					}

					if (m2 != -2 && m3 != -2) {
						v1.x = 0.0f; v1.y = -1.0f; v1.z = m2;
						v2.x = 1.0f; v2.y = 0.0f; v2.z = m3;
						n.cross(v1, v2);
						n.normalize();
						if (n.z < 0.0) {
                                                    n.z = -n.z;
                                                }
						normal.add(n);
						count++;
					}

					if (m3 != -2 && m4 != -2) {
						v1.x = 1.0f; v1.y = 0.0f; v1.z = m3;
						v2.x = 0.0f; v2.y = 1.0f; v2.z = m4;
						n.cross(v1, v2);
						n.normalize();
						if (n.z < 0.0) {
                                                    n.z = -n.z;
                                                }
						normal.add(n);
						count++;
					}

					// Average the four normals
					normal.x /= count;
					normal.y /= count;
					normal.z /= count;
				}

/* For testing - generate a sphere bump map
				double dx = x-120;
				double dy = y-80;
				double r2 = dx*dx+dy*dy;
//				double r = Math.sqrt( r2 );
//				double t = Math.atan2( dy, dx );
				if ( r2 < 80*80 ) {
					double z = Math.sqrt( 80*80 - r2 );
					normal.x = (float)dx;
					normal.y = (float)dy;
					normal.z = (float)z;
					normal.normalize();
				} else {
					normal.x = 0;
					normal.y = 0;
					normal.z = 1;
				}
*/

				if (invertBumps) {
					normal.x = -normal.x;
					normal.y = -normal.y;
				}
				position.x = x;

				if (normal.z >= 0) {
					// Get the material colour at this point
					if (environmentMap != null) {
						//FIXME-too much normalizing going on here
						tmpv2.set(viewpoint);
						tmpv2.sub(position);
						tmpv2.normalize();
						tmpv.set(normal);
						tmpv.normalize();

						// Reflect
						tmpv.scale( 2.0f*tmpv.dot(tmpv2) );
						tmpv.sub(v);
						
						tmpv.normalize();
						setFromRGB(c, getEnvironmentMapP(normal, inPixels, width, height));//FIXME-interpolate()
						int alpha = inPixels[index] & 0xff000000;
						int rgb = ((int)(c.x * 255) << 16) | ((int)(c.y * 255) << 8) | (int)(c.z * 255);
						outPixels[index++] = alpha | rgb;
					} else {
                                            outPixels[index++] = 0;
                                        }
				} else {
                                    outPixels[index++] = 0;
                                }
			}
		}
		return outPixels;
	}

	private int getEnvironmentMapP(Vector3f normal, int[] inPixels, int width, int height) {
		if (environmentMap != null) {
			float x = 0.5f * (1 + normal.x);
			float y = 0.5f * (1 + normal.y);
			x = ImageMath.clamp(x * envWidth, 0, envWidth-1);
			y = ImageMath.clamp(y * envHeight, 0, envHeight-1);
			int ix = (int)x;
			int iy = (int)y;

			float xWeight = x-ix;
			float yWeight = y-iy;
			int i = envWidth*iy + ix;
			int dx = ix == envWidth-1 ? 0 : 1;
			int dy = iy == envHeight-1 ? 0 : envWidth;
			return ImageMath.bilinearInterpolate( xWeight, yWeight, envPixels[i], envPixels[i+dx], envPixels[i+dy], envPixels[i+dx+dy] );
		}
		return 0;
	}
	
        @Override
	public String toString() {
		return "Stylize/Shade...";
	}

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone(); //To change body of generated methods, choose Tools | Templates.
    }

}
