import { GLTF2 } from 'babylonjs-loaders';
import { GIZMOS, GUI, TRANSPARENCY_MODES } from '../constants';
import { orthoOptions } from '../enums';
import { downscaling } from '../enums/downscaling';
import {
  scene,
  utilLayer,
  gizmoManager,
  getUserMeshes,
  highlightManager,
  hoverHighlightManager,
  engine,
  guiTexture,
  ground,
  mirrorGround,
  getUserMaterials,
  getUserTextures,
  selectedMeshes,
  hiddenMeshes,
  getBoundingMeshData,
  singleMeshTNode,
  updateEnvironment,
  multiMeshTNode,
  resetSelectedMeshes,
  attachToSelectMeshesNode,
  getUserNodes,
  canvas,
} from '../helpers';
import { reactProps as props } from '../Canvas';
import * as BABYLON from 'babylonjs';
import * as SERIALIZERS from 'babylonjs-serializers';
import _ from 'lodash';

export let autoRotation;

export const newVector = (x, y, z) => new BABYLON.Vector3(x || 0, y || 0, z || 0);

export const toRadians = value => BABYLON.Tools.ToRadians(value || 0);

export const toDegrees = value => BABYLON.Tools.ToDegrees(value || 0);

export const addAutoRotation = () => {
  autoRotation = new BABYLON.AutoRotationBehavior();
};

export const removeAutoRotation = () => {
  autoRotation = null;
};

export const defaultCameraPosition = () => newVector(0, 3, 11);

export const serializeScene = () => {
  const serializedData = { metadata: scene?.metadata };
  return serializedData;
};

export const addHighlightExclusion = mesh => {
  if (props.previewMode) return;
  highlightManager.addExcludedMesh(mesh);
  hoverHighlightManager.addExcludedMesh(mesh);
};

export const newMetaDataEntry = (key, value) => {
  const { metadata } = scene;
  const newMetadata = { ...metadata, [key]: value };

  scene.metadata = newMetadata;
};

export const newTexture = (url, isEnvTexture, isSkyBox) => {
  const onError = error => console.error(error);
  const onLoad = () => {
    if (isEnvTexture) {
      const rotation = scene.metadata.environmentRotation || 0;

      updateEnvironment({
        payload: {
          url,
          transforms: {
            rotation: { ry: rotation },
          },
        },
      });
    }

    props.clearNotifications?.();
    props.setSerializedData?.(serializeScene());
  };

  if (isEnvTexture) {
    const isUserFile = _.startsWith(url, 'blob');
    const extension = `.${url.split('.').pop()}`;
    const type = BABYLON.Constants.TEXTUREFORMAT_RGBA;
    const options = [ url, scene, null, false, null, null, null, type, false, extension ];
    const hdrOptions = [ url, scene, 256, false, true, false, true, onLoad, onError ];
    const texture = isUserFile
      ? new BABYLON.HDRCubeTexture(...hdrOptions)
      : new BABYLON.CubeTexture(...options);

    isSkyBox
      ? texture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE
      : texture.coordinatesMode = BABYLON.Texture.CUBIC_MODE;

    return texture;
  }

  const deleteBuffer = true;
  const buffer = url;
  const samplingMode = BABYLON.Texture.NEAREST_SAMPLINGMODE;
  const texture = new BABYLON.Texture(url, scene, false, false, samplingMode, onLoad, onError, buffer, deleteBuffer);

  return texture;
};

export const newColor = (r, g, b, a) => {
  const isHex = _.startsWith(r, '#');
  const hexHasAlpha = isHex && r.length === 9;

  if (isHex) {
    return hexHasAlpha ? new BABYLON.Color4.FromHexString(r) : new BABYLON.Color3.FromHexString(r);
  }

  return a ? new BABYLON.Color4(r, g, b, a) : new BABYLON.Color3(r, g, b);
};

