// Copyright 2020-2024 CesiumGS, Inc. and Contributors

#include "CesiumEncodedMetadataUtility.h"
#include "CesiumEncodedMetadataComponent.h"
#include "CesiumFeatureIdAttribute.h"
#include "CesiumFeatureIdTexture.h"
#include "CesiumLifetime.h"
#include "CesiumMetadataPrimitive.h"
#include "CesiumModelMetadata.h"
#include "CesiumPropertyArray.h"
#include "CesiumPropertyArrayBlueprintLibrary.h"
#include "CesiumPropertyTable.h"
#include "CesiumPropertyTableProperty.h"
#include "CesiumPropertyTexture.h"
#include "CesiumRuntime.h"
#include "Containers/Map.h"
#include "PixelFormat.h"
#include "TextureResource.h"
#include "UnrealMetadataConversions.h"
#include <CesiumGltf/FeatureIdTextureView.h>
#include <CesiumGltf/PropertyTexturePropertyView.h>
#include <CesiumGltf/PropertyTextureView.h>
#include <CesiumUtility/Tracing.h>
#include <unordered_map>

using namespace CesiumTextureUtility;

PRAGMA_DISABLE_DEPRECATION_WARNINGS

namespace CesiumEncodedMetadataUtility {

namespace {

struct EncodedPixelFormat {
  EPixelFormat format;
  int32_t bytesPerChannel;
  int32_t channels;
};

// TODO: consider picking better pixel formats when they are available for the
// current platform.
EncodedPixelFormat getPixelFormat(
    ECesiumMetadataPackedGpuType_DEPRECATED type,
    int64 componentCount,
    bool isNormalized) {

  switch (type) {
  case ECesiumMetadataPackedGpuType_DEPRECATED::Uint8_DEPRECATED:
    switch (componentCount) {
    case 1:
      return {
          isNormalized ? EPixelFormat::PF_R8 : EPixelFormat::PF_R8_UINT,
          1,
          1};
    case 2:
    case 3:
    case 4:
      return {
          isNormalized ? EPixelFormat::PF_R8G8B8A8
                       : EPixelFormat::PF_R8G8B8A8_UINT,
          1,
          4};
    default:
      return {EPixelFormat::PF_Unknown, 0};
    }
  case ECesiumMetadataPackedGpuType_DEPRECATED::Float_DEPRECATED:
    switch (componentCount) {
    case 1:
      return {EPixelFormat::PF_R32_FLOAT, 4, 1};
    case 2:
    case 3:
    case 4:
      // Note this is ABGR
      return {EPixelFormat::PF_A32B32G32R32F, 4, 4};
    }
  default:
    return {EPixelFormat::PF_Unknown, 0, 0};
  }
}
} // namespace

EncodedMetadataFeatureTable encodeMetadataFeatureTableAnyThreadPart(
    const FFeatureTableDescription& featureTableDescription,
    const FCesiumPropertyTable& featureTable) {

  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeFeatureTable)

  EncodedMetadataFeatureTable encodedFeatureTable;

  int64 featureCount =
      UCesiumPropertyTableBlueprintLibrary::GetPropertyTableCount(featureTable);

  const TMap<FString, FCesiumPropertyTableProperty>& properties =
      UCesiumPropertyTableBlueprintLibrary::GetProperties(featureTable);

