import State from './state';
import { enums } from './enums';

import Texture2D from './texture-2d';
import TextureCube from './texture-cube';

const GL_INT = 5124;
const GL_FLOAT = 5126;
const GL_FLOAT_VEC2 = 35664;
const GL_FLOAT_VEC3 = 35665;
const GL_FLOAT_VEC4 = 35666;
const GL_INT_VEC2 = 35667;
const GL_INT_VEC3 = 35668;
const GL_INT_VEC4 = 35669;
const GL_BOOL = 35670;
const GL_BOOL_VEC2 = 35671;
const GL_BOOL_VEC3 = 35672;
const GL_BOOL_VEC4 = 35673;
const GL_FLOAT_MAT2 = 35674;
const GL_FLOAT_MAT3 = 35675;
const GL_FLOAT_MAT4 = 35676;
const GL_SAMPLER_2D = 35678;
const GL_SAMPLER_CUBE = 35680;

/**
 * _type2uniformCommit
 */
let _type2uniformCommit = {
  [GL_INT]: function (gl, id, value) {
    gl.uniform1i(id, value);
  },

  [GL_FLOAT]: function (gl, id, value) {
    gl.uniform1f(id, value);
  },

  [GL_FLOAT_VEC2]: function (gl, id, value) {
    gl.uniform2fv(id, value);
  },

  [GL_FLOAT_VEC3]: function (gl, id, value) {
    gl.uniform3fv(id, value);
  },

  [GL_FLOAT_VEC4]: function (gl, id, value) {
    gl.uniform4fv(id, value);
  },

  [GL_INT_VEC2]: function (gl, id, value) {
    gl.uniform2iv(id, value);
  },

  [GL_INT_VEC3]: function (gl, id, value) {
    gl.uniform3iv(id, value);
  },

  [GL_INT_VEC4]: function (gl, id, value) {
    gl.uniform4iv(id, value);
  },

  [GL_BOOL]: function (gl, id, value) {
    gl.uniform1i(id, value);
  },

  [GL_BOOL_VEC2]: function (gl, id, value) {
    gl.uniform2iv(id, value);
  },

  [GL_BOOL_VEC3]: function (gl, id, value) {
    gl.uniform3iv(id, value);
  },

  [GL_BOOL_VEC4]: function (gl, id, value) {
    gl.uniform4iv(id, value);
  },

  [GL_FLOAT_MAT2]: function (gl, id, value) {
    gl.uniformMatrix2fv(id, false, value);
  },

  [GL_FLOAT_MAT3]: function (gl, id, value) {
    gl.uniformMatrix3fv(id, false, value);
  },

  [GL_FLOAT_MAT4]: function (gl, id, value) {
    gl.uniformMatrix4fv(id, false, value);
  },

  [GL_SAMPLER_2D]: function (gl, id, value) {
    gl.uniform1i(id, value);
  },

  [GL_SAMPLER_CUBE]: function (gl, id, value) {
    gl.uniform1i(id, value);
  },
};

/**
 * _type2uniformArrayCommit
 */
let _type2uniformArrayCommit = {
  [GL_INT]: function (gl, id, value) {
    gl.uniform1iv(id, value);
  },

  [GL_FLOAT]: function (gl, id, value) {
    gl.uniform1fv(id, value);
  },

  [GL_FLOAT_VEC2]: function (gl, id, value) {
    gl.uniform2fv(id, value);
  },

  [GL_FLOAT_VEC3]: function (gl, id, value) {
    gl.uniform3fv(id, value);
  },

  [GL_FLOAT_VEC4]: function (gl, id, value) {
    gl.uniform4fv(id, value);
  },

  [GL_INT_VEC2]: function (gl, id, value) {
    gl.uniform2iv(id, value);
  },

  [GL_INT_VEC3]: function (gl, id, value) {
    gl.uniform3iv(id, value);
  },

  [GL_INT_VEC4]: function (gl, id, value) {
    gl.uniform4iv(id, value);
  },

  [GL_BOOL]: function (gl, id, value) {
    gl.uniform1iv(id, value);
  },

  [GL_BOOL_VEC2]: function (gl, id, value) {
    gl.uniform2iv(id, value);
  },

  [GL_BOOL_VEC3]: function (gl, id, value) {
    gl.uniform3iv(id, value);
  },

  [GL_BOOL_VEC4]: function (gl, id, value) {
    gl.uniform4iv(id, value);
  },

  [GL_FLOAT_MAT2]: function (gl, id, value) {
    gl.uniformMatrix2fv(id, false, value);
  },

  [GL_FLOAT_MAT3]: function (gl, id, value) {
    gl.uniformMatrix3fv(id, false, value);
  },

  [GL_FLOAT_MAT4]: function (gl, id, value) {
    gl.uniformMatrix4fv(id, false, value);
  },

  [GL_SAMPLER_2D]: function (gl, id, value) {
    gl.uniform1iv(id, value);
  },

  [GL_SAMPLER_CUBE]: function (gl, id, value) {
    gl.uniform1iv(id, value);
  },
};

/**
 * _commitBlendStates
 */
