package com.jme3.scene.plugins.blender.textures;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;

import jme3tools.converters.ImageToAwt;

import com.jme3.bounding.BoundingBox;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.textures.blending.TextureBlender;
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;

/**
 * This texture holds a set of images for each face in the specified mesh. It
 * helps to flatten 3D texture, merge 3D and 2D textures and merge 2D textures
 * with different UV coordinates.
 * 
 * @author Marcin Roguski (Kaelthas)
 */
/* package */class TriangulatedTexture extends Texture2D {
    /** The result image format. */
    private Format                             format;
    /** The collection of images for each face. */
    private Collection<TriangleTextureElement> faceTextures;
    /**
     * The maximum texture size (width/height). This is taken from the blender
     * key.
     */
    private int                                maxTextureSize;
    /** A variable that can prevent removing identical textures. */
    private boolean                            keepIdenticalTextures = false;
    /** The result texture. */
    private Texture2D                          resultTexture;
    /** The result texture's UV coordinates. */
    private List<Vector2f>                     resultUVS;

    /**
     * This method triangulates the given flat texture. The given texture is not
     * changed.
     * 
     * @param texture2d
     *            the texture to be triangulated
     * @param uvs
     *            the UV coordinates for each face
     */
    public TriangulatedTexture(Texture2D texture2d, List<Vector2f> uvs, BlenderContext blenderContext) {
        maxTextureSize = blenderContext.getBlenderKey().getMaxTextureSize();
        faceTextures = new TreeSet<TriangleTextureElement>(new Comparator<TriangleTextureElement>() {
            public int compare(TriangleTextureElement o1, TriangleTextureElement o2) {
                return o1.faceIndex - o2.faceIndex;
            }
        });
        int facesCount = uvs.size() / 3;
        for (int i = 0; i < facesCount; ++i) {
            faceTextures.add(new TriangleTextureElement(i, texture2d.getImage(), uvs, true, blenderContext));
        }
        format = texture2d.getImage().getFormat();
    }

    /**
     * Constructor that simply stores precalculated images.
     * 
     * @param faceTextures
     *            a collection of images for the mesh's faces
     * @param blenderContext
     *            the blender context
     */
    public TriangulatedTexture(Collection<TriangleTextureElement> faceTextures, BlenderContext blenderContext) {
        maxTextureSize = blenderContext.getBlenderKey().getMaxTextureSize();
        this.faceTextures = faceTextures;
        for (TriangleTextureElement faceTextureElement : faceTextures) {
            if (format == null) {
                format = faceTextureElement.image.getFormat();
            } else if (format != faceTextureElement.image.getFormat()) {
                throw new IllegalArgumentException("Face texture element images MUST have the same image format!");
            }
        }
    }

    /**
     * This method blends the each image using the given blender and taking base
     * texture into consideration.
     * 
     * @param textureBlender
     *            the texture blender that holds the blending definition
     * @param baseTexture
     *            the texture that is 'below' the current texture (can be null)
     * @param blenderContext
     *            the blender context
     */
    public void blend(TextureBlender textureBlender, TriangulatedTexture baseTexture, BlenderContext blenderContext) {
        Format newFormat = null;
        for (TriangleTextureElement triangleTextureElement : faceTextures) {
            Image baseImage = baseTexture == null ? null : baseTexture.getFaceTextureElement(triangleTextureElement.faceIndex).image;
            triangleTextureElement.image = textureBlender.blend(triangleTextureElement.image, baseImage, blenderContext);
            if (newFormat == null) {
                newFormat = triangleTextureElement.image.getFormat();
            } else if (newFormat != triangleTextureElement.image.getFormat()) {
                throw new IllegalArgumentException("Face texture element images MUST have the same image format!");
            }
        }
        format = newFormat;
    }

    /**
     * This method alters the images to fit them into UV coordinates of the
     * given target texture.
     * 
     * @param targetTexture
     *            the texture to whose UV coordinates we fit current images
     * @param blenderContext
     *            the blender context
     */
    public void castToUVS(TriangulatedTexture targetTexture, BlenderContext blenderContext) {
        int[] sourceSize = new int[2], targetSize = new int[2];
        ImageLoader imageLoader = new ImageLoader();
        TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
        for (TriangleTextureElement entry : faceTextures) {
            TriangleTextureElement targetFaceTextureElement = targetTexture.getFaceTextureElement(entry.faceIndex);
            Vector2f[] dest = targetFaceTextureElement.uv;

            // get the sizes of the source and target images
            sourceSize[0] = entry.image.getWidth();
            sourceSize[1] = entry.image.getHeight();
            targetSize[0] = targetFaceTextureElement.image.getWidth();
            targetSize[1] = targetFaceTextureElement.image.getHeight();

            // create triangle transformation
            AffineTransform affineTransform = textureHelper.createAffineTransform(entry.uv, dest, sourceSize, targetSize);

            // compute the result texture
            BufferedImage sourceImage = ImageToAwt.convert(entry.image, false, true, 0);

            BufferedImage targetImage = new BufferedImage(targetSize[0], targetSize[1], sourceImage.getType());
            Graphics2D g = targetImage.createGraphics();
            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g.drawImage(sourceImage, affineTransform, null);
            g.dispose();

            Image output = imageLoader.load(targetImage, false);
            entry.image = output;
            entry.uv[0].set(dest[0]);
            entry.uv[1].set(dest[1]);
            entry.uv[2].set(dest[2]);
        }
    }

    /**
     * This method returns the flat texture. It is calculated if required or if
     * it was not created before. Images that are identical are discarded to
     * reduce the texture size.
     * 
     * @param rebuild
     *            a variable that forces texture recomputation (even if it was
     *            computed vefore)
     * @return flat result texture (all images merged into one)
     */
    public Texture2D getResultTexture(boolean rebuild) {
        if (resultTexture == null || rebuild) {
            // sorting the parts by their height (from highest to the lowest)
            List<TriangleTextureElement> list = new ArrayList<TriangleTextureElement>(faceTextures);
            Collections.sort(list, new Comparator<TriangleTextureElement>() {
                public int compare(TriangleTextureElement o1, TriangleTextureElement o2) {
                    return o2.image.getHeight() - o1.image.getHeight();
                }
            });

            // arraging the images on the resulting image (calculating the result image width and height)
            Set<Integer> duplicatedFaceIndexes = new HashSet<Integer>();
            int resultImageHeight = list.get(0).image.getHeight();
            int resultImageWidth = 0;
            int currentXPos = 0, currentYPos = 0;
            Map<TriangleTextureElement, Integer[]> imageLayoutData = new HashMap<TriangleTextureElement, Integer[]>(list.size());
            while (list.size() > 0) {
                TriangleTextureElement currentElement = list.remove(0);
                if (currentXPos + currentElement.image.getWidth() > maxTextureSize) {
                    currentXPos = 0;
                    currentYPos = resultImageHeight;
                    resultImageHeight += currentElement.image.getHeight();
                }
                Integer[] currentPositions = new Integer[] { currentXPos, currentYPos };
                imageLayoutData.put(currentElement, currentPositions);

                if (keepIdenticalTextures) {// removing identical images
                    for (int i = 0; i < list.size(); ++i) {
                        if (currentElement.image.equals(list.get(i).image)) {
                            duplicatedFaceIndexes.add(list.get(i).faceIndex);
                            imageLayoutData.put(list.remove(i--), currentPositions);
                        }
                    }
                }

                currentXPos += currentElement.image.getWidth();
                resultImageWidth = Math.max(resultImageWidth, currentXPos);
                // currentYPos += currentElement.image.getHeight();

                // TODO: implement that to compact the result image
                // try to add smaller images below the current one
                // int remainingHeight = resultImageHeight -
                // currentElement.image.getHeight();
                // while(remainingHeight > 0) {
                // for(int i=list.size() - 1;i>=0;--i) {
                //
                // }
                // }
            }

            // computing the result UV coordinates
            resultUVS = new ArrayList<Vector2f>(imageLayoutData.size() * 3);
            for (int i = 0; i < imageLayoutData.size() * 3; ++i) {
                resultUVS.add(null);
            }
            Vector2f[] uvs = new Vector2f[3];
            for (Entry<TriangleTextureElement, Integer[]> entry : imageLayoutData.entrySet()) {
                Integer[] position = entry.getValue();
                entry.getKey().computeFinalUVCoordinates(resultImageWidth, resultImageHeight, position[0], position[1], uvs);
                resultUVS.set(entry.getKey().faceIndex * 3, uvs[0]);
                resultUVS.set(entry.getKey().faceIndex * 3 + 1, uvs[1]);
                resultUVS.set(entry.getKey().faceIndex * 3 + 2, uvs[2]);
            }

            Image resultImage = new Image(format, resultImageWidth, resultImageHeight, BufferUtils.createByteBuffer(resultImageWidth * resultImageHeight * (format.getBitsPerPixel() >> 3)), ColorSpace.Linear);
            resultTexture = new Texture2D(resultImage);
            for (Entry<TriangleTextureElement, Integer[]> entry : imageLayoutData.entrySet()) {
                if (!duplicatedFaceIndexes.contains(entry.getKey().faceIndex)) {
                    this.draw(resultImage, entry.getKey().image, entry.getValue()[0], entry.getValue()[1]);
                }
            }

            // setting additional data
            resultTexture.setWrap(WrapAxis.S, this.getWrap(WrapAxis.S));
            resultTexture.setWrap(WrapAxis.T, this.getWrap(WrapAxis.T));
            resultTexture.setMagFilter(this.getMagFilter());
            resultTexture.setMinFilter(this.getMinFilter());
        }
        return resultTexture;
    }

    /**
     * @return the result flat texture
     */
    public Texture2D getResultTexture() {
        return this.getResultTexture(false);
    }

    /**
     * @return the result texture's UV coordinates
     */
    public List<Vector2f> getResultUVS() {
        this.getResultTexture();// this is called here to make sure that the result UVS are computed
        return resultUVS;
    }

    /**
     * This method returns a single image element for the given face index.
     * 
     * @param faceIndex
     *            the face index
     * @return image element for the required face index
     * @throws IllegalStateException
     *             this exception is thrown if the current image set does not
     *             contain an image for the given face index
     */
    public TriangleTextureElement getFaceTextureElement(int faceIndex) {
        for (TriangleTextureElement textureElement : faceTextures) {
            if (textureElement.faceIndex == faceIndex) {
                return textureElement;
            }
        }
        throw new IllegalStateException("No face texture element found for index: " + faceIndex);
    }

    /**
     * @return the amount of texture faces
     */
    public int getFaceTextureCount() {
        return faceTextures.size();
    }

    /**
     * Tells the object wheather to keep or reduce identical face textures.
     * 
     * @param keepIdenticalTextures
     *            keeps or discards identical textures
     */
    public void setKeepIdenticalTextures(boolean keepIdenticalTextures) {
        this.keepIdenticalTextures = keepIdenticalTextures;
    }

    /**
     * This method draws the source image on the target image starting with the
     * specified positions.
     * 
     * @param target
     *            the target image
     * @param source
     *            the source image
     * @param targetXPos
     *            start X position on the target image
     * @param targetYPos
     *            start Y position on the target image
     */
    private void draw(Image target, Image source, int targetXPos, int targetYPos) {
        PixelInputOutput sourceIO = PixelIOFactory.getPixelIO(source.getFormat());
        PixelInputOutput targetIO = PixelIOFactory.getPixelIO(target.getFormat());
        TexturePixel pixel = new TexturePixel();

        for (int x = 0; x < source.getWidth(); ++x) {
            for (int y = 0; y < source.getHeight(); ++y) {
                sourceIO.read(source, 0, pixel, x, y);
                targetIO.write(target, 0, pixel, targetXPos + x, targetYPos + y);
            }
        }
    }

    /**
     * A class that represents an image for a single face of the mesh.
     * 
     * @author Marcin Roguski (Kaelthas)
     */
    /* package */static class TriangleTextureElement {
        /** The image for the face. */
        public Image            image;
        /** The UV coordinates for the image. */
        public final Vector2f[] uv;
        /** The index of the face this image refers to. */
        public final int        faceIndex;

        /**
         * Constructor that creates the image element from the given texture and
         * UV coordinates (it cuts out the smallest rectasngle possible from the
         * given image that will hold the triangle defined by the given UV
         * coordinates). After the image is cut out the UV coordinates are
         * recalculated to be fit for the new image.
         * 
         * @param faceIndex
         *            the index of mesh's face this image refers to
         * @param sourceImage
         *            the source image
         * @param uvCoordinates
         *            the UV coordinates that define the image
         */
        public TriangleTextureElement(int faceIndex, Image sourceImage, List<Vector2f> uvCoordinates, boolean wholeUVList, BlenderContext blenderContext) {
            TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
            this.faceIndex = faceIndex;

            uv = wholeUVList ? new Vector2f[] { uvCoordinates.get(faceIndex * 3).clone(), uvCoordinates.get(faceIndex * 3 + 1).clone(), uvCoordinates.get(faceIndex * 3 + 2).clone() } : new Vector2f[] { uvCoordinates.get(0).clone(), uvCoordinates.get(1).clone(), uvCoordinates.get(2).clone() };

            // be careful here, floating point operations might cause the
            // texture positions to be inapropriate
            int[][] texturePosition = new int[3][2];
            for (int i = 0; i < texturePosition.length; ++i) {
                texturePosition[i][0] = textureHelper.getPixelPosition(uv[i].x, sourceImage.getWidth());
                texturePosition[i][1] = textureHelper.getPixelPosition(uv[i].y, sourceImage.getHeight());
            }

            // calculating the extent of the texture
            int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE;
            int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
            float minUVX = Float.MAX_VALUE, minUVY = Float.MAX_VALUE;
            float maxUVX = Float.MIN_VALUE, maxUVY = Float.MIN_VALUE;

            for (int i = 0; i < texturePosition.length; ++i) {
                minX = Math.min(texturePosition[i][0], minX);
                minY = Math.min(texturePosition[i][1], minY);

                maxX = Math.max(texturePosition[i][0], maxX);
                maxY = Math.max(texturePosition[i][1], maxY);

                minUVX = Math.min(uv[i].x, minUVX);
                minUVY = Math.min(uv[i].y, minUVY);
                maxUVX = Math.max(uv[i].x, maxUVX);
                maxUVY = Math.max(uv[i].y, maxUVY);
            }
            int width = maxX - minX;
            int height = maxY - minY;

            if (width == 0) {
                width = 1;
            }
            if (height == 0) {
                height = 1;
            }

            // copy the pixel from the texture to the result image
            PixelInputOutput pixelReader = PixelIOFactory.getPixelIO(sourceImage.getFormat());
            TexturePixel pixel = new TexturePixel();
            ByteBuffer data = BufferUtils.createByteBuffer(width * height * 4);
            for (int y = minY; y < maxY; ++y) {
                for (int x = minX; x < maxX; ++x) {
                    int xPos = x >= sourceImage.getWidth() ? x - sourceImage.getWidth() : x;
                    int yPos = y >= sourceImage.getHeight() ? y - sourceImage.getHeight() : y;
                    pixelReader.read(sourceImage, 0, pixel, xPos, yPos);
                    data.put(pixel.getR8());
                    data.put(pixel.getG8());
                    data.put(pixel.getB8());
                    data.put(pixel.getA8());
                }
            }
            image = new Image(Format.RGBA8, width, height, data, ColorSpace.Linear);

            // modify the UV values so that they fit the new image
            float heightUV = maxUVY - minUVY;
            float widthUV = maxUVX - minUVX;
            for (int i = 0; i < uv.length; ++i) {
                // first translate it to the image borders
                uv[i].x -= minUVX;
                uv[i].y -= minUVY;
                // then scale so that it fills the whole area
                uv[i].x /= widthUV;
                uv[i].y /= heightUV;
            }
        }

        /**
         * Constructor that creates an image element from the 3D texture
         * (generated texture). It computes a flat smallest rectangle that can
         * hold a (3D) triangle defined by the given UV coordinates. Then it
         * defines the image pixels for points in 3D space that define the
         * calculated rectangle.
         * 
         * @param faceIndex
         *            the face index this image refers to
         * @param boundingBox
         *            the bounding box of the mesh
         * @param texture
         *            the texture that allows to compute a pixel value in 3D
         *            space
         * @param uv
         *            the UV coordinates of the mesh
         * @param blenderContext
         *            the blender context
         */
        public TriangleTextureElement(int faceIndex, BoundingBox boundingBox, GeneratedTexture texture, Vector3f[] uv, int[] uvIndices, BlenderContext blenderContext) {
            this.faceIndex = faceIndex;

            // compute the face vertices from the UV coordinates
            float width = boundingBox.getXExtent() * 2;
            float height = boundingBox.getYExtent() * 2;
            float depth = boundingBox.getZExtent() * 2;

            Vector3f min = boundingBox.getMin(null);
            Vector3f v1 = min.add(uv[uvIndices[0]].x * width, uv[uvIndices[0]].y * height, uv[uvIndices[0]].z * depth);
            Vector3f v2 = min.add(uv[uvIndices[1]].x * width, uv[uvIndices[1]].y * height, uv[uvIndices[1]].z * depth);
            Vector3f v3 = min.add(uv[uvIndices[2]].x * width, uv[uvIndices[2]].y * height, uv[uvIndices[2]].z * depth);

            // get the rectangle envelope for the triangle
            RectangleEnvelope envelope = this.getTriangleEnvelope(v1, v2, v3);

            // create the result image
            Format imageFormat = texture.getImage().getFormat();
            int imageWidth = (int) (envelope.width * blenderContext.getBlenderKey().getGeneratedTexturePPU());
            if (imageWidth == 0) {
                imageWidth = 1;
            }
            int imageHeight = (int) (envelope.height * blenderContext.getBlenderKey().getGeneratedTexturePPU());
            if (imageHeight == 0) {
                imageHeight = 1;
            }
            ByteBuffer data = BufferUtils.createByteBuffer(imageWidth * imageHeight * (imageFormat.getBitsPerPixel() >> 3));
            image = new Image(texture.getImage().getFormat(), imageWidth, imageHeight, data, ColorSpace.Linear);

            // computing the pixels
            PixelInputOutput pixelWriter = PixelIOFactory.getPixelIO(imageFormat);
            TexturePixel pixel = new TexturePixel();
            float[] uvs = new float[3];
            Vector3f point = new Vector3f(envelope.min);
            Vector3f vecY = new Vector3f();
            Vector3f wDelta = new Vector3f(envelope.w).multLocal(1.0f / imageWidth);
            Vector3f hDelta = new Vector3f(envelope.h).multLocal(1.0f / imageHeight);
            for (int x = 0; x < imageWidth; ++x) {
                for (int y = 0; y < imageHeight; ++y) {
                    this.toTextureUV(boundingBox, point, uvs);
                    texture.getPixel(pixel, uvs[0], uvs[1], uvs[2]);
                    pixelWriter.write(image, 0, pixel, x, y);
                    point.addLocal(hDelta);
                }

                vecY.addLocal(wDelta);
                point.set(envelope.min).addLocal(vecY);
            }

            // preparing UV coordinates for the flatted texture
            this.uv = new Vector2f[3];
            this.uv[0] = new Vector2f(FastMath.clamp(v1.subtract(envelope.min).length(), 0, Float.MAX_VALUE) / envelope.height, 0);
            Vector3f heightDropPoint = v2.subtract(envelope.w);// w is directed from the base to v2
            this.uv[1] = new Vector2f(1, heightDropPoint.subtractLocal(envelope.min).length() / envelope.height);
            this.uv[2] = new Vector2f(0, 1);
        }

        /**
         * This method computes the final UV coordinates for the image (after it
         * is combined with other images and drawed on the result image).
         * 
         * @param totalImageWidth
         *            the result image width
         * @param totalImageHeight
         *            the result image height
         * @param xPos
         *            the most left x coordinate of the image
         * @param yPos
         *            the most top y coordinate of the image
         * @param result
         *            a vector where the result is stored
         */
        public void computeFinalUVCoordinates(int totalImageWidth, int totalImageHeight, int xPos, int yPos, Vector2f[] result) {
            for (int i = 0; i < 3; ++i) {
                result[i] = new Vector2f();
                result[i].x = xPos / (float) totalImageWidth + uv[i].x * (image.getWidth() / (float) totalImageWidth);
                result[i].y = yPos / (float) totalImageHeight + uv[i].y * (image.getHeight() / (float) totalImageHeight);
            }
        }

        /**
         * This method converts the given point into 3D UV coordinates.
         * 
         * @param boundingBox
         *            the bounding box of the mesh
         * @param point
         *            the point to be transformed
         * @param uvs
         *            the result UV coordinates
         */
        private void toTextureUV(BoundingBox boundingBox, Vector3f point, float[] uvs) {
            uvs[0] = (point.x - boundingBox.getCenter().x) / (boundingBox.getXExtent() == 0 ? 1 : boundingBox.getXExtent());
            uvs[1] = (point.y - boundingBox.getCenter().y) / (boundingBox.getYExtent() == 0 ? 1 : boundingBox.getYExtent());
            uvs[2] = (point.z - boundingBox.getCenter().z) / (boundingBox.getZExtent() == 0 ? 1 : boundingBox.getZExtent());
            // UVS cannot go outside <0, 1> range, but since we are generating texture for triangle envelope it might happen that
            // some points of the envelope will exceet the bounding box of the mesh thus generating uvs outside the range
            for (int i = 0; i < 3; ++i) {
                uvs[i] = FastMath.clamp(uvs[i], 0, 1);
            }
        }

        /**
         * This method returns an envelope of a minimal rectangle, that is set
         * in 3D space, and contains the given triangle.
         * 
         * @param triangle
         *            the triangle
         * @return a rectangle minimum and maximum point and height and width
         */
        private RectangleEnvelope getTriangleEnvelope(Vector3f v1, Vector3f v2, Vector3f v3) {
            Vector3f h = v3.subtract(v1);// the height of the resulting rectangle
            Vector3f temp = v2.subtract(v1);

            float field = 0.5f * h.cross(temp).length();// the field of the rectangle: Field = 0.5 * ||h x temp||
            if (field <= 0.0f) {
                return new RectangleEnvelope(v1);// return single point envelope
            }

            float cosAlpha = h.dot(temp) / (h.length() * temp.length());// the cosinus of angle betweenh and temp

            float triangleHeight = 2 * field / h.length();// the base of the height is the h vector
            // now calculate the distance between v1 vertex and the point where
            // the above calculated height 'touches' the base line (it can be
            // settled outside the h vector)
            float x = Math.abs((float) Math.sqrt(FastMath.clamp(temp.lengthSquared() - triangleHeight * triangleHeight, 0, Float.MAX_VALUE))) * Math.signum(cosAlpha);
            // now get the height base point
            Vector3f xPoint = v1.add(h.normalize().multLocal(x));

            // get the minimum point of the envelope
            Vector3f min = x < 0 ? xPoint : v1;
            if (x < 0) {
                h = v3.subtract(min);
            } else if (x > h.length()) {
                h = xPoint.subtract(min);
            }

            Vector3f envelopeWidth = v2.subtract(xPoint);
            return new RectangleEnvelope(min, envelopeWidth, h);
        }
    }

    /**
     * A class that represents a flat rectangle in 3D space that is built on a
     * triangle in 3D space.
     * 
     * @author Marcin Roguski (Kaelthas)
     */
    private static class RectangleEnvelope {
        /** The minimum point of the rectangle. */
        public final Vector3f min;
        /** The width vector. */
        public final Vector3f w;
        /** The height vector. */
        public final Vector3f h;
        /** The width of the rectangle. */
        public final float    width;
        /** The height of the rectangle. */
        public final float    height;

        /**
         * Constructs a rectangle that actually holds a point, not a triangle.
         * This is a special case that is sometimes used when generating a
         * texture where UV coordinates are defined by normals instead of
         * vertices.
         * 
         * @param pointPosition
         *            a position in 3D space
         */
        public RectangleEnvelope(Vector3f pointPosition) {
            min = pointPosition;
            h = w = Vector3f.ZERO;
            width = height = 1;
        }

        /**
         * Constructs a rectangle envelope.
         * 
         * @param min
         *            the minimum rectangle point
         * @param w
         *            the width vector
         * @param h
         *            the height vector
         */
        public RectangleEnvelope(Vector3f min, Vector3f w, Vector3f h) {
            this.min = min;
            this.h = h;
            this.w = w;
            width = w.length();
            height = h.length();
        }

        @Override
        public String toString() {
            return "Envelope[min = " + min + ", w = " + w + ", h = " + h + "]";
        }
    }

    @Override
    public Texture createSimpleClone() {
        return null;
    }
}
