import * as BABYLON from 'babylonjs';
import * as MATERIALS from 'babylonjs-materials';
import * as BABYLONGUI from 'babylonjs-gui';
import { LISTENERS, LIGHTS, CAMERAS, VIEWPORT } from '../constants';
import { shortcuts, ratios, downscaling, exclusionMeshes, exclusionMaterials } from '../enums';
import {
  newScene,
  newEngine,
  newCamera,
  newLight,
  newVector,
  newHighlightManager,
  handleMouseListeners,
  handleShortcutListeners,
  prepareCamera,
  newGround,
  newGridMaterial,
  newColor,
  setDefaultGridProperties,
  prepareAxes,
  getBoundingDistance,
  newGizmoManager,
  getUserMeshes,
  getUserMaterials,
  newPBRMaterial,
  createSafeFrame,
  createDragSelectBox,
  addInitialGizmoBehaviors,
  loadFromBlob,
  loadFromGuidURL,
  prepareMaterialCamera,
  loadFromDragDrop,
  addHighlightExclusion,
  warningChecks,
  toRadians,
  toDegrees,
  getStatisticsMetadata,
  newMetaDataEntry,
  serializeScene,
  buildMeshPositionsArray,
  buildMaterialsArray,
  buildAnimationsArray,
  updatePublish,
  buildSelectedMaterialArray,
  buildMaterialVariantsArray,
  removeAutoRotation,
  getUserNodes,
  buildLightsArray,
  resetAnimationSync,
} from '../helpers';
import { modelToOrigin, focusCamera } from '../actions';
import { reactProps as props } from '../Canvas';
import _ from 'lodash';

const node0Name = 'node0';
const resize = LISTENERS.resize;
const idleStopDelay = 250;

export let engine;
export let canvas;
export let scene;
export let highlightManager;
export let hoverHighlightManager;
export let theme;
export let $selectedTheme;
export let ground;
export let mirrorGround;
export let shadowGenerator;
export let utilLayer;
export let gizmoManager;
export let hdrSphere;
export let guiTexture;
export let multiMeshTNode;
export let singleMeshTNode;
export let dragDropFileInput;
export let iblShadowPipeline;
export let shortcutArray = [];
export const renderState = {
  dirty: true,
  interaction: false,
  running: false,
  idleAt: null,
};

export const groundOptions = () => {
  const meshSize = getBoundingDistance() || 10;
  const gridOffset = 10;
  const width = meshSize + gridOffset;
  const height = meshSize + gridOffset;

  const grid = { width, height };

  return grid;
};

const resizeEngine = () => {
  engine?.resize();
  requestRender();
};

export const renderScene = () => {
  const camera = scene?.activeCamera;
  const cameraMoving = [
    camera?.inertialAlphaOffset,
    camera?.inertialBetaOffset,
    camera?.inertialRadiusOffset,
    camera?.inertialPanningX,
    camera?.inertialPanningY,
  ].some(value => Math.abs(value || 0) > 0.001);
  const animationPlaying = scene?.animationGroups?.some(group => group.isPlaying);
  const active = renderState.interaction
    || animationPlaying
    || camera?.useAutoRotationBehavior
    || cameraMoving;

  const now = performance.now();
  const idleFor = renderState.idleAt ? now - renderState.idleAt : 0;

  if (!renderState.dirty && !active) {
    if (!renderState.idleAt) {
      renderState.idleAt = now;
      return;
    }

    if (idleFor > idleStopDelay) {
      engine?.stopRenderLoop(renderScene);
      renderState.running = false;
      renderState.idleAt = null;
    }

    return;
  }

  scene?.render();
  renderState.dirty = false;
  renderState.idleAt = active ? null : now;
};

export const requestRender = () => {
  renderState.dirty = true;
  renderState.idleAt = null;

  if (!engine || renderState.running) return;

  renderState.running = true;
  engine.runRenderLoop(renderScene);
};

export const initSceneObjects = () => {
  initCamera();
  initLights();
  if (!props.previewMode) initHighlightManagers();
  if (!props.previewMode) initGridGround();
  initMirrorGround();
  initGizmos();
  if (!props.previewMode) prepareAxes();
  initEnvironment();
  initGUI();
  checkForRootNode();
};

const initHighlightManagers = () => {
  highlightManager = newHighlightManager('selectHighlightManager');
  hoverHighlightManager = newHighlightManager('hoverHighlightManager');
};