function _commitBlendStates(gl, cur, next) {
  // enable/disable blend
  if (cur.blend !== next.blend) {
    if (!next.blend) {
      gl.disable(gl.BLEND);
      return;
    }

    gl.enable(gl.BLEND);

    if (
      next.blendSrc === enums.BLEND_CONSTANT_COLOR ||
      next.blendSrc === enums.BLEND_ONE_MINUS_CONSTANT_COLOR ||
      next.blendDst === enums.BLEND_CONSTANT_COLOR ||
      next.blendDst === enums.BLEND_ONE_MINUS_CONSTANT_COLOR
    ) {
      gl.blendColor(
        (next.blendColor >> 24) / 255,
        (next.blendColor >> 16 & 0xff) / 255,
        (next.blendColor >> 8 & 0xff) / 255,
        (next.blendColor & 0xff) / 255
      );
    }

    if (next.blendSep) {
      gl.blendFuncSeparate(next.blendSrc, next.blendDst, next.blendSrcAlpha, next.blendDstAlpha);
      gl.blendEquationSeparate(next.blendEq, next.blendAlphaEq);
    } else {
      gl.blendFunc(next.blendSrc, next.blendDst);
      gl.blendEquation(next.blendEq);
    }

    return;
  }

  // nothing to update
  if (next.blend === false) {
    return;
  }

  // blend-color
  if (cur.blendColor !== next.blendColor) {
    gl.blendColor(
      (next.blendColor >> 24) / 255,
      (next.blendColor >> 16 & 0xff) / 255,
      (next.blendColor >> 8 & 0xff) / 255,
      (next.blendColor & 0xff) / 255
    );
  }

  // separate diff, reset all
  if (cur.blendSep !== next.blendSep) {
    if (next.blendSep) {
      gl.blendFuncSeparate(next.blendSrc, next.blendDst, next.blendSrcAlpha, next.blendDstAlpha);
      gl.blendEquationSeparate(next.blendEq, next.blendAlphaEq);
    } else {
      gl.blendFunc(next.blendSrc, next.blendDst);
      gl.blendEquation(next.blendEq);
    }

    return;
  }

  if (next.blendSep) {
    // blend-func-separate
    if (
      cur.blendSrc !== next.blendSrc ||
      cur.blendDst !== next.blendDst ||
      cur.blendSrcAlpha !== next.blendSrcAlpha ||
      cur.blendDstAlpha !== next.blendDstAlpha
    ) {
      gl.blendFuncSeparate(next.blendSrc, next.blendDst, next.blendSrcAlpha, next.blendDstAlpha);
    }

    // blend-equation-separate
    if (
      cur.blendEq !== next.blendEq ||
      cur.blendAlphaEq !== next.blendAlphaEq
    ) {
      gl.blendEquationSeparate(next.blendEq, next.blendAlphaEq);
    }
  } else {
    // blend-func
    if (
      cur.blendSrc !== next.blendSrc ||
      cur.blendDst !== next.blendDst
    ) {
      gl.blendFunc(next.blendSrc, next.blendDst);
    }

    // blend-equation
    if (cur.blendEq !== next.blendEq) {
      gl.blendEquation(next.blendEq);
    }
  }
}

/**
 * _commitDepthStates
 */
function _commitDepthStates(gl, cur, next) {
  // enable/disable depth-test
  if (cur.depthTest !== next.depthTest) {
    if (!next.depthTest) {
      gl.disable(gl.DEPTH_TEST);
      return;
    }

    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(next.depthFunc);
    gl.depthMask(next.depthWrite);

    return;
  }

  // commit depth-write
  if (cur.depthWrite !== next.depthWrite) {
    gl.depthMask(next.depthWrite);
  }

  // check if depth-write enabled
  if (next.depthTest === false) {
    if (next.depthWrite) {
      next.depthTest = true;
      next.depthFunc = enums.DS_FUNC_ALWAYS;

      gl.enable(gl.DEPTH_TEST);
      gl.depthFunc(next.depthFunc);
    }

    return;
  }

  // depth-func
  if (cur.depthFunc !== next.depthFunc) {
    gl.depthFunc(next.depthFunc);
  }
}

/**
 * _commitStencilStates
 */
