text: var DrawableClosure = require("../../renderer/scene/drawableclosure.js");
  var GLMesh = require("../base/mesh.js");
  var XflowUtils = require("./utils.js");
  var EVENT_TYPE = require("../../renderer/scene/constants.js").EVENT_TYPE;
  var MaterialEvents = require("../materials/events.js");
  var XC = require("../../../xflow/interface/constants.js");
  var ComputeRequest = require("../../../xflow/interface/request.js").ComputeRequest;
  var assert = require("assert");
  var GL = require("../constants.js");
  
  var CHANGE_STATE = {
      NOTHING_CHANGED: 0,
      STRUCTURE_CHANGED: 1,
      TYPE_DATA_CHANGED: 2,
      VS_DATA_CHANGED: 4,
      TYPE_CHANGED: 2 + 1,
      VS_CHANGED: 4 + 1,
      SHADER_CHANGED: 32
  };
  var SHADER_CLOSURE_NEEDS_UPDATE = CHANGE_STATE.STRUCTURE_CHANGED | CHANGE_STATE.SHADER_CHANGED;
  
  var READY_STATE = DrawableClosure.READY_STATE;
  
  
  var MESH_PARAMETERS = {
      attributeData: {"position": XC.DATA_TYPE.FLOAT3},
      typeData: {
          "index": XC.DATA_TYPE.INT, "solid": XC.DATA_TYPE.BOOL, "vertexCount": XC.DATA_TYPE.INT, "type": XC.DATA_TYPE.STRING
      }, bboxFix: {
          "boundingBox": XC.DATA_TYPE.FLOAT3
      }, bboxCompute: {
          "position": XC.DATA_TYPE.FLOAT3
      }
  };
  
  
  /**
   * Class that synchronizes data changes from Xflow with a GLMesh instance
   *
   * @param {GLContext} context
   * @param {object} meshConfig
   * @param {object} opt
   * @extends {DrawableClosure}
   * @constructor
   */
  var XflowMesh = function (context, meshConfig, opt) {
      DrawableClosure.call(this, context, DrawableClosure.TYPES.MESH);
      assert(meshConfig.data);
      assert(meshConfig.type);
      opt = opt || {};
  
      this.mesh = new GLMesh(context);
  
      /**
       * Data Node of the renderObject
       * @type {Xflow.DataNode}
       */
      this.dataNode = meshConfig.data;
  
      this.primitiveType = meshConfig.type;
  
      /**
       * Shader Composer that will provide ShaderClosure and Program
       * @type {AbstractShaderComposer}
       */
      this.shaderComposer = null;
  
      /**
       * Shader Closure used by this mesh
       * @type {AbstractShaderClosure}
       */
      this.shaderClosure = null;
  
      /**
       * Attributes required to create the GLMesh
       * @type {ComputeRequest}
       */
      this.typeRequest = null;
  
      /**
       * Are all attributes required by drawable available?
       * @type {boolean}
       */
      this.typeDataValid = true;
  
      /**
       * Attributes and uniforms values for the shader
       * @type {Xflow.Request}
       */
      this.objectShaderRequest = null;
  
      /**
       * Bitfield that records the changes reported by Xflow
       * @private
       * @type {number}
       */
      this.changeState = CHANGE_STATE.STRUCTURE_CHANGED;
  
      /**
       * Callback if bounding box has changed. Gets only called if
       * this.boundingBoxRequired is true.
       * @type {*|function(Float32Array)}
       */
      this.boundingBoxChanged = opt.boundingBoxChanged || function () {
      };
  
      this.initialize();
  };
  
  XML3D.createClass(XflowMesh, DrawableClosure, {
      initialize: function () {
          this.typeDataChanged(this.typeRequest, XC.RESULT_STATE.CHANGED_STRUCTURE);
          this.shaderChanged();
      },
  
      setShaderComposer: function (shaderComposer) {
          if (!this.bindedShaderChanged) this.bindedShaderChanged = this.shaderChanged.bind(this);
  
          if (this.shaderComposer)
              this.shaderComposer.removeListener(MaterialEvents.MATERIAL_STRUCTURE_CHANGED, this.bindedShaderChanged);
  
          this.shaderComposer = shaderComposer;
          if (this.shaderComposer)
              this.shaderComposer.on(MaterialEvents.MATERIAL_STRUCTURE_CHANGED, this.bindedShaderChanged);
  
          this.changeState |= CHANGE_STATE.SHADER_CHANGED;
      },
  
      update: function (scene) {
          if (this.changeState === CHANGE_STATE.NOTHING_CHANGED) {
              return;
          }
          XML3D.debug.logDebug("Update mesh closure", this.changeState);
  
          var oldValid = !!this.shaderClosure && this.typeDataValid, someError = null, typeDataResolved = false;
  
          try {
              if (this.changeState & SHADER_CLOSURE_NEEDS_UPDATE) {
                  this.mesh.clear();
              }
  
              if (this.changeState & CHANGE_STATE.TYPE_CHANGED) {
                  this.updateTypeData();
              }
  
              typeDataResolved = true;
  
              if (this.changeState & (SHADER_CLOSURE_NEEDS_UPDATE | CHANGE_STATE.TYPE_CHANGED)) {
                  this.updateIndexBuffer();
              }
  
              if (this.changeState & SHADER_CLOSURE_NEEDS_UPDATE) {
                  this.updateObjectShaderRequest();
                  this.updateShaderClosure(scene);
                  this.updateObjectShaderData();
              } else if (this.changeState & CHANGE_STATE.VS_CHANGED) {
                  this.updateObjectShaderData();
              }
  
              if (this.dataNode.isSubtreeLoading()) {
                  this.shaderClosure = null;
                  this.typeDataValid = false;
              }
          } catch (e) {
              someError = e;
              if (!typeDataResolved)
                  this.typeDataValid = false; else
                  this.shaderClosure = null;
          }
  
  
          var newValid = !!this.shaderClosure && this.typeDataValid;
  
          if (oldValid != newValid) {
              this.emit(EVENT_TYPE.DRAWABLE_STATE_CHANGED,
                  newValid ? READY_STATE.COMPLETE : READY_STATE.INCOMPLETE,
                  oldValid ? READY_STATE.COMPLETE : READY_STATE.INCOMPLETE
              );
          }
          this.changeState = CHANGE_STATE.NOTHING_CHANGED;
  
          if (someError) throw someError;
      },
  
      calculateBoundingBox: (function () {
          var c_empty = new XML3D.Box();
  
          return function () {
              // compute bounding box from positions and indices, if present
              var dataResult = this.typeRequest.getResult();
              var boundingBoxEntry = dataResult.getOutputData("boundingBox");
              if (boundingBoxEntry) {
                  this.boundingBoxChanged(XflowUtils.calculateBoundingBox(boundingBoxEntry.getValue(), null));
                  return;
              }
              var positionEntry = dataResult.getOutputData("position");
              if (!positionEntry) {
                  this.boundingBoxChanged(c_empty);
                  return;
              }
              var indexEntry = dataResult.getOutputData("index");
              this.boundingBoxChanged(XflowUtils.calculateBoundingBox(positionEntry.getValue(), indexEntry ? indexEntry.getValue() : null));
          }
      }()), /**
       *
       * @param {ComputeRequest} request
       * @param {XC.RESULT_STATE} state
       */
      typeDataChanged: function (request, state) {
          this.changeState |= state == XC.RESULT_STATE.CHANGED_STRUCTURE ? CHANGE_STATE.STRUCTURE_CHANGED : CHANGE_STATE.TYPE_DATA_CHANGED;
          this.emit(EVENT_TYPE.SCENE_SHAPE_CHANGED);
          this.context.requestRedraw("Mesh Type Data Change");
          XML3D.debug.logDebug("XflowMesh: Type data changed", request, state, this.changeState);
      },
  
      getMesh: function () {
          return this.mesh;
      },
  
      updateObjectShaderRequest: function () {
          if (this.objectShaderRequest) this.objectShaderRequest.clear();
          this.objectShaderRequest = null;
          if (this.dataNode.isSubtreeLoading())
              return;
  
          this.objectShaderRequest = this.shaderComposer.createObjectDataRequest(this.dataNode, this.shaderInputDataChanged.bind(this));
      },
  
      updateShaderClosure: function (scene) {
          this.shaderClosure = null;
          if (!this.dataNode.isSubtreeLoading() && !this.dataNode.getOutputChannelInfo("position")) {
              throw new Error("Mesh does not have 'position' attribute.");
              //XML3D.debug.logError("Mesh does not have 'position' attribute.", this.mesh, this.getMeshType());
          } else if (!this.dataNode.isSubtreeLoading()) {
              var objectShaderResult = this.objectShaderRequest.getResult();
              if (!objectShaderResult.loading)
                  this.shaderClosure = this.shaderComposer.getShaderClosure(scene, this.objectShaderRequest);
          }
      },
  
      updateIndexBuffer: function () {
          // Add Index buffer, if available
          var dataResult = this.typeRequest.getResult();
          var entry = dataResult.getOutputData("index");
          if (entry && entry.getValue())
              this.handleBuffer("index", entry, true);
      },
  
      updateObjectShaderData: function () {
          if (!this.shaderClosure) {
              return; // if only the data has changed, it can't get valid after update
          }
  
          if (!this.bindedHandleBuffer) this.bindedHandleBuffer = this.handleBuffer.bind(this);
          if (!this.bindedHandleUniform) this.bindedHandleUniform = this.handleUniform.bind(this);
  
          this.shaderComposer.distributeObjectShaderData(this.objectShaderRequest, this.bindedHandleBuffer, this.bindedHandleUniform);
  
          if (!this.mesh.isReadyToRender()) {
              throw new Error("Mesh has empty vertex attributes.");
          }
      },
  
      updateTypeData: function () {
          if (!this.typeDataValid && !(this.changeState & CHANGE_STATE.STRUCTURE_CHANGED)) {
              return; // only if structure has changed, it can't get valid after update
          }
          // Be pessimistic. We set this to true if mesh passes all checks;
          this.typeDataValid = false;
  
          this.updateTypeRequest();
  
          this.calculateBoundingBox();
  
          var dataResult = this.typeRequest.getResult();
  
          var entry = dataResult.getOutputData("vertexCount");
          this.mesh.setVertexCount(entry && entry.getValue() ? entry.getValue() : null);
  
          var primitiveType = this.primitiveType;
          if(primitiveType == "derived") {
              entry = dataResult.getOutputData("type");
              primitiveType =  entry ? entry.getValue()[0] : null;
          }
  
          var glType = getGLTypeFromString(primitiveType);
          if(glType == undefined) {
              if (this.primitiveType == "derived") {
                  if (this.dataNode.isSubtreeLoading()) {
                      return; // There might be a type coming
                  }
                  if (primitiveType == null) {
                      throw new Error("Could not derive primitive type");
                  }
              }
              throw new Error("Unknown primitive type: " + primitiveType);
          }
          this.mesh.setPrimitiveType(glType);
          this.typeDataValid = true;
      },
  
      /**
       * @param {string} name
       * @param {Xflow.BufferEntry} xflowDataEntry
       * @param {boolean=} isIndex
       */
      handleBuffer: function (name, xflowDataEntry, isIndex) {
          isIndex = isIndex || false;
          var mesh = this.mesh;
  
          if (name == "position" && !xflowDataEntry) {
              throw new Error("'position' attribute of mesh is empty.");
          }
  
          if (!xflowDataEntry) {
              this.mesh.removeBuffer(name);
              return;
          }
  
          if (xflowDataEntry.type == XC.DATA_TYPE.TEXTURE) {
              XML3D.debug.logError("Texture as mesh parameter is not yet supported");
              return;
          }
  
          var buffer = XflowUtils.getGLBufferFromXflowDataEntry(xflowDataEntry, this.context, name == "index");
          if (isIndex) {
              this.updateIndexRange(xflowDataEntry);
          } else {
              this.mesh.checkBufferCompatible(name, xflowDataEntry);
          }
          // In every case, set the buffer, because other meshes might have already
          // performed one or more of the tasks above
          mesh.setBuffer(name, buffer);
      },
  
      updateIndexRange: function (xflowDataEntry) {
          var webglData = this.context.getXflowEntryWebGlData(xflowDataEntry);
          this.mesh.setIndexRange(webglData.minIndex, webglData.maxIndex);
      },
  
      handleUniform: function (name, xflowDataEntry) {
          var value = XflowUtils.getGLUniformValueFromXflowDataEntry(xflowDataEntry, this.context);
          if (name === 'opacity') {
              this.emit(EVENT_TYPE.OPACITY_STATE_CHANGED, this.mesh.getUniformOverride(name), value);
          }
          this.mesh.setUniformOverride(name, value);
      },
  
      updateTypeRequest: function () {
          var requestNames = this.getTypeRequestNames(MESH_PARAMETERS);
  
          if (!this.typeRequest || this.typeRequest.filter != requestNames) {
              if (this.typeRequest) this.typeRequest.clear();
              this.typeRequest = new ComputeRequest(this.dataNode, requestNames, this.typeDataChanged.bind(this));
          }
      },
  
      getTypeRequestNames: function (meshConfig) {
          var requestNames = [];
          requestNames.push.apply(requestNames, Object.keys(meshConfig.typeData));
          // We always request fixed bounding box values: that way we can react, when those values get available
          requestNames.push.apply(requestNames, Object.keys(meshConfig.bboxFix));
          var computeBBox = !this.checkXflowTypes(this.dataNode, meshConfig.bboxFix);
  
          if (computeBBox) {
              if (!this.checkXflowTypes(this.dataNode, meshConfig.bboxCompute)) {
                  this.typeDataValid = false;
              }
              requestNames.push.apply(requestNames, Object.keys(meshConfig.bboxCompute));
          }
          return requestNames;
      },
  
      checkXflowTypes: function (dataNode, requirements) {
          for (var name in requirements) {
              var info = dataNode.getOutputChannelInfo(name);
              if (!info) return false;
              if (info.type != requirements[name])
                  return false;
          }
          return true;
      },
  
      /**
       * @param {ComputeRequest} request
       * @param {XC.RESULT_STATE} state
       */
      shaderInputDataChanged: function (request, state) {
          this.changeState |= state != XC.RESULT_STATE.CHANGED_DATA_VALUE ? CHANGE_STATE.STRUCTURE_CHANGED : CHANGE_STATE.VS_DATA_CHANGED;
          // TODO: We don't know if the change of data only influences the surface shading or the actual mesh shape
          this.emit(EVENT_TYPE.SCENE_SHAPE_CHANGED);
          this.context.requestRedraw("Mesh Attribute Data Changed");
          XML3D.debug.logDebug("XflowMesh: Attribute data changed", request, state, this.changeState);
      },
  
      shaderChanged: function () {
          this.changeState |= CHANGE_STATE.SHADER_CHANGED;
      },
  
      getShaderClosure: function () {
          return this.shaderClosure;
      },
  
      destroy: function() {
          this.mesh && this.mesh.clear();
          this.objectShaderRequest && this.objectShaderRequest.clear();
          this.typeRequest && this.typeRequest.clear();
          this.setShaderComposer(null);
          delete this.dataNode;
      }
  
  });
  
  /**
   * @param {string} typeName
   */
  var getGLTypeFromString = function (typeName) {
      if (typeName && typeName.toLowerCase)
          typeName = typeName.toLowerCase();
      switch (typeName) {
          case "triangles":
              return GL.TRIANGLES;
          case "tristrips":
              return GL.TRIANGLE_STRIP;
          case "points":
              return GL.POINTS;
          case "lines":
              return GL.LINES;
          case "linestrips":
              return GL.LINE_STRIP;
          default:
              return undefined;
      }
  };
  
  module.exports = XflowMesh;
  
  