import * as BABYLON from 'babylonjs';
import * as BABYLONGUI from 'babylonjs-gui';
import {
  getUserMeshes,
  gizmoManager,
  newFraming,
  scene,
  selectedMeshes,
  theme,
  $selectedTheme,
  newColor,
  toggleCameraMode,
  getParamOfSelectedMeshes,
  getUserMaterials,
  guiTexture,
  getBoundingMeshData,
  singleMeshTNode,
  multiMeshTNode,
  createBoundingMesh,
  restoreParents,
  createSafeFrame,
  newMetaDataEntry,
  resetSelectedMeshes,
  buildMaterialsArray,
  buildSelectedMaterialArray,
  addAutoRotation,
  removeAutoRotation,
  autoRotation,
  buildMeshPositionsArray,
} from '../helpers';
import { GIZMOS, GUI, MESH_PARAMS } from '../constants';
import { exclusionMaterials, orthoOptions, dimensions, scaleUnits } from '../enums';
import _ from 'lodash';

export let cameraView;

export const resetManagerGizmos = type => {
  Object.values(GIZMOS).forEach(value => {
    if (value !== type) {
      const propertyName = _.camelCase(`${value}Enabled`);
      gizmoManager[propertyName] = false;
    }
  });

  dimensions.forEach(dimension => {
    guiTexture.getControlByName(dimension.rectangle)?.dispose?.();
    guiTexture.getControlByName(dimension.label)?.dispose?.();
    guiTexture.getControlByName(dimension.container)?.dispose?.();
  });

  switch(selectedMeshes.length) {
    case 1:
      gizmoManager.attachToNode(singleMeshTNode);
      break;

    default:
      gizmoManager.attachToNode(multiMeshTNode);
      break;
  }
};

export const toggleTurntable = override => {
  const camera = scene.activeCamera;
  const rotationEnabled = camera.useAutoRotationBehavior;

  if (!autoRotation) addAutoRotation();

  if (override !== undefined) {
    camera.useAutoRotationBehavior = override;

    if (override) {
      autoRotation.attach(camera);
      autoRotation.idleRotationSpeed = -0.1;
      return;
    }

    autoRotation.detach(camera);
    removeAutoRotation();
    return;
  }

  if (rotationEnabled) {
    autoRotation.detach(camera);
    camera.useAutoRotationBehavior = false;
    removeAutoRotation();
    return;
  }

  autoRotation.attach(camera);
  camera.useAutoRotationBehavior = true;
  autoRotation.idleRotationSpeed = -0.1;
};

export const focusCamera = () => {
  const camera = scene.activeCamera;
  const userMeshes = getUserMeshes();
  const framingBehavior = newFraming();
  const babylonOrthographic = BABYLON.Camera.ORTHOGRAPHIC_CAMERA;
  const isOrtho = camera.mode === babylonOrthographic;
  const boundingMesh = scene.getMeshById('boundingMesh');
  const meshCenter = getParamOfSelectedMeshes(MESH_PARAMS.center);
  const framingMeshes = boundingMesh ? [ boundingMesh ] : userMeshes;

  framingBehavior.framingTime = 0;
  framingBehavior.defaultElevation = 0;

  framingBehavior.attach?.(camera);

  !_.isEmpty(userMeshes)
    ? framingBehavior.zoomOnMeshesHierarchy(framingMeshes)
    : camera.setTarget(BABYLON.Vector3.Zero());

  camera.beta = Math.PI / 2;

  if (isOrtho) {
    camera.target = meshCenter;
    camera.alpha = orthoOptions[cameraView].alpha;
    camera.beta = orthoOptions[cameraView].beta;
  }

  framingBehavior.detach?.(camera);
};

export const toggleSafeFrame = (e, aspectRatio, override) => {
  let outerFrame = guiTexture.getControlByName(GUI.outerSafeFrame);

  if (aspectRatio) {
    outerFrame?.dispose();
    createSafeFrame(aspectRatio);
    outerFrame = guiTexture.getControlByName(GUI.outerSafeFrame);
    outerFrame.isVisible = override;
    newMetaDataEntry('safeFrameEnabled', outerFrame.isVisible);
    return;
  }

  if (override === undefined && outerFrame) {
    outerFrame.isVisible = !outerFrame.isVisible;
    newMetaDataEntry('safeFrameEnabled', outerFrame.isVisible);
    return;
  }
};

export const toggleInspector = () => {
  const inspector = scene.debugLayer;
  inspector.isVisible() ? inspector.hide() : inspector.show();
};

export const toggleMoveWidget = () => {
  if (!_.isEmpty(selectedMeshes)) {
    resetManagerGizmos(GIZMOS.PositionGizmo);
    gizmoManager.positionGizmoEnabled = !gizmoManager.positionGizmoEnabled;
  }
};

