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

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.Savable;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Geometry;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Format;
import com.jme3.scene.VertexBuffer.Usage;
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.materials.MaterialHelper.DiffuseShader;
import com.jme3.scene.plugins.blender.materials.MaterialHelper.SpecularShader;
import com.jme3.scene.plugins.blender.textures.CombinedTexture;
import com.jme3.scene.plugins.blender.textures.TextureHelper;
import com.jme3.texture.Texture;
import com.jme3.util.BufferUtils;

/**
 * This class holds the data about the material.
 * @author Marcin Roguski (Kaelthas)
 */
public final class MaterialContext implements Savable {
    private static final Logger              LOGGER     = Logger.getLogger(MaterialContext.class.getName());

    // texture mapping types
    public static final int                  MTEX_COL   = 0x01;
    public static final int                  MTEX_NOR   = 0x02;
    public static final int                  MTEX_SPEC  = 0x04;
    public static final int                  MTEX_EMIT  = 0x40;
    public static final int                  MTEX_ALPHA = 0x80;
    public static final int                  MTEX_AMB   = 0x800;

    /* package */final String                name;
    /* package */final List<CombinedTexture> loadedTextures;

    /* package */final ColorRGBA             diffuseColor;
    /* package */final DiffuseShader         diffuseShader;
    /* package */final SpecularShader        specularShader;
    /* package */final ColorRGBA             specularColor;
    /* package */final float                 ambientFactor;
    /* package */final float                 shininess;
    /* package */final boolean               shadeless;
    /* package */final boolean               vertexColor;
    /* package */final boolean               transparent;
    /* package */final boolean               vTangent;
    /* package */FaceCullMode                faceCullMode;

    /* package */MaterialContext(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
        name = structure.getName();

        int mode = ((Number) structure.getFieldValue("mode")).intValue();
        shadeless = (mode & 0x4) != 0;
        vertexColor = (mode & 0x80) != 0;
        vTangent = (mode & 0x4000000) != 0; // NOTE: Requires tangents

        int diff_shader = ((Number) structure.getFieldValue("diff_shader")).intValue();
        diffuseShader = DiffuseShader.values()[diff_shader];
        ambientFactor = ((Number) structure.getFieldValue("amb")).floatValue();

        if (shadeless) {
            float r = ((Number) structure.getFieldValue("r")).floatValue();
            float g = ((Number) structure.getFieldValue("g")).floatValue();
            float b = ((Number) structure.getFieldValue("b")).floatValue();
            float alpha = ((Number) structure.getFieldValue("alpha")).floatValue();

            diffuseColor = new ColorRGBA(r, g, b, alpha);
            specularShader = null;
            specularColor = null;
            shininess = 0.0f;
        } else {
            diffuseColor = this.readDiffuseColor(structure, diffuseShader);

            int spec_shader = ((Number) structure.getFieldValue("spec_shader")).intValue();
            specularShader = SpecularShader.values()[spec_shader];
            specularColor = this.readSpecularColor(structure);
            float shininess = ((Number) structure.getFieldValue("har")).floatValue();// this is (probably) the specular hardness in blender
            this.shininess = shininess > 0.0f ? shininess : MaterialHelper.DEFAULT_SHININESS;
        }

        TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
        loadedTextures = textureHelper.readTextureData(structure, new float[] { diffuseColor.r, diffuseColor.g, diffuseColor.b, diffuseColor.a }, false);

        // veryfying if the transparency is present
        // (in blender transparent mask is 0x10000 but its better to verify it because blender can indicate transparency when
        // it is not required
        boolean transparent = false;
        if (diffuseColor != null) {
            transparent = diffuseColor.a < 1.0f;
            if (loadedTextures.size() > 0) {// texutre covers the material color
                diffuseColor.set(1, 1, 1, 1);
            }
        }
        if (specularColor != null) {
            transparent = transparent || specularColor.a < 1.0f;
        }
        this.transparent = transparent;
    }

    /**
     * @return the name of the material
     */
    public String getName() {
        return name;
    }

