/* <copyright>
This file contains proprietary software owned by Motorola Mobility, Inc.<br/>
No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.<br/>
(c) Copyright 2011 Motorola Mobility, Inc.  All Rights Reserved.
</copyright> */

/*	
this API should be familiar to anyone who has worked with 
HLSL effect files.
*/

/*
 *	A map of types to uniform 'binding' functions
 */
bindMap={};
bindMap['int']		= function(ctx, a,b) { ctx.uniform1iv(a,b); };
bindMap['float']	= function(ctx, a,b) { ctx.uniform1fv(a,b); };
bindMap['vec2']		= function(ctx, a,b) { ctx.uniform2fv(a,b); };
bindMap['vec3']		= function(ctx, a,b) { ctx.uniform3fv(a,b); };
bindMap['vec4']		= function(ctx, a,b) { ctx.uniform4fv(a,b); };
bindMap['mat3']		= function(ctx, a,b) { ctx.uniformMatrix3fv(a,false,b); };
bindMap['mat4']		= function(ctx, a,b) 
{ 
	ctx.uniformMatrix4fv(a,false,b); 
	g_Engine.getContext().debug.mat4CallCount++;
};

bindMap['tex2d']	= function(ctx, a,b) 
{
	ctx.activeTexture(ctx.TEXTURE0+b[0]);
	ctx.bindTexture(ctx.TEXTURE_2D, b[1]);
	ctx.uniform1iv(a,[b[0]]);
};

bindMap['texCube']=function(ctx, a,b) 
{
	ctx.activeTexture(ctx.TEXTURE0+b[0]);
	ctx.bindTexture(ctx.TEXTURE_CUBE_MAP, b[1]);
	ctx.uniform1iv(a,[b[0]]);
};

lightDataMap = 
[
	function(ctx, loc, lightNode) { ctx.uniform3fv(loc, lightNode.position); },
	function(ctx, loc, lightNode) { ctx.uniform4fv(loc, lightNode.lightDiffuse); },
	function(ctx, loc, lightNode) { ctx.uniform4fv(loc, lightNode.lightAmbient); },
	function(ctx, loc, lightNode) { ctx.uniform4fv(loc, lightNode.lightSpecular); }
];

paramTypeNameMapping = null;

