(function () {
  var ns = $.namespace('pskl.utils');

  /**
   *********
   *  META *
   *********
   * // Piskel
   * [0] = model version
   * [1] = width
   * [2] = height
   * [3] = fps
   *
   * // Descriptor
   * [4] = name length
   * [5] = description length
   *
   * // Layers
   * [6] = layers count
   * [layer data index start] = layer name length
   * [layer data index start + 1] = opacity
   * [layer data index start + 2] = frame count
   * [layer data index start + 3] = base 64 png data url length (upper 16 bits)
   * [layer data index start + 4] = base 64 png data url length (lower 16 bits)
   *
   *********
   *  DATA *
   *********
   * [7..name length-1] = name
   * [name length..description length-1] = description
   * [layer data index start + 4..layer name length-1] = layer name
   * [layer name length..base 64 png data url length-1] = base 64 png data url
   *
   */

  ns.Serializer = {
    calculateRequiredBytes : function(piskel, framesData) {
      var width = piskel.getWidth();
      var height = piskel.getHeight();
      var descriptorNameLength = piskel.getDescriptor().name.length;
      var descriptorDescriptionLength = piskel.getDescriptor().description.length;
      var layersLength = piskel.getLayers().length;

      var bytes = 0;

      /********/
      /* META */
      /********/
      // Piskel meta
      bytes += 4 * 2;

      // Descriptor meta
      bytes += 2 * 2;

      // Layers meta
      bytes += 1 * 2;

      /********/
      /* DATA */
      /********/
      // Descriptor name
      bytes += descriptorNameLength * 2;

      // Descriptor description
      bytes += descriptorDescriptionLength * 2;

      // Layers
      for (var i = 0, layers = piskel.getLayers(); i < layers.length; i++) {
        bytes += 3 * 2;
        bytes += layers[i].name.length * 2;
        bytes += framesData[i].length;
        if (bytes % 2 == 1) {
          bytes++;
        }
      }

      return bytes;
    },

    serializePiskel : function (piskel, expanded) {
      var i;
      var j;
      var layers;
      var dataUri;
      var dataUriLength;

      // Render frames
      var framesData = [];
      for (i = 0, layers = piskel.getLayers(); i < layers.length; i++) {
        var renderer = new pskl.rendering.FramesheetRenderer(layers[i].getFrames());
        dataUri = atob(renderer.renderAsCanvas().toDataURL().split(',')[1]);
        dataUriLength = dataUri.length;
        framesData.push({uri: dataUri, length: dataUriLength});
      }

      var bytes = pskl.utils.Serializer.calculateRequiredBytes(piskel, framesData);

      var buffer = new ArrayBuffer(bytes);
      var arr8 = new Uint8Array(buffer);
      var arr16 = new Uint16Array(buffer);
      var sub;

      var width = piskel.getWidth();
      var height = piskel.getHeight();
      var descriptorName = piskel.getDescriptor().name;
      var descriptorNameLength = descriptorName.length;
      var descriptorDescription = piskel.getDescriptor().description;
      var descriptorDescriptionLength = descriptorDescription.length;

      /********/
      /* META */
      /********/
      // Piskel meta
      arr16[0] = Constants.MODEL_VERSION;
      arr16[1] = width;
      arr16[2] = height;
      arr16[3] = pskl.app.piskelController.getFPS();

      // Descriptor meta
      arr16[4] = descriptorNameLength;
      arr16[5] = descriptorDescriptionLength;

      // Layers meta
      arr16[6] = piskel.getLayers().length;

      /********/
      /* DATA */
      /********/
      // Descriptor name
      for (i = 0; i < descriptorNameLength; i++) {
        arr16[7 + i] = descriptorName.charCodeAt(i);
      }

      // Descriptor description
      for (i = 0; i < descriptorDescriptionLength; i++) {
        arr16[7 + descriptorNameLength + i] = descriptorDescription.charCodeAt(i);
      }

      // Layers
      var layerStartIndex = 7 + descriptorNameLength + descriptorDescriptionLength;
      for (i = 0, layers = piskel.getLayers(); i < layers.length; i++) {
        var layer = layers[i];
        var frames = layer.getFrames();

        var layerName = layer.getName();
        var layerNameLength = layerName.length;
        var opacity = layer.getOpacity();
        var frameCount = frames.length;

        dataUri = framesData[i].uri;
        dataUriLength = framesData[i].length;

        // Meta
        arr16[layerStartIndex] = layerNameLength;
        arr16[layerStartIndex + 1] = Math.floor(opacity * 65535);
        arr16[layerStartIndex + 2] = frameCount;
        arr16[layerStartIndex + 3] = ((dataUriLength & 0xffff0000) >> 16) >>> 0; // Upper 16
        arr16[layerStartIndex + 4] = ((dataUriLength & 0x0000ffff)) >>> 0;       // Lower 16

        // Name
        for (j = 0; j < layerNameLength; j++) {
          arr16[layerStartIndex + 5 + j] = layerName.charCodeAt(j);
        }

        // Data URI
        for (j = 0; j < dataUriLength; j++) {
          arr8[(layerStartIndex + 5 + layerNameLength) * 2 + j] = dataUri.charCodeAt(j);
        }

        layerStartIndex += Math.ceil(5 + layerNameLength + (dataUriLength / 2));
      }

      return buffer;
    }
  };
})();