export const newHighlightManager = name => {
  const manager = new BABYLON.HighlightLayer(name, scene);

  manager.blurHorizontalSize = 1.5;
  manager.blurVerticalSize = 1.5;
  manager.innerGlow = false;

  return manager;
};

export const newFraming = () => new BABYLON.FramingBehavior();

export const newScreenshot = (isBillboard, callback) =>  {
  let onSuccess = data => callback?.(data);

  if (!props.previewMode) {
    const outerFrame = guiTexture.getControlByName(GUI.outerSafeFrame);

    const xAxisMesh = scene.getMeshByName('xAxisMesh');
    const yAxisMesh = scene.getMeshByName('yAxisMesh');
    const zAxisMesh = scene.getMeshByName('zAxisMesh');
    const hdrSkyBox = scene.getMeshByName('hdrSkyBox');
    const axesVisible = xAxisMesh.isVisible;
    const skyBoxVisible = hdrSkyBox.isVisible;
    const frameVisible = outerFrame.isVisible;
    const groundVisible = ground.isVisible;
    const mirrorGroundVisible = mirrorGround.isVisible;
    const {
      positionGizmoEnabled,
      rotationGizmoEnabled,
      boundingBoxGizmoEnabled,
      scaleGizmoEnabled,
    } = gizmoManager;

    const toggleUI = restore => {
      if (isBillboard) {
        mirrorGround.isVisible = restore ? mirrorGroundVisible : false;
      }

      ground.isVisible = restore ? groundVisible : false;
      xAxisMesh.isVisible = restore ? axesVisible : false;
      yAxisMesh.isVisible = restore ? axesVisible : false;
      zAxisMesh.isVisible = restore ? axesVisible : false;
      hdrSkyBox.isVisible = restore ? skyBoxVisible : false;
      outerFrame.isVisible = restore ? frameVisible : false;
      gizmoManager.positionGizmoEnabled = restore ? positionGizmoEnabled : false;
      gizmoManager.rotationGizmoEnabled = restore ? rotationGizmoEnabled : false;
      gizmoManager.boundingBoxGizmoEnabled = restore ? boundingBoxGizmoEnabled : false;
      gizmoManager.scaleGizmoEnabled = restore ? scaleGizmoEnabled : false;
    };

    onSuccess = data => {
      toggleUI(true);
      callback?.(data);
    };

    toggleUI();
  }

  BABYLON.Tools.CreateScreenshot(engine, scene.activeCamera, { height: canvas.height, width: canvas.width }, onSuccess);
};

export const newGizmo = (type, options = {}) => {
  if (type === GIZMOS.PositionGizmo || type === GIZMOS.ScaleGizmo) {
    const { thickness } = options;
    return new BABYLON[type](utilLayer, thickness, gizmoManager);
  }

  if (type === GIZMOS.RotationGizmo) {
    const { tessellation, useEulerRotation, thickness, rotationGizmoOptions } = options;
    return new BABYLON[type](utilLayer, tessellation, useEulerRotation, thickness, gizmoManager, rotationGizmoOptions);
  }

  if (type === GIZMOS.BoundingBoxGizmo) {
    const { color } = options;
    return new BABYLON[type](utilLayer, color, gizmoManager);
  }
};

export const getColor = (props, key) => {
  const hasProps = props.theme && props.$selectedTheme;

  if (hasProps) {
    const themeColors = props.theme.colors[props.$selectedTheme];
    return themeColors[key];
  }
};

export const blobToGLB = (blob, name) => {
  const file = blob;
  file.lastModifiedDate = new Date();
  file.name = `${name}.glb`;
  return file;
};

