/*
 * 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.ogre;

import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLoader;
import com.jme3.asset.AssetManager;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.asset.TextureKey;
import com.jme3.material.Material;
import com.jme3.material.MaterialList;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.plugins.ogre.matext.MaterialExtensionLoader;
import com.jme3.scene.plugins.ogre.matext.MaterialExtensionSet;
import com.jme3.scene.plugins.ogre.matext.OgreMaterialKey;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;
import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils;
import com.jme3.util.blockparser.BlockLanguageParser;
import com.jme3.util.blockparser.Statement;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

public class MaterialLoader implements AssetLoader {

    private static final Logger logger = Logger.getLogger(MaterialLoader.class.getName());

    private String folderName;
    private AssetManager assetManager;
    private ColorRGBA ambient, diffuse, specular, emissive;
    private Texture[] textures = new Texture[4];
    private String texName;
    private String matName;
    private float shinines;
    private boolean vcolor = false;
    private boolean blend = false;
    private boolean twoSide = false;
    private boolean noLight = false;
    private boolean separateTexCoord = false;
    private int texUnit = 0;

    private ColorRGBA readColor(String content){
        String[] split = content.split(" ");
        
        ColorRGBA color = new ColorRGBA();
        color.r = Float.parseFloat(split[0]);
        color.g = Float.parseFloat(split[1]);
        color.b = Float.parseFloat(split[2]);
        if (split.length >= 4){
            color.a = Float.parseFloat(split[3]);
        }
        return color;
    }

    private void readTextureImage(String content){
        // texture image def
        String path = null;

        // find extension
        int extStart = content.lastIndexOf(".");
        for (int i = extStart; i < content.length(); i++){
            char c = content.charAt(i);
            if (Character.isWhitespace(c)){
                // extension ends here
                path = content.substring(0, i).trim();
                content   = content.substring(i+1).trim();
                break;
            }
        }
        if (path == null){
            path = content.trim();
            content = "";
        }

        Scanner lnScan = new Scanner(content);
        String mips = null;
        String type = null;
        if (lnScan.hasNext()){
            // more params
            type = lnScan.next();
//            if (!lnScan.hasNext("\n") && lnScan.hasNext()){
//                mips = lnScan.next();
//                if (lnScan.hasNext()){
                    // even more params..
                    // will have to ignore
//                }
//            }
        }

        boolean genMips = true;
        boolean cubic = false;
        if (type != null && type.equals("0"))
            genMips = false;

        if (type != null && type.equals("cubic")){
            cubic = true;
        }

        TextureKey key = new TextureKey(folderName + path, false);
        key.setGenerateMips(genMips);
        key.setAsCube(cubic);

        Texture loadedTexture;
        try {
            loadedTexture = assetManager.loadTexture(key);
        } catch (AssetNotFoundException ex){
            logger.log(Level.WARNING, "Failed to load texture " + key + " for material " + matName, ex);
            loadedTexture = null;
        }
        if (loadedTexture == null){
            ByteBuffer tempData = BufferUtils.createByteBuffer(3);
            tempData.put((byte)0xFF).put((byte)0x00).put((byte)0x00);
            textures[texUnit].setImage(new Image(Format.RGB8, 1,1,tempData));
            logger.log(Level.WARNING, "Using RED texture instead of {0}", path);
        }else{
            textures[texUnit].setImage(loadedTexture.getImage());
            textures[texUnit].setMinFilter(loadedTexture.getMinFilter());
            textures[texUnit].setKey(loadedTexture.getKey());

            // XXX: Is this really neccessary?
            textures[texUnit].setWrap(WrapMode.Repeat);
            if (texName != null){
                textures[texUnit].setName(texName);
                texName = null;
            }else{
                textures[texUnit].setName(key.getName());
            }
        }
    }

    private void readTextureUnitStatement(Statement statement){
        String[] split = statement.getLine().split(" ", 2);
        String keyword = split[0];
        if (keyword.equals("texture")){
            readTextureImage(split[1]);
        }else if (keyword.equals("tex_address_mode")){
            String mode = split[1];
            if (mode.equals("wrap")){
                textures[texUnit].setWrap(WrapMode.Repeat);
            }else if (mode.equals("clamp")){
                textures[texUnit].setWrap(WrapMode.Clamp);
            }else if (mode.equals("mirror")){
                textures[texUnit].setWrap(WrapMode.MirroredRepeat);
            }else if (mode.equals("border")){
                textures[texUnit].setWrap(WrapMode.BorderClamp);
            }
        }else if (keyword.equals("filtering")){
            // ignored.. only anisotropy is considered
        }else if (keyword.equals("tex_coord_set")){
            int texCoord = Integer.parseInt(split[1]);
            if (texCoord == 1){
                separateTexCoord = true;
            }
        }else if (keyword.equals("max_anisotropy")){
            int amount = Integer.parseInt(split[1]);
            textures[texUnit].setAnisotropicFilter(amount);
        }else{
            logger.log(Level.WARNING, "Unsupported texture_unit directive: {0}", keyword);
        }
    }

    private void readTextureUnit(Statement statement){
        String[] split = statement.getLine().split(" ", 2); 
        // name is optional
        if (split.length == 2){
            texName = split[1];
        }else{
            texName = null;
        }

        textures[texUnit] = new Texture2D();
        for (Statement texUnitStat : statement.getContents()){
            readTextureUnitStatement(texUnitStat);
        }
        if (textures[texUnit].getImage() != null){
            texUnit++;
        }else{
            // no image was loaded, ignore
            textures[texUnit] = null;
        }
    }

    private void readPassStatement(Statement statement){
        // read until newline
        String[] split = statement.getLine().split(" ", 2);
        String keyword = split[0];
        if (keyword.equals("diffuse")){
            if (split[1].equals("vertexcolour")){
                // use vertex colors
                diffuse = ColorRGBA.White;
                vcolor = true;
            }else{
                diffuse = readColor(split[1]);
            }
        }else if(keyword.equals("ambient")) {
           if (split[1].equals("vertexcolour")){
                // use vertex colors
               ambient = ColorRGBA.White;
            }else{
               ambient = readColor(split[1]);
            }
        }else if (keyword.equals("emissive")){
            emissive = readColor(split[1]);
        }else if (keyword.equals("specular")){
            String[] subsplit = split[1].split(" ");
            specular = new ColorRGBA();
            specular.r = Float.parseFloat(subsplit[0]);
            specular.g = Float.parseFloat(subsplit[1]);
            specular.b = Float.parseFloat(subsplit[2]);
            float unknown = Float.parseFloat(subsplit[3]);
            if (subsplit.length >= 5){
                // using 5 float values
                specular.a = unknown;
                shinines = Float.parseFloat(subsplit[4]);
            }else{
                // using 4 float values
                specular.a = 1f;
                shinines = unknown;
            }
        }else if (keyword.equals("texture_unit")){
            readTextureUnit(statement);
        }else if (keyword.equals("scene_blend")){
            String mode = split[1];
            if (mode.equals("alpha_blend")){
                blend = true;
            }
        }else if (keyword.equals("cull_hardware")){
            String mode = split[1];
            if (mode.equals("none")){
                twoSide = true;
            }
        }else if (keyword.equals("cull_software")){
            // ignore
        }else if (keyword.equals("lighting")){
            String isOn = split[1];
            if (isOn.equals("on")){
                noLight = false;
            }else if (isOn.equals("off")){
                noLight = true;
            }
        }else{
            logger.log(Level.WARNING, "Unsupported pass directive: {0}", keyword);
        }
    }

    private void readPass(Statement statement){
        String name;
        String[] split = statement.getLine().split(" ", 2);
        if (split.length == 1){
            // no name
            name = null;
        }else{
            name = split[1];
        }
        
        for (Statement passStat : statement.getContents()){
            readPassStatement(passStat);
        }
        
        texUnit = 0;
    }

    private void readTechnique(Statement statement){
        String[] split = statement.getLine().split(" ", 2);
        String name;
        if (split.length == 1){
            // no name
            name = null;
        }else{
            name = split[1];
        }
        for (Statement techStat : statement.getContents()){
            readPass(techStat);
        }
    }

    private void readMaterialStatement(Statement statement){
        if (statement.getLine().startsWith("technique")){
            readTechnique(statement);
        }else if (statement.getLine().startsWith("receive_shadows")){
            String isOn = statement.getLine().split(" ")[1];
            if (isOn != null && isOn.equals("true")){
            }
        }
    }

    @SuppressWarnings("empty-statement")
    private void readMaterial(Statement statement){
        for (Statement materialStat : statement.getContents()){
            readMaterialStatement(materialStat);
        }
    }

    private Material compileMaterial(){
        Material mat;
        if (noLight){
           mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        }else{
           mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        }
        if (blend){
            RenderState rs = mat.getAdditionalRenderState();
            rs.setAlphaTest(true);
            rs.setAlphaFallOff(0.01f);
            rs.setBlendMode(RenderState.BlendMode.Alpha);
            if (twoSide)
                rs.setFaceCullMode(RenderState.FaceCullMode.Off);
//            rs.setDepthWrite(false);
            mat.setTransparent(true);
            if (!noLight)
                mat.setBoolean("UseAlpha", true);
        }else{
            if (twoSide){
                RenderState rs = mat.getAdditionalRenderState();
                rs.setFaceCullMode(RenderState.FaceCullMode.Off);
            }
        }

        if (!noLight){
            if (shinines > 0f) {
                mat.setFloat("Shininess", shinines);
            } else {
                mat.setFloat("Shininess", 16f); // set shininess to some value anyway..
            }
            
            if (vcolor)
                mat.setBoolean("UseVertexColor", true);

            if (textures[0] != null)
                mat.setTexture("DiffuseMap", textures[0]);

            mat.setBoolean("UseMaterialColors", true);
            if(diffuse != null){
                mat.setColor("Diffuse",  diffuse);
            }else{
                mat.setColor("Diffuse", ColorRGBA.White);
            }

            if(ambient != null){
                mat.setColor("Ambient",  ambient);
            }else{
                mat.setColor("Ambient", ColorRGBA.DarkGray);
            }

            if(specular != null){
                mat.setColor("Specular", specular);
            }else{
                mat.setColor("Specular", ColorRGBA.Black);
            }
            
            if (emissive != null){
                mat.setColor("GlowColor", emissive);
            }
        }else{
            if (vcolor) {
                mat.setBoolean("VertexColor", true);
            }

            if (textures[0] != null && textures[1] == null){
                if (separateTexCoord){
                    mat.setTexture("LightMap", textures[0]);
                    mat.setBoolean("SeparateTexCoord", true);
                }else{
                    mat.setTexture("ColorMap", textures[0]);
                }
            }else if (textures[1] != null){
                mat.setTexture("ColorMap", textures[0]);
                mat.setTexture("LightMap", textures[1]);
                if (separateTexCoord){
                    mat.setBoolean("SeparateTexCoord", true);
                }
            }
                 
            if(diffuse != null){
                mat.setColor("Color", diffuse);
            }
            
            if (emissive != null){
                mat.setColor("GlowColor", emissive);
            }
        }

        noLight = false;
        Arrays.fill(textures, null);
        diffuse = null;
        specular = null;
        shinines = 0f;
        vcolor = false;
        blend = false;
        texUnit = 0;
        separateTexCoord = false;
        return mat;
    }
    
    private MaterialList load(AssetManager assetManager, AssetKey key, InputStream in) throws IOException{
        folderName = key.getFolder();
        this.assetManager = assetManager;
        
        MaterialList list = null;
        List<Statement> statements = BlockLanguageParser.parse(in);
        
        for (Statement statement : statements){
            if (statement.getLine().startsWith("import")){
                MaterialExtensionSet matExts = null;
                if (key instanceof OgreMaterialKey){
                     matExts = ((OgreMaterialKey)key).getMaterialExtensionSet();
                }

                if (matExts == null){
                    throw new IOException("Must specify MaterialExtensionSet when loading\n"+
                                          "Ogre3D materials with extended materials");
                }

                list = new MaterialExtensionLoader().load(assetManager, matExts, statements);
                break;
            }else if (statement.getLine().startsWith("material")){
                if (list == null){
                    list = new MaterialList();
                }
                String[] split = statement.getLine().split(" ", 2);
                matName = split[1].trim();
                readMaterial(statement);
                Material mat = compileMaterial();
                list.put(matName, mat);
            }
        }
        
        return list;
    }

    public Object load(AssetInfo info) throws IOException {
        InputStream in = null;
        try {
            in = info.openStream();
            return load(info.getManager(), info.getKey(), in);
        } finally {
            if (in != null){
                in.close();
            }
        }
    }

}