function _commitStencilStates(gl, cur, next) {
  // inherit stencil states
  if (next.stencilTest === enums.STENCIL_INHERIT) {
    return;
  }

  if (next.stencilTest !== cur.stencilTest) {
    if (next.stencilTest === enums.STENCIL_DISABLE) {
      gl.disable(gl.STENCIL_TEST);
      return;
    }

    gl.enable(gl.STENCIL_TEST);

    if (next.stencilSep) {
      gl.stencilFuncSeparate(gl.FRONT, next.stencilFuncFront, next.stencilRefFront, next.stencilMaskFront);
      gl.stencilMaskSeparate(gl.FRONT, next.stencilWriteMaskFront);
      gl.stencilOpSeparate(gl.FRONT, next.stencilFailOpFront, next.stencilZFailOpFront, next.stencilZPassOpFront);
      gl.stencilFuncSeparate(gl.BACK, next.stencilFuncBack, next.stencilRefBack, next.stencilMaskBack);
      gl.stencilMaskSeparate(gl.BACK, next.stencilWriteMaskBack);
      gl.stencilOpSeparate(gl.BACK, next.stencilFailOpBack, next.stencilZFailOpBack, next.stencilZPassOpBack);
    } else {
      gl.stencilFunc(next.stencilFuncFront, next.stencilRefFront, next.stencilMaskFront);
      gl.stencilMask(next.stencilWriteMaskFront);
      gl.stencilOp(next.stencilFailOpFront, next.stencilZFailOpFront, next.stencilZPassOpFront);
    }

    return;
  }

  // fast return
  if (next.stencilTest === enums.STENCIL_DISABLE) {
    return;
  }

  if (cur.stencilSep !== next.stencilSep) {
    if (next.stencilSep) {
      gl.stencilFuncSeparate(gl.FRONT, next.stencilFuncFront, next.stencilRefFront, next.stencilMaskFront);
      gl.stencilMaskSeparate(gl.FRONT, next.stencilWriteMaskFront);
      gl.stencilOpSeparate(gl.FRONT, next.stencilFailOpFront, next.stencilZFailOpFront, next.stencilZPassOpFront);
      gl.stencilFuncSeparate(gl.BACK, next.stencilFuncBack, next.stencilRefBack, next.stencilMaskBack);
      gl.stencilMaskSeparate(gl.BACK, next.stencilWriteMaskBack);
      gl.stencilOpSeparate(gl.BACK, next.stencilFailOpBack, next.stencilZFailOpBack, next.stencilZPassOpBack);
    } else {
      gl.stencilFunc(next.stencilFuncFront, next.stencilRefFront, next.stencilMaskFront);
      gl.stencilMask(next.stencilWriteMaskFront);
      gl.stencilOp(next.stencilFailOpFront, next.stencilZFailOpFront, next.stencilZPassOpFront);
    }
    return;
  }

  if (next.stencilSep) {
    // front
    if (
      cur.stencilFuncFront !== next.stencilFuncFront ||
      cur.stencilRefFront !== next.stencilRefFront ||
      cur.stencilMaskFront !== next.stencilMaskFront
    ) {
      gl.stencilFuncSeparate(gl.FRONT, next.stencilFuncFront, next.stencilRefFront, next.stencilMaskFront);
    }
    if (cur.stencilWriteMaskFront !== next.stencilWriteMaskFront) {
      gl.stencilMaskSeparate(gl.FRONT, next.stencilWriteMaskFront);
    }
    if (
      cur.stencilFailOpFront !== next.stencilFailOpFront ||
      cur.stencilZFailOpFront !== next.stencilZFailOpFront ||
      cur.stencilZPassOpFront !== next.stencilZPassOpFront
    ) {
      gl.stencilOpSeparate(gl.FRONT, next.stencilFailOpFront, next.stencilZFailOpFront, next.stencilZPassOpFront);
    }

    // back
    if (
      cur.stencilFuncBack !== next.stencilFuncBack ||
      cur.stencilRefBack !== next.stencilRefBack ||
      cur.stencilMaskBack !== next.stencilMaskBack
    ) {
      gl.stencilFuncSeparate(gl.BACK, next.stencilFuncBack, next.stencilRefBack, next.stencilMaskBack);
    }
    if (cur.stencilWriteMaskBack !== next.stencilWriteMaskBack) {
      gl.stencilMaskSeparate(gl.BACK, next.stencilWriteMaskBack);
    }
    if (
      cur.stencilFailOpBack !== next.stencilFailOpBack ||
      cur.stencilZFailOpBack !== next.stencilZFailOpBack ||
      cur.stencilZPassOpBack !== next.stencilZPassOpBack
    ) {
      gl.stencilOpSeparate(gl.BACK, next.stencilFailOpBack, next.stencilZFailOpBack, next.stencilZPassOpBack);
    }
  } else {
    if (
      cur.stencilFuncFront !== next.stencilFuncFront ||
      cur.stencilRefFront !== next.stencilRefFront ||
      cur.stencilMaskFront !== next.stencilMaskFront
    ) {
      gl.stencilFunc(next.stencilFuncFront, next.stencilRefFront, next.stencilMaskFront);
    }
    if (cur.stencilWriteMaskFront !== next.stencilWriteMaskFront) {
      gl.stencilMask(next.stencilWriteMaskFront);
    }
    if (
      cur.stencilFailOpFront !== next.stencilFailOpFront ||
      cur.stencilZFailOpFront !== next.stencilZFailOpFront ||
      cur.stencilZPassOpFront !== next.stencilZPassOpFront
    ) {
      gl.stencilOp(next.stencilFailOpFront, next.stencilZFailOpFront, next.stencilZPassOpFront);
    }
  }

}

/**
 * _commitCullMode
 */
function _commitCullMode(gl, cur, next) {
  if (cur.cullMode === next.cullMode) {
    return;
  }

  if (next.cullMode === enums.CULL_NONE) {
    gl.disable(gl.CULL_FACE);
    return;
  }

  gl.enable(gl.CULL_FACE);
  gl.cullFace(next.cullMode);
}

/**
 * _commitVertexBuffers
 */