  encodedFeatureTable.encodedProperties.Reserve(properties.Num());
  for (const auto& pair : properties) {
    const FCesiumPropertyTableProperty& property = pair.Value;

    const FPropertyDescription* pExpectedProperty =
        featureTableDescription.Properties.FindByPredicate(
            [&key = pair.Key](const FPropertyDescription& expectedProperty) {
              return key == expectedProperty.Name;
            });

    if (!pExpectedProperty) {
      continue;
    }

    FCesiumMetadataValueType trueType =
        UCesiumPropertyTablePropertyBlueprintLibrary::GetValueType(property);
    bool isArray = trueType.bIsArray;
    bool isNormalized =
        UCesiumPropertyTablePropertyBlueprintLibrary::IsNormalized(property);

    int64 componentCount;
    if (isArray) {
      componentCount =
          UCesiumPropertyTablePropertyBlueprintLibrary::GetArraySize(property);
    } else {
      componentCount = 1;
    }

    int32 expectedComponentCount = 1;
    switch (pExpectedProperty->Type) {
    // case ECesiumPropertyType::Scalar:
    //  expectedComponentCount = 1;
    //  break;
    case ECesiumPropertyType_DEPRECATED::Vec2_DEPRECATED:
      expectedComponentCount = 2;
      break;
    case ECesiumPropertyType_DEPRECATED::Vec3_DEPRECATED:
      expectedComponentCount = 3;
      break;
    case ECesiumPropertyType_DEPRECATED::Vec4_DEPRECATED:
      expectedComponentCount = 4;
    };

    if (expectedComponentCount != componentCount) {
      UE_LOG(
          LogCesium,
          Warning,
          TEXT("Unexpected component count in feature table property."));
      continue;
    }

    // Coerce the true type into the expected gpu component type.
    ECesiumMetadataPackedGpuType_DEPRECATED gpuType =
        ECesiumMetadataPackedGpuType_DEPRECATED::None_DEPRECATED;
    if (pExpectedProperty->ComponentType ==
        ECesiumPropertyComponentType_DEPRECATED::Uint8_DEPRECATED) {
      gpuType = ECesiumMetadataPackedGpuType_DEPRECATED::Uint8_DEPRECATED;
    } else /*if (expected type is float)*/ {
      gpuType = ECesiumMetadataPackedGpuType_DEPRECATED::Float_DEPRECATED;
    }

    if (pExpectedProperty->Normalized != isNormalized) {
      if (isNormalized) {
        UE_LOG(
            LogCesium,
            Warning,
            TEXT("Unexpected normalization in feature table property."));
      } else {
        UE_LOG(
            LogCesium,
            Warning,
            TEXT("Feature table property not normalized as expected"));
      }
      continue;
    }

    // Only support normalization of uint8 for now
    if (isNormalized &&
        trueType.ComponentType != ECesiumMetadataComponentType::Uint8) {
      UE_LOG(
          LogCesium,
          Warning,
          TEXT(
              "Feature table property has unexpected type for normalization, only normalization of Uint8 is supported."));
      continue;
    }

    EncodedPixelFormat encodedFormat =
        getPixelFormat(gpuType, componentCount, isNormalized);

    if (encodedFormat.format == EPixelFormat::PF_Unknown) {
      UE_LOG(
          LogCesium,
          Warning,
          TEXT(
              "Unable to determine a suitable GPU format for this feature table property."));
      continue;
    }

    TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodePropertyArray)

    EncodedMetadataProperty& encodedProperty =
        encodedFeatureTable.encodedProperties.Emplace_GetRef();
    encodedProperty.name =
        "FTB_" + featureTableDescription.Name + "_" + pair.Key;

    int64 floorSqrtFeatureCount = glm::sqrt(featureCount);
    int64 ceilSqrtFeatureCount =
        (floorSqrtFeatureCount * floorSqrtFeatureCount == featureCount)
            ? floorSqrtFeatureCount
            : (floorSqrtFeatureCount + 1);

    CesiumUtility::IntrusivePointer<CesiumGltf::ImageAsset> pImage =
        new CesiumGltf::ImageAsset();
    pImage->bytesPerChannel = encodedFormat.bytesPerChannel;
    pImage->channels = encodedFormat.channels;
    pImage->compressedPixelFormat = CesiumGltf::GpuCompressedPixelFormat::NONE;
    pImage->height = pImage->width = ceilSqrtFeatureCount;
    pImage->pixelData.resize(size_t(
        pImage->width * pImage->height * pImage->channels *
        pImage->bytesPerChannel));

