/**
 * @author aleeper / http://adamleeper.com/
 * @author mrdoob / http://mrdoob.com/
 * @author gero3 / https://github.com/gero3
 * @author Mugen87 / https://github.com/Mugen87
 *
 * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.
 *
 * Supports both binary and ASCII encoded files, with automatic detection of type.
 *
 * The loader returns a non-indexed buffer geometry.
 *
 * Limitations:
 *  Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).
 *  There is perhaps some question as to how valid it is to always assume little-endian-ness.
 *  ASCII decoding assumes file is UTF-8.
 *
 * Usage:
 *  var loader = new STLLoader();
 *  loader.load( './models/stl/slotted_disk.stl', function ( geometry ) {
 *    scene.add( new THREE.Mesh( geometry ) );
 *  });
 *
 * For binary STLs geometry might contain colors for vertices. To use it:
 *  // use the same code to load STL as above
 *  if (geometry.hasColors) {
 *    material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: THREE.VertexColors });
 *  } else { .... }
 *  var mesh = new THREE.Mesh( geometry, material );
 */

var STLLoader = function (manager) {
  this.manager = manager !== undefined ? manager : THREE.DefaultLoadingManager;
};

STLLoader.prototype = {
  constructor: STLLoader,

  load: function (url, onLoad, onProgress, onError) {
    var scope = this;

    var loader = new THREE.FileLoader(scope.manager);
    loader.setResponseType("arraybuffer");
    loader.load(
      url,
      function (text) {
        onLoad(scope.parse(text));
      },
      onProgress,
      onError
    );
  },

  parse: function (data) {
    var isBinary = function () {
      var expect, face_size, n_faces, reader;
      reader = new DataView(binData);
      face_size = (32 / 8) * 3 + (32 / 8) * 3 * 3 + 16 / 8;
      n_faces = reader.getUint32(80, true);
      expect = 80 + 32 / 8 + n_faces * face_size;

      if (expect === reader.byteLength) {
        return true;
      }

      // some binary files will have different size from expected,
      // checking characters higher than ASCII to confirm is binary
      var fileLength = reader.byteLength;
      for (var index = 0; index < fileLength; index++) {
        if (reader.getUint8(index, false) > 127) {
          return true;
        }
      }

      return false;
    };

    var binData = this.ensureBinary(data);

    return isBinary()
      ? this.parseBinary(binData)
      : this.parseASCII(this.ensureString(data));
  },

  parseBinary: function (data) {
    var reader = new DataView(data);
    var faces = reader.getUint32(80, true);

    var r,
      g,
      b,
      hasColors = false,
      colors;
    var defaultR, defaultG, defaultB, alpha;

    // process STL header
    // check for default color in header ("COLOR=rgba" sequence).

    for (var index = 0; index < 80 - 10; index++) {
      if (
        reader.getUint32(index, false) == 0x434f4c4f /*COLO*/ &&
        reader.getUint8(index + 4) == 0x52 /*'R'*/ &&
        reader.getUint8(index + 5) == 0x3d /*'='*/
      ) {
        hasColors = true;
        colors = [];

        defaultR = reader.getUint8(index + 6) / 255;
        defaultG = reader.getUint8(index + 7) / 255;
        defaultB = reader.getUint8(index + 8) / 255;
        alpha = reader.getUint8(index + 9) / 255;
      }
    }

    var dataOffset = 84;
    var faceLength = 12 * 4 + 2;

    var geometry = new THREE.BufferGeometry();

    var vertices = [];
    var normals = [];

    for (var face = 0; face < faces; face++) {
      var start = dataOffset + face * faceLength;
      var normalX = reader.getFloat32(start, true);
      var normalY = reader.getFloat32(start + 4, true);
      var normalZ = reader.getFloat32(start + 8, true);

      if (hasColors) {
        var packedColor = reader.getUint16(start + 48, true);

        if ((packedColor & 0x8000) === 0) {
          // facet has its own unique color

          r = (packedColor & 0x1f) / 31;
          g = ((packedColor >> 5) & 0x1f) / 31;
          b = ((packedColor >> 10) & 0x1f) / 31;
        } else {
          r = defaultR;
          g = defaultG;
          b = defaultB;
        }
      }

      for (var i = 1; i <= 3; i++) {
        var vertexstart = start + i * 12;

        vertices.push(reader.getFloat32(vertexstart, true));
        vertices.push(reader.getFloat32(vertexstart + 4, true));
        vertices.push(reader.getFloat32(vertexstart + 8, true));

        normals.push(normalX, normalY, normalZ);

        if (hasColors) {
          colors.push(r, g, b);
        }
      }
    }

    geometry.addAttribute(
      "position",
      new THREE.BufferAttribute(new Float32Array(vertices), 3)
    );
    geometry.addAttribute(
      "normal",
      new THREE.BufferAttribute(new Float32Array(normals), 3)
    );

    if (hasColors) {
      geometry.addAttribute(
        "color",
        new THREE.BufferAttribute(new Float32Array(colors), 3)
      );
      geometry.hasColors = true;
      geometry.alpha = alpha;
    }

    return geometry;
  },

  parseASCII: function (data) {
    var geometry,
      length,
      patternFace,
      patternNormal,
      patternVertex,
      result,
      text;
    geometry = new THREE.BufferGeometry();
    patternFace = /facet([\s\S]*?)endfacet/g;

    var vertices = [];
    var normals = [];

    var normal = new THREE.Vector3();

    while ((result = patternFace.exec(data)) !== null) {
      text = result[0];
      patternNormal = /normal[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g;

      while ((result = patternNormal.exec(text)) !== null) {
        normal.x = parseFloat(result[1]);
        normal.y = parseFloat(result[3]);
        normal.z = parseFloat(result[5]);
      }

      patternVertex = /vertex[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g;

      while ((result = patternVertex.exec(text)) !== null) {
        vertices.push(
          parseFloat(result[1]),
          parseFloat(result[3]),
          parseFloat(result[5])
        );
        normals.push(normal.x, normal.y, normal.z);
      }
    }

    geometry.addAttribute(
      "position",
      new THREE.BufferAttribute(new Float32Array(vertices), 3)
    );
    geometry.addAttribute(
      "normal",
      new THREE.BufferAttribute(new Float32Array(normals), 3)
    );

    return geometry;
  },

  ensureString: function (buf) {
    if (typeof buf !== "string") {
      var array_buffer = new Uint8Array(buf);
      var strArray = [];
      for (var i = 0; i < buf.byteLength; i++) {
        strArray.push(String.fromCharCode(array_buffer[i])); // implicitly assumes little-endian
      }
      return strArray.join("");
    } else {
      return buf;
    }
  },

  ensureBinary: function (buf) {
    if (typeof buf === "string") {
      var array_buffer = new Uint8Array(buf.length);
      for (var i = 0; i < buf.length; i++) {
        array_buffer[i] = buf.charCodeAt(i) & 0xff; // implicitly assumes little-endian
      }
      return array_buffer.buffer || array_buffer;
    } else {
      return buf;
    }
  },
};