    /**
     * Applies material to a given geometry.
     * 
     * @param geometry
     *            the geometry
     * @param geometriesOMA
     *            the geometries OMA
     * @param userDefinedUVCoordinates
     *            UV coords defined by user
     * @param blenderContext
     *            the blender context
     */
    public void applyMaterial(Geometry geometry, Long geometriesOMA, Map<String, List<Vector2f>> userDefinedUVCoordinates, BlenderContext blenderContext) {
        Material material = null;
        if (shadeless) {
            material = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");

            if (!transparent) {
                diffuseColor.a = 1;
            }

            material.setColor("Color", diffuseColor);
        } else {
            material = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Light/Lighting.j3md");
            material.setBoolean("UseMaterialColors", Boolean.TRUE);

            // setting the colors
            if (!transparent) {
                diffuseColor.a = 1;
            }
            material.setColor("Diffuse", diffuseColor);

            material.setColor("Specular", specularColor);
            material.setFloat("Shininess", shininess);

            material.setColor("Ambient", new ColorRGBA(ambientFactor, ambientFactor, ambientFactor, 1f));
        }

        // applying textures
        if (loadedTextures != null && loadedTextures.size() > 0) {
            int textureIndex = 0;
            if (loadedTextures.size() > TextureHelper.TEXCOORD_TYPES.length) {
                LOGGER.log(Level.WARNING, "The blender file has defined more than {0} different textures. JME supports only {0} UV mappings.", TextureHelper.TEXCOORD_TYPES.length);
            }
            for (CombinedTexture combinedTexture : loadedTextures) {
                if (textureIndex < TextureHelper.TEXCOORD_TYPES.length) {
                    combinedTexture.flatten(geometry, geometriesOMA, userDefinedUVCoordinates, blenderContext);

                    this.setTexture(material, combinedTexture.getMappingType(), combinedTexture.getResultTexture());
                    List<Vector2f> uvs = combinedTexture.getResultUVS();
                    if(uvs != null && uvs.size() > 0) {
                        VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]);
                        uvCoordsBuffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(uvs.toArray(new Vector2f[uvs.size()])));
                        geometry.getMesh().setBuffer(uvCoordsBuffer);
                    }//uvs might be null if the user assigned non existing UV coordinates group name to the mesh (this should be fixed in blender file)
                } else {
                    LOGGER.log(Level.WARNING, "The texture could not be applied because JME only supports up to {0} different UV's.", TextureHelper.TEXCOORD_TYPES.length);
                }
            }
        } else if (userDefinedUVCoordinates != null && userDefinedUVCoordinates.size() > 0) {
            LOGGER.fine("No textures found for the mesh, but UV coordinates are applied.");
            int textureIndex = 0;
            if (userDefinedUVCoordinates.size() > TextureHelper.TEXCOORD_TYPES.length) {
                LOGGER.log(Level.WARNING, "The blender file has defined more than {0} different UV coordinates for the mesh. JME supports only {0} UV coordinates buffers.", TextureHelper.TEXCOORD_TYPES.length);
            }
            for (Entry<String, List<Vector2f>> entry : userDefinedUVCoordinates.entrySet()) {
                if (textureIndex < TextureHelper.TEXCOORD_TYPES.length) {
                    List<Vector2f> uvs = entry.getValue();
                    VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]);
                    uvCoordsBuffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(uvs.toArray(new Vector2f[uvs.size()])));
                    geometry.getMesh().setBuffer(uvCoordsBuffer);
                } else {
                    LOGGER.log(Level.WARNING, "The texture could not be applied because JME only supports up to {0} different UV's.", TextureHelper.TEXCOORD_TYPES.length);
                }
            }
        }

        // applying additional data
        material.setName(name);
        if (vertexColor) {
            material.setBoolean(shadeless ? "VertexColor" : "UseVertexColor", true);
        }
        material.getAdditionalRenderState().setFaceCullMode(faceCullMode != null ? faceCullMode : blenderContext.getBlenderKey().getFaceCullMode());
        if (transparent) {
            material.setTransparent(true);
            material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
            geometry.setQueueBucket(Bucket.Transparent);
        }

        geometry.setMaterial(material);
    }

    /**
     * Sets the texture to the given material.
     * 
     * @param material
     *            the material that we add texture to
     * @param mapTo
     *            the texture mapping type
     * @param texture
     *            the added texture
     */
    private void setTexture(Material material, int mapTo, Texture texture) {
        switch (mapTo) {
            case MTEX_COL:
                material.setTexture(shadeless ? MaterialHelper.TEXTURE_TYPE_COLOR : MaterialHelper.TEXTURE_TYPE_DIFFUSE, texture);
                break;
            case MTEX_NOR:
                material.setTexture(MaterialHelper.TEXTURE_TYPE_NORMAL, texture);
                break;
            case MTEX_SPEC:
                material.setTexture(MaterialHelper.TEXTURE_TYPE_SPECULAR, texture);
                break;
            case MTEX_EMIT:
                material.setTexture(MaterialHelper.TEXTURE_TYPE_GLOW, texture);
                break;
            case MTEX_ALPHA:
                if (!shadeless) {
                    material.setTexture(MaterialHelper.TEXTURE_TYPE_ALPHA, texture);
                } else {
                    LOGGER.warning("JME does not support alpha map on unshaded material. Material name is " + name);
                }
                break;
            case MTEX_AMB:
                material.setTexture(MaterialHelper.TEXTURE_TYPE_LIGHTMAP, texture);
                break;
            default:
                LOGGER.severe("Unknown mapping type: " + mapTo);
        }
    }

    /**
     * @return <b>true</b> if the material has at least one generated texture and <b>false</b> otherwise
     */
    public boolean hasGeneratedTextures() {
        if (loadedTextures != null) {
            for (CombinedTexture generatedTextures : loadedTextures) {
                if (generatedTextures.hasGeneratedTextures()) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * This method sets the face cull mode.
     * @param faceCullMode
     *            the face cull mode
     */
    public void setFaceCullMode(FaceCullMode faceCullMode) {
        this.faceCullMode = faceCullMode;
    }

    /**
     * This method returns the diffuse color.
     * 
     * @param materialStructure
     *            the material structure
     * @param diffuseShader
     *            the diffuse shader
     * @return the diffuse color
     */
    private ColorRGBA readDiffuseColor(Structure materialStructure, DiffuseShader diffuseShader) {
        // bitwise 'or' of all textures mappings
        int commonMapto = ((Number) materialStructure.getFieldValue("mapto")).intValue();

        // diffuse color
        float r = ((Number) materialStructure.getFieldValue("r")).floatValue();
        float g = ((Number) materialStructure.getFieldValue("g")).floatValue();
        float b = ((Number) materialStructure.getFieldValue("b")).floatValue();
        float alpha = ((Number) materialStructure.getFieldValue("alpha")).floatValue();
        if ((commonMapto & 0x01) == 0x01) {// Col
            return new ColorRGBA(r, g, b, alpha);
        } else {
            switch (diffuseShader) {
                case FRESNEL:
                case ORENNAYAR:
                case TOON:
                    break;// TODO: find what is the proper modification
                case MINNAERT:
                case LAMBERT:// TODO: check if that is correct
                    float ref = ((Number) materialStructure.getFieldValue("ref")).floatValue();
                    r *= ref;
                    g *= ref;
                    b *= ref;
                    break;
                default:
                    throw new IllegalStateException("Unknown diffuse shader type: " + diffuseShader.toString());
            }
            return new ColorRGBA(r, g, b, alpha);
        }
    }

    /**
     * This method returns a specular color used by the material.
     * 
     * @param materialStructure
     *            the material structure filled with data
     * @return a specular color used by the material
     */
    private ColorRGBA readSpecularColor(Structure materialStructure) {
        float specularIntensity = ((Number) materialStructure.getFieldValue("spec")).floatValue();
        float r = ((Number) materialStructure.getFieldValue("specr")).floatValue() * specularIntensity;
        float g = ((Number) materialStructure.getFieldValue("specg")).floatValue() * specularIntensity;
        float b = ((Number) materialStructure.getFieldValue("specb")).floatValue() * specularIntensity;
        float alpha = ((Number) materialStructure.getFieldValue("alpha")).floatValue();
        return new ColorRGBA(r, g, b, alpha);
    }

    @Override
    public void write(JmeExporter e) throws IOException {
        throw new IOException("Material context is not for saving! It implements savable only to be passed to another blend file as a Savable in user data!");
    }

    @Override
    public void read(JmeImporter e) throws IOException {
        throw new IOException("Material context is not for loading! It implements savable only to be passed to another blend file as a Savable in user data!");
    }
}