    if (isArray) {
      switch (gpuType) {
      case ECesiumMetadataPackedGpuType_DEPRECATED::Uint8_DEPRECATED: {
        uint8* pWritePos = reinterpret_cast<uint8*>(pImage->pixelData.data());
        int64_t pixelSize =
            encodedFormat.channels * encodedFormat.bytesPerChannel;
        for (int64 i = 0; i < featureCount; ++i) {
          FCesiumPropertyArray arrayProperty =
              UCesiumPropertyTablePropertyBlueprintLibrary::GetArray(
                  property,
                  i);
          for (int64 j = 0; j < componentCount; ++j) {
            *(pWritePos + j) =
                UCesiumPropertyArrayBlueprintLibrary::GetByte(arrayProperty, j);
          }
          pWritePos += pixelSize;
        }
      } break;
      case ECesiumMetadataPackedGpuType_DEPRECATED::Float_DEPRECATED: {
        uint8* pWritePos = reinterpret_cast<uint8*>(pImage->pixelData.data());
        int64_t pixelSize =
            encodedFormat.channels * encodedFormat.bytesPerChannel;
        for (int64 i = 0; i < featureCount; ++i) {
          FCesiumPropertyArray arrayProperty =
              UCesiumPropertyTablePropertyBlueprintLibrary::GetArray(
                  property,
                  i);
          // Floats are encoded backwards (e.g., ABGR)
          float* pWritePosF =
              reinterpret_cast<float*>(pWritePos + pixelSize) - 1;
          for (int64 j = 0; j < componentCount; ++j) {
            *pWritePosF = UCesiumPropertyArrayBlueprintLibrary::GetFloat(
                arrayProperty,
                j);
            --pWritePosF;
          }
          pWritePos += pixelSize;
        }
      } break;
      }
    } else {
      switch (gpuType) {
      case ECesiumMetadataPackedGpuType_DEPRECATED::Uint8_DEPRECATED: {
        uint8* pWritePos = reinterpret_cast<uint8*>(pImage->pixelData.data());
        for (int64 i = 0; i < featureCount; ++i) {
          *pWritePos = UCesiumPropertyTablePropertyBlueprintLibrary::GetByte(
              property,
              i);
          ++pWritePos;
        }
      } break;
      case ECesiumMetadataPackedGpuType_DEPRECATED::Float_DEPRECATED: {
        float* pWritePosF = reinterpret_cast<float*>(pImage->pixelData.data());
        for (int64 i = 0; i < featureCount; ++i) {
          *pWritePosF = UCesiumPropertyTablePropertyBlueprintLibrary::GetFloat(
              property,
              i);
          ++pWritePosF;
        }
      } break;
      }
    }

    encodedProperty.pTexture = loadTextureAnyThreadPart(
        *pImage,
        TextureAddress::TA_Clamp,
        TextureAddress::TA_Clamp,
        TextureFilter::TF_Nearest,
        false,
        TEXTUREGROUP_8BitData,
        false,
        encodedFormat.format);
  }

  return encodedFeatureTable;
}

