import * as BABYLON from 'babylonjs';
import { scene, selectedMeshes, newVector, getRadius, getMidpoint, canvas } from '../helpers';
import { exclusionMeshes, exclusionTNodes, scaleUnits } from '../enums';
import { MESH_PARAMS } from '../constants';
import _ from 'lodash';
export const getUserNodes = () => {
  const userNodes = scene.transformNodes.filter(node => {
    const validNode = !exclusionTNodes.includes(node.id);
    if (validNode) return node;
  });
  return userNodes;
};
export const getUserMeshes = () => {
  const meshes = scene.meshes.filter(mesh => {
    const validMesh = !exclusionMeshes.includes(mesh.id) && mesh.subMeshes;
    if (validMesh) return mesh;
  });
  return meshes;
};
export const newGround = (name, groundOptions = {}) => {
  const groundName = name || `ground_${getUserMeshes().length + 1}`;
  return BABYLON.MeshBuilder.CreateGround(groundName, groundOptions, scene);
};
export const getScaleFactor = sourceUnit => {
  const unit = _.toLower(sourceUnit);
  const validUnit = sourceUnit && scaleUnits.find(item => item.name === unit || item.abbreviation === unit);
  return validUnit ? validUnit.scaleFactor : 1;
};
export const getBoundingMeshData = meshArray => {
  const meshes = meshArray || getUserMeshes();
  const singleSelection = meshes.length === 1;
  const singleBox = singleSelection && meshes[0]?.getBoundingInfo?.().boundingBox;
  const scaling = singleSelection && meshes[0].scaling;
  const rotation = singleSelection && meshes[0].rotation;
  let minimum = newVector(0, 0, 0);
  let maximum = newVector(0, 0, 0);
  for (let i = 0; i < meshes.length; i++) {
    const boundingInfo = meshes[i]?.getBoundingInfo?.();
    if (boundingInfo) {
      const workingBoundingBox = boundingInfo.boundingBox;
      if (i === 0) {
        maximum = workingBoundingBox.maximumWorld;
        minimum = workingBoundingBox.minimumWorld;
        continue;
      }
      maximum = maximum.maximizeInPlace(workingBoundingBox.maximumWorld);
      minimum = minimum.minimizeInPlace(workingBoundingBox.minimumWorld);
    }
  }
  const boundingBox = {
    ...(singleBox || {}),
    center: getMidpoint(minimum, maximum),
    minimum,
    maximum,
    radius: getRadius(minimum, maximum),
    scaling: scaling || newVector(1, 1, 1),
    rotation: rotation || newVector(0, 0, 0)
  };
  if (!_.isEmpty(boundingBox)) return boundingBox;
};
export const getBoundingDistance = () => {
  const sceneBoundingMesh = getBoundingMeshData();
  if (sceneBoundingMesh) {
    const boundingBox = sceneBoundingMesh;
    const min = boundingBox.minimum;
    const max = boundingBox.maximum;
    const xDelta = Math.abs(max._x - min._x);
    const yDelta = Math.abs(max._y - min._y);
    const zDelta = Math.abs(max._z - min._z);
    const distance = Math.sqrt(Math.pow(xDelta, 2) + Math.pow(yDelta, 2) + Math.pow(zDelta, 2));
    return distance;
  }
};
export const getParamOfSelectedMeshes = parameter => {
  const meshes = selectedMeshes.length > 0 ? selectedMeshes : getUserMeshes();
  const boundingBox = getBoundingMeshData(meshes);
  if (boundingBox) {
    const {
      center,
      radius,
      rotation,
      scaling,
      centerWorld
    } = boundingBox;
    switch (parameter) {
      case MESH_PARAMS.rotation:
        return rotation;
      case MESH_PARAMS.scaling:
        return scaling;
      case MESH_PARAMS.radius:
        return radius;
      default:
        return centerWorld || center;
    }
  }
};
export const getMeshVertices = (meshes, kind = BABYLON.VertexBuffer.PositionKind) => {
  const groupedVertices = [];
  meshes.forEach(mesh => {
    const buffer = mesh.getVerticesData(kind);
    const vertices = [];
    for (let i = 0; i < buffer.length; i += 3) {
      const vertex = newVector(buffer[i], buffer[i + 1], buffer[i + 2]);
      vertices.push(vertex);
    }
    groupedVertices.push({
      mesh: mesh.id,
      vertices
    });
  });
  return groupedVertices;
};
export const getWorldPositionsByMesh = (meshName, vertices) => {
  const mesh = scene.getMeshById(meshName);
  const meshWorldMatrix = mesh.getWorldMatrix(true);
  const positionArray = vertices.map(vertex => {
    return BABYLON.Vector3.TransformCoordinates(vertex, meshWorldMatrix);
  });
  return positionArray;
};

// Below is from https://forum.unity.com/threads/calculating-world-to-screen-projection-manully.1197199/
// It is converted to work specifically with BabylonJS

const WorldToLocal = (aCamPos, aCamRot, aPos) => {
  const inverse = BABYLON.Quaternion.Inverse(aCamRot);
  const camToVertex = aPos.subtract(aCamPos);
  const toLocal = camToVertex.applyRotationQuaternion(inverse);
  return toLocal;
};
const Project = (aPos, aFov, aAspect) => {
  let f = 1 / Math.tan(aFov * 0.5);
  f = f / aPos.z;
  aPos.x *= f / aAspect;
  aPos.y *= f;
  return aPos;
};
const ClipSpaceToViewport = aPos => {
  aPos.x = aPos.x * 0.5 + 0.5;
  aPos.y = aPos.y * 0.5 + 0.5;
  return aPos;
};
const WorldToViewport = (aCamPos, aCamRot, aFov, aAspect, aPos) => {
  let p = WorldToLocal(aCamPos, aCamRot, aPos);
  p = Project(p, aFov, aAspect);
  return ClipSpaceToViewport(p);
};
const WorldToScreenPos = (aCamPos, aCamRot, aFov, aScrWidth, aScrHeight, aPos) => {
  const p = WorldToViewport(aCamPos, aCamRot, aFov, aScrWidth / aScrHeight, aPos);
  p.x *= aScrWidth;
  p.y *= aScrHeight;
  return p;
};
export const WorldToGUIPos = (aCamPos, aCamRot, aFov, aScrWidth, aScrHeight, aPos) => {
  const xScale = 1 / aScrWidth;
  const yScale = 1 / aScrHeight;
  let p = WorldToScreenPos(aCamPos, aCamRot, aFov, aScrWidth, aScrHeight, aPos);
  p.y = aScrHeight - p.y;
  p = p.multiplyByFloats(xScale, yScale, 1);
  return p;
};
export const getScaleToZeroOne = vertices => {
  const scaledVertices = vertices.map(vertex => {
    const xScale = 1 / canvas.width;
    const yScale = 1 / canvas.height;
    return vertex.multiplyByFloats(xScale, yScale, 1);
  });
  return scaledVertices;
};