import Matrix3 from "../../Core/Matrix3.js";
import defined from "../../Core/defined.js";
import ShaderDestination from "../../Renderer/ShaderDestination.js";
import MetadataStageFS from "../../Shaders/Model/MetadataStageFS.js";
import MetadataStageVS from "../../Shaders/Model/MetadataStageVS.js";
import MetadataType from "../MetadataType.js";
import ModelUtility from "./ModelUtility.js";

/**
 * The metadata pipeline stage processes metadata properties from
 * EXT_structural_metadata and inserts them into a struct in the shader.
 * This struct will be used by {@link CustomShaderPipelineStage} to allow the
 * user to access metadata using {@link CustomShader}
 *
 * @namespace MetadataPipelineStage
 *
 * @private
 */
const MetadataPipelineStage = {
  name: "MetadataPipelineStage",

  STRUCT_ID_METADATA_VS: "MetadataVS",
  STRUCT_ID_METADATA_FS: "MetadataFS",
  STRUCT_NAME_METADATA: "Metadata",

  STRUCT_ID_METADATA_CLASS_VS: "MetadataClassVS",
  STRUCT_ID_METADATA_CLASS_FS: "MetadataClassFS",
  STRUCT_NAME_METADATA_CLASS: "MetadataClass",

  STRUCT_ID_METADATA_STATISTICS_VS: "MetadataStatisticsVS",
  STRUCT_ID_METADATA_STATISTICS_FS: "MetadataStatisticsFS",
  STRUCT_NAME_METADATA_STATISTICS: "MetadataStatistics",

  FUNCTION_ID_INITIALIZE_METADATA_VS: "initializeMetadataVS",
  FUNCTION_ID_INITIALIZE_METADATA_FS: "initializeMetadataFS",
  FUNCTION_SIGNATURE_INITIALIZE_METADATA:
    "void initializeMetadata(out Metadata metadata, out MetadataClass metadataClass, out MetadataStatistics metadataStatistics, ProcessedAttributes attributes)",
  FUNCTION_ID_SET_METADATA_VARYINGS: "setMetadataVaryings",
  FUNCTION_SIGNATURE_SET_METADATA_VARYINGS: "void setMetadataVaryings()",

  // Metadata class and statistics fields:
  // - some must be renamed to avoid reserved words
  // - some always have float/vec values, even for integer/ivec property types
  METADATA_CLASS_FIELDS: [
    { specName: "noData", shaderName: "noData" },
    { specName: "default", shaderName: "defaultValue" },
    { specName: "min", shaderName: "minValue" },
    { specName: "max", shaderName: "maxValue" },
  ],
  METADATA_STATISTICS_FIELDS: [
    { specName: "min", shaderName: "minValue" },
    { specName: "max", shaderName: "maxValue" },
    { specName: "mean", shaderName: "mean", type: "float" },
    { specName: "median", shaderName: "median" },
    {
      specName: "standardDeviation",
      shaderName: "standardDeviation",
      type: "float",
    },
    { specName: "variance", shaderName: "variance", type: "float" },
    { specName: "sum", shaderName: "sum" },
  ],
};

/**
 * Process a primitive. This modifies the following parts of the render
 * resources:
 * <ul>
 *   <li>Adds a Metadata struct to the shader</li>
 *   <li>If the primitive has structural metadata, properties are added to the Metadata struct</li>
 *   <li>dynamic functions are added to the shader to initialize the metadata properties</li>
 *   <li>Adds uniforms for property textures to the uniform map as needed</li>
 *   <li>Adds uniforms for offset/scale to the uniform map as needed</li>
 * </ul>
 * @param {PrimitiveRenderResources} renderResources The render resources for the primitive
 * @param {ModelComponents.Primitive} primitive The primitive to be rendered
 * @param {FrameState} frameState The frame state
 * @private
 */