EncodedFeatureTexture encodeFeatureTextureAnyThreadPart(
    TMap<const CesiumGltf::ImageAsset*, TWeakPtr<LoadedTextureResult>>&
        featureTexturePropertyMap,
    const FFeatureTextureDescription& featureTextureDescription,
    const FString& featureTextureName,
    const FCesiumPropertyTexture& featureTexture) {

  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeFeatureTexture)

  EncodedFeatureTexture encodedFeatureTexture;

  const auto& properties =
      UCesiumPropertyTextureBlueprintLibrary::GetProperties(featureTexture);
  encodedFeatureTexture.properties.Reserve(properties.Num());

  for (const auto& propertyIt : properties) {
    const FFeatureTexturePropertyDescription* pPropertyDescription =
        featureTextureDescription.Properties.FindByPredicate(
            [propertyName = propertyIt.Key](
                const FFeatureTexturePropertyDescription& expectedProperty) {
              return propertyName == expectedProperty.Name;
            });

    if (!pPropertyDescription) {
      continue;
    }

    const FCesiumPropertyTextureProperty& featureTextureProperty =
        propertyIt.Value;

    const CesiumGltf::ImageAsset* pImage = featureTextureProperty.getImage();

    if (!pImage) {
      UE_LOG(
          LogCesium,
          Warning,
          TEXT("This feature texture property does not have a valid image."));
      continue;
    }

    int32 expectedComponentCount = 0;
    switch (pPropertyDescription->Type) {
    case ECesiumPropertyType_DEPRECATED::Scalar_DEPRECATED:
      expectedComponentCount = 1;
      break;
    case ECesiumPropertyType_DEPRECATED::Vec2_DEPRECATED:
      expectedComponentCount = 2;
      break;
    case ECesiumPropertyType_DEPRECATED::Vec3_DEPRECATED:
      expectedComponentCount = 3;
      break;
    case ECesiumPropertyType_DEPRECATED::Vec4_DEPRECATED:
      expectedComponentCount = 4;
      break;
    };

    int32 actualComponentCount = 0;
    FCesiumMetadataValueType valueType =
        UCesiumPropertyTexturePropertyBlueprintLibrary::GetValueType(
            featureTextureProperty);
    switch (valueType.Type) {
    case ECesiumMetadataType::Scalar:
      actualComponentCount = 1;
      break;
    case ECesiumMetadataType::Vec2:
      actualComponentCount = 2;
      break;
    case ECesiumMetadataType::Vec3:
      actualComponentCount = 3;
      break;
    case ECesiumMetadataType::Vec4:
      actualComponentCount = 4;
      break;
    }

    if (expectedComponentCount != actualComponentCount) {
      UE_LOG(
          LogCesium,
          Warning,
          TEXT(
              "This feature texture property does not have the expected component count"));
      continue;
    }

    bool isNormalized =
        UCesiumPropertyTexturePropertyBlueprintLibrary::IsNormalized(
            featureTextureProperty);
    if (pPropertyDescription->Normalized != isNormalized) {
      UE_LOG(
          LogCesium,
          Warning,
          TEXT(
              "This feature texture property does not have the expected normalization."));
      continue;
    }

    TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeFeatureTextureProperty)

    EncodedFeatureTextureProperty& encodedFeatureTextureProperty =
        encodedFeatureTexture.properties.Emplace_GetRef();

    encodedFeatureTextureProperty.baseName =
        "FTX_" + featureTextureName + "_" + pPropertyDescription->Name + "_";
    encodedFeatureTextureProperty.textureCoordinateAttributeId =
        featureTextureProperty.getTexCoordSetIndex();

    const auto& channels =
        UCesiumPropertyTexturePropertyBlueprintLibrary::GetChannels(
            featureTextureProperty);
    encodedFeatureTextureProperty.channelOffsets[0] = channels[0];
    encodedFeatureTextureProperty.channelOffsets[1] = channels[1];
    encodedFeatureTextureProperty.channelOffsets[2] = channels[2];
    encodedFeatureTextureProperty.channelOffsets[3] = channels[3];

    TWeakPtr<LoadedTextureResult>* pMappedUnrealImageIt =
        featureTexturePropertyMap.Find(pImage);
    if (pMappedUnrealImageIt) {
      encodedFeatureTextureProperty.pTexture = pMappedUnrealImageIt->Pin();
    } else {
      CesiumUtility::IntrusivePointer<CesiumGltf::ImageAsset> pImageCopy =
          new CesiumGltf::ImageAsset(*pImage);
      encodedFeatureTextureProperty.pTexture =
          MakeShared<LoadedTextureResult>(std::move(*loadTextureAnyThreadPart(
              *pImageCopy,
              TextureAddress::TA_Clamp,
              TextureAddress::TA_Clamp,
              TextureFilter::TF_Nearest,
              false,
              TEXTUREGROUP_8BitData,
              false,
              // TODO : currently the unnormalized pixels are always in unsigned
              // R8G8B8A8 form, but this does not necessarily need to be the
              // case in the future.
              isNormalized ? EPixelFormat::PF_R8G8B8A8
                           : EPixelFormat::PF_R8G8B8A8_UINT)));
      featureTexturePropertyMap.Emplace(
          pImage,
          encodedFeatureTextureProperty.pTexture);
    }
  }

  return encodedFeatureTexture;
}

