text: /*global define*/
  define([
          '../Core/BoundingSphere',
          '../Core/Cartesian3',
          '../Core/defined',
          '../Core/DeveloperError',
          '../Core/IndexDatatype',
          '../Core/OrientedBoundingBox',
          '../Core/Request',
          '../Core/TileProviderError',
          '../Renderer/Buffer',
          '../Renderer/BufferUsage',
          '../Renderer/VertexArray',
          '../ThirdParty/when',
          './TerrainState',
          './TileBoundingRegion'
      ], function(
          BoundingSphere,
          Cartesian3,
          defined,
          DeveloperError,
          IndexDatatype,
          OrientedBoundingBox,
          Request,
          TileProviderError,
          Buffer,
          BufferUsage,
          VertexArray,
          when,
          TerrainState,
          TileBoundingRegion) {
      'use strict';
  
      /**
       * Manages details of the terrain load or upsample process.
       *
       * @alias TileTerrain
       * @constructor
       * @private
       *
       * @param {TerrainData} [upsampleDetails.data] The terrain data being upsampled.
       * @param {Number} [upsampleDetails.x] The X coordinate of the tile being upsampled.
       * @param {Number} [upsampleDetails.y] The Y coordinate of the tile being upsampled.
       * @param {Number} [upsampleDetails.level] The level coordinate of the tile being upsampled.
       */
      function TileTerrain(upsampleDetails) {
          /**
           * The current state of the terrain in the terrain processing pipeline.
           * @type {TerrainState}
           * @default {@link TerrainState.UNLOADED}
           */
          this.state = TerrainState.UNLOADED;
          this.data = undefined;
          this.mesh = undefined;
          this.vertexArray = undefined;
          this.upsampleDetails = upsampleDetails;
      }
  
      TileTerrain.prototype.freeResources = function() {
          this.state = TerrainState.UNLOADED;
          this.data = undefined;
          this.mesh = undefined;
  
          if (defined(this.vertexArray)) {
              var indexBuffer = this.vertexArray.indexBuffer;
  
              this.vertexArray.destroy();
              this.vertexArray = undefined;
  
              if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) {
                  --indexBuffer.referenceCount;
                  if (indexBuffer.referenceCount === 0) {
                      indexBuffer.destroy();
                  }
              }
          }
      };
  
      TileTerrain.prototype.publishToTile = function(tile) {
          var surfaceTile = tile.data;
  
          var mesh = this.mesh;
          Cartesian3.clone(mesh.center, surfaceTile.center);
          surfaceTile.minimumHeight = mesh.minimumHeight;
          surfaceTile.maximumHeight = mesh.maximumHeight;
          surfaceTile.boundingSphere3D = BoundingSphere.clone(mesh.boundingSphere3D, surfaceTile.boundingSphere3D);
          surfaceTile.orientedBoundingBox = OrientedBoundingBox.clone(mesh.orientedBoundingBox, surfaceTile.orientedBoundingBox);
          surfaceTile.tileBoundingRegion.minimumHeight = mesh.minimumHeight;
          surfaceTile.tileBoundingRegion.maximumHeight = mesh.maximumHeight;
  
          tile.data.occludeePointInScaledSpace = Cartesian3.clone(mesh.occludeePointInScaledSpace, surfaceTile.occludeePointInScaledSpace);
      };
  
      TileTerrain.prototype.processLoadStateMachine = function(frameState, terrainProvider, x, y, level, distance) {
          if (this.state === TerrainState.UNLOADED) {
              requestTileGeometry(this, terrainProvider, x, y, level, distance);
          }
  
          if (this.state === TerrainState.RECEIVED) {
              transform(this, frameState, terrainProvider, x, y, level);
          }
  
          if (this.state === TerrainState.TRANSFORMED) {
              createResources(this, frameState.context, terrainProvider, x, y, level);
          }
      };
  
      function requestTileGeometry(tileTerrain, terrainProvider, x, y, level, distance) {
          function success(terrainData) {
              tileTerrain.data = terrainData;
              tileTerrain.state = TerrainState.RECEIVED;
          }
  
          function failure() {
              // Initially assume failure.  handleError may retry, in which case the state will
              // change to RECEIVING or UNLOADED.
              tileTerrain.state = TerrainState.FAILED;
  
              var message = 'Failed to obtain terrain tile X: ' + x + ' Y: ' + y + ' Level: ' + level + '.';
              terrainProvider._requestError = TileProviderError.handleError(
                  terrainProvider._requestError,
                  terrainProvider,
                  terrainProvider.errorEvent,
                  message,
                  x, y, level,
                  doRequest);
          }
  
          function doRequest() {
              // Request the terrain from the terrain provider.
              var request = new Request({
                  distance : distance
              });
              tileTerrain.data = terrainProvider.requestTileGeometry(x, y, level, request);
  
              // If the request method returns undefined (instead of a promise), the request
              // has been deferred.
              if (defined(tileTerrain.data)) {
                  tileTerrain.state = TerrainState.RECEIVING;
  
                  when(tileTerrain.data, success, failure);
              } else {
                  // Deferred - try again later.
                  tileTerrain.state = TerrainState.UNLOADED;
              }
          }
  
          doRequest();
      }
  
      TileTerrain.prototype.processUpsampleStateMachine = function(frameState, terrainProvider, x, y, level) {
          if (this.state === TerrainState.UNLOADED) {
              var upsampleDetails = this.upsampleDetails;
  
              //>>includeStart('debug', pragmas.debug);
              if (!defined(upsampleDetails)) {
                  throw new DeveloperError('TileTerrain cannot upsample unless provided upsampleDetails.');
              }
              //>>includeEnd('debug');
  
              var sourceData = upsampleDetails.data;
              var sourceX = upsampleDetails.x;
              var sourceY = upsampleDetails.y;
              var sourceLevel = upsampleDetails.level;
  
              this.data = sourceData.upsample(terrainProvider.tilingScheme, sourceX, sourceY, sourceLevel, x, y, level);
              if (!defined(this.data)) {
                  // The upsample request has been deferred - try again later.
                  return;
              }
  
              this.state = TerrainState.RECEIVING;
  
              var that = this;
              when(this.data, function(terrainData) {
                  that.data = terrainData;
                  that.state = TerrainState.RECEIVED;
              }, function() {
                  that.state = TerrainState.FAILED;
              });
          }
  
          if (this.state === TerrainState.RECEIVED) {
              transform(this, frameState, terrainProvider, x, y, level);
          }
  
          if (this.state === TerrainState.TRANSFORMED) {
              createResources(this, frameState.context, terrainProvider, x, y, level);
          }
      };
  
      function transform(tileTerrain, frameState, terrainProvider, x, y, level) {
          var tilingScheme = terrainProvider.tilingScheme;
  
          var terrainData = tileTerrain.data;
          var meshPromise = terrainData.createMesh(tilingScheme, x, y, level, frameState.terrainExaggeration);
  
          if (!defined(meshPromise)) {
              // Postponed.
              return;
          }
  
          tileTerrain.state = TerrainState.TRANSFORMING;
  
          when(meshPromise, function(mesh) {
              tileTerrain.mesh = mesh;
              tileTerrain.state = TerrainState.TRANSFORMED;
          }, function() {
              tileTerrain.state = TerrainState.FAILED;
          });
      }
  
      function createResources(tileTerrain, context, terrainProvider, x, y, level) {
          var typedArray = tileTerrain.mesh.vertices;
          var buffer = Buffer.createVertexBuffer({
              context : context,
              typedArray : typedArray,
              usage : BufferUsage.STATIC_DRAW
          });
          var attributes = tileTerrain.mesh.encoding.getAttributes(buffer);
  
          var indexBuffers = tileTerrain.mesh.indices.indexBuffers || {};
          var indexBuffer = indexBuffers[context.id];
          if (!defined(indexBuffer) || indexBuffer.isDestroyed()) {
              var indices = tileTerrain.mesh.indices;
              var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ?  IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT;
              indexBuffer = Buffer.createIndexBuffer({
                  context : context,
                  typedArray : indices,
                  usage : BufferUsage.STATIC_DRAW,
                  indexDatatype : indexDatatype
              });
              indexBuffer.vertexArrayDestroyable = false;
              indexBuffer.referenceCount = 1;
              indexBuffers[context.id] = indexBuffer;
              tileTerrain.mesh.indices.indexBuffers = indexBuffers;
          } else {
              ++indexBuffer.referenceCount;
          }
  
          tileTerrain.vertexArray = new VertexArray({
              context : context,
              attributes : attributes,
              indexBuffer : indexBuffer
          });
  
          tileTerrain.state = TerrainState.READY;
      }
  
      return TileTerrain;
  });
  