function _commitVertexBuffers(device, gl, cur, next) {
  let attrsDirty = false;

  // nothing changed for vertex buffer
  if (next.maxStream === -1) {
    return;
  }

  if (cur.maxStream !== next.maxStream) {
    attrsDirty = true;
  } else if (cur.program !== next.program) {
    attrsDirty = true;
  } else {
    for (let i = 0; i < next.maxStream + 1; ++i) {
      if (
        cur.vertexBuffers[i] !== next.vertexBuffers[i] ||
        cur.vertexBufferOffsets[i] !== next.vertexBufferOffsets[i]
      ) {
        attrsDirty = true;
        break;
      }
    }
  }

  if (attrsDirty) {
    for (let i = 0; i < device._caps.maxVertexAttribs; ++i) {
      device._newAttributes[i] = 0;
    }

    for (let i = 0; i < next.maxStream + 1; ++i) {
      let vb = next.vertexBuffers[i];
      let vbOffset = next.vertexBufferOffsets[i];
      if (!vb || vb._glID === -1) {
        continue;
      }

      gl.bindBuffer(gl.ARRAY_BUFFER, vb._glID);

      for (let j = 0; j < next.program._attributes.length; ++j) {
        let attr = next.program._attributes[j];

        let el = vb._format.element(attr.name);
        if (!el) {
          console.warn(`Can not find vertex attribute: ${attr.name}`);
          continue;
        }

        if (device._enabledAttributes[attr.location] === 0) {
          gl.enableVertexAttribArray(attr.location);
          device._enabledAttributes[attr.location] = 1;
        }
        device._newAttributes[attr.location] = 1;

        gl.vertexAttribPointer(
          attr.location,
          el.num,
          el.type,
          el.normalize,
          el.stride,
          el.offset + vbOffset * el.stride
        );
      }
    }

    // disable unused attributes
    for (let i = 0; i < device._caps.maxVertexAttribs; ++i) {
      if (device._enabledAttributes[i] !== device._newAttributes[i]) {
        gl.disableVertexAttribArray(i);
        device._enabledAttributes[i] = 0;
      }
    }
  }
}

/**
 * _commitTextures
 */
function _commitTextures(gl, cur, next) {
  for (let i = 0; i < next.maxTextureSlot + 1; ++i) {
    if (cur.textureUnits[i] !== next.textureUnits[i]) {
      let texture = next.textureUnits[i];
      if (texture && texture._glID !== -1) {
        gl.activeTexture(gl.TEXTURE0 + i);
        gl.bindTexture(texture._target, texture._glID);
      }
    }
  }
}

/**
 * _attach
 */
function _attach(gl, location, attachment, face = 0) {
  if (attachment instanceof Texture2D) {
    gl.framebufferTexture2D(
      gl.FRAMEBUFFER,
      location,
      gl.TEXTURE_2D,
      attachment._glID,
      0
    );
  } else if (attachment instanceof TextureCube) {
    gl.framebufferTexture2D(
      gl.FRAMEBUFFER,
      location,
      gl.TEXTURE_CUBE_MAP_POSITIVE_X + face,
      attachment._glID,
      0
    );
  } else {
    gl.framebufferRenderbuffer(
      gl.FRAMEBUFFER,
      location,
      gl.RENDERBUFFER,
      attachment._glID
    );
  }
}

export default class Device {
  /**
   * @property caps
   */
  get caps() {
    return this._caps;
  }

  /**
   * @param {HTMLElement} canvasEL
   * @param {object} opts
   */
  constructor(canvasEL, opts) {
    let gl;

    // default options
    opts = opts || {};
    if (opts.alpha === undefined) {
      opts.alpha = false;
    }
    if (opts.stencil === undefined) {
      opts.stencil = true;
    }
    if (opts.depth === undefined) {
      opts.depth = true;
    }
    if (opts.antialias === undefined) {
      opts.antialias = false;
    }
    // NOTE: it is said the performance improved in mobile device with this flag off.
    if (opts.preserveDrawingBuffer === undefined) {
      opts.preserveDrawingBuffer = false;
    }

    try {
      gl = canvasEL.getContext('webgl', opts)
        || canvasEL.getContext('experimental-webgl', opts)
        || canvasEL.getContext('webkit-3d', opts)
        || canvasEL.getContext('moz-webgl', opts);
    } catch (err) {
      console.error(err);
      return;
    }

    // No errors are thrown using try catch
    // Tested through ios baidu browser 4.14.1
    if (!gl) {
      console.error('This device does not support webgl');
    }

    // statics
    /**
     * @type {WebGLRenderingContext}
     */
    this._gl = gl;
    this._extensions = {};
    this._caps = {}; // capability
    this._stats = {
      texture: 0,
      vb: 0,
      ib: 0,
      drawcalls: 0,
    };

    // https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Using_Extensions
    this._initExtensions([
      'EXT_texture_filter_anisotropic',
      'EXT_shader_texture_lod',
      'OES_standard_derivatives',
      'OES_texture_float',
      'OES_texture_float_linear',
      'OES_texture_half_float',
      'OES_texture_half_float_linear',
      'OES_vertex_array_object',
      'WEBGL_compressed_texture_astc',
      'WEBGL_compressed_texture_etc',
      'WEBGL_compressed_texture_etc1',
      'WEBGL_compressed_texture_pvrtc',
      'WEBGL_compressed_texture_s3tc',
      'WEBGL_depth_texture',
      'WEBGL_draw_buffers',
    ]);
    this._initCaps();
    this._initStates();

    // runtime
    State.initDefault(this);
    this._current = new State(this);
    this._next = new State(this);
    this._uniforms = {}; // name: { value, num, dirty }
    this._vx = this._vy = this._vw = this._vh = 0;
    this._sx = this._sy = this._sw = this._sh = 0;
    this._framebuffer = null;

    //
    this._enabledAttributes = new Array(this._caps.maxVertexAttribs);
    this._newAttributes = new Array(this._caps.maxVertexAttribs);

    for (let i = 0; i < this._caps.maxVertexAttribs; ++i) {
      this._enabledAttributes[i] = 0;
      this._newAttributes[i] = 0;
    }
  }

