/**
   *
   * @param {ObservableArray.<ChargedParticle>} chargedParticles - only chargedParticles that active are in this array
   * @param {ModelViewTransform2} modelViewTransform
   * @param {Property.<boolean>} isVisibleProperty
   * @constructor
   */
  function ElectricPotentialWebGLNode( chargedParticles,
                                       modelViewTransform,
                                       isVisibleProperty ) {
    this.chargedParticles = chargedParticles;
    this.modelViewTransform = modelViewTransform;
    this.isVisibleProperty = isVisibleProperty;

    WebGLNode.call( this, ElectricPotentialPainter, {
      layerSplit: true // ensure we're on our own layer
    } );

    // Invalidate paint on a bunch of changes
    var invalidateSelfListener = this.invalidatePaint.bind( this );
    ChargesAndFieldsColorProfile.electricPotentialGridZeroProperty.link( invalidateSelfListener );
    ChargesAndFieldsColorProfile.electricPotentialGridSaturationPositiveProperty.link( invalidateSelfListener );
    ChargesAndFieldsColorProfile.electricPotentialGridSaturationNegativeProperty.link( invalidateSelfListener );
    isVisibleProperty.link( invalidateSelfListener ); // visibility change
    chargedParticles.addItemAddedListener( function( particle ) {
      particle.positionProperty.link( invalidateSelfListener );
    } ); // particle added
    chargedParticles.addItemRemovedListener( function( particle ) {
      invalidateSelfListener();
      particle.positionProperty.unlink( invalidateSelfListener );
    } ); // particle removed

    this.disposeElectricPotentialWebGLNode = function() {
      isVisibleProperty.unlink( invalidateSelfListener ); // visibility change
    };
  }
    getElectricPotentialColor: function( electricPotential, options ) {

      var finalColor; // {string} e.g. 'rgba(0,0,0,1)'
      var distance; // {number}  between 0 and 1

      // for positive electric potential
      if ( electricPotential > 0 ) {

        // clamped linear interpolation function, output lies between 0 and 1;
        distance = ELECTRIC_POTENTIAL_POSITIVE_LINEAR_FUNCTION( electricPotential );
        finalColor = this.interpolateRGBA(
          // {Color} color that corresponds to the Electric Potential being zero
          ChargesAndFieldsColorProfile.electricPotentialGridZeroProperty.get(),
          // {Color} color of Max Electric Potential
          ChargesAndFieldsColorProfile.electricPotentialGridSaturationPositiveProperty.get(),
          distance, // {number} distance must be between 0 and 1
          options );
      }
      // for negative (or zero) electric potential
      else {

        // clamped linear interpolation function, output lies between 0 and 1
        distance = ELECTRIC_POTENTIAL_NEGATIVE_LINEAR_FUNCTION( electricPotential );
        finalColor = this.interpolateRGBA(
          // {Color} color that corresponds to the lowest (i.e. negative) Electric Potential
          ChargesAndFieldsColorProfile.electricPotentialGridSaturationNegativeProperty.get(),
          // {Color} color that corresponds to the Electric Potential being zero zero
          ChargesAndFieldsColorProfile.electricPotentialGridZeroProperty.get(),
          distance, // {number} distance must be between 0 and 1
          options );
      }
      return finalColor;
    },
  /**
   * @constructor
   *
   * @param {ObservableArray.<ChargedParticle>} chargedParticles - only chargedParticles that active are in this array
   * @param {ModelViewTransform2} modelViewTransform
   * @param {Bounds2} modelBounds - The bounds in the model that need to be drawn
   * @param {Property.<boolean>} isVisibleProperty
   */
  function ElectricPotentialCanvasNode( chargedParticles,
                                        modelViewTransform,
                                        modelBounds,
                                        isVisibleProperty ) {

    CanvasNode.call( this, {
      canvasBounds: modelViewTransform.modelToViewBounds( modelBounds )
    } );

    this.chargeTracker = new ChargeTracker( chargedParticles );

    this.modelViewTransform = modelViewTransform;
    this.modelBounds = modelBounds;
    this.viewBounds = this.modelViewTransform.modelToViewBounds( modelBounds );
    this.isVisibleProperty = isVisibleProperty;

    // Invalidate paint on a bunch of changes
    var invalidateSelfListener = this.forceRepaint.bind( this );
    ChargesAndFieldsColorProfile.electricPotentialGridZeroProperty.link( invalidateSelfListener );
    ChargesAndFieldsColorProfile.electricPotentialGridSaturationPositiveProperty.link( invalidateSelfListener );
    ChargesAndFieldsColorProfile.electricPotentialGridSaturationNegativeProperty.link( invalidateSelfListener );
    isVisibleProperty.link( invalidateSelfListener ); // visibility change
    chargedParticles.addItemAddedListener( function( particle ) {
      particle.positionProperty.link( invalidateSelfListener );
    } ); // particle added
    chargedParticles.addItemRemovedListener( function( particle ) {
      invalidateSelfListener();
      particle.positionProperty.unlink( invalidateSelfListener );
    } ); // particle removed

    isVisibleProperty.linkAttribute( this, 'visible' );

    this.modelPositions = []; // {Array.<Vector2>}
    var width = modelBounds.width;
    var height = modelBounds.height;
    var numHorizontal = Math.ceil( width / ELECTRIC_POTENTIAL_SENSOR_SPACING );
    var numVertical = Math.ceil( height / ELECTRIC_POTENTIAL_SENSOR_SPACING );
    for ( var row = 0; row < numVertical; row++ ) {
      var y = modelBounds.minY + ( row + 0.5 ) * height / numVertical;

      for ( var col = 0; col < numHorizontal; col++ ) {
        var x = modelBounds.minX + ( col + 0.5 ) * width / numHorizontal;

        this.modelPositions.push( new Vector2( x, y ) );
      }
    }

    this.electricPotentials = new Float64Array( this.modelPositions.length ); // eslint-disable-line no-undef

    this.directCanvas = document.createElement( 'canvas' );
    this.directCanvas.width = numHorizontal;
    this.directCanvas.height = numVertical;
    this.directContext = this.directCanvas.getContext( '2d' );
    this.directCanvasDirty = true; // Store a dirty flag, in case there weren't charge changes detected

    this.imageData = this.directContext.getImageData( 0, 0, numHorizontal, numVertical );
    assert && assert( this.imageData.width === numHorizontal );
    assert && assert( this.imageData.height === numVertical );

    this.disposeElectricPotentialCanvasNode = function() {
      isVisibleProperty.unlink( invalidateSelfListener ); // visibility change
    };

  }
    updateElectricPotentials: function() {
      var kConstant = ChargesAndFieldsConstants.K_CONSTANT;

      var numChanges = this.chargeTracker.queue.length;

      for ( var i = 0; i < numChanges; i++ ) {
        var item = this.chargeTracker.queue[ i ];
        var oldPosition = item.oldPosition;
        var newPosition = item.newPosition;
        var charge = item.charge;

        for ( var j = 0; j < this.modelPositions.length; j++ ) {
          var position = this.modelPositions[ j ];
          var electricPotential = this.electricPotentials[ j ];

          if ( oldPosition ) {
            var oldDistance = position.distance( oldPosition );
            if ( oldDistance !== 0 ) {
              electricPotential -= charge * kConstant / oldDistance;
            }
          }

          if ( newPosition ) {
            var newDistance = position.distance( newPosition );
            if ( newDistance !== 0 ) {
              electricPotential += charge * kConstant / newDistance;
            }
          }

          this.electricPotentials[ j ] = electricPotential;
        }
      }

      this.chargeTracker.clear();

      // Update our direct canvas if necessary
      if ( numChanges || this.directCanvasDirty ) {
        var zeroColor = ChargesAndFieldsColorProfile.electricPotentialGridZeroProperty.get();
        var positiveColor = ChargesAndFieldsColorProfile.electricPotentialGridSaturationPositiveProperty.get();
        var negativeColor = ChargesAndFieldsColorProfile.electricPotentialGridSaturationNegativeProperty.get();
        var data = this.imageData.data;

        for ( var k = 0; k < this.electricPotentials.length; k++ ) {
          var value = this.electricPotentials[ k ] / 40; // mapped with special constant

          var extremeColor;
          if ( value > 0 ) {
            extremeColor = positiveColor;
          }
          else {
            value = -value;
            extremeColor = negativeColor;
          }
          value = Math.min( value, 1 ); // clamp to [0,1]

          var offset = 4 * k;
          data[ offset + 0 ] = extremeColor.r * value + zeroColor.r * ( 1 - value );
          data[ offset + 1 ] = extremeColor.g * value + zeroColor.g * ( 1 - value );
          data[ offset + 2 ] = extremeColor.b * value + zeroColor.b * ( 1 - value );
          data[ offset + 3 ] = 255 * ( extremeColor.a * value + zeroColor.a * ( 1 - value ) );
        }

        this.directContext.putImageData( this.imageData, 0, 0 );

        this.directCanvasDirty = false;
      }
    },
    paint: function( modelViewMatrix, projectionMatrix ) {
      var gl = this.gl;
      var clearShaderProgram = this.clearShaderProgram;
      var computeShaderProgram = this.computeShaderProgram;
      var displayShaderProgram = this.displayShaderProgram;

      // If we're not visible, clear everything and exit. Our layerSplit above guarantees this won't clear other
      // node's renderings.
      if ( !this.node.isVisibleProperty.get() ) {
        return WebGLNode.PAINTED_NOTHING;
      }

      // If our dimensions changed, resize our textures and reinitialize all of our potentials.
      if ( this.canvasWidth !== gl.canvas.width || this.canvasHeight !== gl.canvas.height ) {
        this.sizeTexture( this.currentTexture );
        this.sizeTexture( this.previousTexture );
        this.chargeTracker.rebuild();

        // clears the buffer to be used
        clearShaderProgram.use();
        gl.bindFramebuffer( gl.FRAMEBUFFER, this.framebuffer );
        gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.previousTexture, 0 );

        gl.bindBuffer( gl.ARRAY_BUFFER, this.vertexBuffer );
        gl.vertexAttribPointer( computeShaderProgram.attributeLocations.aPosition, 2, gl.FLOAT, false, 0, 0 );

        gl.drawArrays( gl.TRIANGLE_STRIP, 0, 4 );
        gl.bindFramebuffer( gl.FRAMEBUFFER, null );
        clearShaderProgram.unuse();
      }

      /*---------------------------------------------------------------------------*
       * Compute steps
       *----------------------------------------------------------------------------*/

      computeShaderProgram.use();

      gl.uniform2f( computeShaderProgram.uniformLocations.uCanvasSize, this.canvasWidth, this.canvasHeight );
      gl.uniform2f( computeShaderProgram.uniformLocations.uTextureSize, this.textureWidth, this.textureHeight );

      var matrixInverse = scratchInverseMatrix;
      var projectionMatrixInverse = scratchProjectionMatrix.set( projectionMatrix ).invert();
      matrixInverse.set( this.node.modelViewTransform.getInverse() ).multiplyMatrix( modelViewMatrix.inverted().multiplyMatrix( projectionMatrixInverse ) );
      gl.uniformMatrix3fv( computeShaderProgram.uniformLocations.uMatrixInverse, false, matrixInverse.copyToArray( scratchFloatArray ) );

      // do a draw call for each particle change
      for ( var i = 0; i < this.chargeTracker.queue.length; i++ ) {
        var item = this.chargeTracker.queue[ i ];

        // make future rendering output into currentTexture
        gl.bindFramebuffer( gl.FRAMEBUFFER, this.framebuffer );
        gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.currentTexture, 0 );

        // use our vertex buffer to say where to render (two triangles covering the screen)
        gl.bindBuffer( gl.ARRAY_BUFFER, this.vertexBuffer );
        gl.vertexAttribPointer( computeShaderProgram.attributeLocations.aPosition, 2, gl.FLOAT, false, 0, 0 );

        // make previous data from the other texture available to the shader
        gl.activeTexture( gl.TEXTURE0 );
        gl.bindTexture( gl.TEXTURE_2D, this.previousTexture );
        gl.uniform1i( computeShaderProgram.uniformLocations.uTexture, 0 );

        // make the positions available to the shader
        gl.uniform1f( computeShaderProgram.uniformLocations.uCharge, item.charge );
        if ( item.oldPosition ) {
          gl.uniform2f( computeShaderProgram.uniformLocations.uOldPosition, item.oldPosition.x, item.oldPosition.y );
        }
        else {
          gl.uniform2f( computeShaderProgram.uniformLocations.uOldPosition, 0, 0 );
        }
        if ( item.newPosition ) {
          gl.uniform2f( computeShaderProgram.uniformLocations.uNewPosition, item.newPosition.x, item.newPosition.y );
        }
        else {
          gl.uniform2f( computeShaderProgram.uniformLocations.uNewPosition, 0, 0 );
        }

        // tell the shader the type of change we are making
        var type = item.oldPosition ? ( item.newPosition ? TYPE_MOVE : TYPE_REMOVE ) : TYPE_ADD;
        gl.uniform1i( computeShaderProgram.uniformLocations.uType, type );
        // console.log( type );

        // actually draw it
        gl.drawArrays( gl.TRIANGLE_STRIP, 0, 4 );
        // make future rendering output go into our visual display
        gl.bindFramebuffer( gl.FRAMEBUFFER, null );

        // swap buffers (since currentTexture now has the most up-to-date info, we'll want to use it for reading)
        var tmp = this.currentTexture;
        this.currentTexture = this.previousTexture;
        this.previousTexture = tmp;
      }

      computeShaderProgram.unuse();

      /*---------------------------------------------------------------------------*
       * Display step
       *----------------------------------------------------------------------------*/

      displayShaderProgram.use();

      // tell the shader our colors / scale
      var zeroColor = ChargesAndFieldsColorProfile.electricPotentialGridZeroProperty.get();
      var positiveColor = ChargesAndFieldsColorProfile.electricPotentialGridSaturationPositiveProperty.get();
      var negativeColor = ChargesAndFieldsColorProfile.electricPotentialGridSaturationNegativeProperty.get();
      gl.uniform3f( displayShaderProgram.uniformLocations.uZeroColor, zeroColor.red / 255, zeroColor.green / 255, zeroColor.blue / 255 );
      gl.uniform3f( displayShaderProgram.uniformLocations.uPositiveColor, positiveColor.red / 255, positiveColor.green / 255, positiveColor.blue / 255 );
      gl.uniform3f( displayShaderProgram.uniformLocations.uNegativeColor, negativeColor.red / 255, negativeColor.green / 255, negativeColor.blue / 255 );
      gl.uniform2f( displayShaderProgram.uniformLocations.uScale, this.canvasWidth / this.textureWidth, this.canvasHeight / this.textureHeight );

      // data to draw 2 triangles that cover the screen
      gl.bindBuffer( gl.ARRAY_BUFFER, this.vertexBuffer );
      gl.vertexAttribPointer( displayShaderProgram.attributeLocations.aPosition, 2, gl.FLOAT, false, 0, 0 );

      // read from the most up-to-date texture (our potential data)
      gl.activeTexture( gl.TEXTURE0 );
      gl.bindTexture( gl.TEXTURE_2D, this.previousTexture );
      gl.uniform1i( displayShaderProgram.uniformLocations.uTexture, 0 );

      // actually draw it
      gl.drawArrays( gl.TRIANGLE_STRIP, 0, 4 );

      // release the texture
      gl.bindTexture( gl.TEXTURE_2D, null );

      displayShaderProgram.unuse();

      this.chargeTracker.clear();

      return WebGLNode.PAINTED_SOMETHING;
    },