EncodedMetadataPrimitive encodeMetadataPrimitiveAnyThreadPart(
    const FMetadataDescription& metadataDescription,
    const FCesiumMetadataPrimitive& primitive) {

  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeMetadataPrimitive)

  EncodedMetadataPrimitive result;

  const TArray<FCesiumFeatureIdTexture>& featureIdTextures =
      UCesiumMetadataPrimitiveBlueprintLibrary::GetFeatureIdTextures(primitive);
  const TArray<FCesiumFeatureIdAttribute>& featureIdAttributes =
      UCesiumMetadataPrimitiveBlueprintLibrary::GetFeatureIdAttributes(
          primitive);

  const TArray<FString>& featureTextureNames =
      UCesiumMetadataPrimitiveBlueprintLibrary::GetFeatureTextureNames(
          primitive);
  result.featureTextureNames.Reserve(featureTextureNames.Num());

  for (const FFeatureTextureDescription& expectedFeatureTexture :
       metadataDescription.FeatureTextures) {
    if (featureTextureNames.Find(expectedFeatureTexture.Name) != INDEX_NONE) {
      result.featureTextureNames.Add(expectedFeatureTexture.Name);
    }
  }

  TMap<const CesiumGltf::ImageAsset*, TWeakPtr<LoadedTextureResult>>
      featureIdTextureMap;
  featureIdTextureMap.Reserve(featureIdTextures.Num());

  result.encodedFeatureIdTextures.Reserve(featureIdTextures.Num());
  result.encodedFeatureIdAttributes.Reserve(featureIdAttributes.Num());

  // Imposed implementation limitation: Assume only upto one feature id texture
  // or attribute corresponds to each feature table.
  for (const FFeatureTableDescription& expectedFeatureTable :
       metadataDescription.FeatureTables) {
    const FString& featureTableName = expectedFeatureTable.Name;

    if (expectedFeatureTable.AccessType ==
        ECesiumFeatureTableAccessType_DEPRECATED::Texture_DEPRECATED) {

      const FCesiumFeatureIdTexture* pFeatureIdTexture =
          featureIdTextures.FindByPredicate([&featureTableName](
                                                const FCesiumFeatureIdTexture&
                                                    featureIdTexture) {
            return featureTableName ==
                   UCesiumFeatureIdTextureBlueprintLibrary::GetFeatureTableName(
                       featureIdTexture);
          });

      if (pFeatureIdTexture) {
        const CesiumGltf::FeatureIdTextureView& featureIdTextureView =
            pFeatureIdTexture->getFeatureIdTextureView();
        const CesiumGltf::ImageAsset* pFeatureIdImage =
            featureIdTextureView.getImage();

        if (!pFeatureIdImage) {
          UE_LOG(
              LogCesium,
              Warning,
              TEXT("Feature id texture missing valid image."));
          continue;
        }

        TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeFeatureIdTexture)

        EncodedFeatureIdTexture& encodedFeatureIdTexture =
            result.encodedFeatureIdTextures.Emplace_GetRef();

        const auto channels = featureIdTextureView.getChannels();
        encodedFeatureIdTexture.baseName = "FIT_" + featureTableName + "_";
        encodedFeatureIdTexture.channel = channels.size() ? channels[0] : 0;
        encodedFeatureIdTexture.textureCoordinateAttributeId =
            featureIdTextureView.getTexCoordSetIndex();

        TWeakPtr<LoadedTextureResult>* pMappedUnrealImageIt =
            featureIdTextureMap.Find(pFeatureIdImage);
        if (pMappedUnrealImageIt) {
          encodedFeatureIdTexture.pTexture = pMappedUnrealImageIt->Pin();
        } else {
          CesiumUtility::IntrusivePointer<CesiumGltf::ImageAsset> pImageCopy =
              new CesiumGltf::ImageAsset(*pFeatureIdImage);
          encodedFeatureIdTexture.pTexture = MakeShared<LoadedTextureResult>(
              std::move(*loadTextureAnyThreadPart(
                  *pImageCopy,
                  TextureAddress::TA_Clamp,
                  TextureAddress::TA_Clamp,
                  TextureFilter::TF_Nearest,
                  false,
                  TEXTUREGROUP_8BitData,
                  false,
                  // TODO: currently this is always the case, but doesn't have
                  // to be
                  EPixelFormat::PF_R8G8B8A8_UINT)));
          featureIdTextureMap.Emplace(
              pFeatureIdImage,
              encodedFeatureIdTexture.pTexture);
        }

        encodedFeatureIdTexture.featureTableName = featureTableName;
      }
    } else if (
        expectedFeatureTable.AccessType ==
        ECesiumFeatureTableAccessType_DEPRECATED::Attribute_DEPRECATED) {
      for (size_t i = 0; i < featureIdAttributes.Num(); ++i) {
        const FCesiumFeatureIdAttribute& featureIdAttribute =
            featureIdAttributes[i];

        if (featureTableName ==
            UCesiumFeatureIdAttributeBlueprintLibrary::GetFeatureTableName(
                featureIdAttribute)) {
          EncodedFeatureIdAttribute& encodedFeatureIdAttribute =
              result.encodedFeatureIdAttributes.Emplace_GetRef();

          encodedFeatureIdAttribute.name = "FA_" + featureTableName;
          encodedFeatureIdAttribute.featureTableName = featureTableName;
          encodedFeatureIdAttribute.index = static_cast<int32>(i);

          break;
        }
      }
    }
  }

  return result;
}

