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

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.light.AmbientLight;
import com.jme3.light.Light;
import com.jme3.math.ColorRGBA;
import com.jme3.post.filters.FogFilter;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.textures.ColorBand;
import com.jme3.scene.plugins.blender.textures.CombinedTexture;
import com.jme3.scene.plugins.blender.textures.ImageUtils;
import com.jme3.scene.plugins.blender.textures.TextureHelper;
import com.jme3.scene.plugins.blender.textures.TexturePixel;
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.TextureCubeMap;
import com.jme3.util.SkyFactory;

/**
 * The class that allows to load the following: <li>the ambient light of the scene <li>the sky of the scene (with or without texture)
 * 
 * @author Marcin Roguski (Kaelthas)
 */
public class LandscapeHelper extends AbstractBlenderHelper {
    private static final Logger LOGGER        = Logger.getLogger(LandscapeHelper.class.getName());

    private static final int    SKYTYPE_BLEND = 1;
    private static final int    SKYTYPE_REAL  = 2;
    private static final int    SKYTYPE_PAPER = 4;

    private static final int    MODE_MIST     = 0x01;

    public LandscapeHelper(String blenderVersion, BlenderContext blenderContext) {
        super(blenderVersion, blenderContext);
    }

    /**
     * Loads scene ambient light.
     * @param worldStructure
     *            the world's blender structure
     * @return the scene's ambient light
     */
    public Light toAmbientLight(Structure worldStructure) {
        LOGGER.fine("Loading ambient light.");
        AmbientLight ambientLight = null;
        float ambr = ((Number) worldStructure.getFieldValue("ambr")).floatValue();
        float ambg = ((Number) worldStructure.getFieldValue("ambg")).floatValue();
        float ambb = ((Number) worldStructure.getFieldValue("ambb")).floatValue();
        if (ambr > 0 || ambg > 0 || ambb > 0) {
            ambientLight = new AmbientLight();
            ColorRGBA ambientLightColor = new ColorRGBA(ambr, ambg, ambb, 0.0f);
            ambientLight.setColor(ambientLightColor);
            LOGGER.log(Level.FINE, "Loaded ambient light: {0}.", ambientLightColor);
        } else {
            LOGGER.finer("Ambient light is set to BLACK which means: no ambient light! The ambient light node will not be included in the result.");
        }
        return ambientLight;
    }

    /**
     * The method loads fog for the scene.
     * NOTICE! Remember to manually set the distance and density of the fog.
     * Unfortunately blender's fog parameters in no way fit to the JME.
     * @param worldStructure
     *            the world's structure
     * @return fog filter or null if scene does not define it
     */
    public FogFilter toFog(Structure worldStructure) {
        FogFilter result = null;
        int mode = ((Number) worldStructure.getFieldValue("mode")).intValue();
        if ((mode & MODE_MIST) != 0) {
            LOGGER.fine("Loading fog.");
            result = new FogFilter();
            result.setName("FIfog");
            result.setFogColor(this.toBackgroundColor(worldStructure));
        }
        return result;
    }

    /**
     * Loads the background color.
     * @param worldStructure
     *            the world's structure
     * @return the horizon color of the world which is used as a background color.
     */
    public ColorRGBA toBackgroundColor(Structure worldStructure) {
        float horr = ((Number) worldStructure.getFieldValue("horr")).floatValue();
        float horg = ((Number) worldStructure.getFieldValue("horg")).floatValue();
        float horb = ((Number) worldStructure.getFieldValue("horb")).floatValue();
        return new ColorRGBA(horr, horg, horb, 1);
    }