export const getGLTFData = async (type, download) => {
  const shouldExportNode = node => {
    const userNodes = getUserNodes();
    const userMeshes = getUserMeshes();
    const includesNode = userNodes.includes(node);
    const includesMesh = userMeshes.includes(node);

    return includesNode || includesMesh;
  };

  try {
    const exporterType = type === 'glb' ? 'GLBAsync' : 'GLTFAsync';
    resetSelectedMeshes();
    attachToSelectMeshesNode();
    const gltf = await SERIALIZERS.GLTF2Export[exporterType](scene, 'fileName', { shouldExportNode });
    if (download) gltf.downloadFiles();
    props.setDownloadedModel?.(gltf.glTFFiles);
  } catch (e) {
    console.error(e);
  }
};

export const getMidpoint = (v1, v2) => {
  const halfX = 0.5 * (v1._x + v2._x);
  const halfY = 0.5 * (v1._y + v2._y);
  const halfZ = 0.5 * (v1._z + v2._z);

  const midpoint = newVector(halfX, halfY, halfZ);
  return midpoint;
};

export const getRadius = (minimum, maximum) => {
  const center = getMidpoint(minimum, maximum);
  const delta = axis => Math.pow(Math.abs(axis - center[axis]), 2);
  const radius = Math.sqrt(delta('_x') + delta('_y') + delta('_z'));

  return radius;
};

export const restoreParents = parentList => {
  parentList.forEach(item => {
    const { mesh, parent } = item;
    mesh.setParent(parent);
  });
};

export const getMeshesInCameraView = () => {
  const camera = scene.activeCamera;
  const userMeshes = getUserMeshes();
  const possibleMeshes = userMeshes.map(mesh => {
    if (mesh.isPickable && camera.isInFrustum(mesh)) {
      return mesh;
    }
  });

  return possibleMeshes.filter(mesh => mesh);
};

export const getStatisticsMetadata = props => {
  const userMeshes = getUserMeshes();
  const userMaterials = getUserMaterials();
  const userTextures = getUserTextures();

  const meshesVertices = userMeshes.map(mesh => mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind));
  const meshesIndices = userMeshes.map(mesh => mesh.getIndices());
  const vertices = meshesVertices.reduce((prev, current) => prev + current.length / 3, 0);
  const triangles = meshesIndices.reduce((prev, current) => prev + current.length / 3, 0);

  const getMeshStatisticsArray = () => {
    const meshStats = userMeshes.map(mesh => {
      const meshVertices = mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind).length / 3;
      const meshIndices = mesh.getIndices().length / 3;

      return {
        id: mesh.id,
        vertices: meshVertices,
        triangles: meshIndices,
      };
    });

    return meshStats;
  };

  const value = {
    materialCount: userMaterials.length?.toLocaleString(),
    textures: userTextures.length?.toLocaleString(),
    meshes: {
      count: userMeshes.length?.toLocaleString(),
      meshes: getMeshStatisticsArray(),
    },
    vertices: vertices.toLocaleString(),
    triangles: triangles.toLocaleString(),
  };

  newMetaDataEntry('statistics', value);
  props?.setSerializedData?.(serializeScene());
};

export const buildMeshIdArray = hidden => {
  const arr = hidden
    ? hiddenMeshes.map(mesh => mesh.id)
    : selectedMeshes.map(mesh => ({
      id: mesh.id,
      materialId: mesh.material?.id || 'Default',
      textures: mesh.material?.getActiveTextures().map(texture => texture.name),
     }));

  return arr;
};

export const buildMaterialVariantsArray = () => {
  const rootMesh = scene.getMeshByID('__root__');
  const khrExtension = GLTF2.KHR_materials_variants;
  const variants = khrExtension.GetAvailableVariants(rootMesh);

  return variants;
};


export const buildAnimationsArray = () => {
  const animationGroups = scene?.animationGroups || [];

  return animationGroups.map((group, index) => {
    const fallbackLabel = `Animation ${index + 1}`;
    const label = group.name || group.id || fallbackLabel;

    return {
      id: `${index}`,
      index,
      label,
      name: group.name || '',
    };
  });
};