const tweakSceneForShaderball = () => {
  const xAxis = scene.getMeshById('xAxisMesh');
  const yAxis = scene.getMeshById('yAxisMesh');
  const zAxis = scene.getMeshById('zAxisMesh');

  if (xAxis) xAxis.isVisible = false;
  if (yAxis) yAxis.isVisible = false;
  if (zAxis) zAxis.isVisible = false;
  if (ground) ground.isVisible = false;

  prepareMaterialCamera();
  props.clearNotifications?.();
};

const loadDemoMeshes = () => {
  const sphereOptions = { diameter: 2, segments: 32 };
  const sphere = BABYLON.MeshBuilder.CreateSphere('sphere', sphereOptions, scene);
  const rect = BABYLON.MeshBuilder.CreateBox('box', { width: 4, height: 2, depth: 4 }, scene);
  rect.position.x = 2;
  rect.position.y = 1;
  rect.position.z = -2;
  rect.material = newPBRMaterial('pbrRect');
  rect.material.metallic = 0;
  sphere.position.x = -4;
  sphere.position.y = 1;
  sphere.position.z = 2;
  sphere.material = newPBRMaterial('pbrSphere');
  sphere.material.roughness = 0.05;
};

const initCamera = () => {
  const cameraMeta = { alpha: 0, beta: 0, radius: 10 };
  const cameraLocation = { x: 0, y: 0, z: 0 };
  newCamera('defaultCamera', cameraLocation, CAMERAS.ArcRotateCamera, cameraMeta);

  if (!props.materialMode) {
    prepareCamera();
  }
};

const initGUI = () => {
  const aspectRatio = props.ratio?.value || ratios.broadcast.value;
  guiTexture = new BABYLONGUI.AdvancedDynamicTexture.CreateFullscreenUI('GUI');

  createSafeFrame(aspectRatio);
  createDragSelectBox();
};

const initEnvironment = () => {
  const envIntensity = 0.5;
  const envRotation = toRadians(props.envRotation || 0);
  const envBlur = props.envBlur || 0.5;
  const url = props.defaultEnvironments?.find?.(env => _.toLower(env.id) === 'default')?.envURL;
  const hdrTexture = new BABYLON.CubeTexture(url, scene);
  const isPBR = true;
  const scale = 1000;
  const isGlobalTexture = true;
  const createNewSkybox = () => scene.createDefaultSkybox(hdrTexture, isPBR, scale, envBlur, isGlobalTexture);
  const hdrSkyBox = !scene.getMeshById('hdrSkyBox') ? createNewSkybox() : scene.getMeshById('hdrSkyBox');
  const skyBox = scene.getMaterialById('skyBox');
  const backgroundColor = newColor('#00000000');

  if (scene.environmentTexture) {
    scene.environmentTexture.rotationY = envRotation;
  }

  scene.environmentIntensity = envIntensity;
  scene.clearColor = backgroundColor;

  if (skyBox.reflectionTexture) {
    skyBox.reflectionTexture.rotationY = envRotation;
  }

  skyBox.microSurface = 1 - envBlur;

  hdrSkyBox.setEnabled(false);

  scene.metadata = {
    ...scene.metadata,
    environmentIntensity: envIntensity,
    environmentRotation: toDegrees(envRotation),
    environmentBlur: skyBox.microSurface,
    viewportEnvironment: hdrSkyBox.isEnabled(),
  };

  if (scene.metadata.publish) {
    props.setSerializedData?.(serializeScene());
  }
};

export const initGizmos = () => {
  gizmoManager = newGizmoManager();

  if (multiMeshTNode || singleMeshTNode) {
    singleMeshTNode.dispose();
    multiMeshTNode.dispose();
  }

  singleMeshTNode = new BABYLON.TransformNode('singleMeshTransforms', scene);
  multiMeshTNode = new BABYLON.TransformNode('multiMeshTransforms', scene);

  addInitialGizmoBehaviors();
};