jshader = function(addr) {
    this.name = addr;
    this.def = null;
    this.technique = {};
    this.params = {};
    this.compiledShaders = {};
    this.resetRS = false;
    this.currentPass = 0;
    this.type_jshader = {};
    this.global = {};
    this.renderer = g_Engine.getContext().renderer;
    this.ctx = this.renderer.ctx;

    // load jshader definition at addr (if provided)
    if (addr != undefined && addr != null) {
        // a synchronous ajax request
        request = new XMLHttpRequest();
        request.open("GET", addr, false);
        request.send(null);
        this.def = JSON.parse(request.responseText);
    }

    if (!paramTypeNameMapping) {
        var gl = this.ctx;
        paramTypeNameMapping = {};
        paramTypeNameMapping[gl.BOOL] = "bool";
        paramTypeNameMapping[gl.INT] = "int";
        paramTypeNameMapping[gl.FLOAT] = "float";
        paramTypeNameMapping[gl.FLOAT_VEC2] = "vec2";
        paramTypeNameMapping[gl.FLOAT_VEC3] = "vec3";
        paramTypeNameMapping[gl.FLOAT_VEC4] = "vec4";
        paramTypeNameMapping[gl.INT_VEC2] = "vec2";
        paramTypeNameMapping[gl.INT_VEC3] = "vec3";
        paramTypeNameMapping[gl.INT_VEC4] = "vec4";
        paramTypeNameMapping[gl.BOOL_VEC2] = "vec2";
        paramTypeNameMapping[gl.BOOL_VEC3] = "vec3";
        paramTypeNameMapping[gl.BOOL_VEC4] = "vec4";
        paramTypeNameMapping[gl.FLOAT_MAT2] = "mat2";
        paramTypeNameMapping[gl.FLOAT_MAT3] = "mat3";
        paramTypeNameMapping[gl.FLOAT_MAT4] = "mat4";
        paramTypeNameMapping[gl.SAMPLER_2D] = "tex2d";
        paramTypeNameMapping[gl.SAMPLER_CUBE] = "texCube";
    }

    /*
    *	private helper functions
    */
    this.bindParameters = function(pass) {
        var params = pass.defParamsList; // global parameters to start with
        var lightParams = pass.lightParams;
        var lightContext = pass.lightContext;
        var length = params.length;
        var idx = 0;
        var texArg = new Array(2)

        // global parameters
        var texUnit = 0;
        for (idx = 0; idx < length; ++idx) {
            if (params[idx].type == 'tex2d' || params[idx].type == 'texCube') {
                texArg[0] = texUnit++;
                texArg[1] = params[idx].data[0];
                bindMap[params[idx].type](this.ctx, params[idx].loc, texArg);
            }
            else {
                bindMap[params[idx].type](this.ctx, params[idx].loc, rdgeGlobalParameters[params[idx].name].data);
            }
        }

        // light settings defined by the material
        var len = rdgeConstants.MAX_MATERIAL_LIGHTS;
        for (var i = 0; i < len; ++i) {
            // if there is a context for a light check to see if we have a binding to the light
            if (lightContext[i] != null) {
                // see if we have parameters to bind to this light
                if (lightParams[i]) {
                    // something is here lets bind it
                    var numParams = lightParams[i].length;
                    for (var lp = 0; lp < numParams; ++lp) {
                        // bind the parameters using the lightDataMap function lookup, dataIndex is the key
                        lightDataMap[lightParams[i][lp].dataIndex](this.ctx, lightParams[i][lp].loc, lightContext[i]);
                    }
                }
            }
        }

        // let locally defined uniforms stomp globally defined uniforms
        texUnit = this.renderer.usedTextureUnits; // start adding texture after the default textures
        params = pass.paramsList;
        length = params.length;
        for (idx = 0; idx < length; ++idx) {
            if (params[idx].type == 'tex2d' || params[idx].type == 'texCube') {
                texArg[0] = texUnit++;
                texArg[1] = params[idx].data[0];
                bindMap[params[idx].type](this.ctx, params[idx].loc, texArg);
            }
            else {
                bindMap[params[idx].type](this.ctx, params[idx].loc, params[idx].data);
            }
        }
    };

    /*
    *	helper function for setting up a texture
    */
    createJShaderTexture = function(ctx, param) {
        var texHandle = null;
        if (typeof param.data == "string") {
            texHandle = ctx.canvas.renderer.getTextureByName(param.data, param.wrap, param.repeat, param.mips);
        }
        else {
            texHandle = ctx.canvas.renderer.getTextureByName(param.data.lookUpName, param.wrap, param.repeat, param.mips);
        }

        return [texHandle];
    }

    paramType = function(ctx, name, def, program, technique) {
        var texUnit = 0;

        // Get the uniform location and store it
        this.loc = ctx.getUniformLocation(program, name);

        // if the parameter does not exist in the shader cull it from the pass
        if (this.loc == null) {
            window.console.log("ctx:" + ctx.canvas.rdgeid + ", technique: " + technique + ", uniform: " + name + " was not found, jshader param will have no affect");
            //return;
        }

        var param = def[name];
        this.type = param.type;

        // if data was not provided then create default data
        if (param.data == undefined) {
            switch (param.type) {
                case "vec4": this.data = vec4.zero(); break;
                case "vec3": this.data = vec3.zero(); break;
                case "vec2": this.data = vec2.zero(); break;
                case "mat4": this.data = mat4.zero(); break;
                case "mat3": this.data = new Array(9); break;
                case "mat2": this.data = [0, 0, 0, 0]; break;
                case "float": this.data = [0]; break;
                case "int": this.data = [0]; break;
                case "tex2d": this.data = [ctx.canvas.renderer.getTextureByName(g_Engine._assetPath+"images/white.png")]; break;
                case "texCube": this.data = [ctx.canvas.renderer.getTextureByName(g_Engine._assetPath+"images/white.png")]; break;
            }
        }
        else {
            if (param.type == 'tex2d' || param.type == 'texCube') {
                this.data = createJShaderTexture(ctx, param);
            }
            else {
                this.data = param.data.slice();
            }
        }

        this.get = function() {
            return this.data.slice();
        }

        this.set = function(v) {
            if (this.type == 'tex2d' || this.type == 'texCube') {
                if (typeof v == "string") {
                    v = ctx.canvas.renderer.getTextureByName(v);
                }

                this.data[0] = v;
            }
            else {
                var len = this.data.length;
                for (var i = 0; i < len; ++i)
                    this.data[i] = v[i];
            }
        }
    }

    globalParam = function(ctx, name, param, program) {
        this.type = param.type;

        this.data = param.data;

        // Get the uniform location and store it
        this.loc = ctx.getUniformLocation(program, name);

        // if data was not provided then create default data
        if (!this.data) {
            switch (param.type) {
                case "vec4": this.data = vec4.zero(); break;
                case "vec3": this.data = vec3.zero(); break;
                case "vec2": this.data = vec2.zero(); break;
                case "mat4": this.data = mat4.zero(); break;
                case "mat3": this.data = new Array(9); break;
                case "mat2": this.data = [0, 0, 0, 0]; break;
                case "float": this.data = [0]; break;
                case "int": this.data = [0]; break;
                case "tex2d": this.data = [ctx.canvas.renderer.getTextureByName(g_Engine._assetPath+"images/white.png")]; break;
                case "texCube": this.data = [ctx.canvas.renderer.getTextureByName(g_Engine._assetPath+"images/white.png")]; break;
            }
        }
        else {
            if (param.type == 'tex2d' || param.type == 'texCube') {
                this.data = createJShaderTexture(ctx, param);
            }
            else {
                this.data = param.data.slice();
            }
        }

        this.get = function() {
            return this.data.slice();
        }
        this.set = function(v) {
            if (this.type == 'tex2d' || this.type == 'texCube') {
                if (typeof v == "string") {
                    v = ctx.canvas.renderer.getTextureByName(v);
                }

                this.data[0] = v;
            }
            else {
                var len = this.data.length;
                for (var i = 0; i < len; ++i)
                    this.data[i] = v[i];
            }

        }
    }


    this.init = function() {
        var techniques = this.def.techniques;
        var defaultTech = null;
        for (t in techniques) {
            defaultTech = t;
            var curTechnique = techniques[t];
            this[t] =
			{
			    'passes': []
			};
            var numPasses = curTechnique.length;
            var i = 0;
            while (i < numPasses) {
                var program = this.buildProgram(curTechnique[i]);
                this.ctx.useProgram(program);

                // automatically create a parameter def for every active attribute in the shader.
                var numAttribs = this.ctx.getProgramParameter(program, this.ctx.ACTIVE_ATTRIBUTES);
                for (j = 0; j < numAttribs; ++j) {
                    var attribInfo = this.ctx.getActiveAttrib(program, j);
                    curTechnique[i].attributes[attribInfo.name] = { 'type': paramTypeNameMapping[attribInfo.type] };
                }
                // automatically create a parameter def for every active uniform in the shader.
                var numUniforms = this.ctx.getProgramParameter(program, this.ctx.ACTIVE_UNIFORMS);
                for (j = 0; j < numUniforms; ++j) {
                    var uniformInfo = this.ctx.getActiveUniform(program, j);
                    if (!rdgeGlobalParameters[uniformInfo.name]) {
                        curTechnique[i].params[uniformInfo.name] = { 'type': paramTypeNameMapping[uniformInfo.type] };
                    }
                }

                program.ctxId = this.ctx.canvas.rdgeid;
                if (!program) {
                    this.renderer.console.log("Build errors found in technique: " + t);
                    this.def[t] = null; // remove bad technique
                    break;
                } else {
                    this[t].passes.push({ "program": program, "params": {}, "defParams": {}, "states": curTechnique[i].states, "attributes": curTechnique[i].attribPairs });
                }

                // init default parameters
                for (var p in rdgeGlobalParameters) {
                    var gp = new globalParam(this.ctx, p, rdgeGlobalParameters[p], program);

                    if (gp.loc != null) {
                        gp.loc.ctxID = this.ctx.canvas.rdgeid;
                        this[t].passes[i].defParams[p] = gp;
                        this.global[p] = gp;
                    }
                }

                // attach light parameters and container to light context
                this[t].passes[i].lightParams = [null, null, null, null];
                this[t].passes[i].lightContext = [null, null, null, null];

                // attach a parameter list that will be used to optimize binding attributes
                if (!this[t].passes[i].paramsList)
                    this[t].passes[i].paramsList = [];

                // locate individual light parameters to bind with local context
                var totalLights = rdgeConstants.MAX_MATERIAL_LIGHTS;
                for (var lightIdx = 0; lightIdx < totalLights; ++lightIdx) {

                    // clear parameter
                    this[t].passes[i].lightParams[lightIdx] = null;

                    // 0 = pos, 1 = diff, 2 = amb, 3 = spec
                    // this is order assumed for light parameters

                    // the parameter index key - lets us know which piece of data we are getting/setting
                    var lightDataIndex = 0;

                    for (var lp in g_Engine.lightManager.lightUniforms[lightIdx]) {
                        loc = this.ctx.getUniformLocation(program, lp);

                        // if item found enable this light param and set parameters to bind and lookup data
                        if (loc != null) {
                            if (!this[t].passes[i].lightParams[lightIdx])
                                this[t].passes[i].lightParams[lightIdx] = [];

                            this[t].passes[i].lightParams[lightIdx].push({ 'loc': loc, 'name': lp, 'dataIndex': lightDataIndex });
                        }

                        lightDataIndex++;
                    }
                }



                // init user defined parameters
                for (var p in curTechnique[i].params) {
                    if (typeof curTechnique[i].params[p] == 'string') {
                        continue;
                    }

                    var newParam = new paramType(this.ctx, p, curTechnique[i].params, program, t);

                    // if(newParam.loc != null)
                    // {
                    this[t].passes[i].params[p] = newParam;
                    this[t][p] = newParam;
                    // }
                }

                // link up aliases
                for (var p in curTechnique[i].params) {
                    if (typeof curTechnique[i].params[p] == 'string') {
                        // this just redirects to an already existing parameter.                     
                        this[t][p] = this[t].passes[i].params[p];
                    }
                }

                i++;
            }
        }

        // create linear lists of parameters - optimization
        for (t in techniques) {
            var numPasses = this[t].passes.length;

            for (var i = 0; i < numPasses; ++i) {

                this[t].passes[i].defParamsList = [];

                for (var p in this[t].passes[i].params) {
                    var param = this[t].passes[i].params[p];
                    param.name = p;
                    this[t].passes[i].paramsList.push(param);
                }

                for (var p in this[t].passes[i].defParams) {
                    var param = this[t].passes[i].defParams[p];
                    param.name = p;
                    this[t].passes[i].defParamsList.push(param);
                }
            }
        }

        this.setTechnique(defaultTech);
    }

    /*
    *	Init a local parameter at any time during the life of the jshader.
    *  This will add the parameter to the list of parameters to be bound  
    *  before rendering
    */
    this.initLocalParameter = function(name, param) {
        var techniques = this.def.techniques;
        for (t in techniques) {
            var curTechnique = techniques[t];
            var numPasses = curTechnique.length;
            var i = 0;
            while (i < numPasses) {
                var newParam = new paramType(this.ctx, name, param, curTechnique[i].program, t);
                if (newParam) {
                    curTechnique[i][name] = newParam;

                    // this params list is created here because a parameter could be added before the jshader is initialized
                    if (!curTechnique[i].paramsList)
                        curTechnique[i].paramsList = [];

                    curTechnique[i].paramsList.push(newParam);
                }

                i++;
            }
        }
    }

    this.buildShader = function(shaderType, shaderStr) {
        // pre-pend preprocessor settings
        var preProcessor = "#define PC\n"
        preProcessor += shaderStr;
        shaderStr = preProcessor;

        // Create the shader object
        var shader = this.ctx.createShader(shaderType);
        if (shader == null) {
            this.renderer.console.log("*** Error: unable to create shader '" + shaderType + "'");
            return null;
        }

        // Load the shader source
        this.ctx.shaderSource(shader, shaderStr);

        // Compile the shader
        this.ctx.compileShader(shader);

        // Check the compile status
        var compiled = this.ctx.getShaderParameter(shader, this.ctx.COMPILE_STATUS);
        if (!compiled) {
            // compile failed, report error.
            var error = this.ctx.getShaderInfoLog(shader);
            window.console.error("*** Error compiling shader '" + shaderType + "':" + error);
            this.ctx.deleteShader(shader);
            return null;
        }

        return shader;
    }

    this.buildProgram = function(t) {
        window.console.log("building shader pair: <" + t.vshader + ", " + t.fshader + ">");
        var vShaderDef = this.def.shaders[t.vshader];
        var fShaderDef = this.def.shaders[t.fshader];

        this.ctx.useProgram(null);

        var vertexShader = null;
        //		if (this.compiledShaders[t.vshader] != undefined) {
        //			vertexShader = this.compiledShaders[t.vshader];
        //		} else 
        {
            var source = null;

            if (vShaderDef.indexOf('{') != -1) {
                source = vShaderDef;
            } else {
                var vshaderRequest = new XMLHttpRequest();
                var urlVertShader = vShaderDef;
                vshaderRequest.open("GET", urlVertShader, false);
                vshaderRequest.send(null);
                source = vshaderRequest.responseText;
            }

            vertexShader = this.buildShader(this.ctx.VERTEX_SHADER, source);

        }

        var fragmentShader = null;
        //		if (this.compiledShaders[t.fshader] != undefined) 
        //		{
        //			fragmentShader = this.compiledShaders[t.vshader];
        //		} else 
        {
            var source = null;
            if (vShaderDef.indexOf('{') != -1) {
                source = fShaderDef;
            } else {
                var vshaderRequest = new XMLHttpRequest();
                var urlFragShader = fShaderDef;
                vshaderRequest.open("GET", urlFragShader, false);
                vshaderRequest.send(null);
                source = vshaderRequest.responseText;
            }

            fragmentShader = this.buildShader(this.ctx.FRAGMENT_SHADER, source);
        }

        if (!vertexShader || !fragmentShader) {
            return null;
        }

        this.compiledShaders[t.vshader] = vertexShader;
        this.compiledShaders[t.fshader] = fragmentShader;

        // Create the program object
        var program = this.ctx.createProgram();
        if (!program) {
            return null;
        }

        // Attach our two shaders to the program
        this.ctx.attachShader(program, vertexShader);
        this.ctx.attachShader(program, fragmentShader);

        // Bind attributes
        var idx = 0;
        t.attribPairs = [];
        for (var i in t.attributes) {
            t.attribPairs.push({ 'loc': idx, 'name': i });
            this.ctx.bindAttribLocation(program, idx++, i);
        }

        // Link the program
        this.ctx.linkProgram(program);

        // Check the link status
        var linked = this.ctx.getProgramParameter(program, this.ctx.LINK_STATUS);
        if (!linked) {
            // failed to link
            var error = this.ctx.getProgramInfoLog(program);

            window.console.log("Error in program linking:" + error);

            this.ctx.deleteProgram(program);
            this.ctx.deleteProgram(fragmentShader);
            this.ctx.deleteProgram(vertexShader);

            return null;
        }

        return program;
    }

    /*
    *	Set the light nodes used by this jshader
    * array item 0 corresponds to light 0, item 1 tp light 1 and so on
    * place null for lights that are not there
    */
    this.setLightContext = function(lightRefArray) {
        for (t in this.technique) {
            var len = this.technique.passes.length;
            for (var i = 0; i < len; ++i) {
                this.technique.passes[i].lightContext = lightRefArray.slice();
            }
        }
    }

    /*
    *	Called by the system to add material textures settings to the jshader
    */
    this.setTextureContext = function(textureList) {
        var passCount = this.technique.passes.length;
        var param = null;

        for (var t = 0, texCount = textureList.length; t < texCount; ++t) {
            for (var i = 0; i < passCount; ++i) {
                var param = textureList[t];

                // set the rdge global parameters if the texture is in the list
                if (this.technique.passes[i].defParams[param.name])
                    this.technique.passes[i].defParams[param.name].set(param.handle);

                // and set the local parameters if the texture is in the list
                if (this.technique.passes[i].params[param.name])
                    this.technique.passes[i].params[param.name].set(param.data[0]);
            }
        }
    }

    this.setTechnique = function(name) {
        if (this[name] != undefined) {
            this.technique = this[name];
            return true;
        }

        this.ctx.console.log("Failed to set technique:" + name);
        return false;
    }

    this.beginRenderState = function(i) {
        var states = this.technique.passes[i].states;
        if (states == undefined) {
            return;
        }

        // depth enabled by default.
        var depthEnable = states.depthEnable != undefined ? states.depthEnable : true;
        if (!depthEnable) {
            this.ctx.disable(this.ctx.DEPTH_TEST);
            var depthFunc = states.depthFunc != undefined ? states.depthFunc : "LESS";
            this.ctx.depthFunc(this.ctx[states.depthFunc]);
            this.ctx.depthMask(true);
        }
        else {

            if (states.depthFunc) {
                this.ctx.depthFunc(this.ctx[states.depthFunc]);
            }

            if (states.offset) {
                this.ctx.enable(this.ctx.POLYGON_OFFSET_FILL);
                this.ctx.polygonOffset(states.offset[0], states.offset[1]);
            }

            // depth write
            if (states.depthWrite) {
                this.ctx.depthMask(states.depthWrite);
            }

            if (states.depthRangeMin) {
                this.ctx.depthRange(states.depthRangeMin);
            }

            if (states.depthRangeMax) {
                this.ctx.depthRange(states.depthRangeMax);
            }
        }

        // blend enabled by default.
        var blendEnabled = states.blendEnable != undefined ? states.blendEnable : false;
        if (blendEnabled) {
            var srcBlend = states.srcBlend != undefined ? states.srcBlend : "ONE"; // default src blend
            var dstBlend = states.dstBlend != undefined ? states.dstBlend : "ZERO"; // default dst blend
            this.ctx.enable(this.ctx.BLEND);
            this.ctx.blendFunc(this.ctx[srcBlend], this.ctx[dstBlend]);
        }

        if (states.culling) {
            if (states.culling)
                this.ctx.enable(this.ctx.CULL_FACE);
            else
                this.ctx.disable(this.ctx.CULL_FACE);

        }

        if (states.cullFace) {
            this.ctx.cullFace(this.ctx[states.cullFace]);
        }

        if (states.pointsprite) {
            if (states.pointsprite === true)
                this.renderer.enablePointSprites();
            else
                this.renderer.disablePointSprites();
        }

        this.resetRS = this.technique.passes[i].states.reset == undefined || this.technique.passes[i].states.reset == true;
    }

    this.endRenderState = function() {
        // restore render states to some default state.
        var ctx = this.ctx;
        if (this.resetRS) {
            ctx.enable(this.ctx.DEPTH_TEST);
            ctx.disable(this.ctx.BLEND);
            ctx.depthFunc(this.ctx.LESS);
            ctx.disable(this.ctx.POLYGON_OFFSET_FILL);
            ctx.disable(this.ctx.CULL_FACE);
            //            this.renderer.disablePointSprites();
            //ctx.enable(ctx.CULL_FACE);
            //ctx.cullFace(ctx.BACK);
        }
    }

    this.begin = function() {
        this.currentPass = null;
        if (this.def == null || this.technique == null) {
            return 0;
        }
        return this.technique.passes.length;
    }

    this.beginPass = function(i) {
        this.currentPass = this.technique.passes[i];
        this.ctx.useProgram(this.currentPass.program);
        this.bindParameters(this.currentPass);
        this.beginRenderState(i);
        return this.currentPass;
    }

    this.endPass = function() {
        this.endRenderState();
        this.ctx.useProgram(null);
    }

    this.end = function() {
    }

    this.exportShader = function() {

        for (t in this.def.techniques) {
            var numPasses = this[t].passes.length;

            for (var i = 0; i < numPasses; ++i) {
                this[t].passes[i].paramsList = [];
                this[t].passes[i].defParamsList = [];

                for (var p in this[t].passes[i].params) {
                    var tech = this.def.techniques[t][i];
                    if (tech && this[t].passes[i].params[p].type != "tex2d" && this[t].passes[i].params[p] != "texCube")
                        tech.params[p].data = this[t].passes[i].params[p].data;
                }
            }
        }

        return JSON.stringify(this.def);

    }
}