export const buildSelectedMaterialArray = () => {
  if (props.materialMode) {
    const mainMaterial = scene.metadata?.materials?.find?.(item => item.materialId === 'material');
    return [ mainMaterial ];
  }

  const arr = selectedMeshes?.map?.(mesh => {
    const material = mesh.material;
    if (material) {
      const existingMaterial = scene.metadata.materials?.find(mat => mat.materialId === material.id);
      return {
        ...existingMaterial || {},
        ...materialData(material),
      };
    }
  }).filter(val => val);

  if (_.isEmpty(arr)) {
    if (!_.isEmpty(scene.metadata.selectedMaterials)) {
      const currentSelectedMaterial = scene.metadata.selectedMaterials[0];
      const materialMatch = scene.metadata.materials?.find(material => (
        currentSelectedMaterial.materialId === material.materialId
      )) || {};

      return [ materialMatch ];
    }

    return [ scene.metadata.materials?.[0] || {} ];
  }

  return arr;
};

const updatedMeshTrackingRotation = id => {
  const zAxisUp = scene.metadata.selectedAxisCompensation === 'Z-Axis up';
  const xRotation = zAxisUp ? 270 : 0;
  const meshSelected = selectedMeshes.length > 0;
  const workingMesh = scene.metadata.meshChangeTracking?.find(mesh => mesh.meshId === id) || {};
  const workingValues = { rx: xRotation, ry: 0, rz: 0 };

  if (meshSelected) {
    const tNode = selectedMeshes.length > 1 ? multiMeshTNode : singleMeshTNode;
    const { rotation } = tNode;
    const newRotation = Object.values(rotation).map(rad => toDegrees(rad))?.slice(1);
    const childMeshes = tNode.getChildMeshes();
    const includesMesh = childMeshes.some(mesh => mesh.id === id);

    if (includesMesh && newRotation) {
      const [ rx, ry, rz ] = newRotation;
      const rotateX = rx <= 0 ? (360 - rx * -1) % 360 : Math.abs(rx) % 360;
      const rotateY = ry <= 0 ? (360 - ry * -1) % 360 : Math.abs(ry) % 360;
      const rotateZ = rz <= 0 ? (360 - rz * -1) % 360 : Math.abs(rz) % 360;

      return { rx: rotateX, ry: rotateY, rz: rotateZ };
    }

    if (!_.isEmpty(workingMesh)) {
      const { rx, ry, rz } = workingMesh;
      return { rx, ry, rz };
    }
  }

  return workingValues;
};

const baseMeshScales = [];

const updateMeshTrackingScale = mesh => {
  const baseMesh = baseMeshScales.find(bMesh => bMesh.id === mesh.id);

  if (_.isEmpty(baseMesh)) {
    const parent = mesh.parent;
    mesh.setParent(singleMeshTNode);
    const meshScaleObj = {
      id: mesh.id,
      sx: mesh.scaling.x,
      sy: mesh.scaling.y,
      sz: mesh.scaling.z,
    };

    baseMeshScales.push(meshScaleObj);
    mesh.setParent(parent);
    return meshScaleObj;
  }

  return baseMesh;
};

export const buildMeshPositionsArray = args => {
  return getUserMeshes().map(mesh => {
    const { rx, ry, rz } = updatedMeshTrackingRotation(mesh.id);
    const { sx, sy, sz } = updateMeshTrackingScale(mesh);
    const boundingInfo = getBoundingMeshData([ mesh ]);
    const { center, centerWorld } = boundingInfo;
    const pos = center || centerWorld;
    const scaleX = mesh?.scaling.x || 1;
    const scaleY = mesh?.scaling.y || 1;
    const scaleZ = mesh?.scaling.z || 1;
    const meshChangeTracking = scene.metadata?.meshChangeTracking;
    const prevMeshTransforms = meshChangeTracking?.find(trackedMesh => trackedMesh.meshId === mesh.id) || {};

    let defaultMeshPosition = {
      ...prevMeshTransforms,
      meshId: mesh?.id || '',
      tx: pos.x,
      ty: pos.y,
      tz: pos.z,
    };

    if (!args?.preserveScale) {
      defaultMeshPosition = {
        ...defaultMeshPosition,
        sx: scaleX === 1 ? 1 : scaleX / sx,
        sy: scaleY === 1 ? 1 : scaleY / sy,
        sz: scaleZ === 1 ? 1 : scaleZ / sz,
      };
    }

    if (!args?.preserveRotation) {
      defaultMeshPosition = {
        ...defaultMeshPosition,
        rx,
        ry,
        rz,
      };
    }

    return defaultMeshPosition;
  });
};