const initLights = () => {
  const meshes = getUserMeshes();
  const dirLight = newLight('directional', { x: -1, y: -2, z: -1 }, LIGHTS.DirectionalLight);
  dirLight.position = newVector(100, 200, 100);
  dirLight.intensity = 0.5;
  dirLight.shadowMinZ = 2;
  dirLight.shadowMaxZ = 300;

  const shadowLightOptions = { mapSize: VIEWPORT.shadowMapSize, dirLight };
  shadowGenerator = newLight('shadowLight', BABYLON.Vector3.Zero(), LIGHTS.ShadowGenerator, shadowLightOptions);

  meshes.forEach(mesh => shadowGenerator.addShadowCaster(mesh, true));
  shadowGenerator.transparencyShadow = true;
  shadowGenerator.useContactHardeningShadow = true;
  shadowGenerator.contactHardeningLightSizeUVRatio = 0.2;
  shadowGenerator.bias = 0.00001;
  shadowGenerator.normalBias = 0.01;

  iblShadowPipeline = new BABYLON.IblShadowsRenderPipeline(
    'archvision_ibl_shadows',
    scene,
  );

  meshes.forEach(mesh => iblShadowPipeline.addShadowCastingMesh(mesh));
  const materials = getUserMaterials();
  materials.forEach(mat => iblShadowPipeline.addShadowReceivingMaterial(mat));
  exclusionMeshes.forEach(name => {
    const mesh = scene.getMeshByName(name);
    iblShadowPipeline.removeShadowCastingMesh(mesh);
  });
  exclusionMaterials.forEach(name => {
    const mat = scene.getMaterialByName(name);
    iblShadowPipeline.removeShadowReceivingMaterial(mat);
  });
  iblShadowPipeline.toggleShadow(false);
};

const initGridGround = () => {
  const gridMaterial = newGridMaterial('gridMat');
  setDefaultGridProperties(gridMaterial);

  ground = newGround('ground', groundOptions());
  ground.material = gridMaterial;
  ground.isPickable = false;

  addHighlightExclusion(ground);
  newMetaDataEntry('viewportGround', ground.isVisible);
};

const initMirrorGround = () => {
  const shadowMat = new MATERIALS.ShadowOnlyMaterial('ground', scene);
  shadowMat.specularColor = newColor(0, 0, 0);
  shadowMat.diffuseColor = newColor(theme.colors[$selectedTheme].primary);
  shadowMat.shadowLevel = 0.4;
  shadowMat.alpha = 0.5;
  shadowMat.backFaceCulling = true;
  mirrorGround = newGround('mirrorGround', groundOptions());
  mirrorGround.material = shadowMat;
  mirrorGround.isPickable = false;
  mirrorGround.receiveShadows = props.integration?.shadows;

  addHighlightExclusion(mirrorGround);
  newMetaDataEntry('viewportShadows', mirrorGround.receiveShadows);
};

export const initScene = async args => {
  const { canvasRef } = args;
  canvas = canvasRef?.current;

  if (!engine && canvas) {
    engine = await newEngine();
    const nullEngine = engine instanceof BABYLON.NullEngine;

    window.removeEventListener(resize, resizeEngine);
    window.addEventListener(resize, resizeEngine);

    if (!nullEngine) {
      createScene();
      requestRender();
    }

    return;
  }

  createScene();
  requestRender();
};

export const createScene = createNewScene => {
  if (!scene || createNewScene) {
    scene = newScene();
    scene.useRightHandedSystem = false;
  }

  if (!theme && !$selectedTheme && props.theme && props.$selectedTheme) {
    theme = props.theme;
    $selectedTheme = props.$selectedTheme;
  }

  utilLayer = new BABYLON.UtilityLayerRenderer(scene, false);

  initSceneObjects();
  initListeners();

  if (props.materialMode) tweakSceneForShaderball();
  if (props.demoScene) loadDemoMeshes();

  if (props.file) {
    loadFromBlob(props.file);
    return;
  }

  if (props.guidURL && !props.reinitialize) loadFromGuidURL(props.guidURL);
};