export const toggleRotateWidget = () => {
  if (!_.isEmpty(selectedMeshes)) {
    resetManagerGizmos(GIZMOS.RotationGizmo);
    gizmoManager.rotationGizmoEnabled = !gizmoManager.rotationGizmoEnabled;
  }
};

export const toggleScaleWidget = () => {
  if (!_.isEmpty(selectedMeshes)) {
    resetManagerGizmos(GIZMOS.ScaleGizmo);
    gizmoManager.scaleGizmoEnabled = !gizmoManager.scaleGizmoEnabled;
  }
};

export const toggleBoundingBoxWidget = () => {
  const boundingMesh = scene.getMeshById('boundingMesh');
  const boxColor = newColor(theme.colors[$selectedTheme].accent);
  const isEnabled = gizmoManager.boundingBoxGizmoEnabled;
  const boundingInfo = getBoundingMeshData([ boundingMesh ]);

  dimensions.forEach(dimension => {
    guiTexture.getControlByName(dimension.rectangle)?.dispose?.();
    guiTexture.getControlByName(dimension.label)?.dispose?.();
    guiTexture.getControlByName(dimension.container)?.dispose?.();
  });

  if (!gizmoManager.boundingBoxGizmoEnabled) {
    resetManagerGizmos(GIZMOS.BoundingBoxGizmo);
    gizmoManager.attachToMesh(boundingMesh);
  }

  gizmoManager.boundingBoxGizmoEnabled = !isEnabled;
  gizmoManager.boundingBoxDragBehavior.rotateDraggedObject = false;
  gizmoManager.boundingBoxDragBehavior.disableMovement = true;
  gizmoManager.gizmos.boundingBoxGizmo.setEnabledScaling(false);
  gizmoManager.gizmos.boundingBoxGizmo.setEnabledRotationAxis('');
  gizmoManager.gizmos.boundingBoxGizmo.setColor(boxColor);
  newMetaDataEntry('boundingBoxEnabled', !isEnabled);

  // ================ Bounding Box Labels ================
  const buildUnitLabel = dimension => {
    const { name, color, label, rectangle, container } = dimension;
    const { center, minimum, maximumWorld, minimumWorld } = boundingInfo;
    const displayUnit = scene.metadata?.selectedDisplayUnit || scene.metadata?.selectedSourceUnit;
    const labelHeight = 36;
    let x, y, z, value;

    const buildValue = axis => {
      const workingValue = Math.abs(minimumWorld[axis] - maximumWorld[axis]);
      const meterScaleFactor = scaleUnits.find(unit => unit.abbreviation === 'm').scaleFactor;
      const displayScaleFactor = scaleUnits.find(unit => (
        unit.abbreviation === displayUnit || unit.name === displayUnit
      ))?.scaleFactor || meterScaleFactor;

      return (workingValue / displayScaleFactor).toFixed(6);
    };

    switch(name) {
      case 'length':
        x = center.x + (maximumWorld.x - minimumWorld.x) / 2;
        y = center.y - Math.abs(minimum.y);
        z = center.z;
        value = buildValue('z');
      break;
      case 'width':
        x = center.x;
        y = center.y - Math.abs(minimum.y);
        z = center.z + (maximumWorld.z - minimumWorld.z) / 2;
        value = buildValue('x');
      break;

      default:
        x = center.x + (maximumWorld.x - minimumWorld.x) / 2;
        y = center.y;
        z = center.z + (maximumWorld.z - minimumWorld.z) / 2;
        value = buildValue('y');
    }

    const lengthPosition = new BABYLON.Vector3(x, y, z);
    const labelRectangle = new BABYLONGUI.Rectangle(rectangle);
    labelRectangle.width = displayUnit ? 0.14 : 0.12;
    labelRectangle.heightInPixels = labelHeight;
    labelRectangle.cornerRadius = 3;
    labelRectangle.color = color;
    labelRectangle.thickness = 1;
    labelRectangle.background = color + 20;

    guiTexture.addControl(labelRectangle);

    const lengthLabel = new BABYLONGUI.TextBlock(label);

    lengthLabel.text = `${_.startCase(name)}: ${value}${displayUnit || ''}`;
    labelRectangle.addControl(lengthLabel);

    const labelLinkBox = BABYLON.MeshBuilder.CreateBox(container, scene);
    labelLinkBox.position = lengthPosition;
    labelLinkBox.isPickable = false;
    labelLinkBox.visibility = 0;
    labelLinkBox.setParent(boundingMesh);

    labelRectangle.linkWithMesh(labelLinkBox);
    labelRectangle.linkOffsetX = 30;
    labelRectangle.linkOffsetY = 30;
  };

  if (gizmoManager.boundingBoxGizmoEnabled) {
    dimensions.forEach(dimension => buildUnitLabel(dimension));
  }
};