EncodedMetadata encodeMetadataAnyThreadPart(
    const FMetadataDescription& metadataDescription,
    const FCesiumModelMetadata& metadata) {

  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeMetadataModel)

  EncodedMetadata result;

  const TMap<FString, FCesiumPropertyTable>& featureTables =
      UCesiumModelMetadataBlueprintLibrary::GetFeatureTables(metadata);
  result.encodedFeatureTables.Reserve(featureTables.Num());
  for (const auto& featureTableIt : featureTables) {
    const FString& featureTableName = featureTableIt.Key;

    const FFeatureTableDescription* pExpectedFeatureTable =
        metadataDescription.FeatureTables.FindByPredicate(
            [&featureTableName](
                const FFeatureTableDescription& expectedFeatureTable) {
              return featureTableName == expectedFeatureTable.Name;
            });

    if (pExpectedFeatureTable) {
      TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeFeatureTable)

      result.encodedFeatureTables.Emplace(
          featureTableName,
          encodeMetadataFeatureTableAnyThreadPart(
              *pExpectedFeatureTable,
              featureTableIt.Value));
    }
  }

  const TMap<FString, FCesiumPropertyTexture>& featureTextures =
      UCesiumModelMetadataBlueprintLibrary::GetFeatureTextures(metadata);
  result.encodedFeatureTextures.Reserve(featureTextures.Num());
  TMap<const CesiumGltf::ImageAsset*, TWeakPtr<LoadedTextureResult>>
      featureTexturePropertyMap;
  featureTexturePropertyMap.Reserve(featureTextures.Num());
  for (const auto& featureTextureIt : featureTextures) {
    const FString& featureTextureName = featureTextureIt.Key;

    const FFeatureTextureDescription* pExpectedFeatureTexture =
        metadataDescription.FeatureTextures.FindByPredicate(
            [&featureTextureName](
                const FFeatureTextureDescription& expectedFeatureTexture) {
              return featureTextureName == expectedFeatureTexture.Name;
            });

    if (pExpectedFeatureTexture) {
      TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeFeatureTexture)

      result.encodedFeatureTextures.Emplace(
          featureTextureName,
          encodeFeatureTextureAnyThreadPart(
              featureTexturePropertyMap,
              *pExpectedFeatureTexture,
              featureTextureName,
              featureTextureIt.Value));
    }
  }

  return result;
}

bool encodeMetadataFeatureTableGameThreadPart(
    EncodedMetadataFeatureTable& encodedFeatureTable) {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeFeatureTable)

  bool success = true;

  for (EncodedMetadataProperty& encodedProperty :
       encodedFeatureTable.encodedProperties) {
    success &=
        loadTextureGameThreadPart(encodedProperty.pTexture.Get()) != nullptr;
  }

  return success;
}

