text: /*global define*/
  define([
      '../ThirdParty/when',
      './BoundingSphere',
      './Cartesian2',
      './Cartesian3',
      './Check',
      './defaultValue',
      './defined',
      './defineProperties',
      './DeveloperError',
      './GeographicTilingScheme',
      './HeightmapTessellator',
      './IndexDatatype',
      './Intersections2D',
      './Math',
      './OrientedBoundingBox',
      './QuantizedMeshTerrainData',
      './Rectangle',
      './TaskProcessor',
      './TerrainEncoding',
      './TerrainMesh'
  ], function(
      when,
      BoundingSphere,
      Cartesian2,
      Cartesian3,
      Check,
      defaultValue,
      defined,
      defineProperties,
      DeveloperError,
      GeographicTilingScheme,
      HeightmapTessellator,
      IndexDatatype,
      Intersections2D,
      CesiumMath,
      OrientedBoundingBox,
      QuantizedMeshTerrainData,
      Rectangle,
      TaskProcessor,
      TerrainEncoding,
      TerrainMesh) {
      'use strict';
  
      /**
       * Terrain data for a single tile from a Google Earth Enterprise server.
       *
       * @alias GoogleEarthEnterpriseTerrainData
       * @constructor
       *
       * @param {Object} options Object with the following properties:
       * @param {ArrayBuffer} options.buffer The buffer containing terrain data.
       * @param {Number} [options.childTileMask=15] A bit mask indicating which of this tile's four children exist.
       *                 If a child's bit is set, geometry will be requested for that tile as well when it
       *                 is needed.  If the bit is cleared, the child tile is not requested and geometry is
       *                 instead upsampled from the parent.  The bit values are as follows:
       *                 <table>
       *                  <tr><th>Bit Position</th><th>Bit Value</th><th>Child Tile</th></tr>
       *                  <tr><td>0</td><td>1</td><td>Southwest</td></tr>
       *                  <tr><td>1</td><td>2</td><td>Southeast</td></tr>
       *                  <tr><td>2</td><td>4</td><td>Northeast</td></tr>
       *                  <tr><td>3</td><td>8</td><td>Northwest</td></tr>
       *                 </table>
       * @param {Boolean} [options.createdByUpsampling=false] True if this instance was created by upsampling another instance;
       *                  otherwise, false.
       *
       *
       * @example
       * var buffer = ...
       * var childTileMask = ...
       * var terrainData = new Cesium.GoogleEarthEnterpriseTerrainData({
       *   buffer : heightBuffer,
       *   childTileMask : childTileMask
       * });
       *
       * @see TerrainData
       * @see HeightTerrainData
       * @see QuantizedMeshTerrainData
       */
      function GoogleEarthEnterpriseTerrainData(options) {
          options = defaultValue(options, defaultValue.EMPTY_OBJECT);
          //>>includeStart('debug', pragmas.debug);
          Check.typeOf.object('options.buffer', options.buffer);
          //>>includeEnd('debug');
  
          this._buffer = options.buffer;
  
          // Convert from google layout to layout of other providers
          // 3 2 -> 2 3
          // 0 1 -> 0 1
          var googleChildTileMask = defaultValue(options.childTileMask, 15);
          var childTileMask = googleChildTileMask & 3; // Bottom row is identical
          childTileMask |= (googleChildTileMask & 4) ? 8 : 0; // NE
          childTileMask |= (googleChildTileMask & 8) ? 4 : 0; // NW
  
          this._childTileMask = childTileMask;
  
          this._createdByUpsampling = defaultValue(options.createdByUpsampling, false);
  
          this._skirtHeight = undefined;
          this._bufferType = this._buffer.constructor;
          this._mesh = undefined;
          this._minimumHeight = undefined;
          this._maximumHeight = undefined;
          this._vertexCountWithoutSkirts = undefined;
          this._skirtIndex = undefined;
      }
  
      defineProperties(GoogleEarthEnterpriseTerrainData.prototype, {
          /**
           * The water mask included in this terrain data, if any.  A water mask is a rectangular
           * Uint8Array or image where a value of 255 indicates water and a value of 0 indicates land.
           * Values in between 0 and 255 are allowed as well to smoothly blend between land and water.
           * @memberof GoogleEarthEnterpriseTerrainData.prototype
           * @type {Uint8Array|Image|Canvas}
           */
          waterMask : {
              get : function() {
                  return undefined;
              }
          }
      });
  
      var taskProcessor = new TaskProcessor('createVerticesFromGoogleEarthEnterpriseBuffer');
      
      var nativeRectangleScratch = new Rectangle();
      var rectangleScratch = new Rectangle();
  
      /**
       * Creates a {@link TerrainMesh} from this terrain data.
       *
       * @private
       *
       * @param {TilingScheme} tilingScheme The tiling scheme to which this tile belongs.
       * @param {Number} x The X coordinate of the tile for which to create the terrain data.
       * @param {Number} y The Y coordinate of the tile for which to create the terrain data.
       * @param {Number} level The level of the tile for which to create the terrain data.
       * @param {Number} [exaggeration=1.0] The scale used to exaggerate the terrain.
       * @returns {Promise.<TerrainMesh>|undefined} A promise for the terrain mesh, or undefined if too many
       *          asynchronous mesh creations are already in progress and the operation should
       *          be retried later.
       */
      GoogleEarthEnterpriseTerrainData.prototype.createMesh = function(tilingScheme, x, y, level, exaggeration) {
          //>>includeStart('debug', pragmas.debug);
          Check.typeOf.object('tilingScheme', tilingScheme);
          Check.typeOf.number('x', x);
          Check.typeOf.number('y', y);
          Check.typeOf.number('level', level);
          //>>includeEnd('debug');
  
          var ellipsoid = tilingScheme.ellipsoid;
          tilingScheme.tileXYToNativeRectangle(x, y, level, nativeRectangleScratch);
          tilingScheme.tileXYToRectangle(x, y, level, rectangleScratch);
          exaggeration = defaultValue(exaggeration, 1.0);
  
          // Compute the center of the tile for RTC rendering.
          var center = ellipsoid.cartographicToCartesian(Rectangle.center(rectangleScratch));
  
          var levelZeroMaxError = 40075.16; // From Google's Doc
          var thisLevelMaxError = levelZeroMaxError / (1 << level);
          this._skirtHeight = Math.min(thisLevelMaxError * 8.0, 1000.0);
  
          var verticesPromise = taskProcessor.scheduleTask({
              buffer : this._buffer,
              nativeRectangle : nativeRectangleScratch,
              rectangle : rectangleScratch,
              relativeToCenter : center,
              ellipsoid : ellipsoid,
              skirtHeight : this._skirtHeight,
              exaggeration : exaggeration,
              includeWebMercatorT : true
          });
  
          if (!defined(verticesPromise)) {
              // Postponed
              return undefined;
          }
  
          var that = this;
          return verticesPromise
              .then(function(result) {
                  that._mesh = new TerrainMesh(
                      center,
                      new Float32Array(result.vertices),
                      new Uint16Array(result.indices),
                      result.minimumHeight,
                      result.maximumHeight,
                      result.boundingSphere3D,
                      result.occludeePointInScaledSpace,
                      result.numberOfAttributes,
                      result.orientedBoundingBox,
                      TerrainEncoding.clone(result.encoding),
                      exaggeration);
  
                  that._vertexCountWithoutSkirts = result.vertexCountWithoutSkirts;
                  that._skirtIndex = result.skirtIndex;
                  that._minimumHeight = result.minimumHeight;
                  that._maximumHeight = result.maximumHeight;
  
                  // Free memory received from server after mesh is created.
                  that._buffer = undefined;
                  return that._mesh;
              });
      };
  
      /**
       * Computes the terrain height at a specified longitude and latitude.
       *
       * @param {Rectangle} rectangle The rectangle covered by this terrain data.
       * @param {Number} longitude The longitude in radians.
       * @param {Number} latitude The latitude in radians.
       * @returns {Number} The terrain height at the specified position.  If the position
       *          is outside the rectangle, this method will extrapolate the height, which is likely to be wildly
       *          incorrect for positions far outside the rectangle.
       */
      GoogleEarthEnterpriseTerrainData.prototype.interpolateHeight = function(rectangle, longitude, latitude) {
          var u = CesiumMath.clamp((longitude - rectangle.west) / rectangle.width, 0.0, 1.0);
          var v = CesiumMath.clamp((latitude - rectangle.south) / rectangle.height, 0.0, 1.0);
  
          if (!defined(this._mesh)) {
              return interpolateHeight(this, u, v, rectangle);
          }
  
          return interpolateMeshHeight(this, u, v);
      };
  
      var upsampleTaskProcessor = new TaskProcessor('upsampleQuantizedTerrainMesh');
  
      /**
       * Upsamples this terrain data for use by a descendant tile.  The resulting instance will contain a subset of the
       * height samples in this instance, interpolated if necessary.
       *
       * @param {TilingScheme} tilingScheme The tiling scheme of this terrain data.
       * @param {Number} thisX The X coordinate of this tile in the tiling scheme.
       * @param {Number} thisY The Y coordinate of this tile in the tiling scheme.
       * @param {Number} thisLevel The level of this tile in the tiling scheme.
       * @param {Number} descendantX The X coordinate within the tiling scheme of the descendant tile for which we are upsampling.
       * @param {Number} descendantY The Y coordinate within the tiling scheme of the descendant tile for which we are upsampling.
       * @param {Number} descendantLevel The level within the tiling scheme of the descendant tile for which we are upsampling.
       * @returns {Promise.<HeightmapTerrainData>|undefined} A promise for upsampled heightmap terrain data for the descendant tile,
       *          or undefined if too many asynchronous upsample operations are in progress and the request has been
       *          deferred.
       */
      GoogleEarthEnterpriseTerrainData.prototype.upsample = function(tilingScheme, thisX, thisY, thisLevel, descendantX, descendantY, descendantLevel) {
          //>>includeStart('debug', pragmas.debug);
          Check.typeOf.object('tilingScheme', tilingScheme);
          Check.typeOf.number('thisX', thisX);
          Check.typeOf.number('thisY', thisY);
          Check.typeOf.number('thisLevel', thisLevel);
          Check.typeOf.number('descendantX', descendantX);
          Check.typeOf.number('descendantY', descendantY);
          Check.typeOf.number('descendantLevel', descendantLevel);
          var levelDifference = descendantLevel - thisLevel;
          if (levelDifference > 1) {
              throw new DeveloperError('Upsampling through more than one level at a time is not currently supported.');
          }
          //>>includeEnd('debug');
  
          var mesh = this._mesh;
          if (!defined(this._mesh)) {
              return undefined;
          }
  
          var isEastChild = thisX * 2 !== descendantX;
          var isNorthChild = thisY * 2 === descendantY;
  
          var ellipsoid = tilingScheme.ellipsoid;
          var childRectangle = tilingScheme.tileXYToRectangle(descendantX, descendantY, descendantLevel);
  
          var upsamplePromise = upsampleTaskProcessor.scheduleTask({
              vertices : mesh.vertices,
              vertexCountWithoutSkirts : this._vertexCountWithoutSkirts,
              indices : mesh.indices,
              skirtIndex : this._skirtIndex,
              encoding : mesh.encoding,
              minimumHeight : this._minimumHeight,
              maximumHeight : this._maximumHeight,
              isEastChild : isEastChild,
              isNorthChild : isNorthChild,
              childRectangle : childRectangle,
              ellipsoid : ellipsoid,
              exaggeration : mesh.exaggeration
          });
  
          if (!defined(upsamplePromise)) {
              // Postponed
              return undefined;
          }
  
          var that = this;
          return upsamplePromise
              .then(function(result) {
                  var quantizedVertices = new Uint16Array(result.vertices);
                  var indicesTypedArray = IndexDatatype.createTypedArray(quantizedVertices.length / 3, result.indices);
  
                  var skirtHeight = that._skirtHeight;
  
                  // Use QuantizedMeshTerrainData since we have what we need already parsed
                  return new QuantizedMeshTerrainData({
                      quantizedVertices : quantizedVertices,
                      indices : indicesTypedArray,
                      minimumHeight : result.minimumHeight,
                      maximumHeight : result.maximumHeight,
                      boundingSphere : BoundingSphere.clone(result.boundingSphere),
                      orientedBoundingBox : OrientedBoundingBox.clone(result.orientedBoundingBox),
                      horizonOcclusionPoint : Cartesian3.clone(result.horizonOcclusionPoint),
                      westIndices : result.westIndices,
                      southIndices : result.southIndices,
                      eastIndices : result.eastIndices,
                      northIndices : result.northIndices,
                      westSkirtHeight : skirtHeight,
                      southSkirtHeight : skirtHeight,
                      eastSkirtHeight : skirtHeight,
                      northSkirtHeight : skirtHeight,
                      childTileMask : 0,
                      createdByUpsampling : true
                  });
              });
      };
  
      /**
       * Determines if a given child tile is available, based on the
       * {@link HeightmapTerrainData.childTileMask}.  The given child tile coordinates are assumed
       * to be one of the four children of this tile.  If non-child tile coordinates are
       * given, the availability of the southeast child tile is returned.
       *
       * @param {Number} thisX The tile X coordinate of this (the parent) tile.
       * @param {Number} thisY The tile Y coordinate of this (the parent) tile.
       * @param {Number} childX The tile X coordinate of the child tile to check for availability.
       * @param {Number} childY The tile Y coordinate of the child tile to check for availability.
       * @returns {Boolean} True if the child tile is available; otherwise, false.
       */
      GoogleEarthEnterpriseTerrainData.prototype.isChildAvailable = function(thisX, thisY, childX, childY) {
          //>>includeStart('debug', pragmas.debug);
          Check.typeOf.number('thisX', thisX);
          Check.typeOf.number('thisY', thisY);
          Check.typeOf.number('childX', childX);
          Check.typeOf.number('childY', childY);
          //>>includeEnd('debug');
  
          var bitNumber = 2; // northwest child
          if (childX !== thisX * 2) {
              ++bitNumber; // east child
          }
          if (childY !== thisY * 2) {
              bitNumber -= 2; // south child
          }
  
          return (this._childTileMask & (1 << bitNumber)) !== 0;
      };
  
      /**
       * Gets a value indicating whether or not this terrain data was created by upsampling lower resolution
       * terrain data.  If this value is false, the data was obtained from some other source, such
       * as by downloading it from a remote server.  This method should return true for instances
       * returned from a call to {@link HeightmapTerrainData#upsample}.
       *
       * @returns {Boolean} True if this instance was created by upsampling; otherwise, false.
       */
      GoogleEarthEnterpriseTerrainData.prototype.wasCreatedByUpsampling = function() {
          return this._createdByUpsampling;
      };
  
      var texCoordScratch0 = new Cartesian2();
      var texCoordScratch1 = new Cartesian2();
      var texCoordScratch2 = new Cartesian2();
      var barycentricCoordinateScratch = new Cartesian3();
  
      function interpolateMeshHeight(terrainData, u, v) {
          var mesh = terrainData._mesh;
          var vertices = mesh.vertices;
          var encoding = mesh.encoding;
          var indices = mesh.indices;
  
          for (var i = 0, len = indices.length; i < len; i += 3) {
              var i0 = indices[i];
              var i1 = indices[i + 1];
              var i2 = indices[i + 2];
  
              var uv0 = encoding.decodeTextureCoordinates(vertices, i0, texCoordScratch0);
              var uv1 = encoding.decodeTextureCoordinates(vertices, i1, texCoordScratch1);
              var uv2 = encoding.decodeTextureCoordinates(vertices, i2, texCoordScratch2);
  
              var barycentric = Intersections2D.computeBarycentricCoordinates(u, v, uv0.x, uv0.y, uv1.x, uv1.y, uv2.x, uv2.y, barycentricCoordinateScratch);
              if (barycentric.x >= -1e-15 && barycentric.y >= -1e-15 && barycentric.z >= -1e-15) {
                  var h0 = encoding.decodeHeight(vertices, i0);
                  var h1 = encoding.decodeHeight(vertices, i1);
                  var h2 = encoding.decodeHeight(vertices, i2);
                  return barycentric.x * h0 + barycentric.y * h1 + barycentric.z * h2;
              }
          }
  
          // Position does not lie in any triangle in this mesh.
          return undefined;
      }
  
      var sizeOfUint16 = Uint16Array.BYTES_PER_ELEMENT;
      var sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT;
      var sizeOfInt32 = Int32Array.BYTES_PER_ELEMENT;
      var sizeOfFloat = Float32Array.BYTES_PER_ELEMENT;
      var sizeOfDouble = Float64Array.BYTES_PER_ELEMENT;
  
      function interpolateHeight(terrainData, u, v, rectangle) {
          var buffer = terrainData._buffer;
          var quad = 0; // SW
          var uStart = 0.0;
          var vStart = 0.0;
          if (v > 0.5) { // Upper row
              if (u > 0.5) { // NE
                  quad = 2;
                  uStart = 0.5;
              } else { // NW
                  quad = 3;
              }
              vStart = 0.5;
          } else if (u > 0.5) { // SE
              quad = 1;
              uStart = 0.5;
          }
  
          var dv = new DataView(buffer);
          var offset = 0;
          for (var q = 0; q < quad; ++q) {
              offset += dv.getUint32(offset, true);
              offset += sizeOfUint32;
          }
          offset += sizeOfUint32; // Skip length of quad
          offset += 2 * sizeOfDouble; // Skip origin
  
          // Read sizes
          var xSize = CesiumMath.toRadians(dv.getFloat64(offset, true) * 180.0);
          offset += sizeOfDouble;
          var ySize = CesiumMath.toRadians(dv.getFloat64(offset, true) * 180.0);
          offset += sizeOfDouble;
  
          // Samples per quad
          var xScale = rectangle.width / xSize / 2;
          var yScale = rectangle.height / ySize / 2;
  
          // Number of points
          var numPoints = dv.getInt32(offset, true);
          offset += sizeOfInt32;
  
          // Number of faces
          var numIndices = dv.getInt32(offset, true) * 3;
          offset += sizeOfInt32;
  
          offset += sizeOfInt32; // Skip Level
  
          var uBuffer = new Array(numPoints);
          var vBuffer = new Array(numPoints);
          var heights = new Array(numPoints);
          for (var i = 0; i < numPoints; ++i) {
              uBuffer[i] = uStart + (dv.getUint8(offset++) * xScale);
              vBuffer[i] = vStart + (dv.getUint8(offset++) * yScale);
  
              // Height is stored in units of (1/EarthRadius) or (1/6371010.0)
              heights[i] = (dv.getFloat32(offset, true) * 6371010.0);
              offset += sizeOfFloat;
          }
  
          var indices = new Array(numIndices);
          for (i = 0; i < numIndices; ++i) {
              indices[i] = dv.getUint16(offset, true);
              offset += sizeOfUint16;
          }
  
          for (i = 0; i < numIndices; i += 3) {
              var i0 = indices[i];
              var i1 = indices[i + 1];
              var i2 = indices[i + 2];
  
              var u0 = uBuffer[i0];
              var u1 = uBuffer[i1];
              var u2 = uBuffer[i2];
  
              var v0 = vBuffer[i0];
              var v1 = vBuffer[i1];
              var v2 = vBuffer[i2];
  
              var barycentric = Intersections2D.computeBarycentricCoordinates(u, v, u0, v0, u1, v1, u2, v2, barycentricCoordinateScratch);
              if (barycentric.x >= -1e-15 && barycentric.y >= -1e-15 && barycentric.z >= -1e-15) {
                  return barycentric.x * heights[i0] +
                         barycentric.y * heights[i1] +
                         barycentric.z * heights[i2];
              }
          }
  
          // Position does not lie in any triangle in this mesh.
          return undefined;
      }
  
      return GoogleEarthEnterpriseTerrainData;
  });
  