export const getImageFileFromBuffer = args => {
  const { buffer, asDataUrl } = args || {};

  if (_.isEmpty(buffer)) return '';
  if (!_.isObject(buffer) && !_.isArray(buffer)) return buffer;

  if (asDataUrl) {
    // Convert buffer to data:image/png;base64 string
    const Uint8ToString = u8a => {
      if (_.isEmpty(u8a)) return '';

      const maxChunkSize = 8192;
      const chunks = [];

      for (let i = 0; i < u8a.length; i += maxChunkSize) {
        const newChunk = String.fromCharCode.apply(null, u8a.subarray(i, i + maxChunkSize));
        chunks.push(newChunk);
      }

      return chunks.join('');
    };

    const ascii = new Uint8Array(buffer);
    const dataString = btoa(Uint8ToString(ascii));
    const dataUrl = `data:image/png;base64,${dataString}`;

    return dataUrl;
  }

  const blob = new Blob([ buffer ], { type: 'image/png' });
  const blobUrl = URL.createObjectURL(blob);

  return blobUrl;
};

export const getTextureUrl = args => {
  const { texture, asDataUrl } = args || {};
  const buffer = texture?._buffer;

  if (_.isEmpty(buffer)) return texture?.url || '';
  return getImageFileFromBuffer({ buffer, asDataUrl });
};

export const getExistingUVSettings = args => {
  const { materialId } = args || {};
  let settingsFound = false;
  let uvSettings = {
    uvXScale: 1,
    uvYScale: 1,
    uvXRotation: 0,
    uvYRotation: 0,
    uvZRotation: 0,
    uvXOffset: 0,
    uvYOffset: 0,
  };

  const userMaterials = getUserMaterials();
  const material = userMaterials.find(mat => mat.id === materialId);

  if (material) {
    Object.keys(material)?.find?.(key => {
      const validMaterialWithTexture = _.isObject(material[key])
        && !_.isArray(material[key])
        && key.endsWith('Texture');

      const texture = material[key];
      const isEnvironment = key.toLowerCase().includes('environment');
      const hasAllKeys = texture && [ 'uAng', 'wAng', 'vAng', 'uOffset', 'vOffset', 'uScale', 'vScale' ]
        .every(key => texture[key] !== undefined);

        // NOTE: All UV's are adjusted globally, at the same time in canvasUpdateHelpers.js
        // when **any** UV setting is changed.  So we only need to find the first one to
        // restore the UV settings.
      if (!isEnvironment && validMaterialWithTexture && hasAllKeys && !settingsFound) {
        settingsFound = true;
        uvSettings = {
          uvXScale: texture.uScale,
          uvYScale: texture.vScale,
          uvXRotation: texture.uAng,
          uvYRotation: texture.wAng,
          uvZRotation: texture.vAng,
          uvXOffset: texture.uOffset,
          uvYOffset: texture.vOffset,
        };
      }
    });
  }

  return uvSettings;
};

