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

import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import com.jme3.bounding.BoundingBox;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.plugins.blender.textures.TriangulatedTexture.TriangleTextureElement;
import com.jme3.scene.plugins.blender.textures.UVCoordinatesGenerator.UVCoordinatesType;
import com.jme3.scene.plugins.blender.textures.generating.TextureGenerator;
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.TextureCubeMap;
import com.jme3.util.TempVars;

/**
 * The generated texture loaded from blender file. The texture is not generated
 * after being read. This class rather stores all required data and can compute
 * a pixel in the required 3D space position.
 * 
 * @author Marcin Roguski (Kaelthas)
 */
/* package */class GeneratedTexture extends Texture {
    private static final int            POSITIVE_X       = 0;
    private static final int            NEGATIVE_X       = 1;
    private static final int            POSITIVE_Y       = 2;
    private static final int            NEGATIVE_Y       = 3;
    private static final int            POSITIVE_Z       = 4;
    private static final int            NEGATIVE_Z       = 5;

    // flag values
    public static final int             TEX_COLORBAND    = 1;
    public static final int             TEX_FLIPBLEND    = 2;
    public static final int             TEX_NEGALPHA     = 4;
    public static final int             TEX_CHECKER_ODD  = 8;
    public static final int             TEX_CHECKER_EVEN = 16;
    public static final int             TEX_PRV_ALPHA    = 32;
    public static final int             TEX_PRV_NOR      = 64;
    public static final int             TEX_REPEAT_XMIR  = 128;
    public static final int             TEX_REPEAT_YMIR  = 256;
    public static final int             TEX_FLAG_MASK    = TEX_COLORBAND | TEX_FLIPBLEND | TEX_NEGALPHA | TEX_CHECKER_ODD | TEX_CHECKER_EVEN | TEX_PRV_ALPHA | TEX_PRV_NOR | TEX_REPEAT_XMIR | TEX_REPEAT_YMIR;

    /** Material-texture link structure. */
    private final Structure             mTex;
    /** Texture generateo for the specified texture type. */
    private final TextureGenerator      textureGenerator;
    /**
     * The generated texture cast functions. They are used to cas a given point on a plane to a specified shape in 3D space.
     * The functions should be ordered as the ordinal of a BlenderKey.CastFunction enums.
     */
    private final static CastFunction[] CAST_FUNCTIONS    = new CastFunction[] {
        /**
         * The cube casting function (does nothing except scaling if needed because the given points are already on a cube).
         */
        new CastFunction() {
            @Override
            public void cast(Vector3f pointToCast, float radius) {
                //computed using the Thales' theorem
                float length = 2 * pointToCast.subtractLocal(0.5f, 0.5f, 0.5f).length() * radius;
                pointToCast.normalizeLocal().addLocal(0.5f, 0.5f, 0.5f).multLocal(length);
            }
        },
        
        /**
         * The sphere casting function.
         */
        new CastFunction() {
            /**
             * The method casts a point on a plane to a sphere.
             * The plane is one of the faces of a cube that has a edge of length 1 and center in (0.5 0.5, 0.5). This cube is a basic 3d area where generated texture
             * is created.
             * To cast a point on a cube face to a sphere that is inside the cube we perform several easy vector operations.
             * 1. create a vector from the cube's center to the point
             * 2. setting its length to 0.5 (the radius of the sphere)
             * 3. adding the value of the cube's center to get a point on the sphere
             * 
             * The result is stored in the given vector.
             * 
             * @param pointToCast
             *            the point on a plane that will be cast to a sphere
             * @param radius
             *            the radius of the sphere
             */
            @Override
            public void cast(Vector3f pointToCast, float radius) {
                pointToCast.subtractLocal(0.5f, 0.5f, 0.5f).normalizeLocal().multLocal(radius).addLocal(0.5f, 0.5f, 0.5f);
            }
        }
    };

    /**
     * Constructor. Reads the required data from the 'tex' structure.
     * 
     * @param tex
     *            the texture structure
     * @param mTex
     *            the material-texture link data structure
     * @param textureGenerator
     *            the generator for the required texture type
     * @param blenderContext
     *            the blender context
     */
    public GeneratedTexture(Structure tex, Structure mTex, TextureGenerator textureGenerator, BlenderContext blenderContext) {
        this.mTex = mTex;
        this.textureGenerator = textureGenerator;
        this.textureGenerator.readData(tex, blenderContext);
        super.setImage(new GeneratedTextureImage(textureGenerator.getImageFormat()));
    }

    /**
     * This method computes the textyre color/intensity at the specified (u, v,
     * s) position in 3D space.
     * 
     * @param pixel
     *            the pixel where the result is stored
     * @param u
     *            the U factor
     * @param v
     *            the V factor
     * @param s
     *            the S factor
     */
    public void getPixel(TexturePixel pixel, float u, float v, float s) {
        textureGenerator.getPixel(pixel, u, v, s);
    }

    /**
     * This method triangulates the texture. In the result we get a set of small
     * flat textures for each face of the given mesh. This can be later merged
     * into one flat texture.
     * 
     * @param mesh
     *            the mesh we create the texture for
     * @param geometriesOMA
     *            the old memory address of the geometries group that the given
     *            mesh belongs to (required for bounding box calculations)
     * @param coordinatesType
     *            the types of UV coordinates
     * @param blenderContext
     *            the blender context
     * @return triangulated texture
     */
    public TriangulatedTexture triangulate(Mesh mesh, Long geometriesOMA, UVCoordinatesType coordinatesType, BlenderContext blenderContext) {
        TemporalMesh geometries = (TemporalMesh) blenderContext.getLoadedFeature(geometriesOMA, LoadedDataType.TEMPORAL_MESH);

        int[] coordinatesSwappingIndexes = new int[] { ((Number) mTex.getFieldValue("projx")).intValue(), ((Number) mTex.getFieldValue("projy")).intValue(), ((Number) mTex.getFieldValue("projz")).intValue() };
        List<Vector3f> uvs = UVCoordinatesGenerator.generateUVCoordinatesFor3DTexture(mesh, coordinatesType, coordinatesSwappingIndexes, geometries);
        Vector3f[] uvsArray = uvs.toArray(new Vector3f[uvs.size()]);
        BoundingBox boundingBox = UVCoordinatesGenerator.getBoundingBox(geometries);
        Set<TriangleTextureElement> triangleTextureElements = new TreeSet<TriangleTextureElement>(new Comparator<TriangleTextureElement>() {
            public int compare(TriangleTextureElement o1, TriangleTextureElement o2) {
                return o1.faceIndex - o2.faceIndex;
            }
        });
        int[] indices = new int[3];
        for (int i = 0; i < mesh.getTriangleCount(); ++i) {
            mesh.getTriangle(i, indices);
            triangleTextureElements.add(new TriangleTextureElement(i, boundingBox, this, uvsArray, indices, blenderContext));
        }
        return new TriangulatedTexture(triangleTextureElements, blenderContext);
    }

    /**
     * Creates a texture for the sky. The result texture has 6 layers.
     * @param size
     *            the size of the texture (width and height are equal)
     * @param horizontalColor
     *            the horizon color
     * @param zenithColor
     *            the zenith color
     * @param blenderContext
     *            the blender context
     * @return the sky texture
     */
    public TextureCubeMap generateSkyTexture(int size, ColorRGBA horizontalColor, ColorRGBA zenithColor, BlenderContext blenderContext) {
        Image image = ImageUtils.createEmptyImage(Format.RGB8, size, size, 6);
        PixelInputOutput pixelIO = PixelIOFactory.getPixelIO(image.getFormat());
        TexturePixel pixel = new TexturePixel();

        float delta = 1 / (float) (size - 1);
        float sideV, sideS = 1, forwardU = 1, forwardV, upS;
        TempVars tempVars = TempVars.get();
        CastFunction castFunction = CAST_FUNCTIONS[blenderContext.getBlenderKey().getSkyGeneratedTextureShape().ordinal()];
        float castRadius = blenderContext.getBlenderKey().getSkyGeneratedTextureRadius();

        for (int x = 0; x < size; ++x) {
            sideV = 1;
            forwardV = 1;
            upS = 0;
            for (int y = 0; y < size; ++y) {
                castFunction.cast(tempVars.vect1.set(1, sideV, sideS), castRadius);
                textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
                pixelIO.write(image, NEGATIVE_X, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// right

                castFunction.cast(tempVars.vect1.set(0, sideV, 1 - sideS), castRadius);
                textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
                pixelIO.write(image, POSITIVE_X, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// left

                castFunction.cast(tempVars.vect1.set(forwardU, forwardV, 0), castRadius);
                textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
                pixelIO.write(image, POSITIVE_Z, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// front

                castFunction.cast(tempVars.vect1.set(1 - forwardU, forwardV, 1), castRadius);
                textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
                pixelIO.write(image, NEGATIVE_Z, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// back

                castFunction.cast(tempVars.vect1.set(forwardU, 0, upS), castRadius);
                textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
                pixelIO.write(image, NEGATIVE_Y, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// top

                castFunction.cast(tempVars.vect1.set(forwardU, 1, 1 - upS), castRadius);
                textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
                pixelIO.write(image, POSITIVE_Y, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// bottom

                sideV = FastMath.clamp(sideV - delta, 0, 1);
                forwardV = FastMath.clamp(forwardV - delta, 0, 1);
                upS = FastMath.clamp(upS + delta, 0, 1);
            }
            sideS = FastMath.clamp(sideS - delta, 0, 1);
            forwardU = FastMath.clamp(forwardU - delta, 0, 1);
        }
        tempVars.release();

        return new TextureCubeMap(image);
    }

    @Override
    public void setWrap(WrapAxis axis, WrapMode mode) {
    }

    @Override
    public void setWrap(WrapMode mode) {
    }

    @Override
    public WrapMode getWrap(WrapAxis axis) {
        return null;
    }

    @Override
    public Type getType() {
        return Type.ThreeDimensional;
    }

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

    /**
     * Private class to give the format of the 'virtual' 3D texture image.
     * 
     * @author Marcin Roguski (Kaelthas)
     */
    private static class GeneratedTextureImage extends Image {
        public GeneratedTextureImage(Format imageFormat) {
            super.format = imageFormat;
        }
    }

    /**
     * The casting functions to create a sky generated texture against selected shape of a selected size.
     * 
     * @author Marcin Roguski (Kaelthas)
     */
    private static interface CastFunction {
        void cast(Vector3f pointToCast, float radius);
    }
}