MetadataPipelineStage.process = function (
  renderResources,
  primitive,
  frameState,
) {
  const { shaderBuilder, model } = renderResources;
  const { structuralMetadata = {}, content } = model;
  const statistics = content?.tileset.metadataExtension?.statistics;

  const propertyAttributesInfo = getPropertyAttributesInfo(
    structuralMetadata.propertyAttributes,
    primitive,
    statistics,
  );
  const propertyTexturesInfo = getPropertyTexturesInfo(
    structuralMetadata.propertyTextures,
    statistics,
  );

  // Declare <type>MetadataClass and <type>MetadataStatistics structs as needed
  const allPropertyInfos = propertyAttributesInfo.concat(propertyTexturesInfo);
  declareMetadataTypeStructs(shaderBuilder, allPropertyInfos);

  // Always declare the Metadata, MetadataClass, and MetadataStatistics structs
  // and the initializeMetadata() function, even if not used
  declareStructsAndFunctions(shaderBuilder);
  shaderBuilder.addVertexLines(MetadataStageVS);
  shaderBuilder.addFragmentLines(MetadataStageFS);

  for (let i = 0; i < propertyAttributesInfo.length; i++) {
    const info = propertyAttributesInfo[i];
    processPropertyAttributeProperty(renderResources, info);
  }
  for (let i = 0; i < propertyTexturesInfo.length; i++) {
    const info = propertyTexturesInfo[i];
    processPropertyTextureProperty(renderResources, info);
  }
};

/**
 * Collect info about all properties of all propertyAttributes, and
 * return as a flattened Array
 * @param {PropertyAttribute[]} propertyAttributes The PropertyAttributes with properties to be described
 * @param {ModelComponents.Primitive} primitive The primitive to be rendered
 * @param {object} [statistics] Statistics about the properties (if the model is from a 3DTiles tileset)
 * @returns {object[]} An array of objects containing information about each PropertyAttributeProperty
 * @private
 */
function getPropertyAttributesInfo(propertyAttributes, primitive, statistics) {
  if (!defined(propertyAttributes)) {
    return [];
  }
  return propertyAttributes.flatMap((propertyAttribute) =>
    getPropertyAttributeInfo(propertyAttribute, primitive, statistics),
  );
}

/**
 * Collect info about the properties of a single PropertyAttribute
 * @param {PropertyAttribute} propertyAttribute The PropertyAttribute with properties to be described
 * @param {ModelComponents.Primitive} primitive The primitive to be rendered
 * @param {object} [statistics] Statistics about the properties (if the model is from a 3DTiles tileset)
 * @returns {object[]} An array of objects containing information about each PropertyAttributeProperty
 * @private
 */
function getPropertyAttributeInfo(propertyAttribute, primitive, statistics) {
  const { getAttributeByName, getAttributeInfo, sanitizeGlslIdentifier } =
    ModelUtility;

  const classId = propertyAttribute.class.id;
  const classStatistics = statistics?.classes[classId];

  const propertiesArray = Object.entries(propertyAttribute.properties);
  const infoArray = new Array(propertiesArray.length);

  for (let i = 0; i < propertiesArray.length; i++) {
    const [propertyId, property] = propertiesArray[i];
    const modelAttribute = getAttributeByName(primitive, property.attribute);
    const { glslType, variableName } = getAttributeInfo(modelAttribute);

    infoArray[i] = {
      metadataVariable: sanitizeGlslIdentifier(propertyId),
      property,
      type: property.classProperty.type,
      glslType,
      variableName,
      propertyStatistics: classStatistics?.properties[propertyId],
      shaderDestination: ShaderDestination.BOTH,
    };
  }

  return infoArray;
}

/**
 * Collect info about all properties of all propertyTextures, and
 * return as a flattened Array
 * @param {PropertyTexture[]} propertyTextures The PropertyTextures with properties to be described
 * @param {object} [statistics] Statistics about the properties (if the model is from a 3DTiles tileset)
 * @returns {object[]} An array of objects containing information about each PropertyTextureProperty
 * @private
 */
function getPropertyTexturesInfo(propertyTextures, statistics) {
  if (!defined(propertyTextures)) {
    return [];
  }
  return propertyTextures.flatMap((propertyTexture) =>
    getPropertyTextureInfo(propertyTexture, statistics),
  );
}

/**
 * Collect info about the properties of a single PropertyTexture
 * @param {PropertyTexture} propertyTexture The PropertyTexture with properties to be described
 * @param {object} [statistics] Statistics about the properties (if the model is from a 3DTiles tileset)
 * @returns {object[]} An array of objects containing information about each PropertyTextureProperty
 * @private
 */