const initMetadataKeyDefaults = () => {
  const { position, rotation, target } = scene.activeCamera;
  const [ tx, ty, tz ] = [ position.x, position.y, position.z ];
  const [ rx, ry, rz ] = [ rotation.x, rotation.y, rotation.z ];
  const [ tarX, tarY, tarZ ] = [ target.x, target.y, target.z ];

  const cameraSchema = { id: 'Default', tx, ty, tz, rx, ry, rz, tarX, tarY, tarZ };
  const downscaleDefault = downscaling.find(size => size.name === '4K').value;
  const defaultEnvironment = props.defaultEnvironments?.find?.(env => _.toLower(env.id) === 'default');
  const defaultGlobalTransforms = { tx: 0, ty: 0, tz: 0, rx: 0, ry: 0, rz: 0, sx: 1, sy: 1, sz: 1 };
  const publishSchema = {
    title: '',
    category: 'Other',
    tags: [],
    proxy: 'billboard',
    meshSimplification: {
      enabled: false,
      isControl: false,
      decimateAmount: 0.5,
      type: 'custom',
      optimizationType: 'estimated triangles',
      totalTriangles: 0,
      estimatedTriangles: 99,
      userTargetTriangles: 0,
      targetTriangles: 0,
    },
    textureDownscaling: {
      enabled: false,
      size: downscaleDefault,
    },
  };

  newMetaDataEntry('defaultEnvironment', defaultEnvironment?.envURL);
  newMetaDataEntry('defaultCameraPosition', {});
  newMetaDataEntry('materials', []);
  newMetaDataEntry('publish', publishSchema);
  newMetaDataEntry('selectedMeshes', []);
  newMetaDataEntry('materialVariants', []);
  newMetaDataEntry('hiddenMeshes', []);
  newMetaDataEntry('userMeshPositions', []);
  newMetaDataEntry('userCameraViews', []);
  newMetaDataEntry('userEnvironments', []);
  newMetaDataEntry('selectedAxisCompensation', 'Y-Axis up');
  newMetaDataEntry('boundingBoxEnabled', false);
  newMetaDataEntry('selectedMaterials', []);
  newMetaDataEntry('selectedMaterialVariant', null);
  newMetaDataEntry('selectedUserMeshPosition', {});
  newMetaDataEntry('selectedUserCameraView', cameraSchema);
  newMetaDataEntry('selectedOrthoView', '');
  newMetaDataEntry('selectedRatio', ratios.broadcast);
  newMetaDataEntry('screenshotData', '');
  newMetaDataEntry('safeFrameEnabled', false);
  newMetaDataEntry('selectedUserEnvironment', { id: 'Default', url: defaultEnvironment?.envURL });
  newMetaDataEntry('selectedSourceUnit', null);
  newMetaDataEntry('selectedDisplayUnit', null);
  newMetaDataEntry('meshChangeTracking', []);
  newMetaDataEntry('globalTransforms', defaultGlobalTransforms);
  newMetaDataEntry('viewportEnvironment', false);
  newMetaDataEntry('viewportGround', true);
  newMetaDataEntry('viewportAxes', true);
  newMetaDataEntry('viewportShadows',  mirrorGround.receiveShadows);
  newMetaDataEntry('iblShadowPipelineEnabled', false);
  newMetaDataEntry('fileName', null);
  newMetaDataEntry('uvScaleLock', false);
  newMetaDataEntry('lights', []);
  newMetaDataEntry('animations', []);
  newMetaDataEntry('selectedAnimation', null);
};

const checkForRootNode = () => {
  let rootNode = scene.getNodeById('__root__');
  let node0 = rootNode?.getChildren()?.find?.((child, index) => child.name === node0Name && index === 0);
  const isTransformNode = item => item?.getClassName() === 'TransformNode';
  const userMeshes = getUserMeshes();
  const userNodes = getUserNodes();
  const topLevelUserNodes = userNodes.filter(node => node.parent === null);
  const otherNode0Nodes = userNodes?.filter(node => node.name === node0Name && node.parent !== rootNode);

  if (!rootNode) {
    rootNode = new BABYLON.Mesh('__root__', scene);
    rootNode.isPickable = false;
    rootNode.isVisible = false;
    rootNode.isBlocker = true;
    rootNode.isPointerBlocker = true;
    rootNode.isHitTestVisible = false;
  }

  if (!node0 || !isTransformNode(node0)) {
    node0 = new BABYLON.TransformNode(node0Name, scene);
    node0.isPickable = false;
    node0.isVisible = false;
    node0.isBlocker = true;
    node0.isPointerBlocker = true;
    node0.isHitTestVisible = false;
    node0.parent = rootNode;
  }

  // NOTE: This renames all other node0's to avoid conflict with the main node0 for transformations etc.
  if (!_.isEmpty(otherNode0Nodes)) {
    otherNode0Nodes?.forEach((node, index) => node.name = `node0_(${index})`);
  }

  // NOTE: This handles correcting any third-party files that have __root__ and children
  // but not node0, and makes sure everything is a child of the newly created node0.
  rootNode.getChildren().forEach(child => {
    const transformNode = isTransformNode(child);
    const isNode0 = child.name === node0Name && transformNode;
    const parentedToNode0 = child.parent.name === node0Name && transformNode;
    const hasNode0AsChild = child.getChildren().find(node => node.name === node0Name);

    if (!isNode0 && !parentedToNode0 && !hasNode0AsChild) {
      child.setParent(node0);
    }
  });

  // NOTE: This handles correcting any parentless user meshes from a third-party file and makes
  // sure they are named with the prefix "mesh" and parented to node0.
  userMeshes.forEach(mesh => {
    if (mesh.name === node0Name && !isTransformNode(mesh)) mesh.name = 'mesh0';
    if (mesh.parent === null) mesh.setParent(node0);
  });

  // NOTE: This handles correcting any parentless user nodes from a third-party file and makes
  // sure they are parented to node0.
  topLevelUserNodes?.forEach(node => node.setParent(node0));

  // Clean up empty nodes after above normalization
  rootNode.getChildren().forEach(child => {
    const transformNode = isTransformNode(child);
    const noChildren = child.getChildren().length === 0;

    if (transformNode && noChildren) child.dispose();
  });
};