bool encodeFeatureTextureGameThreadPart(
    TArray<LoadedTextureResult*>& uniqueTextures,
    EncodedFeatureTexture& encodedFeatureTexture) {
  bool success = true;

  for (EncodedFeatureTextureProperty& property :
       encodedFeatureTexture.properties) {
    if (uniqueTextures.Find(property.pTexture.Get()) == INDEX_NONE) {
      success &= loadTextureGameThreadPart(property.pTexture.Get()) != nullptr;
      uniqueTextures.Emplace(property.pTexture.Get());
    }
  }

  return success;
}

bool encodeMetadataPrimitiveGameThreadPart(
    EncodedMetadataPrimitive& encodedPrimitive) {
  bool success = true;

  TArray<const LoadedTextureResult*> uniqueFeatureIdImages;
  uniqueFeatureIdImages.Reserve(
      encodedPrimitive.encodedFeatureIdTextures.Num());

  for (EncodedFeatureIdTexture& encodedFeatureIdTexture :
       encodedPrimitive.encodedFeatureIdTextures) {
    if (uniqueFeatureIdImages.Find(encodedFeatureIdTexture.pTexture.Get()) ==
        INDEX_NONE) {
      success &= loadTextureGameThreadPart(
                     encodedFeatureIdTexture.pTexture.Get()) != nullptr;
      uniqueFeatureIdImages.Emplace(encodedFeatureIdTexture.pTexture.Get());
    }
  }

  return success;
}

bool encodeMetadataGameThreadPart(EncodedMetadata& encodedMetadata) {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeMetadata)

  bool success = true;

  TArray<LoadedTextureResult*> uniqueTextures;
  uniqueTextures.Reserve(encodedMetadata.encodedFeatureTextures.Num());
  for (auto& encodedFeatureTextureIt : encodedMetadata.encodedFeatureTextures) {
    success &= encodeFeatureTextureGameThreadPart(
        uniqueTextures,
        encodedFeatureTextureIt.Value);
  }

  for (auto& encodedFeatureTableIt : encodedMetadata.encodedFeatureTables) {
    success &=
        encodeMetadataFeatureTableGameThreadPart(encodedFeatureTableIt.Value);
  }

  return success;
}

void destroyEncodedMetadataPrimitive(
    EncodedMetadataPrimitive& encodedPrimitive) {
  for (EncodedFeatureIdTexture& encodedFeatureIdTexture :
       encodedPrimitive.encodedFeatureIdTextures) {

    if (encodedFeatureIdTexture.pTexture) {
      encodedFeatureIdTexture.pTexture->pTexture = nullptr;
    }
  }
}

void destroyEncodedMetadata(EncodedMetadata& encodedMetadata) {

  // Destroy encoded feature tables.
  for (auto& encodedFeatureTableIt : encodedMetadata.encodedFeatureTables) {
    for (EncodedMetadataProperty& encodedProperty :
         encodedFeatureTableIt.Value.encodedProperties) {
      if (encodedProperty.pTexture) {
        encodedProperty.pTexture->pTexture = nullptr;
      }
    }
  }

  // Destroy encoded feature textures.
  for (auto& encodedFeatureTextureIt : encodedMetadata.encodedFeatureTextures) {
    for (EncodedFeatureTextureProperty& encodedFeatureTextureProperty :
         encodedFeatureTextureIt.Value.properties) {
      if (encodedFeatureTextureProperty.pTexture) {
        encodedFeatureTextureProperty.pTexture->pTexture = nullptr;
      }
    }
  }
}

// The result should be a safe hlsl identifier, but any name clashes after
// fixing safety will not be automatically handled.
FString createHlslSafeName(const FString& rawName) {
  static const FString identifierHeadChar =
      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
  static const FString identifierTailChar = identifierHeadChar + "0123456789";

  FString safeName = rawName;
  int32 _;
  if (safeName.Len() == 0) {
    return "_";
  } else {
    if (!identifierHeadChar.FindChar(safeName[0], _)) {
      safeName = "_" + safeName;
    }
  }

  for (size_t i = 1; i < safeName.Len(); ++i) {
    if (!identifierTailChar.FindChar(safeName[i], _)) {
      safeName[i] = '_';
    }
  }

  return safeName;
}

} // namespace CesiumEncodedMetadataUtility

PRAGMA_ENABLE_DEPRECATION_WARNINGS