  _initExtensions(extensions) {
    const gl = this._gl;

    for (let i = 0; i < extensions.length; ++i) {
      let name = extensions[i];
      let vendorPrefixes = ["", "WEBKIT_", "MOZ_"];

      for (var j = 0; j < vendorPrefixes.length; j++) {
        try {
          let ext = gl.getExtension(vendorPrefixes[j] + name);
          if (ext) {
            this._extensions[name] = ext;
            break;
          }
        } catch (e) {
          console.error(e);
        }
      }
    }
  }

  _initCaps() {
    const gl = this._gl;
    const extDrawBuffers = this.ext('WEBGL_draw_buffers');

    this._caps.maxVertexStreams = 4;
    this._caps.maxVertexTextures = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
    this._caps.maxFragUniforms = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
    this._caps.maxTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
    this._caps.maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
    this._caps.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);

    this._caps.maxDrawBuffers = extDrawBuffers ? gl.getParameter(extDrawBuffers.MAX_DRAW_BUFFERS_WEBGL) : 1;
    this._caps.maxColorAttachments = extDrawBuffers ? gl.getParameter(extDrawBuffers.MAX_COLOR_ATTACHMENTS_WEBGL) : 1;
  }

  _initStates() {
    const gl = this._gl;

    // gl.frontFace(gl.CCW);
    gl.disable(gl.BLEND);
    gl.blendFunc(gl.ONE, gl.ZERO);
    gl.blendEquation(gl.FUNC_ADD);
    gl.blendColor(1,1,1,1);

    gl.colorMask(true, true, true, true);

    gl.enable(gl.CULL_FACE);
    gl.cullFace(gl.BACK);

    gl.disable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LESS);
    gl.depthMask(false);
    gl.disable(gl.POLYGON_OFFSET_FILL);
    gl.depthRange(0,1);

    gl.disable(gl.STENCIL_TEST);
    gl.stencilFunc(gl.ALWAYS, 0, 0xFF);
    gl.stencilMask(0xFF);
    gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);

    // TODO:
    // this.setAlphaToCoverage(false);
    // this.setTransformFeedbackBuffer(null);
    // this.setRaster(true);
    // this.setDepthBias(false);

    gl.clearDepth(1);
    gl.clearColor(0, 0, 0, 0);
    gl.clearStencil(0);

    gl.disable(gl.SCISSOR_TEST);
  }

  _restoreTexture(unit) {
    const gl = this._gl;

    let texture = this._current.textureUnits[unit];
    if (texture && texture._glID !== -1) {
      gl.bindTexture(texture._target, texture._glID);
    } else {
      gl.bindTexture(gl.TEXTURE_2D, null);
    }
  }

  _restoreIndexBuffer () {
    const gl = this._gl;

    let ib = this._current.indexBuffer;
    if (ib && ib._glID !== -1) {
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ib._glID);
    }
    else {
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
    }
  }

  /**
   * @method ext
   * @param {string} name
   */
  ext(name) {
    return this._extensions[name];
  }

  allowFloatTexture() {
    return this.ext("OES_texture_float") != null;
  }

  // ===============================
  // Immediate Settings
  // ===============================

  /**
   * @method setFrameBuffer
   * @param {FrameBuffer} fb - null means use the backbuffer
   */
  setFrameBuffer(fb) {
    if (this._framebuffer === fb) {
      return;
    }

    this._framebuffer = fb;
    const gl = this._gl;

    if (!fb) {
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      return;
    }

    gl.bindFramebuffer(gl.FRAMEBUFFER, fb._glID);

    let numColors = fb._colors.length;
    for (let i = 0; i < numColors; ++i) {
      let colorBuffer = fb._colors[i];
      _attach(gl, gl.COLOR_ATTACHMENT0 + i, colorBuffer);

      // TODO: what about cubemap face??? should be the target parameter for colorBuffer
    }
    for (let i = numColors; i < this._caps.maxColorAttachments; ++i) {
      gl.framebufferTexture2D(
        gl.FRAMEBUFFER,
        gl.COLOR_ATTACHMENT0 + i,
        gl.TEXTURE_2D,
        null,
        0
      );
    }

    if (fb._depth) {
      _attach(gl, gl.DEPTH_ATTACHMENT, fb._depth);
    }

    if (fb._stencil) {
      _attach(gl, gl.STENCIL_ATTACHMENT, fb._stencil);
    }

    if (fb._depthStencil) {
      _attach(gl, gl.DEPTH_STENCIL_ATTACHMENT, fb._depthStencil);
    }
  }

  /**
   * @method setViewport
   * @param {Number} x
   * @param {Number} y
   * @param {Number} w
   * @param {Number} h
   */
  setViewport(x, y, w, h) {
    if (
      this._vx !== x ||
      this._vy !== y ||
      this._vw !== w ||
      this._vh !== h
    ) {
      this._gl.viewport(x, y, w, h);
      this._vx = x;
      this._vy = y;
      this._vw = w;
      this._vh = h;
    }
  }

  /**
   * @method setScissor
   * @param {Number} x
   * @param {Number} y
   * @param {Number} w
   * @param {Number} h
   */
  setScissor(x, y, w, h) {
    if (
      this._sx !== x ||
      this._sy !== y ||
      this._sw !== w ||
      this._sh !== h
    ) {
      this._gl.scissor(x, y, w, h);
      this._sx = x;
      this._sy = y;
      this._sw = w;
      this._sh = h;
    }
  }

  /**
   * @method clear
   * @param {Object} opts
   * @param {Array} opts.color
   * @param {Number} opts.depth
   * @param {Number} opts.stencil
   */
  clear(opts) {
    if (opts.color === undefined && opts.depth === undefined && opts.stencil === undefined) {
        return;
    }
    const gl = this._gl;
    let flags = 0;

    if (opts.color !== undefined) {
      flags |= gl.COLOR_BUFFER_BIT;
      gl.clearColor(opts.color[0], opts.color[1], opts.color[2], opts.color[3]);
    }

    if (opts.depth !== undefined) {
      flags |= gl.DEPTH_BUFFER_BIT;
      gl.clearDepth(opts.depth);

      gl.enable(gl.DEPTH_TEST);
      gl.depthMask(true);
      gl.depthFunc(gl.ALWAYS);
    }

    if (opts.stencil !== undefined) {
      flags |= gl.STENCIL_BUFFER_BIT;
      gl.clearStencil(opts.stencil);
    }

    gl.clear(flags);

    // restore depth-write
    if (opts.depth !== undefined) {
      if (this._current.depthTest === false) {
        gl.disable(gl.DEPTH_TEST);
      } else {
        if (this._current.depthWrite === false) {
          gl.depthMask(false);
        }
        if (this._current.depthFunc !== enums.DS_FUNC_ALWAYS) {
          gl.depthFunc(this._current.depthFunc);
        }
      }
    }
  }

  // ===============================
  // Deferred States
  // ===============================

  /**
   * @method enableBlend
   */
  enableBlend() {
    this._next.blend = true;
  }

  /**
   * @method enableDepthTest
   */
  enableDepthTest() {
    this._next.depthTest = true;
  }

  /**
   * @method enableDepthWrite
   */
  enableDepthWrite() {
    this._next.depthWrite = true;
  }

  /**
   * @method enableStencilTest
   * @param {Number} stencilTest
   */
  setStencilTest(stencilTest) {
    this._next.stencilTest = stencilTest;
  }

  /**
   * @method setStencilFunc
   * @param {DS_FUNC_*} func
   * @param {Number} ref
   * @param {Number} mask
   */
  setStencilFunc(func, ref, mask) {
    this._next.stencilSep = false;
    this._next.stencilFuncFront = this._next.stencilFuncBack = func;
    this._next.stencilRefFront = this._next.stencilRefBack = ref;
    this._next.stencilMaskFront = this._next.stencilMaskBack = mask;
  }

  /**
   * @method setStencilFuncFront
   * @param {DS_FUNC_*} func
   * @param {Number} ref
   * @param {Number} mask
   */
  setStencilFuncFront(func, ref, mask) {
    this._next.stencilSep = true;
    this._next.stencilFuncFront = func;
    this._next.stencilRefFront = ref;
    this._next.stencilMaskFront = mask;
  }

  /**
   * @method setStencilFuncBack
   * @param {DS_FUNC_*} func
   * @param {Number} ref
   * @param {Number} mask
   */
  setStencilFuncBack(func, ref, mask) {
    this._next.stencilSep = true;
    this._next.stencilFuncBack = func;
    this._next.stencilRefBack = ref;
    this._next.stencilMaskBack = mask;
  }

  /**
   * @method setStencilOp
   * @param {STENCIL_OP_*} failOp
   * @param {STENCIL_OP_*} zFailOp
   * @param {STENCIL_OP_*} zPassOp
   * @param {Number} writeMask
   */
  setStencilOp(failOp, zFailOp, zPassOp, writeMask) {
    this._next.stencilFailOpFront = this._next.stencilFailOpBack = failOp;
    this._next.stencilZFailOpFront = this._next.stencilZFailOpBack = zFailOp;
    this._next.stencilZPassOpFront = this._next.stencilZPassOpBack = zPassOp;
    this._next.stencilWriteMaskFront = this._next.stencilWriteMaskBack = writeMask;
  }

  /**
   * @method setStencilOpFront
   * @param {STENCIL_OP_*} failOp
   * @param {STENCIL_OP_*} zFailOp
   * @param {STENCIL_OP_*} zPassOp
   * @param {Number} writeMask
   */
  setStencilOpFront(failOp, zFailOp, zPassOp, writeMask) {
    this._next.stencilSep = true;
    this._next.stencilFailOpFront = failOp;
    this._next.stencilZFailOpFront = zFailOp;
    this._next.stencilZPassOpFront = zPassOp;
    this._next.stencilWriteMaskFront = writeMask;
  }

  /**
   * @method setStencilOpBack
   * @param {STENCIL_OP_*} failOp
   * @param {STENCIL_OP_*} zFailOp
   * @param {STENCIL_OP_*} zPassOp
   * @param {Number} writeMask
   */
  setStencilOpBack(failOp, zFailOp, zPassOp, writeMask) {
    this._next.stencilSep = true;
    this._next.stencilFailOpBack = failOp;
    this._next.stencilZFailOpBack = zFailOp;
    this._next.stencilZPassOpBack = zPassOp;
    this._next.stencilWriteMaskBack = writeMask;
  }

  /**
   * @method setDepthFunc
   * @param {DS_FUNC_*} depthFunc
   */
  setDepthFunc(depthFunc) {
    this._next.depthFunc = depthFunc;
  }

  /**
   * @method setBlendColor32
   * @param {Number} rgba
   */
  setBlendColor32(rgba) {
    this._next.blendColor = rgba;
  }

  /**
   * @method setBlendColor
   * @param {Number} r
   * @param {Number} g
   * @param {Number} b
   * @param {Number} a
   */
  setBlendColor(r, g, b, a) {
    this._next.blendColor = ((r * 255) << 24 | (g * 255) << 16 | (b * 255) << 8 | a * 255) >>> 0;
  }

  /**
   * @method setBlendFunc
   * @param {BELND_*} src
   * @param {BELND_*} dst
   */
  setBlendFunc(src, dst) {
    this._next.blendSep = false;
    this._next.blendSrc = src;
    this._next.blendDst = dst;
  }

  /**
   * @method setBlendFuncSep
   * @param {BELND_*} src
   * @param {BELND_*} dst
   * @param {BELND_*} srcAlpha
   * @param {BELND_*} dstAlpha
   */
  setBlendFuncSep(src, dst, srcAlpha, dstAlpha) {
    this._next.blendSep = true;
    this._next.blendSrc = src;
    this._next.blendDst = dst;
    this._next.blendSrcAlpha = srcAlpha;
    this._next.blendDstAlpha = dstAlpha;
  }

  /**
   * @method setBlendEq
   * @param {BELND_FUNC_*} eq
   */
  setBlendEq(eq) {
    this._next.blendSep = false;
    this._next.blendEq = eq;
  }

  /**
   * @method setBlendEqSep
   * @param {BELND_FUNC_*} eq
   * @param {BELND_FUNC_*} alphaEq
   */
  setBlendEqSep(eq, alphaEq) {
    this._next.blendSep = true;
    this._next.blendEq = eq;
    this._next.blendAlphaEq = alphaEq;
  }

  /**
   * @method setCullMode
   * @param {CULL_*} mode
   */
  setCullMode(mode) {
    this._next.cullMode = mode;
  }

  /**
   * @method setVertexBuffer
   * @param {Number} stream
   * @param {VertexBuffer} buffer
   * @param {Number} start - start vertex
   */
  setVertexBuffer(stream, buffer, start = 0) {
    this._next.vertexBuffers[stream] = buffer;
    this._next.vertexBufferOffsets[stream] = start;
    if (this._next.maxStream < stream) {
      this._next.maxStream = stream;
    }
  }

  /**
   * @method setIndexBuffer
   * @param {IndexBuffer} buffer
   */
  setIndexBuffer(buffer) {
    this._next.indexBuffer = buffer;
  }

  /**
   * @method setProgram
   * @param {Program} program
   */
  setProgram(program) {
    this._next.program = program;
  }

  /**
   * @method setTexture
   * @param {String} name
   * @param {Texture} texture
   * @param {Number} slot
   */
  setTexture(name, texture, slot) {
    if (slot >= this._caps.maxTextureUnits) {
      console.warn(`Can not set texture ${name} at stage ${slot}, max texture exceed: ${this._caps.maxTextureUnits}`);
      return;
    }

    this._next.textureUnits[slot] = texture;
    this.setUniform(name, slot);

    if (this._next.maxTextureSlot < slot) {
      this._next.maxTextureSlot = slot;
    }
  }

  /**
   * @method setTextureArray
   * @param {String} name
   * @param {Array} textures
   * @param {Int32Array} slots
   */
  setTextureArray(name, textures, slots) {
    let len = textures.length;
    if (len >= this._caps.maxTextureUnits) {
      console.warn(`Can not set ${len} textures for ${name}, max texture exceed: ${this._caps.maxTextureUnits}`);
      return;
    }
    for (let i = 0; i < len; ++i) {
      let slot = slots[i];
      this._next.textureUnits[slot] = textures[i];

      if (this._next.maxTextureSlot < slot) {
        this._next.maxTextureSlot = slot;
      }
    }
    this.setUniform(name, slots);
  }

  /**
   * @method setUniform
   * @param {String} name
   * @param {*} value
   */
  setUniform(name, value) {
    let uniform = this._uniforms[name];

    let sameType = false;
    let isArray = false, isFloat32Array = false, isInt32Array = false;
    do {
      if (!uniform) {
        break;
      }

      isFloat32Array = Array.isArray(value) || value instanceof Float32Array;
      isInt32Array = value instanceof Int32Array;
      isArray = isFloat32Array || isInt32Array;
      if (uniform.isArray !== isArray) {
        break;
      }

      if (uniform.isArray && uniform.value.length !== value.length) {
        break;
      }

      sameType = true;
    } while (false);

    if (!sameType) {
      let newValue = value;
      if (isFloat32Array) {
        newValue = new Float32Array(value);
      }
      else if (isInt32Array) {
        newValue = new Int32Array(value);
      }

      uniform = {
        dirty: true,
        value: newValue,
        isArray: isArray
      };
    } else {
      let oldValue = uniform.value;
      let dirty = false;
      if (uniform.isArray) {
        for (let i = 0, l = oldValue.length; i < l; i++) {
          if (oldValue[i] !== value[i]) {
            dirty = true;
            oldValue[i] = value[i];
          }
        }
      }
      else {
        if (oldValue !== value) {
          dirty = true;
          uniform.value = value;
        }
      }

      if (dirty) {
        uniform.dirty = true;
      }
    }
    this._uniforms[name] = uniform;
  }

  setUniformDirectly(name, value) {
    let uniform = this._uniforms[name];
    if (!uniform) {
      this._uniforms[name] = uniform = {};
    }
    uniform.dirty = true;
    uniform.value = value;
  }

  /**
   * @method setPrimitiveType
   * @param {PT_*} type
   */
  setPrimitiveType(type) {
    this._next.primitiveType = type;
  }

  /**
   * @method resetDrawCalls
   */
  resetDrawCalls () {
    this._stats.drawcalls = 0;
  }
  
  /**
   * @method getDrawCalls
   */
  getDrawCalls () {
    return this._stats.drawcalls;
  }

  /**
   * @method draw
   * @param {Number} base
   * @param {Number} count
   */
  draw(base, count) {
    const gl = this._gl;
    let cur = this._current;
    let next = this._next;

    // commit blend
    _commitBlendStates(gl, cur, next);

    // commit depth
    _commitDepthStates(gl, cur, next);

    // commit stencil
    _commitStencilStates(gl, cur, next);

    // commit cull
    _commitCullMode(gl, cur, next);

    // commit vertex-buffer
    _commitVertexBuffers(this, gl, cur, next);

    // commit index-buffer
    if (cur.indexBuffer !== next.indexBuffer) {
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, next.indexBuffer && next.indexBuffer._glID !== -1 ? next.indexBuffer._glID : null);
    }

    // commit program
    let programDirty = false;
    if (cur.program !== next.program) {
      if (next.program._linked) {
        gl.useProgram(next.program._glID);
      } else {
        console.warn('Failed to use program: has not linked yet.');
      }
      programDirty = true;
    }

    // commit texture/sampler
    _commitTextures(gl, cur, next);

    // commit uniforms
    for (let i = 0; i < next.program._uniforms.length; ++i) {
      let uniformInfo = next.program._uniforms[i];
      let uniform = this._uniforms[uniformInfo.name];
      if (!uniform) {
        // console.warn(`Can not find uniform ${uniformInfo.name}`);
        continue;
      }

      if (!programDirty && !uniform.dirty) {
        continue;
      }

      uniform.dirty = false;

      // TODO: please consider array uniform: uniformInfo.size > 0

      let commitFunc = (uniformInfo.size === undefined) ? _type2uniformCommit[uniformInfo.type] : _type2uniformArrayCommit[uniformInfo.type];
      if (!commitFunc) {
        console.warn(`Can not find commit function for uniform ${uniformInfo.name}`);
        continue;
      }

      commitFunc(gl, uniformInfo.location, uniform.value);
    }

    if (count) {
      // drawPrimitives
      if (next.indexBuffer) {
        gl.drawElements(
          this._next.primitiveType,
          count,
          next.indexBuffer._format,
          base * next.indexBuffer._bytesPerIndex
        );
      } else {
        gl.drawArrays(
          this._next.primitiveType,
          base,
          count
        );
      }

      // update stats
      this._stats.drawcalls++;
    }

    // TODO: autogen mipmap for color buffer
    // if (this._framebuffer && this._framebuffer.colors[0].mipmap) {
    //   gl.bindTexture(this._framebuffer.colors[i]._target, colors[i]._glID);
    //   gl.generateMipmap(this._framebuffer.colors[i]._target);
    // }

    // reset states
    cur.set(next);
    next.reset();
  }
}