/*
 * BufferGeometry helpers
 */

import THREE from 'three';
import {TextEncoder, TextDecoder} from 'text-encoding';

var Buffer = (function() {
  // Merge TypedArrays of the same type
  // Returns merged array as well as indexes for splitting the array
  var mergeFloat32Arrays = function(arrays) {
    var size = 0;
    var map = new Int32Array(arrays.length * 2);

    var lastIndex = 0;
    var length;

    // Find size of each array
    arrays.forEach((_array, index) => {
      length = _array.length;
      size += length;
      map.set([lastIndex, lastIndex + length], index * 2);
      lastIndex += length;
    });

    // Create a new array of total size
    var mergedArray = new Float32Array(size);

    // Add each array to the new array
    arrays.forEach((_array, index) => {
      mergedArray.set(_array, map[index * 2]);
    });

    return [
      mergedArray,
      map
    ];
  };

  var splitFloat32Array = function(data) {
    var arr = data[0];
    var map = data[1];

    var start;
    var arrays = [];

    // Iterate over map
    for (var i = 0; i < map.length / 2; i++) {
      start = i * 2;
      arrays.push(arr.subarray(map[start], map[start + 1]));
    }

    return arrays;
  };

  // TODO: Create a generic method that can work for any typed array
  var mergeUint8Arrays = function(arrays) {
    var size = 0;
    var map = new Int32Array(arrays.length * 2);

    var lastIndex = 0;
    var length;

    // Find size of each array
    arrays.forEach((_array, index) => {
      length = _array.length;
      size += length;
      map.set([lastIndex, lastIndex + length], index * 2);
      lastIndex += length;
    });

    // Create a new array of total size
    var mergedArray = new Uint8Array(size);

    // Add each array to the new array
    arrays.forEach((_array, index) => {
      mergedArray.set(_array, map[index * 2]);
    });

    return [
      mergedArray,
      map
    ];
  };

  // TODO: Dedupe with splitFloat32Array
  var splitUint8Array = function(data) {
    var arr = data[0];
    var map = data[1];

    var start;
    var arrays = [];

    // Iterate over map
    for (var i = 0; i < map.length / 2; i++) {
      start = i * 2;
      arrays.push(arr.subarray(map[start], map[start + 1]));
    }

    return arrays;
  };

  // Merge multiple attribute objects into a single attribute object
  //
  // Attribute objects must all use the same attribute keys
  var mergeAttributes = function(attributes) {
    var lengths = {};

    // Find array lengths
    attributes.forEach(_attributes => {
      for (var k in _attributes) {
        if (!lengths[k]) {
          lengths[k] = 0;
        }

        lengths[k] += _attributes[k].length;
      }
    });

    var mergedAttributes = {};

    // Set up arrays to merge into
    for (var k in lengths) {
      mergedAttributes[k] = new Float32Array(lengths[k]);
    }

    var lastLengths = {};

    attributes.forEach(_attributes => {
      for (var k in _attributes) {
        if (!lastLengths[k]) {
          lastLengths[k] = 0;
        }

        mergedAttributes[k].set(_attributes[k], lastLengths[k]);

        lastLengths[k] += _attributes[k].length;
      }
    });

    return mergedAttributes;
  };

  var createLineGeometry = function(lines, offset) {
    var geometry = new THREE.BufferGeometry();

    var vertices = new Float32Array(lines.verticesCount * 3);
    var colours = new Float32Array(lines.verticesCount * 3);

    var pickingIds;
    if (lines.pickingIds) {
      // One component per vertex (1)
      pickingIds = new Float32Array(lines.verticesCount);
    }

    var _vertices;
    var _colour;
    var _pickingId;

    var lastIndex = 0;

    for (var i = 0; i < lines.vertices.length; i++) {
      _vertices = lines.vertices[i];
      _colour = lines.colours[i];

      if (pickingIds) {
        _pickingId = lines.pickingIds[i];
      }

      for (var j = 0; j < _vertices.length; j++) {
        var ax = _vertices[j][0] + offset.x;
        var ay = _vertices[j][1];
        var az = _vertices[j][2] + offset.y;

        var c1 = _colour[j];

        vertices[lastIndex * 3 + 0] = ax;
        vertices[lastIndex * 3 + 1] = ay;
        vertices[lastIndex * 3 + 2] = az;

        colours[lastIndex * 3 + 0] = c1[0];
        colours[lastIndex * 3 + 1] = c1[1];
        colours[lastIndex * 3 + 2] = c1[2];

        if (pickingIds) {
          pickingIds[lastIndex] = _pickingId;
        }

        lastIndex++;
      }
    }

    // itemSize = 3 because there are 3 values (components) per vertex
    geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
    geometry.addAttribute('color', new THREE.BufferAttribute(colours, 3));

    if (pickingIds) {
      geometry.addAttribute('pickingId', new THREE.BufferAttribute(pickingIds, 1));
    }

    geometry.computeBoundingBox();

    return geometry;
  };

  // TODO: Make picking IDs optional
  var createGeometry = function(attributes, offset) {
    var geometry = new THREE.BufferGeometry();

    // Three components per vertex per face (3 x 3 = 9)
    var vertices = new Float32Array(attributes.facesCount * 9);
    var normals = new Float32Array(attributes.facesCount * 9);
    var colours = new Float32Array(attributes.facesCount * 9);

    var pickingIds;
    if (attributes.pickingIds) {
      // One component per vertex per face (1 x 3 = 3)
      pickingIds = new Float32Array(attributes.facesCount * 3);
    }

    var pA = new THREE.Vector3();
    var pB = new THREE.Vector3();
    var pC = new THREE.Vector3();

    var cb = new THREE.Vector3();
    var ab = new THREE.Vector3();

    var index;
    var _faces;
    var _vertices;
    var _colour;
    var _pickingId;
    var lastIndex = 0;
    for (var i = 0; i < attributes.faces.length; i++) {
      _faces = attributes.faces[i];
      _vertices = attributes.vertices[i];
      _colour = attributes.colours[i];

      if (pickingIds) {
        _pickingId = attributes.pickingIds[i];
      }

      for (var j = 0; j < _faces.length; j++) {
        // Array of vertex indexes for the face
        index = _faces[j][0];

        var ax = _vertices[index][0] + offset.x;
        var ay = _vertices[index][1];
        var az = _vertices[index][2] + offset.y;

        var c1 = _colour[j][0];

        index = _faces[j][1];

        var bx = _vertices[index][0] + offset.x;
        var by = _vertices[index][1];
        var bz = _vertices[index][2] + offset.y;

        var c2 = _colour[j][1];

        index = _faces[j][2];

        var cx = _vertices[index][0] + offset.x;
        var cy = _vertices[index][1];
        var cz = _vertices[index][2] + offset.y;

        var c3 = _colour[j][2];

        // Flat face normals
        // From: http://threejs.org/examples/webgl_buffergeometry.html
        pA.set(ax, ay, az);
        pB.set(bx, by, bz);
        pC.set(cx, cy, cz);

        cb.subVectors(pC, pB);
        ab.subVectors(pA, pB);
        cb.cross(ab);

        cb.normalize();

        var nx = cb.x;
        var ny = cb.y;
        var nz = cb.z;

        vertices[lastIndex * 9 + 0] = ax;
        vertices[lastIndex * 9 + 1] = ay;
        vertices[lastIndex * 9 + 2] = az;

        normals[lastIndex * 9 + 0] = nx;
        normals[lastIndex * 9 + 1] = ny;
        normals[lastIndex * 9 + 2] = nz;

        colours[lastIndex * 9 + 0] = c1[0];
        colours[lastIndex * 9 + 1] = c1[1];
        colours[lastIndex * 9 + 2] = c1[2];

        vertices[lastIndex * 9 + 3] = bx;
        vertices[lastIndex * 9 + 4] = by;
        vertices[lastIndex * 9 + 5] = bz;

        normals[lastIndex * 9 + 3] = nx;
        normals[lastIndex * 9 + 4] = ny;
        normals[lastIndex * 9 + 5] = nz;

        colours[lastIndex * 9 + 3] = c2[0];
        colours[lastIndex * 9 + 4] = c2[1];
        colours[lastIndex * 9 + 5] = c2[2];

        vertices[lastIndex * 9 + 6] = cx;
        vertices[lastIndex * 9 + 7] = cy;
        vertices[lastIndex * 9 + 8] = cz;

        normals[lastIndex * 9 + 6] = nx;
        normals[lastIndex * 9 + 7] = ny;
        normals[lastIndex * 9 + 8] = nz;

        colours[lastIndex * 9 + 6] = c3[0];
        colours[lastIndex * 9 + 7] = c3[1];
        colours[lastIndex * 9 + 8] = c3[2];

        if (pickingIds) {
          pickingIds[lastIndex * 3 + 0] = _pickingId;
          pickingIds[lastIndex * 3 + 1] = _pickingId;
          pickingIds[lastIndex * 3 + 2] = _pickingId;
        }

        lastIndex++;
      }
    }

    // itemSize = 3 because there are 3 values (components) per vertex
    geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
    geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3));
    geometry.addAttribute('color', new THREE.BufferAttribute(colours, 3));

    if (pickingIds) {
      geometry.addAttribute('pickingId', new THREE.BufferAttribute(pickingIds, 1));
    }

    geometry.computeBoundingBox();

    return geometry;
  };

  var textEncoder = new TextEncoder('utf-8');
  var textDecoder = new TextDecoder('utf-8');

  var stringToUint8Array = function(str) {
    return textEncoder.encode(str);
  };

  var uint8ArrayToString = function(ab) {
    return textDecoder.decode(ab);
  };

  var fillTypedArray = function(arr, value) {
    for (var i = 0; i < arr.length; i++) {
      arr[i] = value;
    }
  };

  return {
    mergeFloat32Arrays: mergeFloat32Arrays,
    splitFloat32Array: splitFloat32Array,
    mergeUint8Arrays: mergeUint8Arrays,
    splitUint8Array: splitUint8Array,
    mergeAttributes: mergeAttributes,
    createLineGeometry: createLineGeometry,
    createGeometry: createGeometry,
    stringToUint8Array: stringToUint8Array,
    uint8ArrayToString: uint8ArrayToString,
    fillTypedArray: fillTypedArray
  };
})();

export default Buffer;