export const initSceneFromFile = (sceneFromFile, fileName) => {
  if (sceneFromFile) {
    const preserveEngine = true;
    cleanup(preserveEngine);
    scene = sceneFromFile;
    scene.useRightHandedSystem = false;
    utilLayer = new BABYLON.UtilityLayerRenderer(scene, false);

    window.removeEventListener(resize, resizeEngine);
    window.addEventListener(resize, resizeEngine);

    initListeners();
    initSceneObjects();
    modelToOrigin();
    focusCamera();
    requestRender();

    const { position, rotation } = scene.activeCamera;
    const [ tx, ty, tz ] = [ position.x, position.y, position.z ];
    const [ rx, ry, rz ] = [ rotation.x, rotation.y, rotation.z ];
    const node0 = scene.getNodeByName(node0Name);
    const defaultMeshPositions = buildMeshPositionsArray().map(positionData => {
      return { ...positionData, positionId: 'Default' };
    });

    const globalsAfterSceneLoad = {
      tx: node0?.position?.x || 0,
      ty: node0?.position?.y || 0,
      tz: node0?.position?.z || 0,
      rx: node0?.rotation?.x || 0,
      ry: node0?.rotation?.y || 0,
      rz: node0?.rotation?.z || 0,
      sx: node0?.scaling?.x || 1,
      sy: node0?.scaling?.y || 1,
      sz: node0?.scaling?.z || 1,
    };

    warningChecks();
    if (props.materialMode) tweakSceneForShaderball();

    initMetadataKeyDefaults();
    getStatisticsMetadata(props);
    newMetaDataEntry('defaultCameraPosition', { id: 'default', tx, ty, tz, rx, ry, rz });
    newMetaDataEntry('userMeshPositions', defaultMeshPositions);
    newMetaDataEntry('selectedUserMeshPositions', buildMeshPositionsArray());
    newMetaDataEntry('meshChangeTracking', buildMeshPositionsArray());
    newMetaDataEntry('globalTransforms', globalsAfterSceneLoad);
    newMetaDataEntry('materials', buildMaterialsArray());
    newMetaDataEntry('selectedMaterials', buildSelectedMaterialArray());
    newMetaDataEntry('fileName', fileName);
    newMetaDataEntry('materialVariants', buildMaterialVariantsArray());
    const animations = buildAnimationsArray();
    const activeAnimationIndex = scene.animationGroups?.findIndex(group => group.isPlaying) ?? -1;
    newMetaDataEntry('animations', animations);
    newMetaDataEntry(
      'selectedAnimation',
      activeAnimationIndex >= 0 ? `${activeAnimationIndex}` : null,
    );
    newMetaDataEntry('uvScaleLock', scene.metadata.uvScaleLock || false);
    newMetaDataEntry('lights', buildLightsArray());

    updatePublish(fileName ? { payload: { title: fileName } } : {});

    props.setSerializedData?.(serializeScene());
    scene.executeWhenReady?.(requestRender);
  }
};

const initListeners = () => {
  shortcutArray = [];

  if (!props.materialMode) {
    const cameraShortcuts = Object.values(shortcuts.camera);
    const viewportShortcuts = Object.values(shortcuts.viewport);
    const modelShortcuts = Object.values(shortcuts.model);
    const generalListeners = [ ...cameraShortcuts, ...viewportShortcuts, ...modelShortcuts ];
    shortcutArray = [ ...shortcutArray, ...generalListeners ];

    handleMouseListeners();
  }

  if (!props.materialMode && !props.previewMode) loadFromDragDrop();

  if (!props.previewMode) {
    const utilitiesShortcuts = Object.values(shortcuts.utilities);
    const previewListeners = [ ...utilitiesShortcuts ];
    shortcutArray = [ ...shortcutArray, ...previewListeners ];
  }

  handleShortcutListeners();
};

export const cleanup = preserveEngine => {
  const remove = true;
  handleShortcutListeners(remove);
  props.setSerializedData?.(null);
  removeAutoRotation();
  renderState.dirty = false;
  renderState.interaction = false;
  renderState.running = false;
  renderState.idleAt = null;

  window.removeEventListener(resize, resizeEngine);

  if (!preserveEngine) {
    engine?.dispose();
    engine = null;
  }

  resetAnimationSync();
  scene?.dispose();
  scene = null;
};