function getPropertyTextureInfo(propertyTexture, statistics) {
  const { sanitizeGlslIdentifier } = ModelUtility;

  const classId = propertyTexture.class.id;
  const classStatistics = statistics?.classes[classId];

  const propertiesArray = Object.entries(propertyTexture.properties).filter(
    ([id, property]) => property.isGpuCompatible(),
  );
  const infoArray = new Array(propertiesArray.length);

  for (let i = 0; i < propertiesArray.length; i++) {
    const [propertyId, property] = propertiesArray[i];

    infoArray[i] = {
      metadataVariable: sanitizeGlslIdentifier(propertyId),
      property,
      type: property.classProperty.type,
      glslType: property.getGlslType(),
      propertyStatistics: classStatistics?.properties[propertyId],
      shaderDestination: ShaderDestination.FRAGMENT,
    };
  }

  return infoArray;
}

/**
 * Declare <type>MetadataClass structs in the shader for each PropertyAttributeProperty and PropertyTextureProperty
 * @param {ShaderBuilder} shaderBuilder The shader builder for the primitive
 * @param {object[]} propertyInfos Information about the PropertyAttributeProperties and PropertyTextureProperties
 * @private
 */
function declareMetadataTypeStructs(shaderBuilder, propertyInfos) {
  const classTypes = new Set();
  const statisticsTypes = new Set();

  for (let i = 0; i < propertyInfos.length; i++) {
    const { type, glslType, propertyStatistics } = propertyInfos[i];
    classTypes.add(glslType);
    if (!defined(propertyStatistics)) {
      continue;
    }
    if (type !== MetadataType.ENUM) {
      statisticsTypes.add(glslType);
    }
  }

  const classFields = MetadataPipelineStage.METADATA_CLASS_FIELDS;
  for (const metadataType of classTypes) {
    const classStructName = `${metadataType}MetadataClass`;
    declareTypeStruct(classStructName, metadataType, classFields);
  }

  const statisticsFields = MetadataPipelineStage.METADATA_STATISTICS_FIELDS;
  for (const metadataType of statisticsTypes) {
    const statisticsStructName = `${metadataType}MetadataStatistics`;
    declareTypeStruct(statisticsStructName, metadataType, statisticsFields);
  }

  function declareTypeStruct(structName, type, fields) {
    shaderBuilder.addStruct(structName, structName, ShaderDestination.BOTH);

    for (let i = 0; i < fields.length; i++) {
      const { shaderName } = fields[i];
      const shaderType =
        fields[i].type === "float" ? convertToFloatComponents(type) : type;
      shaderBuilder.addStructField(structName, shaderType, shaderName);
    }
  }
}

const floatConversions = {
  int: "float",
  ivec2: "vec2",
  ivec3: "vec3",
  ivec4: "vec4",
};

/**
 * For a type with integer components, find a corresponding float-component type
 * @param {string} type The name of a GLSL type with integer components
 * @returns {string} The name of a GLSL type of the same dimension with float components, if available; otherwise the input type
 * @private
 */
function convertToFloatComponents(type) {
  const converted = floatConversions[type];
  return defined(converted) ? converted : type;
}

/**
 * Declare the main Metadata, MetadataClass, and MetadataStatistics structs
 * and the initializeMetadata() function.
 * @param {ShaderBuilder} shaderBuilder The shader builder for the primitive
 * @private
 */