    /**
     * Loads scene's sky. Sky can be plain or textured.
     * If no sky type is selected in blender then no sky is loaded.
     * @param worldStructure
     *            the world's structure
     * @return the scene's sky
     * @throws BlenderFileException
     *             blender exception is thrown when problems with blender file occur
     */
    public Spatial toSky(Structure worldStructure) throws BlenderFileException {
        int skytype = ((Number) worldStructure.getFieldValue("skytype")).intValue();
        if (skytype == 0) {
            return null;
        }

        LOGGER.fine("Loading sky.");
        ColorRGBA horizontalColor = this.toBackgroundColor(worldStructure);

        float zenr = ((Number) worldStructure.getFieldValue("zenr")).floatValue();
        float zeng = ((Number) worldStructure.getFieldValue("zeng")).floatValue();
        float zenb = ((Number) worldStructure.getFieldValue("zenb")).floatValue();
        ColorRGBA zenithColor = new ColorRGBA(zenr, zeng, zenb, 1);

        // jutr for this case load generated textures wheather user had set it or not because those might be needed to properly load the sky
        boolean loadGeneratedTextures = blenderContext.getBlenderKey().isLoadGeneratedTextures();
        blenderContext.getBlenderKey().setLoadGeneratedTextures(true);

        TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
        List<CombinedTexture> loadedTextures = null;
        try {
            loadedTextures = textureHelper.readTextureData(worldStructure, new float[] { horizontalColor.r, horizontalColor.g, horizontalColor.b, horizontalColor.a }, true);
        } finally {
            blenderContext.getBlenderKey().setLoadGeneratedTextures(loadGeneratedTextures);
        }

        TextureCubeMap texture = null;
        if (loadedTextures != null && loadedTextures.size() > 0) {
            if (loadedTextures.size() > 1) {
                throw new IllegalStateException("There should be only one combined texture for sky!");
            }
            CombinedTexture combinedTexture = loadedTextures.get(0);
            texture = combinedTexture.generateSkyTexture(horizontalColor, zenithColor, blenderContext);
        } else {
            LOGGER.fine("Preparing colors for colorband.");
            int colorbandType = ColorBand.IPO_CARDINAL;
            List<ColorRGBA> colorbandColors = new ArrayList<ColorRGBA>(3);
            colorbandColors.add(horizontalColor);
            if ((skytype & SKYTYPE_BLEND) != 0) {
                if ((skytype & SKYTYPE_PAPER) != 0) {
                    colorbandType = ColorBand.IPO_LINEAR;
                }
                if ((skytype & SKYTYPE_REAL) != 0) {
                    colorbandColors.add(0, zenithColor);
                }
                colorbandColors.add(zenithColor);
            }

            int size = blenderContext.getBlenderKey().getSkyGeneratedTextureSize();

            List<Integer> positions = new ArrayList<Integer>(colorbandColors.size());
            positions.add(0);
            if (colorbandColors.size() == 2) {
                positions.add(size - 1);
            } else if (colorbandColors.size() == 3) {
                positions.add(size / 2);
                positions.add(size - 1);
            }

            LOGGER.fine("Generating sky texture.");
            float[][] values = new ColorBand(colorbandType, colorbandColors, positions, size).computeValues();

            Image image = ImageUtils.createEmptyImage(Format.RGB8, size, size, 6);
            PixelInputOutput pixelIO = PixelIOFactory.getPixelIO(image.getFormat());
            TexturePixel pixel = new TexturePixel();

            LOGGER.fine("Creating side textures.");
            int[] sideImagesIndexes = new int[] { 0, 1, 4, 5 };
            for (int i : sideImagesIndexes) {
                for (int y = 0; y < size; ++y) {
                    pixel.red = values[y][0];
                    pixel.green = values[y][1];
                    pixel.blue = values[y][2];

                    for (int x = 0; x < size; ++x) {
                        pixelIO.write(image, i, pixel, x, y);
                    }
                }
            }

            LOGGER.fine("Creating top texture.");
            pixelIO.read(image, 0, pixel, 0, image.getHeight() - 1);
            for (int y = 0; y < size; ++y) {
                for (int x = 0; x < size; ++x) {
                    pixelIO.write(image, 3, pixel, x, y);
                }
            }

            LOGGER.fine("Creating bottom texture.");
            pixelIO.read(image, 0, pixel, 0, 0);
            for (int y = 0; y < size; ++y) {
                for (int x = 0; x < size; ++x) {
                    pixelIO.write(image, 2, pixel, x, y);
                }
            }

            texture = new TextureCubeMap(image);
        }

        LOGGER.fine("Sky texture created. Creating sky.");
        return SkyFactory.createSky(blenderContext.getAssetManager(), texture, SkyFactory.EnvMapType.CubeMap);
    }
}
