/*
 * Copyright (c) 2009-2010 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.scene.plugins.blender.textures;

import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.math.FastMath;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.exceptions.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.texture.Texture;

/**
 * This class is a base class for texture generators.
 * @author Marcin Roguski (Kaelthas)
 */
/* package */abstract class TextureGenerator {
	private static final Logger	LOGGER	= Logger.getLogger(TextureGenerator.class.getName());

	protected NoiseGenerator	noiseGenerator;
	
	public TextureGenerator(NoiseGenerator noiseGenerator) {
		this.noiseGenerator = noiseGenerator;
	}

	/**
	 * This method generates the texture.
	 * @param tex
	 *        texture's structure
	 * @param width
	 *        the width of the result texture
	 * @param height
	 *        the height of the result texture
	 * @param depth
	 *        the depth of the texture
	 * @param blenderContext
	 *        the blender context
	 * @return newly generated texture
	 */
	protected abstract Texture generate(Structure tex, int width, int height, int depth, BlenderContext blenderContext);

	/**
	 * This method reads the colorband data from the given texture structure.
	 * 
	 * @param tex
	 *        the texture structure
	 * @param blenderContext
	 *        the blender context
	 * @return read colorband or null if not present
	 */
	private ColorBand readColorband(Structure tex, BlenderContext blenderContext) {
		ColorBand result = null;
		int flag = ((Number) tex.getFieldValue("flag")).intValue();
		if ((flag & NoiseGenerator.TEX_COLORBAND) != 0) {
			Pointer pColorband = (Pointer) tex.getFieldValue("coba");
			Structure colorbandStructure;
			try {
				colorbandStructure = pColorband.fetchData(blenderContext.getInputStream()).get(0);
				result = new ColorBand(colorbandStructure);
			} catch (BlenderFileException e) {
				LOGGER.log(Level.WARNING, "Cannot fetch the colorband structure. The reason: {0}", e.getLocalizedMessage());
			}
		}
		return result;
	}
	
	protected float[][] computeColorband(Structure tex, BlenderContext blenderContext) {
		ColorBand colorBand = this.readColorband(tex, blenderContext);
		float[][] result = null;
		if(colorBand!=null) {
			result = new float[1001][4];//1001 - amount of possible cursor positions; 4 = [r, g, b, a]
			ColorBandData[] dataArray = colorBand.data;
			
			if(dataArray.length==1) {//special case; use only one color for all types of colorband interpolation
				for(int i=0;i<result.length;++i) {
					result[i][0] = dataArray[0].r;
					result[i][1] = dataArray[0].g;
					result[i][2] = dataArray[0].b;
					result[i][3] = dataArray[0].a;
				}
			} else {
				int currentCursor = 0;
				ColorBandData currentData = dataArray[0];
				ColorBandData nextData = dataArray[0];
				switch(colorBand.ipoType) {
					case ColorBand.IPO_LINEAR:
						float rDiff = 0, gDiff = 0, bDiff = 0, aDiff = 0, posDiff;
						for(int i=0;i<result.length;++i) {
							posDiff = i - currentData.pos;
							result[i][0] = currentData.r + rDiff * posDiff;
							result[i][1] = currentData.g + gDiff * posDiff;
							result[i][2] = currentData.b + bDiff * posDiff;
							result[i][3] = currentData.a + aDiff * posDiff;
							if(nextData.pos==i) {
								currentData = dataArray[currentCursor++];
								if(currentCursor < dataArray.length) {
									nextData = dataArray[currentCursor];
									//calculate differences
									int d = nextData.pos - currentData.pos;
									rDiff = (nextData.r - currentData.r)/d;
									gDiff = (nextData.g - currentData.g)/d;
									bDiff = (nextData.b - currentData.b)/d;
									aDiff = (nextData.a - currentData.a)/d;
								} else {
									rDiff = gDiff = bDiff = aDiff = 0;
								}							
							}
						}
						break;
					case ColorBand.IPO_BSPLINE:
					case ColorBand.IPO_CARDINAL:
						Map<Integer, ColorBandData> cbDataMap = new TreeMap<Integer, ColorBandData>();
						for(int i=0;i<colorBand.data.length;++i) {
							cbDataMap.put(Integer.valueOf(i), colorBand.data[i]);
						}
						
						if(colorBand.data[0].pos==0) {
							cbDataMap.put(Integer.valueOf(-1), colorBand.data[0]);
						} else {
							ColorBandData cbData = colorBand.data[0].clone();
							cbData.pos = 0;
							cbDataMap.put(Integer.valueOf(-1), cbData);
							cbDataMap.put(Integer.valueOf(-2), cbData);
						}
						
						if(colorBand.data[colorBand.data.length - 1].pos==1000) {
							cbDataMap.put(Integer.valueOf(colorBand.data.length), colorBand.data[colorBand.data.length - 1]);
						} else {
							ColorBandData cbData = colorBand.data[colorBand.data.length - 1].clone();
							cbData.pos = 1000;
							cbDataMap.put(Integer.valueOf(colorBand.data.length), cbData);
							cbDataMap.put(Integer.valueOf(colorBand.data.length + 1), cbData);
						}
						
						float[] ipoFactors = new float[4];
						float f;
						
						ColorBandData data0 = cbDataMap.get(currentCursor - 2);
						ColorBandData data1 = cbDataMap.get(currentCursor - 1);
						ColorBandData data2 = cbDataMap.get(currentCursor);
						ColorBandData data3 = cbDataMap.get(currentCursor + 1);
						
						for(int i=0;i<result.length;++i) {
							if (data2.pos != data1.pos) {
		                        f = (i - data2.pos) / (float)(data1.pos - data2.pos);
		                    } else {
		                        f = 0.0f;
		                    }
							
							f = FastMath.clamp(f, 0.0f, 1.0f);
							
							this.getIpoData(colorBand, f, ipoFactors);
							result[i][0] = ipoFactors[3] * data0.r + ipoFactors[2] * data1.r + ipoFactors[1] * data2.r + ipoFactors[0] * data3.r;
							result[i][1] = ipoFactors[3] * data0.g + ipoFactors[2] * data1.g + ipoFactors[1] * data2.g + ipoFactors[0] * data3.g;
							result[i][2] = ipoFactors[3] * data0.b + ipoFactors[2] * data1.b + ipoFactors[1] * data2.b + ipoFactors[0] * data3.b;
							result[i][3] = ipoFactors[3] * data0.a + ipoFactors[2] * data1.a + ipoFactors[1] * data2.a + ipoFactors[0] * data3.a;
							result[i][0] = FastMath.clamp(result[i][0], 0.0f, 1.0f);
							result[i][1] = FastMath.clamp(result[i][1], 0.0f, 1.0f);
							result[i][2] = FastMath.clamp(result[i][2], 0.0f, 1.0f);
							result[i][3] = FastMath.clamp(result[i][3], 0.0f, 1.0f);
							
							if(nextData.pos==i) {
								++currentCursor;
								data0 = cbDataMap.get(currentCursor - 2);
								data1 = cbDataMap.get(currentCursor - 1);
								data2 = cbDataMap.get(currentCursor);
								data3 = cbDataMap.get(currentCursor + 1);
							}
						}
						break;
					case ColorBand.IPO_EASE:
						float d, a, b, d2;
						for(int i=0;i<result.length;++i) {
							if(nextData.pos != currentData.pos) {
								d = (i - currentData.pos) / (float)(nextData.pos - currentData.pos);
								d2 = d * d;
								a = 3.0f * d2 - 2.0f * d * d2;
								b = 1.0f - a;
							} else {
								d = a = 0.0f;
								b = 1.0f;
							}
							
							result[i][0] = b * currentData.r + a * nextData.r;
							result[i][1] = b * currentData.g + a * nextData.g;
							result[i][2] = b * currentData.b + a * nextData.b;
							result[i][3] = b * currentData.a + a * nextData.a;
							if(nextData.pos==i) {
								currentData = dataArray[currentCursor++];
								if(currentCursor < dataArray.length) {
									nextData = dataArray[currentCursor];
								}						
							}
						}
						break;
					case ColorBand.IPO_CONSTANT:
						for(int i=0;i<result.length;++i) {
							result[i][0] = currentData.r;
							result[i][1] = currentData.g;
							result[i][2] = currentData.b;
							result[i][3] = currentData.a;
							if(nextData.pos==i) {
								currentData = dataArray[currentCursor++];
								if(currentCursor < dataArray.length) {
									nextData = dataArray[currentCursor];
								}
							}
						}
						break;
					default:
						throw new IllegalStateException("Unknown interpolation type: " + colorBand.ipoType);
				}
			}
		}
		return result;
	}
	
	/**
	 * This method returns the data for either B-spline of Cardinal interpolation.
	 * @param colorBand the color band
	 * @param d distance factor for the current intensity
	 * @param ipoFactors table to store the results (size of the table must be at least 4)
	 */
	private void getIpoData(ColorBand colorBand, float d, float[] ipoFactors) {
		float d2 = d * d;
		float d3 = d2 * d;
		if(colorBand.ipoType==ColorBand.IPO_BSPLINE) {
			ipoFactors[0] = -0.71f * d3 + 1.42f * d2 - 0.71f * d;
			ipoFactors[1] = 1.29f * d3 - 2.29f * d2 + 1.0f;
			ipoFactors[2] = -1.29f * d3 + 1.58f * d2 + 0.71f * d;
			ipoFactors[3] = 0.71f * d3 - 0.71f * d2;
		} else if(colorBand.ipoType==ColorBand.IPO_CARDINAL) {
			ipoFactors[0] = -0.16666666f * d3 + 0.5f * d2 - 0.5f * d + 0.16666666f;
			ipoFactors[1] = 0.5f * d3 - d2 + 0.6666666f;
			ipoFactors[2] = -0.5f * d3 + 0.5f * d2 + 0.5f * d + 0.16666666f;
			ipoFactors[3] = 0.16666666f * d3;
		} else {
			throw new IllegalStateException("Cannot get interpolation data for other colorband types than B-spline and Cardinal!");
		}
	}
	
	/**
	 * This method applies brightness and contrast for RGB textures.
	 * @param tex texture structure
	 * @param texres
	 */
	protected void applyBrightnessAndContrast(BrightnessAndContrastData bacd, TexturePixel texres) {
        texres.red = (texres.red - 0.5f) * bacd.contrast + bacd.brightness;
        if (texres.red < 0.0f) {
            texres.red = 0.0f;
        }
        texres.green =(texres.green - 0.5f) * bacd.contrast + bacd.brightness;
        if (texres.green < 0.0f) {
            texres.green = 0.0f;
        }
        texres.blue = (texres.blue - 0.5f) * bacd.contrast + bacd.brightness;
        if (texres.blue < 0.0f) {
            texres.blue = 0.0f;
        }
    }
	
	/**
	 * This method applies brightness and contrast for Luminance textures.
	 * @param texres
	 * @param contrast
	 * @param brightness
	 */
	protected void applyBrightnessAndContrast(TexturePixel texres, float contrast, float brightness) {
        texres.intensity = (texres.intensity - 0.5f) * contrast + brightness;
        if (texres.intensity < 0.0f) {
            texres.intensity = 0.0f;
        } else if (texres.intensity > 1.0f) {
            texres.intensity = 1.0f;
        }
    }
	
	/**
	 * A class constaining the colorband data.
	 * 
	 * @author Marcin Roguski (Kaelthas)
	 */
	protected static class ColorBand {
		//interpolation types
		public static final int IPO_LINEAR 		= 0;
		public static final int IPO_EASE 		= 1;
		public static final int IPO_BSPLINE 	= 2;
		public static final int IPO_CARDINAL 	= 3;
		public static final int IPO_CONSTANT 	= 4;

		public int		cursorsAmount, ipoType;
		public ColorBandData[]	data;

		/**
		 * Constructor. Loads the data from the given structure.
		 * 
		 * @param cbdataStructure
		 *        the colorband structure
		 */
		@SuppressWarnings("unchecked")
		public ColorBand(Structure colorbandStructure) {
			this.cursorsAmount = ((Number) colorbandStructure.getFieldValue("tot")).intValue();
			this.ipoType = ((Number) colorbandStructure.getFieldValue("ipotype")).intValue();
			this.data = new ColorBandData[this.cursorsAmount];
			DynamicArray<Structure> data = (DynamicArray<Structure>) colorbandStructure.getFieldValue("data");
			for (int i = 0; i < this.cursorsAmount; ++i) {
				this.data[i] = new ColorBandData(data.get(i));
			}
		}
	}

	/**
	 * Class to store the single colorband cursor data.
	 * 
	 * @author Marcin Roguski (Kaelthas)
	 */
	protected static class ColorBandData implements Cloneable {
		public final float	r, g, b, a;
		public int 	pos;

		/**
		 * Copy constructor.
		 */
		private ColorBandData(ColorBandData data) {
			this.r = data.r;
			this.g = data.g;
			this.b = data.b;
			this.a = data.a;
			this.pos = data.pos;
		}

		/**
		 * Constructor. Loads the data from the given structure.
		 * 
		 * @param cbdataStructure
		 *        the structure containing the CBData object
		 */
		public ColorBandData(Structure cbdataStructure) {
			this.r = ((Number) cbdataStructure.getFieldValue("r")).floatValue();
			this.g = ((Number) cbdataStructure.getFieldValue("g")).floatValue();
			this.b = ((Number) cbdataStructure.getFieldValue("b")).floatValue();
			this.a = ((Number) cbdataStructure.getFieldValue("a")).floatValue();
			this.pos = (int) (((Number) cbdataStructure.getFieldValue("pos")).floatValue() * 1000.0f);
		}

		@Override
		public ColorBandData clone() {
			try {
				return (ColorBandData) super.clone();
			} catch (CloneNotSupportedException e) {
				return new ColorBandData(this);
			}
		}

		@Override
		public String toString() {
			return "P: " + this.pos + " [" + this.r+", "+this.g+", "+this.b+", "+this.a+"]";
		}
	}
	
	/**
	 * This class contains brightness and contrast data.
	 * @author Marcin Roguski (Kaelthas)
	 */
	protected static class BrightnessAndContrastData {
		public final float contrast;
        public final float brightness;
        public final float rFactor;
        public final float gFactor;
        public final float bFactor;
        
        /**
         * Constructor reads the required data from the given structure.
         * @param tex texture structure
         */
		public BrightnessAndContrastData(Structure tex) {
			contrast = ((Number) tex.getFieldValue("contrast")).floatValue();
	        brightness = ((Number) tex.getFieldValue("bright")).floatValue() - 0.5f;
	        rFactor = ((Number) tex.getFieldValue("rfac")).floatValue();
	        gFactor = ((Number) tex.getFieldValue("gfac")).floatValue();
	        bFactor = ((Number) tex.getFieldValue("bfac")).floatValue();
		}
	}
}