export const materialData = material => {
  const meshesWithMaterial = getUserMeshes().map(mesh => {
    if (mesh.material.name === material.id) return mesh;
  }).filter(mesh => mesh);

  const allMeshes = scene.metadata?.statistics.meshes;
  const totalTriangles = allMeshes.meshes.reduce((prev, current) => {
    const includedMesh = meshesWithMaterial.some(mesh => mesh.id === current.id);
    if (includedMesh) return prev + current.triangles;

    return prev;
  }, 0);

  return {
    materialId: material.id,
    name: material.name,

    // General
    backfaceCullingEnabled: material.backFaceCulling || false,
    wireFrameEnabled: material.wireframe || false,
    pointsCloudEnabled: material.pointsCloud || false,

    // Basic
    albedoColor: material.albedoColor?.toHexString() || props?.theme?.colors?.[props.$selectedTheme]?.black,
    albedoTexture: {
      name: material.albedoTexture?.name,
      url: getTextureUrl({ texture: material.albedoTexture, asDataUrl: true }),
    },
    albedoHasAlpha: material.albedoTexture?.hasAlpha || false,
    environmentIntensity: material.environmentIntensity,
    metallic: material.metallic,
    metallicTexture: {
      name: material.metallicTexture?.name,
      url: getTextureUrl({ texture: material.metallicTexture, asDataUrl: true }),
    },
    roughness: material.roughness,
    microSurfaceTexture: {
      name: material.microSurfaceTexture?.name,
      url: getTextureUrl({ texture: material.microSurfaceTexture, asDataUrl: true }),
    },
    emissiveColor: material.emissiveColor?.toHexString() || props?.theme?.colors?.[props.$selectedTheme]?.black,
    emissiveIntensity: material.emissiveIntensity,
    emissiveTexture: {
      name: material.emissiveTexture?.name,
      url: getTextureUrl({ texture: material.emissiveTexture, asDataUrl: true }),
    },
      ambientTextureStength: material.ambientTextureStrength,
      ambientTexture: {
          name: material.ambientTexture?.name,
          url: getTextureUrl({ texture: material.ambientTexture, asDataUrl: true }),
      },
      ambientColor: material.ambientColor?.toHexString() || props?.theme?.colors?.[props.$selectedTheme]?.black,
    bumpIntensity: material.bumpTexture?.level ?? 1,
    bumpTexture: {
      name: material.bumpTexture?.name,
      url: getTextureUrl({ texture: material.bumpTexture, asDataUrl: true }),
    },

    // UV
    uvSettings: {
      ...getExistingUVSettings({ materialId: material.id }),
    },

    // Advanced
    clearCoatEnabled: material.clearCoat?.isEnabled || false,
    clearCoatIntensity: material.clearCoat?.intensity,
    clearCoatRoughness: material.clearCoat?.roughness,
    clearCoatIOR: material.clearCoat?.indexOfRefraction,
    alpha: material.alpha,
    opacityTexture: {
      name: material.opacityTexture?.name,
      url: getTextureUrl({ texture: material.opacityTexture, asDataUrl: true }),
    },
    transparencyMode: material.transparencyMode,
    sssRefractionEnabled: material.subSurface?.isRefractionEnabled || false,
    sssRefractionIntensity: material.subSurface?.refractionIntensity,
    sssIOR: material.subSurface?.indexOfRefraction,
    transparencyEnabled: material?.transparencyEnabled || false,
    transparencyType: material?.albedoTexture?.hasAlpha
      ? TRANSPARENCY_MODES.stencil
      : material?.transparencyType || TRANSPARENCY_MODES.simple,

    meshSimplification: {
      enabled: material.meshSimplification?.enabled || false,
      type: material.meshSimplification?.type || 'custom', // [ 'low, 'medium', 'high', 'custom' ]
      optimizationType: material.meshSimplification?.optimizationType || 'estimated triangles',
      totalTriangles,
      estimatedTriangles: material.meshSimplification?.estimatedTriangles || 99, // Percent
      userTargetTriangles: material.meshSimplification?.userTargetTriangles
        || totalTriangles || 0,
      targetTriangles: Math.round(
        (totalTriangles || +(scene.metadata.statistics?.triangles?.replace?.(/,/g, ''))) *
        ((material.meshSimplification?.estimatedTriangles || 99) / 100)) || 0,
    },
    textureDownscaling: {
      enabled: material.textureDownscaling?.enabled || false,
      size: material.textureDownscaling?.size || downscaling.find(size => size.name === '4K').value,
    },
  };
};