export const hideSelected = () => {
  selectedMeshes.forEach(mesh => {
    mesh.isVisible = false;
  });

  resetManagerGizmos();
};

export const unhideAll = () => {
  scene.meshes.forEach(mesh => {
    if (!mesh.isVisible) {
      mesh.isVisible = true;
    }
  });
};

export const deleteSelected = () => {
  selectedMeshes.forEach(mesh => {
    const material = mesh.material;
    if (material?.getBindedMeshes().length === 1) {
      const subMaterials = material.subMaterials;
      const defaultMaterial = name => exclusionMaterials.includes(name);

      if (subMaterials) {
        subMaterials?.forEach(subMaterial => !defaultMaterial(subMaterial.name) && subMaterial.dispose());
      }

      if (!defaultMaterial(material.name)) material.dispose();
      material.getActiveTextures().forEach(texture => texture.dispose());
    }

    resetManagerGizmos();
    mesh.dispose();
  });

  resetSelectedMeshes();
};

export const toggleOrthographicViews = (e, orthoCamera) => {
  const isDifferent = cameraView !== orthoCamera;
  const babylonOrthographic = BABYLON.Camera.ORTHOGRAPHIC_CAMERA;
  const babylonPerspective = BABYLON.Camera.PERSPECTIVE_CAMERA;

  if (scene?.debugLayer.isVisible()) return;

  if (isDifferent) {
    cameraView = orthoCamera;
    toggleCameraMode(orthoCamera, babylonOrthographic);
    newMetaDataEntry('selectedOrthoView', orthoCamera);
    return;
  }

  toggleCameraMode(orthoCamera, babylonPerspective);
  newMetaDataEntry('selectedOrthoView', '');
  cameraView = null;
};

export const toggleCulling = () => {
  const materials = getUserMaterials();

  materials.forEach(material => {
    const isCullingBackfaces = material.backFaceCulling;
    if (isCullingBackfaces) return (material.backFaceCulling = false);
    material.backFaceCulling = !material.backFaceCulling;
  });

  newMetaDataEntry('materials', buildMaterialsArray());
  newMetaDataEntry('selectedMaterials', buildSelectedMaterialArray());
};

export const modelToOrigin = sourceUnitScaleOperation => {
  const boundingBoxEnabled = gizmoManager.boundingBoxGizmoEnabled;
  const meshes = selectedMeshes.length > 0 ? selectedMeshes : getUserMeshes();
  const parentList = [];

  const handleBoundingBox = () => {
    const newBox = createBoundingMesh();
    if (boundingBoxEnabled && !sourceUnitScaleOperation) toggleBoundingBoxWidget();
    if (gizmoManager.boundingBoxGizmoEnabled) gizmoManager.attachToMesh(newBox);
    resetManagerGizmos(GIZMOS.BoundingBoxGizmo);
  };

  // Single selections or only single mesh in scene.
  if (meshes.length === 1) {
    const mesh = meshes[0];
    const boundingBox = mesh.getBoundingInfo().boundingBox;
    const meshCenter = boundingBox.centerWorld;
    const parent = mesh.parent;

    parentList.push({ mesh, parent });
    mesh.setParent(singleMeshTNode);

    singleMeshTNode.position.subtractInPlace(meshCenter);
    singleMeshTNode.position.y += boundingBox.extendSizeWorld.y;
    selectedMeshes.length === 1 ? mesh.setParent(singleMeshTNode) : restoreParents(parentList);

    handleBoundingBox();
    newMetaDataEntry('meshChangeTracking', buildMeshPositionsArray({ preserveScale: true, preserveRotation: true }));
    return;
  }

  // Multiple selections or multiple meshes in scene.
  const multiBoundingData = getBoundingMeshData(meshes);
  const boundingCenter = multiBoundingData.center;
  const compensationY = multiBoundingData.maximum.y - boundingCenter.y;

  meshes.forEach(mesh => {
    const parent = mesh.parent;
    parentList.push({ mesh, parent });
    mesh.setParent(multiMeshTNode);
  });

  if (!sourceUnitScaleOperation) {
    multiMeshTNode.position.subtractInPlace(boundingCenter);
    multiMeshTNode.position.y += compensationY;
  }

  restoreParents(parentList);
  handleBoundingBox();
  newMetaDataEntry('meshChangeTracking', buildMeshPositionsArray({ preserveScale: true, preserveRotation: true }));
};