function declareStructsAndFunctions(shaderBuilder) {
  // Declare the Metadata struct.
  shaderBuilder.addStruct(
    MetadataPipelineStage.STRUCT_ID_METADATA_VS,
    MetadataPipelineStage.STRUCT_NAME_METADATA,
    ShaderDestination.VERTEX,
  );
  shaderBuilder.addStruct(
    MetadataPipelineStage.STRUCT_ID_METADATA_FS,
    MetadataPipelineStage.STRUCT_NAME_METADATA,
    ShaderDestination.FRAGMENT,
  );

  // Declare the MetadataClass struct
  shaderBuilder.addStruct(
    MetadataPipelineStage.STRUCT_ID_METADATA_CLASS_VS,
    MetadataPipelineStage.STRUCT_NAME_METADATA_CLASS,
    ShaderDestination.VERTEX,
  );
  shaderBuilder.addStruct(
    MetadataPipelineStage.STRUCT_ID_METADATA_CLASS_FS,
    MetadataPipelineStage.STRUCT_NAME_METADATA_CLASS,
    ShaderDestination.FRAGMENT,
  );

  // Declare the MetadataStatistics struct
  shaderBuilder.addStruct(
    MetadataPipelineStage.STRUCT_ID_METADATA_STATISTICS_VS,
    MetadataPipelineStage.STRUCT_NAME_METADATA_STATISTICS,
    ShaderDestination.VERTEX,
  );
  shaderBuilder.addStruct(
    MetadataPipelineStage.STRUCT_ID_METADATA_STATISTICS_FS,
    MetadataPipelineStage.STRUCT_NAME_METADATA_STATISTICS,
    ShaderDestination.FRAGMENT,
  );

  // declare the initializeMetadata() function. The details may differ
  // between vertex and fragment shader
  shaderBuilder.addFunction(
    MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
    MetadataPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_METADATA,
    ShaderDestination.VERTEX,
  );
  shaderBuilder.addFunction(
    MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
    MetadataPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_METADATA,
    ShaderDestination.FRAGMENT,
  );

  // declare the setMetadataVaryings() function in the vertex shader only.
  shaderBuilder.addFunction(
    MetadataPipelineStage.FUNCTION_ID_SET_METADATA_VARYINGS,
    MetadataPipelineStage.FUNCTION_SIGNATURE_SET_METADATA_VARYINGS,
    ShaderDestination.VERTEX,
  );
}

/**
 * Update the shader for a single PropertyAttributeProperty
 * @param {PrimitiveRenderResources} renderResources The render resources for the primitive
 * @param {object} propertyInfo Info about the PropertyAttributeProperty
 * @private
 */
function processPropertyAttributeProperty(renderResources, propertyInfo) {
  addPropertyAttributePropertyMetadata(renderResources, propertyInfo);
  addPropertyMetadataClass(renderResources.shaderBuilder, propertyInfo);
  addPropertyMetadataStatistics(renderResources.shaderBuilder, propertyInfo);
}

/**
 * Add fields to the Metadata struct, and metadata value assignments to the
 * initializeMetadata function, for a PropertyAttributeProperty
 * @param {PrimitiveRenderResources} renderResources The render resources for the primitive
 * @param {object} propertyInfo Info about the PropertyAttributeProperty
 * @private
 */
function addPropertyAttributePropertyMetadata(renderResources, propertyInfo) {
  const { shaderBuilder } = renderResources;
  const { metadataVariable, property, glslType } = propertyInfo;

  const valueExpression = addValueTransformUniforms({
    valueExpression: `attributes.${propertyInfo.variableName}`,
    renderResources: renderResources,
    glslType: glslType,
    metadataVariable: metadataVariable,
    shaderDestination: ShaderDestination.BOTH,
    property: property,
  });

  // declare the struct field
  shaderBuilder.addStructField(
    MetadataPipelineStage.STRUCT_ID_METADATA_VS,
    glslType,
    metadataVariable,
  );
  shaderBuilder.addStructField(
    MetadataPipelineStage.STRUCT_ID_METADATA_FS,
    glslType,
    metadataVariable,
  );

  // assign the result to the metadata struct property.
  const initializationLine = `metadata.${metadataVariable} = ${valueExpression};`;
  shaderBuilder.addFunctionLines(
    MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
    [initializationLine],
  );
  shaderBuilder.addFunctionLines(
    MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
    [initializationLine],
  );
}

/**
 * Update the shader for a single PropertyTextureProperty
 * @param {PrimitiveRenderResources} renderResources The render resources for the primitive
 * @param {object[]} propertyInfo Info about the PropertyTextureProperty
 * @private
 */
function processPropertyTextureProperty(renderResources, propertyInfo) {
  addPropertyTexturePropertyMetadata(renderResources, propertyInfo);
  addPropertyMetadataClass(renderResources.shaderBuilder, propertyInfo);
  addPropertyMetadataStatistics(renderResources.shaderBuilder, propertyInfo);
}

/**
 * Add fields to the Metadata struct, and metadata value expressions to the
 * initializeMetadata function, for a PropertyTextureProperty
 * @param {PrimitiveRenderResources} renderResources The render resources for the primitive
 * @param {object} propertyInfo Info about the PropertyTextureProperty
 * @private
 */