export const buildMaterialsArray = () => {
  const materials = getUserMaterials();
  const materialsArray = materials.map(material => materialData(material));

  return materialsArray;
};

export const buildLightsArray = () => {
  const lights = scene.lights;
  return lights.map(light => {
    const { position } = light;
    const { x, y, z } = position;
    return {
      ...light.serialize(),
      rotation: light?.rotation ?? 0,
      distance:
        light?.distance ??
        Math.sqrt(Math.pow(Math.abs(x), 2) + Math.pow(Math.abs(y), 2) + Math.pow(Math.abs(z), 2)),
    };
  });
};

const billboardScreenshot = view => {
  const camera = scene.activeCamera.clone(view);

  if (view !== 'perspective') {
    const alpha = orthoOptions[view].alpha;
    const beta = orthoOptions[view].beta;

    camera.alpha = alpha;
    camera.beta = beta;
    camera.mode = BABYLON.Camera.ORTHOGRAPHIC_CAMERA;
  }

  return BABYLON.Tools.CreateScreenshotUsingRenderTargetAsync(engine, camera, 2048);
};

export const takePreviewScreenshots = () => {
  const baseView = [ 'perspective' ];

  const outerFrame = guiTexture.getControlByName(GUI.outerSafeFrame);
  const xAxisMesh = scene.getMeshByName('xAxisMesh');
  const yAxisMesh = scene.getMeshByName('yAxisMesh');
  const zAxisMesh = scene.getMeshByName('zAxisMesh');
  const hdrSkyBox = scene.getMeshByName('hdrSkyBox');
  const axesVisible = xAxisMesh?.isVisible;
  const skyBoxVisible = hdrSkyBox?.isVisible;
  const frameVisible = outerFrame?.isVisible;
  const groundVisible = ground?.isVisible;
  const mirrorGroundVisible = mirrorGround?.isVisible;
  const {
    positionGizmoEnabled,
    rotationGizmoEnabled,
    boundingBoxGizmoEnabled,
    scaleGizmoEnabled,
  } = gizmoManager;

  const toggleUI = restore => {
    const setVisibility = (object, isVisible) => {
      if (object !== null && object !== undefined) {
        object.isVisible = isVisible;
      }
    };

    setVisibility(mirrorGround, restore ? mirrorGroundVisible : false);
    setVisibility(ground, restore ? groundVisible : false);
    setVisibility(xAxisMesh, restore ? axesVisible : false);
    setVisibility(yAxisMesh, restore ? axesVisible : false);
    setVisibility(zAxisMesh, restore ? axesVisible : false);
    setVisibility(hdrSkyBox, restore ? skyBoxVisible : false);
    setVisibility(outerFrame, restore ? frameVisible : false);

    gizmoManager.positionGizmoEnabled = restore ? positionGizmoEnabled : false;
    gizmoManager.rotationGizmoEnabled = restore ? rotationGizmoEnabled : false;
    gizmoManager.boundingBoxGizmoEnabled = restore ? boundingBoxGizmoEnabled : false;
    gizmoManager.scaleGizmoEnabled = restore ? scaleGizmoEnabled : false;
  };

  toggleUI();

  try {
    const screenshots = baseView.map(async view => {
      const imageData = await billboardScreenshot(view);
      return { name: view, imageData };
    });

    Promise.all(screenshots).then(screenshots => {
      const hasAllScreenshots = screenshots.length === baseView.length;
      if (hasAllScreenshots) props.setBillboardImages?.(screenshots);

      screenshots.forEach(screenshot => {
        scene.getCameraByName(screenshot.name)?.dispose();
      });
    });
  } catch(e) {
    console.error('Unable to take screenshots.', e);
  } finally {
    toggleUI(true);
  }
};
