AutodeskNamespace("Autodesk.ADN.Viewing.Tool");

Autodesk.ADN.Viewing.Tool.Snapper = function(viewer) {

  var _tool = this;

  var _viewer = viewer;

  var _names = ["snapper"];
  var _active = false;

  var _faceOverlayName = 'MeasureTool-snapper-face';
  var _vertexOverlayName = 'MeasureTool-snapper-vertex';
  var _edgeOverlayName = 'MeasureTool-snapper-edge';

  var _radius = 0.05;
  var _distanceToEdge = null;
  var _distanceToVertex = null;

  var _geomFace = null;
  var _geomEdge = null;
  var _geomVertex = null;
  var _snapNode = null;

  var _geomHighlighted = null; //  {"VERTEX": 0, "EDGE": 1, "FACE": 2}

  var _intersectPoint = null;
  var _faceNormal = null;

  var _isDragging = false;

  var _snappingNode = null;

  var _isSnapped = false;


  var _selectionFilter = [];

  this.isActive = function() {
    return _active;
  };

  this.getNames = function() {
    return _names;
  };

  this.getName = function() {
    return _names[0];
  };

  this.activate = function() {

    _active = true;
    _isDragging = false;
    _selectionFilter = [];

    loadCSS();
    createTooltip();
  };

  this.deactivate = function() {
    _active = false;
    this.destroy();
  };

  this.getFace = function() {
    return _geomFace;
  };

  this.getEdge = function() {
    return _geomEdge;
  };

  this.getVertex = function() {
    return _geomVertex;
  };

  this.getSnapNode = function() {
    return _snapNode;
  }

  this.getHighlightGeometry = function() {
    return _geomHighlighted;
  };

  this.getIntersectPoint = function() {
    return _intersectPoint;
  };

  this.getFaceNormal = function() {
    return _faceNormal;
  };

  this.getEndPointsInEdge = function(edge) {

    var vertices = edge.vertices;
    var endPoints = [];

    for (var i = 0; i < vertices.length; ++i) {

      var duplicate = false;

      for (var j = 0; j < vertices.length; ++j) {

        if (j !== i && vertices[j].equals(vertices[i])) {

          duplicate = true;
          break;
        }
      }

      if (!duplicate) {

        endPoints.push(vertices[i]);

      }
    }

    return endPoints;
  };

  this.isSnapped = function() {
    return _isSnapped;
  };

  this.isEqualWithPrecision = function(a, b) {

    if (a <= b + SNAP_PRECISION && a >= b - SNAP_PRECISION) {
      return true;
    }

    return false;
  };

  this.isEqualVectorsWithPrecision = function(v1, v2) {

    if (v1.x <= v2.x + SNAP_PRECISION && v1.x >= v2.x - SNAP_PRECISION && v1.y <= v2.y + SNAP_PRECISION && v1.y >= v2.y - SNAP_PRECISION
      && v1.z <= v2.z + SNAP_PRECISION && v1.z >= v2.z - SNAP_PRECISION) {

      return true;
    }

    return false;
  };

  this.isInverseVectorsWithPrecision = function(v1, v2) {

    if (v1.x <= -v2.x + SNAP_PRECISION && v1.x >= -v2.x - SNAP_PRECISION && v1.y <= -v2.y + SNAP_PRECISION && v1.y >= -v2.y - SNAP_PRECISION
      && v1.z <= -v2.z + SNAP_PRECISION && v1.z >= -v2.z - SNAP_PRECISION) {

      return true;
    }

    return false;
  };

  /**
   * 3D Snapping
   * @param result -Result of Hit Test.
   */
  this.snapping3D = function(result) {

    _snapNode = result.dbId;

    var face = result.face;
    var intersectPoint = result.intersectPoint;
    var fragIds;

    if (result.fragId.length === undefined) {
      fragIds = [result.fragId];
    } else {
      fragIds = result.fragId;
    }

    for (var fi = 0; fi < fragIds.length; ++fi) {

      var fragId = fragIds[fi];
      var mesh = _viewer.impl.getRenderProxy(_viewer.model, fragId);
      var geometry = mesh.geometry;

      _geomFace = this.faceSnapping(face, geometry);

      if (_geomFace) {

        _geomEdge = this.edgeSnapping(_geomFace, intersectPoint, mesh);

        _geomVertex = this.vertexSnapping(_geomEdge, intersectPoint);

        _geomFace.applyMatrix(mesh.matrixWorld);
        _geomEdge.applyMatrix(mesh.matrixWorld);
        _geomVertex.applyMatrix4(mesh.matrixWorld);

        _intersectPoint = intersectPoint.applyMatrix4(mesh.matrixWorld);

        var normalMatrix = new THREE.Matrix3().getNormalMatrix(mesh.matrixWorld);
        _faceNormal = face.normal.applyMatrix3(normalMatrix).normalize();

        // Determine which one should be drawn: face , edge or vertex
        //_radius = this.getDetectRadius(_intersectPoint);

        _isSnapped = true;

        if (_distanceToVertex < _detectRadius) {

          if(vertexSnap(_geomVertex))
            return;
        }

        if (_distanceToEdge < _detectRadius) {

          if(edgeSnap(_geomEdge))
            return;
        }

        faceSnap(_geomFace);

        break;
      }
    }
  };

  var _tooltipId = guid();

  var _snappedGeometry = null;
  var _snappedGeometryType = null;

  var _detectRadius = 0.1;

  var _onEdgeSnapped = null;
  var _onFaceSnapped = null;
  var _onVertexSnapped = null;
  var _onGeometrySelected = null;
  var _onSelectionCancelled = null;

  this.setDetectRadius = function(radius) {

    _detectRadius = radius;
  }

  this.addSelectionFilter = function (filter){

    if(Array.isArray(filter)){

      _selectionFilter = filter;
    }
    else {

      _selectionFilter.push(filter);
    }
  }

  this.removeSelectionFilter = function (filter) {

    if(Array.isArray(filter)){

      _selectionFilter = _.filter(
        _selectionFilter, function(item){
          return filter.indexOf(item) < 0;
        });
    }
    else {

      _selectionFilter = _.filter(
        _selectionFilter, function(item){
          return item != filter;
        });
    }
  }

  this.clearSelectionFilter = function () {

    _selectionFilter = [];
  }

  this.onVertexSnapped = function(callback) {

    _onVertexSnapped = callback;
  }

  this.onEdgeSnapped = function(callback) {

    _onEdgeSnapped = callback;
  }

  this.onFaceSnapped = function(callback) {

    _onFaceSnapped = callback;
  }

  this.onGeometrySelected = function(callback) {

    _onGeometrySelected = callback;
  }

  this.onSelectionCancelled = function(callback) {

    _onSelectionCancelled = callback;
  }

  function vertexSnap(geometry) {

    if(!_selectionFilter.length || _selectionFilter.indexOf('vertex') > -1) {

      _snappedGeometry = geometry;

      _snappedGeometryType = 'vertex';

      _tool.drawPoint(geometry);

      if(_onVertexSnapped) {

        _onVertexSnapped(geometry);
      }

      return true;
    }

    return false;
  }

  function edgeSnap(geometry) {

    if(!_selectionFilter.length || _selectionFilter.indexOf('edge') > -1) {

      _snappedGeometry = geometry;

      _snappedGeometryType = 'edge';

      _tool.drawLine(geometry);

      if(_onEdgeSnapped) {

        _onEdgeSnapped(geometry);
      }

      return true;
    }

    return false;
  }

  function faceSnap(geometry) {

    if(!_selectionFilter.length || _selectionFilter.indexOf('face') > -1) {

      _snappedGeometry = geometry;

      _snappedGeometryType = 'face';

      _tool.drawFace(geometry);

      if(_onFaceSnapped) {

        _onFaceSnapped(geometry);
      }

      return true;
    }

    return false;
  }

  function createTooltip() {

    var html = [

      '<div class="snapper-tooltip" id="' + _tooltipId + '">',
      '</div>',

    ].join('\n');

    $(viewer.container).append(html);
  }

  this.showTooltip = function(show, text) {

    $('#' + _tooltipId).text(text);

    $('#' + _tooltipId).css({
      'visibility': (show ? 'visible' : 'hidden')
    });
  }

  /////////////////////////////////////////////////////////
  // Generates random guid
  //
  /////////////////////////////////////////////////////////
  function guid() {

    var d = new Date().getTime();

    var guid = 'xxxx-xxxx-xxxx'.replace(
      /[xy]/g,
      function (c) {
        var r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16);
      });

    return guid;
  }

  /////////////////////////////////////////////////////////
  //
  //
  /////////////////////////////////////////////////////////
  function loadCSS() {

    var css = [

      'div.snapper-tooltip {',
        'position:absolute;',
        'font-family:arial;',
        'color:#3D4042;',
        'font-size:16px;',
        'visibility:hidden;',
        'pointer-events:none;',
      '}',

    ].join('\n');

    $('<style type="text/css">' + css + '</style>').appendTo('head');
  }

  /**
   * Find the closest face next to the cast ray
   * @param face - the intersect triangle of Hit Test.
   * @param geometry - the geometry of mesh
   */
  this.faceSnapping = function(face, geometry) {

    var vA = new THREE.Vector3();
    var vB = new THREE.Vector3();
    var vC = new THREE.Vector3();

    var geom = new THREE.Geometry();  //Geometry which includes all the triangles on the same plane.

    var attributes = geometry.attributes;

    if (attributes.index !== undefined) {

      var indices = attributes.index.array || geometry.ib;
      var positions = geometry.vb ? geometry.vb : attributes.position.array;
      var stride = geometry.vb ? geometry.vbstride : 3;
      var offsets = geometry.offsets;

      if ( !offsets || offsets.length === 0) {

        offsets = [{start: 0, count: indices.length, index: 0}];

      }

      for (var oi = 0; oi < offsets.length; ++oi) {

        var start = offsets[oi].start;
        var count = offsets[oi].count;
        var index = offsets[oi].index;

        for (var i = start; i < start + count; i += 3) {

          var a = index + indices[i];
          var b = index + indices[i + 1];
          var c = index + indices[i + 2];

          vA.set(
            positions[a * stride],
            positions[a * stride + 1],
            positions[a * stride + 2]
          );
          vB.set(
            positions[b * stride],
            positions[b * stride + 1],
            positions[b * stride + 2]
          );
          vC.set(
            positions[c * stride],
            positions[c * stride + 1],
            positions[c * stride + 2]
          );

          var faceNormal = THREE.Triangle.normal(vA, vB, vC);

          var va = new THREE.Vector3();
          va.set(
            positions[ face.a * stride ],
            positions[ face.a * stride + 1 ],
            positions[ face.a * stride + 2 ]
          );

          if (this.isEqualVectorsWithPrecision(faceNormal, face.normal) && this.isEqualWithPrecision(faceNormal.dot(vA), face.normal.dot(va)))
          {

            var vIndex = geom.vertices.length;

            geom.vertices.push(vA.clone());
            geom.vertices.push(vB.clone());
            geom.vertices.push(vC.clone());

            geom.faces.push(new THREE.Face3(vIndex, vIndex + 1, vIndex + 2));

          }
        }
      }
    }

    if (geom.vertices.length > 0) {

      return this.getTrianglesOnSameFace(geom, face, positions, stride);
    }
    else {

      return null;
    }
  };

  /**
   * Find triangles on the same face with the triangle intersected with the cast ray
   * @param geom -Geometry which includes all the triangles on the same plane.
   * @param face -Triangle which intersects with the cast ray.
   * @param positions -Positions of all vertices.
   * @param stride -Stride for the interleaved buffer.
   */
  this.getTrianglesOnSameFace = function(geom, face, positions, stride) {

    var isIncludeFace = false; // Check if the intersect face is in the mesh
    var vertexIndices = geom.vertices.slice();

    var va = new THREE.Vector3();
    va.set(
      positions[ face.a * stride ],
      positions[ face.a * stride + 1 ],
      positions[ face.a * stride + 2 ]
    );
    var vb = new THREE.Vector3();
    vb.set(
      positions[ face.b * stride ],
      positions[ face.b * stride + 1 ],
      positions[ face.b * stride + 2 ]
    );
    var vc = new THREE.Vector3();
    vc.set(
      positions[ face.c * stride ],
      positions[ face.c * stride + 1 ],
      positions[ face.c * stride + 2 ]
    );
    var intersectFace = new THREE.Geometry();
    intersectFace.vertices.push(va);
    intersectFace.vertices.push(vb);
    intersectFace.vertices.push(vc);
    intersectFace.faces.push(new THREE.Face3(0, 1, 2));

    var vCount = [];

    do {

      vCount = [];

      for (var j = 0; j < vertexIndices.length; j += 3) {

        // The triangle which is intersected with the ray
        if (vertexIndices[j].equals(va) && vertexIndices[j + 1].equals(vb) && vertexIndices[j + 2].equals(vc)) {

          isIncludeFace = true;
          vCount.push(j);
          continue;
        }

        for (var k = 0; k < intersectFace.vertices.length; k += 3) {

          // The triangles which are on the same face with the intersected triangle
          if (this.trianglesSharedEdge(vertexIndices[j], vertexIndices[j + 1], vertexIndices[j + 2],
              intersectFace.vertices[k], intersectFace.vertices[k + 1], intersectFace.vertices[k + 2])) {

            var vIndex = intersectFace.vertices.length;
            intersectFace.vertices.push(vertexIndices[j].clone());
            intersectFace.vertices.push(vertexIndices[j + 1].clone());
            intersectFace.vertices.push(vertexIndices[j + 2].clone());
            intersectFace.faces.push(new THREE.Face3(vIndex, vIndex + 1, vIndex + 2));

            vCount.push(j);
            break;
          }
        }
      }

      for (var ci = vCount.length - 1; ci >= 0; --ci) {

        vertexIndices.splice(vCount[ci], 3);

      }

    } while (vCount.length > 0);

    if (isIncludeFace) {
      return intersectFace;
    }
    else {
      return null;
    }

  };

  /**
   * Check if the two triangle share edge, the inputs are their vertices
   */
  this.trianglesSharedEdge = function(a1, a2, a3, b1, b2, b3) {

    var c1 = false;
    var c2 = false;
    var c3 = false;

    if (a1.equals(b1) || a1.equals(b2) || a1.equals(b3)) {
      c1 = true;
    }
    if (a2.equals(b1) || a2.equals(b2) || a2.equals(b3)) {
      c2 = true;
    }
    if (a3.equals(b1) || a3.equals(b2) || a3.equals(b3)) {
      c3 = true;
    }

    if (c1 & c2 || c1 & c3 || c2 & c3) {
      return true;
    }

    return false;
  };

  /**
   * Find the closest edge next to the intersect point
   * @param face -Face which is found by faceSnapping.
   * @param intersectPoint -IntersectPoint between cast ray and face.
   * @param mesh -The whole mesh of one fragment.
   */
  this.edgeSnapping = function(face, intersectPoint, mesh) {

    var lineGeom = new THREE.Geometry();
    var isEdge_12 = true;
    var isEdge_13 = true;
    var isEdge_23 = true;

    for (var i = 0; i < face.vertices.length; i += 3) {

      for (var j = 0; j < face.vertices.length; j += 3) {

        if ( i !== j ) {
          // Check edge 12
          if ((face.vertices[i].equals(face.vertices[j]) || face.vertices[i].equals(face.vertices[j + 1])
            || face.vertices[i].equals(face.vertices[j + 2]))
            && (face.vertices[i + 1].equals(face.vertices[j]) || face.vertices[i + 1].equals(face.vertices[j + 1])
            || face.vertices[i + 1].equals(face.vertices[j + 2]))) {

            isEdge_12 = false;

          }
          // Check edge 13
          if ((face.vertices[i].equals(face.vertices[j]) || face.vertices[i].equals(face.vertices[j + 1])
            || face.vertices[i].equals(face.vertices[j + 2]))
            && (face.vertices[i + 2].equals(face.vertices[j]) || face.vertices[i + 2].equals(face.vertices[j + 1])
            || face.vertices[i + 2].equals(face.vertices[j + 2]))) {

            isEdge_13 = false;

          }
          // Check edge 23
          if ((face.vertices[i + 1].equals(face.vertices[j]) || face.vertices[i + 1].equals(face.vertices[j + 1])
            || face.vertices[i + 1].equals(face.vertices[j + 2]))
            && (face.vertices[i + 2].equals(face.vertices[j]) || face.vertices[i + 2].equals(face.vertices[j + 1])
            || face.vertices[i + 2].equals(face.vertices[j + 2]))) {

            isEdge_23 = false;

          }
        }
      }

      if (isEdge_12) {

        lineGeom.vertices.push(face.vertices[i].clone());
        lineGeom.vertices.push(face.vertices[i + 1].clone());

      }
      if (isEdge_13) {

        lineGeom.vertices.push(face.vertices[i].clone());
        lineGeom.vertices.push(face.vertices[i + 2].clone());

      }
      if (isEdge_23) {

        lineGeom.vertices.push(face.vertices[i + 1].clone());
        lineGeom.vertices.push(face.vertices[i + 2].clone());

      }

      isEdge_12 = true;
      isEdge_13 = true;
      isEdge_23 = true;

    }

    //return lineGeom;

    var edgeGeom = new THREE.Geometry();
    var minDistIndex;
    var minDist = Number.MAX_VALUE;
    var matrix = new THREE.Matrix4();
    matrix.getInverse(mesh.matrixWorld);
    intersectPoint.applyMatrix4(matrix);

    for (var k = 0; k < lineGeom.vertices.length; k += 2) {

      var dist = this.distancePointToLine(intersectPoint, lineGeom.vertices[k], lineGeom.vertices[k + 1]);

      if (dist < minDist) {
        minDist = dist;
        minDistIndex = k;
      }

    }

    edgeGeom.vertices.push(lineGeom.vertices[ minDistIndex ].clone());
    edgeGeom.vertices.push(lineGeom.vertices[ minDistIndex + 1 ].clone());

    edgeGeom.vertices = this.getConnectedLineSegmentsOnSameLine(lineGeom, edgeGeom.vertices);

    _distanceToEdge = minDist;

    return edgeGeom;

  };

  this.distancePointToLine = function (point, lineStart, lineEnd) {

    var X0 = new THREE.Vector3();
    var X1 = new THREE.Vector3();
    var distance;
    var param;

    X0.subVectors(lineStart, point);
    X1.subVectors(lineEnd, lineStart);
    param = X0.dot(X1);
    X0.subVectors(lineEnd, lineStart);
    param = -param / X0.dot(X0);

    if (param < 0) {
      distance = point.distanceTo(lineStart);
    }
    else if (param > 1) {
      distance = point.distanceTo(lineEnd);
    }
    else {
      X0.subVectors(point, lineStart);
      X1.subVectors(point, lineEnd);
      X0.cross(X1);
      X1.subVectors(lineEnd, lineStart);

      distance = Math.sqrt(X0.dot(X0)) / Math.sqrt(X1.dot(X1));
    }

    return distance;
  };

  this.getConnectedLineSegmentsOnSameLine = function(lineGeom, edgeVertices) {

    var vertices = lineGeom.vertices.slice();
    var va = edgeVertices[0];
    var vb = edgeVertices[1];

    var vCount = [];

    do {

      vCount = [];

      for (var j = 0; j < vertices.length; j += 2) {

        // The line which has min distance to intersection point
        if (vertices[j].equals(va) && vertices[j + 1].equals(vb)) {

          continue;
        }

        for (var k = 0; k < edgeVertices.length; k += 2) {

          // The line segments which are connected on the same line
          if (vertices[j].equals(edgeVertices[k]) || vertices[j + 1].equals(edgeVertices[k]) ||
            vertices[j].equals(edgeVertices[k + 1]) || vertices[j + 1].equals(edgeVertices[k + 1])) {

            var V0 = new THREE.Vector3();
            var V1 = new THREE.Vector3();

            V0.subVectors(edgeVertices[k],  edgeVertices[k + 1]);
            V0.normalize();
            V1.subVectors(vertices[j],vertices[j + 1]);
            V1.normalize();

            //if (V0.equals(V1) || V0.equals(V1.negate())) {
            if (this.isEqualVectorsWithPrecision(V0, V1) || this.isInverseVectorsWithPrecision(V0, V1))
            {

              vCount.push(j);
              break;

            }
          }
        }
      }

      for (var ci = vCount.length - 1; ci >= 0; --ci) {

        edgeVertices.push(vertices[ vCount[ci] ]);
        edgeVertices.push(vertices[ vCount[ci] + 1 ]);
        vertices.splice(vCount[ci], 2);

      }

    } while (vCount.length > 0);

    return edgeVertices;

  };

  /**
   * Find the closest vertex next to the intersect point
   * @param edge -Edge which is found by edgeSnapping.
   * @param intersectPoint -IntersectPoint between cast ray and face.
   */
  this.vertexSnapping = function(edge, intersectPoint) {

    var minDist = Number.MAX_VALUE;
    var point;

    for (var i = 0; i < edge.vertices.length; ++i) {

      var dist = intersectPoint.distanceTo(edge.vertices[i]);

      if (dist < minDist - SNAP_PRECISION) {

        minDist = dist;
        point = edge.vertices[i].clone();

      }
    }

    _distanceToVertex = minDist;

    return point;
  };

  this.angleVector2 = function(vector) {

    if (vector.x > 0 && vector.y >= 0) {
      return Math.atan(vector.y / vector.x);
    }
    else if (vector.x >= 0 && vector.y < 0) {
      return Math.atan(vector.y / vector.x) + Math.PI * 2;
    }
    else if (vector.x < 0 && vector.y <= 0) {
      return Math.atan(vector.y / vector.x) + Math.PI;
    }
    else if (vector.x <= 0 && vector.y > 0) {
      return Math.atan(vector.y / vector.x) + Math.PI;
    }
    else{ // x = 0, y = 0
      return null;
    }
  };

  function GeometryCallback(viewer, snapper) {
    this.viewer = viewer;
    this.snapper = snapper;

    this.lineGeom = new THREE.Geometry();
    this.circularArc = null;
    this.circularArcCenter;
    this.ellipticalArc = null;

    this.minDist = Number.MAX_VALUE;
  }

  GeometryCallback.prototype.onLineSegment = function(x1, y1, x2, y2) {
    //stderr("line segment");
    var vertices = this.lineGeom.vertices;
    var v1 = new THREE.Vector3(x1, y1, 0);
    var v2 = new THREE.Vector3(x2, y2, 0);

    var intersectPoint = this.snapper.getIntersectPoint();
    var dist = this.snapper.distancePointToLine(intersectPoint, v1, v2);
    if (dist < this.minDist) {

      vertices.splice(0, 2, v1, v2);
      this.minDist = dist;
    }
  };

  GeometryCallback.prototype.onCircularArc = function(cx, cy, start, end, radius) {
    //stderr("circular arc");
    var intersectPoint = this.snapper.getIntersectPoint();
    var point = new THREE.Vector2(intersectPoint.x, intersectPoint.y);

    var center = new THREE.Vector2(cx, cy);
    var dist = point.distanceTo(center);
    point.sub(center);

    var angle = this.snapper.angleVector2(point);

    if (dist <= radius + 0.1 && dist >= radius - 0.1) {

      if (end > start && angle >= start && angle <= end) {
        var arc = new THREE.CircleGeometry(radius, 100, start, end - start);
      }
      else if (end < start && (angle >= start || angle <= end)) {
        var arc = new THREE.CircleGeometry(radius, 100, start, Math.PI * 2 - start + end);
      }
      else {
        return;
      }
      arc.vertices.splice(0, 1);
      this.circularArc = arc;
      this.circularArcCenter = new THREE.Vector3(cx, cy, 0);
    }
  };

  GeometryCallback.prototype.onEllipticalArc = function(cx, cy, start, end, major, minor, tilt) {
    //stderr("elliptical arc");
    //console.log("cx " + cx + " cy " + cy + " major " + major + " minor " + minor + " start " + start + " end " + end + " tilt " + tilt);
    var intersectPoint = this.snapper.getIntersectPoint();
    var point = new THREE.Vector2(intersectPoint.x, intersectPoint.y);

    var equation = (point.x - cx) * (point.x - cx) / (major * major) + (point.y - cy) * (point.y - cy) / (minor * minor);

    var center = new THREE.Vector2(cx, cy);
    point.sub(center);
    point.x *= minor;
    point.y *= major;
    var angle = this.snapper.angleVector2(point);

    if (end > Math.PI * 2) {
      end = Math.PI * 2;
    }

    if (equation <= 1 + 0.1 && equation >= 1 - 0.1) {

      if ((end > start && angle >= start && angle <= end) || (end < start && (angle >= start || angle <= end))){
        var curve = new THREE.EllipseCurve(cx, cy, major, minor, start, end, false);
        var path = new THREE.Path(curve.getPoints(50));
        var arc = path.createPointsGeometry(50);

        if (!this.isEqualWithPrecision(end - start, Math.PI * 2))
        {
          arc.vertices.pop();
        }
        this.ellipticalArc = arc;
      }
    }
  };


  this.snapping2D = function(result) {

    if (!result) {
      return;
    }

    var intersectPoint = result.intersectPoint;
    var fragIds = result.fragId;

    if (typeof fragIds === "undefined") {
      return;
    }
    else if (!Array.isArray(fragIds)) {
      fragIds = [fragIds];
    }

    _intersectPoint = intersectPoint;

    var gc = new GeometryCallback(_viewer, this);

    for (var fi = 0; fi < fragIds.length; ++fi) {

      var mesh = _viewer.impl.getRenderProxy(_viewer.model, fragIds[fi]);

      var vbr = new Autodesk.Viewing.Private.VertexBufferReader(mesh.geometry);
      vbr.enumGeomsForObject(result.dbId, gc);
    }

    if (gc.circularArc) {

      this.drawArc(gc.circularArc, gc.circularArcCenter);
      gc.circularArc = null;

      _geomVertex = gc.circularArcCenter;

      _geomHighlighted = SNAP_VERTEX;

      _isSnapped = true;
    }
    else if (gc.ellipticalArc) {

      this.drawArc(gc.ellipticalArc);
      gc.ellipticalArc = null;
    }
    else if (gc.lineGeom.vertices.length) {

      // Determine which one should be drawn: line segment or point
      _radius = this.getDetectRadius(intersectPoint);
      
      if (intersectPoint.distanceTo(gc.lineGeom.vertices[0]) < _radius) {

        _geomVertex = gc.lineGeom.vertices[0];
        this.drawPoint(_geomVertex);
        _geomHighlighted = SNAP_VERTEX;
      }
      else if (intersectPoint.distanceTo(gc.lineGeom.vertices[1]) < _radius) {

        _geomVertex = gc.lineGeom.vertices[1];
        this.drawPoint(_geomVertex);
        _geomHighlighted = SNAP_VERTEX;
      }
      else {

        _geomEdge = gc.lineGeom;
        this.drawLine(_geomEdge);
        _geomHighlighted = SNAP_EDGE;
      }

      _isSnapped = true;
    }
  };

  this.createOverlay = function(overlayName) {

    _viewer.impl.createOverlayScene(overlayName);
  };

  this.addOverlay = function(overlayName, mesh) {

    _viewer.impl.addOverlay(overlayName, mesh);
  };

  this.clearOverlay = function() {

    if (_viewer.impl.overlayScenes[_faceOverlayName]) {
      _viewer.impl.clearOverlay(_faceOverlayName);
    }

    if (_viewer.impl.overlayScenes[_vertexOverlayName]) {
      _viewer.impl.clearOverlay(_vertexOverlayName);
    }

    if (_viewer.impl.overlayScenes[_edgeOverlayName]) {
      _viewer.impl.clearOverlay(_edgeOverlayName);
    }
  };

  /**
   * Draw the planar face
   * @param geom -Geometry which needs to be draw.
   * @param mesh -Mesh which is loaded.
   */
  this.drawFace = function(geom) {

    this.createOverlay(_faceOverlayName);

    var planeColor = 0x00CC00;
    var planeOpacity = 0.5;

    var material = new THREE.MeshPhongMaterial({
      color: planeColor,
      ambient: planeColor,
      opacity: planeOpacity,
      transparent: true,
      depthTest: false,
      depthWrite: false,
      side: THREE.DoubleSide
    });

    //geom.attributes.index.array = new geom.attributes.index.array.constructor(indicesNew);
    var snapperPlane = new THREE.Mesh(geom, material, true);
    //snapperPlane.matrixWorld = mesh.matrixWorld;

    this.addOverlay(_faceOverlayName, snapperPlane);

  };

  this.drawLine = function(geom) {

    this.createOverlay(_edgeOverlayName);

    var material = new THREE.LineBasicMaterial({
      color: 0x00CC00,
      opacity: 0.5,
      linewidth: 5,
      depthTest: false,
      depthWrite: false
    });

    var line = new THREE.Line(geom, material, THREE.LinePieces);
    //line.applyMatrix(mesh.matrixWorld);

    this.addOverlay(_edgeOverlayName, line);
  };

  this.drawArc = function(geom, center) {

    this.createOverlay(_edgeOverlayName);

    var material = new THREE.LineBasicMaterial({
      color: 0x00CC00,
      opacity: 0.5,
      linewidth: 5,
      depthTest: false,
      depthWrite: false
    });

    var arc = new THREE.Line(geom, material);
    if (center) {
      arc.position.set(center.x, center.y, center.z);
    }

    this.addOverlay(_edgeOverlayName, arc);
  };

  this.drawPoint = function(point) {

    this.createOverlay(_vertexOverlayName);

    var planeColor = 0x00CC00;
    var planeOpacity = 0.5;

    var material = new THREE.MeshPhongMaterial({
      color: planeColor,
      ambient: planeColor,
      opacity: planeOpacity,
      transparent: true,
      depthTest: false,
      depthWrite: false
    });

    var pointMesh = new THREE.Mesh(new THREE.SphereGeometry(2.0), material);
    //point.applyMatrix4(mesh.matrixWorld);
    pointMesh.position.set(point.x, point.y, point.z);

    this.setPointScale(pointMesh);

    this.addOverlay(_vertexOverlayName, pointMesh);

  };

  this.setPointScale = function (pointMesh) {

    var pixelSize = 5;

    var navapi = _viewer.navigation;
    var camera = navapi.getCamera();

    var view = navapi.getEyeVector();
    var position = navapi.getPosition();

    var point = pointMesh.position.clone();

    var distance = camera.isPerspective ? point.sub(position).dot(view.normalize())
      : navapi.getEyeVector().length();

    var fov = navapi.getVerticalFov();
    var worldHeight = 2.0 * distance * Math.tan(THREE.Math.degToRad(fov * 0.5));

    var viewport = navapi.getScreenViewport();
    var devicePixelRatio = window.devicePixelRatio || 1;
    var scale = pixelSize * worldHeight / (viewport.height * devicePixelRatio);

    pointMesh.scale.x = scale;
    pointMesh.scale.y = scale;
    pointMesh.scale.z = scale;
  };

  this.updatePointScale = function() {

    var overlay = _viewer.impl.overlayScenes[_vertexOverlayName];
    if (overlay) {
      var scene = overlay.scene;

      for (var i = 0; i < scene.children.length; i++) {
        var pointMesh = scene.children[i];
        if (pointMesh) {

          this.setPointScale(pointMesh);
        }
      }
    }
  };

  this.getDetectRadius = function(point) {

    var pixelSize = 1.5;

    var navapi = _viewer.navigation;
    var camera = navapi.getCamera();

    var view = navapi.getEyeVector();
    var position = navapi.getPosition();

    var p = point.clone();

    var distance = camera.isPerspective ? p.sub(position).dot(view.normalize())
      : navapi.getEyeVector().length();

    var fov = navapi.getVerticalFov();
    var worldHeight = 2.0 * distance * Math.tan(THREE.Math.degToRad(fov * 0.5));

    var viewport = navapi.getScreenViewport();
    var devicePixelRatio = window.devicePixelRatio || 1;
    var radius = pixelSize * worldHeight / (viewport.height * devicePixelRatio);

    if (_viewer.impl.is2d) {
      radius *= 10;
    }

    return radius;
  };

  this.drawIntersectFace = function(face, positions, stride, mesh) {

    this.createOverlay();

    var va = new THREE.Vector3();
    va.set(
      positions[ face.a * stride ],
      positions[ face.a * stride + 1 ],
      positions[ face.a * stride + 2 ]
    );
    var vb = new THREE.Vector3();
    vb.set(
      positions[ face.b * stride ],
      positions[ face.b * stride + 1 ],
      positions[ face.b * stride + 2 ]
    );
    var vc = new THREE.Vector3();
    vc.set(
      positions[ face.c * stride ],
      positions[ face.c * stride + 1 ],
      positions[ face.c * stride + 2 ]
    );

    var intersectFace = new THREE.Geometry();
    intersectFace.vertices.push(va);
    intersectFace.vertices.push(vb);
    intersectFace.vertices.push(vc);
    intersectFace.faces.push(new THREE.Face3(0, 1, 2));

    var faceMesh = new THREE.Mesh(intersectFace, mesh.material, true);
    faceMesh.matrixWorld = mesh.matrixWorld;

    this.addOverlay(faceMesh);
  };

  this.handleWheelInput = function (delta) {
    this.updatePointScale();
    return false;
  };

  this.handleButtonDown = function (event, button) {

    _isDragging = true;

    if(_onGeometrySelected && _snappedGeometry) {

      _onGeometrySelected(
        _snappedGeometry,
        _snappedGeometryType);
    }

    return false;
  };

  this.handleButtonUp = function (event, button) {

    _isDragging = false;
    return false;
  };

  this.handleMouseMove = function (event) {

    $('#' + _tooltipId).css({
      'left' : event.canvasX + 'px',
      'top' : event.canvasY - 30 + 'px'
    });

    if (!_isDragging) {

      if (_snappingNode) {

        _viewer.impl.matman().highlightObject2D(_snappingNode.dbId, false);
      }

      _snappedGeometry = null;

      this.clearOverlay();

      _geomFace = null;
      _geomEdge = null;
      _geomVertex = null;

      _isSnapped = false;

      if (_viewer.impl.renderer().hasMRT()) {

        var result = _viewer.impl.snappingHitTest(event.canvasX, event.canvasY, false);
      }
      else {

        var result = _viewer.impl.hitTest(event.canvasX, event.canvasY, false);
      }

      if (result && result.intersectPoint) {

        // 3D Snapping
        if (result.face) {

          this.snapping3D(result);
        }
        // 2D Snapping
        else {

          this.snapping2D(result);
        }
      }
    }
    return false;
  };

  this.handleKeyDown = function(event, keyCode) {

    //ESCAPE
    if(keyCode == 27) {

      if(_onSelectionCancelled) {
        _onSelectionCancelled();
        _onSelectionCancelled = null;
      }

      return true;
    }

    return false;
  };

  this.destroy = function() {

    $('#' + _tooltipId).remove();

    this.clearOverlay();

    _viewer.impl.removeOverlayScene(_faceOverlayName);
    _viewer.impl.removeOverlayScene(_vertexOverlayName);
    _viewer.impl.removeOverlayScene(_edgeOverlayName);

  };
}