function addPropertyTexturePropertyMetadata(renderResources, propertyInfo) {
  const { shaderBuilder, uniformMap } = renderResources;
  const { metadataVariable, glslType, property } = propertyInfo;

  const { texCoord, channels, index, texture, transform } =
    property.textureReader;
  const textureUniformName = `u_propertyTexture_${index}`;

  // Property texture properties may share the same physical texture, so only
  // add the texture uniform the first time we encounter it.
  if (!uniformMap.hasOwnProperty(textureUniformName)) {
    shaderBuilder.addUniform(
      "sampler2D",
      textureUniformName,
      ShaderDestination.FRAGMENT,
    );
    uniformMap[textureUniformName] = () => texture;
  }

  shaderBuilder.addStructField(
    MetadataPipelineStage.STRUCT_ID_METADATA_FS,
    glslType,
    metadataVariable,
  );

  // Get a GLSL expression for the texture coordinates of the property.
  // By default, this will be taken directly from the attributes.
  const texCoordVariable = `attributes.texCoord_${texCoord}`;
  let texCoordVariableExpression = texCoordVariable;

  // Check if the texture defines a `transform` from a `KHR_texture_transform`
  if (defined(transform) && !Matrix3.equals(transform, Matrix3.IDENTITY)) {
    // Add a uniform for the transformation matrix
    const transformUniformName = `${textureUniformName}Transform`;
    shaderBuilder.addUniform(
      "mat3",
      transformUniformName,
      ShaderDestination.FRAGMENT,
    );
    uniformMap[transformUniformName] = function () {
      return transform;
    };

    // Update the expression for the texture coordinates
    // with one that transforms the texture coordinates
    // with the transform matrix first
    texCoordVariableExpression = `vec2(${transformUniformName} * vec3(${texCoordVariable}, 1.0))`;
  }
  const valueExpression = `texture(${textureUniformName}, ${texCoordVariableExpression}).${channels}`;

  // Some types need an unpacking step or two. For example, since texture reads
  // are always normalized, UINT8 (not normalized) properties need to be
  // un-normalized in the shader.
  const unpackedValue = property.unpackInShader(valueExpression);

  const transformedValue = addValueTransformUniforms({
    valueExpression: unpackedValue,
    renderResources: renderResources,
    glslType: glslType,
    metadataVariable: metadataVariable,
    shaderDestination: ShaderDestination.FRAGMENT,
    property: property,
  });

  const initializationLine = `metadata.${metadataVariable} = ${transformedValue};`;
  shaderBuilder.addFunctionLines(
    MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
    [initializationLine],
  );
}

/**
 * Add fields to the MetadataClass struct, and metadataClass value expressions
 * to the initializeMetadata function, for a PropertyAttributeProperty or
 * PropertyTextureProperty
 * @param {ShaderBuilder} shaderBuilder The shader builder for the primitive
 * @param {object} propertyInfo Info about the PropertyAttributeProperty or PropertyTextureProperty
 * @private
 */
function addPropertyMetadataClass(shaderBuilder, propertyInfo) {
  const { classProperty } = propertyInfo.property;
  const { metadataVariable, glslType, shaderDestination } = propertyInfo;

  // Construct assignment statements to set values in the metadataClass struct
  const assignments = getStructAssignments(
    MetadataPipelineStage.METADATA_CLASS_FIELDS,
    classProperty,
    `metadataClass.${metadataVariable}`,
    glslType,
  );

  // Struct field: Prefix to get the appropriate <type>MetadataClass struct
  const metadataType = `${glslType}MetadataClass`;
  shaderBuilder.addStructField(
    MetadataPipelineStage.STRUCT_ID_METADATA_CLASS_FS,
    metadataType,
    metadataVariable,
  );
  shaderBuilder.addFunctionLines(
    MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
    assignments,
  );
  if (!ShaderDestination.includesVertexShader(shaderDestination)) {
    return;
  }
  shaderBuilder.addStructField(
    MetadataPipelineStage.STRUCT_ID_METADATA_CLASS_VS,
    metadataType,
    metadataVariable,
  );
  shaderBuilder.addFunctionLines(
    MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
    assignments,
  );
}

/**
 * Add fields to the MetadataStatistics struct, and metadataStatistics value
 * expressions to the initializeMetadata function, for a
 * PropertyAttributeProperty or PropertyTextureProperty
 * @param {ShaderBuilder} shaderBuilder The shader builder for the primitive
 * @param {object} propertyInfo Info about the PropertyAttributeProperty or PropertyTextureProperty
 * @private
 */
function addPropertyMetadataStatistics(shaderBuilder, propertyInfo) {
  const { propertyStatistics } = propertyInfo;
  if (!defined(propertyStatistics)) {
    return;
  }
  const { metadataVariable, type, glslType } = propertyInfo;
  if (type === MetadataType.ENUM) {
    // enums have an "occurrences" statistic which is not implemented yet
    return;
  }

  // Construct assignment statements to set values in the metadataStatistics struct
  const fields = MetadataPipelineStage.METADATA_STATISTICS_FIELDS;
  const struct = `metadataStatistics.${metadataVariable}`;
  const assignments = getStructAssignments(
    fields,
    propertyStatistics,
    struct,
    glslType,
  );

  // Struct field: Prefix to get the appropriate <type>MetadataStatistics struct
  const statisticsType = `${glslType}MetadataStatistics`;
  shaderBuilder.addStructField(
    MetadataPipelineStage.STRUCT_ID_METADATA_STATISTICS_FS,
    statisticsType,
    metadataVariable,
  );
  shaderBuilder.addFunctionLines(
    MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_FS,
    assignments,
  );
  if (!ShaderDestination.includesVertexShader(propertyInfo.shaderDestination)) {
    return;
  }
  shaderBuilder.addStructField(
    MetadataPipelineStage.STRUCT_ID_METADATA_STATISTICS_VS,
    statisticsType,
    metadataVariable,
  );
  shaderBuilder.addFunctionLines(
    MetadataPipelineStage.FUNCTION_ID_INITIALIZE_METADATA_VS,
    assignments,
  );
}

/**
 * Construct GLSL assignment statements to set metadata spec values in a struct
 * @param {object[]} fieldNames An object with the following properties:
 * @param {string} fieldNames[].specName The name of the property in the spec
 * @param {string} fieldNames[].shaderName The name of the property in the shader
 * @param {object} values A source of property values, keyed on fieldNames[].specName
 * @param {string} struct The name of the struct to which values will be assigned
 * @param {string} type The type of the values to be assigned
 * @returns {Array<{name: string, value: any}>} Objects containing the property name (in the shader) and a GLSL assignment statement for the property value
 * @private
 */
function getStructAssignments(fieldNames, values, struct, type) {
  function constructAssignment(field) {
    const value = values[field.specName];
    if (defined(value)) {
      return `${struct}.${field.shaderName} = ${type}(${value});`;
    }
  }
  return defined(values)
    ? fieldNames.map(constructAssignment).filter(defined)
    : [];
}

/**
 * Handle offset/scale transform for a property value
 * This wraps the GLSL value expression with a czm_valueTransform() call
 *
 * @param {object} options Object with the following properties:
 * @param {string} options.valueExpression The GLSL value expression without the transform
 * @param {string} options.metadataVariable The name of the GLSL variable that will contain the property value
 * @param {string} options.glslType The GLSL type of the variable
 * @param {ShaderDestination} options.shaderDestination Which shader(s) use this variable
 * @param {PrimitiveRenderResources} options.renderResources The render resources for this primitive
 * @param {(PropertyAttributeProperty|PropertyTextureProperty)} options.property The property from which the value is derived
 * @returns {string} A wrapped GLSL value expression
 * @private
 */
function addValueTransformUniforms(options) {
  const { valueExpression, property } = options;

  if (!property.hasValueTransform) {
    return valueExpression;
  }

  const metadataVariable = options.metadataVariable;
  const offsetUniformName = `u_${metadataVariable}_offset`;
  const scaleUniformName = `u_${metadataVariable}_scale`;

  const { shaderBuilder, uniformMap } = options.renderResources;
  const { glslType, shaderDestination } = options;
  shaderBuilder.addUniform(glslType, offsetUniformName, shaderDestination);
  shaderBuilder.addUniform(glslType, scaleUniformName, shaderDestination);

  const { offset, scale } = property;
  uniformMap[offsetUniformName] = () => offset;
  uniformMap[scaleUniformName] = () => scale;

  return `czm_valueTransform(${offsetUniformName}, ${scaleUniformName}, ${valueExpression})`;
}

export default MetadataPipelineStage;
