import * as BABYLON from 'babylonjs';
import * as BABYLONGUI from 'babylonjs-gui';
import {
  scene,
  highlightManager,
  hoverHighlightManager,
  theme,
  $selectedTheme,
  newColor,
  newVector,
  groundOptions,
  guiTexture,
  attachToSelectMeshesNode,
  getMeshesInCameraView,
  getMeshVertices,
  getWorldPositionsByMesh,
  canvas,
  WorldToGUIPos,
  serializeScene,
  toRadians,
  newMetaDataEntry,
  buildMeshIdArray,
  gizmoManager,
  getUserMeshes,
  getUserMaterials,
  buildSelectedMaterialArray,
  renderState,
  requestRender,
} from '../helpers';
import { GUI, MESH_COUNT_THRESHOLD, AXIS_COLORS } from '../constants';
import { resetManagerGizmos } from '../actions';
import { reactProps as props } from '../Canvas';
import { extent } from 'd3-array';
import _ from 'lodash';

const highlightColor = () => newColor(theme.colors[$selectedTheme].accent);
const hoverColor = () => newColor(theme.colors[$selectedTheme].white);
export let selectedMeshes = [];
export let selectedMaterials = [];
export let hoverMesh;
let hadSelection = false;
let tempMesh;
let dragStarted = false;
let hasDragged = false;
let x1;
let y1;
let x2;
let y2;

const isInstanceMesh = mesh => mesh instanceof BABYLON.InstancedMesh;

const isTargetIn = (startPosition, endPosition, target) => {
  const minX = Math.min(startPosition.x, endPosition.x);
  const minY = Math.min(startPosition.y, endPosition.y);
  const maxX = Math.max(startPosition.x, endPosition.x);
  const maxY = Math.max(startPosition.y, endPosition.y);

  return minX < target.maxX && target.minX < maxX && minY < target.maxY && target.minY < maxY;
};

const getClientRectFromMesh = mesh => {
  const meshVectors = mesh?.getBoundingInfo().boundingBox.vectors;

  const worldMatrix = mesh.getWorldMatrix();
  const transformMatrix = scene.getTransformMatrix();
  const viewport = scene.activeCamera.viewport;

  const coordinates = meshVectors.map(v => {
    const proj = BABYLON.Vector3.Project(
      v,
      worldMatrix,
      transformMatrix,
      viewport,
    );
    proj.x = proj.x * canvas.clientWidth;
    proj.y = proj.y * canvas.clientHeight;
    return proj;
  });
  const [ minX, maxX ] = extent(coordinates, c => c.x);
  const [ minY, maxY ] = extent(coordinates, c => c.y);

  const rect = {
    minX,
    minY,
    maxX,
    maxY,
  };

  return rect;
};

export const addNewMesh = selectedMesh => {
  const hasMaterial = !_.isEmpty(selectedMesh.material);
  const selectedMaterial = selectedMesh.material;
  const inSelectedMaterials = selectedMaterials.includes(selectedMaterial);

  if (isInstanceMesh(selectedMesh)) {
    const message = 'Instance mesh selection unavailable. Please use the Materials panel.';
    const config = { type: 'warning' };
    const clearExisting = true;

    props.addNotification([ { message, config } ], clearExisting);
    return;
  }

  if (!selectedMeshes.includes(selectedMesh)) {
    selectedMeshes.push(selectedMesh);
  }

  if (hasMaterial && !inSelectedMaterials) {
    selectedMaterials.push(selectedMesh.material);
  }

  attachToSelectMeshesNode();
};

const removeSelectedMesh = selectedMesh => {
  if (tempMesh) {
    const tempHasMaterial = !_.isEmpty(tempMesh.material);
    const includesTempMaterial = selectedMaterials.includes(tempMesh.material);
    selectedMeshes.push(tempMesh);
    if (tempHasMaterial && !includesTempMaterial) selectedMaterials.push(tempMesh.material);
    tempMesh = null;
  }

  const withoutSelected = selectedMeshes.filter(mesh => !_.isEqual(mesh, selectedMesh) && mesh.subMeshes);
  selectedMeshes = withoutSelected;
  selectedMaterials = [];
  withoutSelected.forEach(mesh => {
    const hasMaterial = !_.isEmpty(mesh.material);
    const includesMaterial = selectedMaterials.includes(mesh.material);

    if (hasMaterial && !includesMaterial) selectedMaterials.push(mesh.material);
  });
  attachToSelectMeshesNode();
};

const addHoverMesh = pickedMesh => {
  if (props.integration?.meshHighlighting === false) return;
  if (props.previewMode) return;

  const isSelected = selectedMeshes.includes(pickedMesh);

  removeHoverMesh();

  if (isSelected) {
    tempMesh = pickedMesh;
    highlightManager.removeMesh(pickedMesh);
    hoverHighlightManager.addMesh(pickedMesh, hoverColor());
    return;
  }

  if (hoverMesh) removeHoverMesh();
  if (isInstanceMesh(pickedMesh)) return;

  hoverMesh = pickedMesh;
  hoverHighlightManager.addMesh(hoverMesh, hoverColor());
};

const removeHoverMesh = () => {
  if (props.integration?.meshHighlighting === false) return;
  if (props.previewMode) return;

  hoverHighlightManager.removeAllMeshes();

  if (tempMesh) {
    highlightManager.addMesh(tempMesh, highlightColor());
    tempMesh = null;
  }

  hoverMesh = null;
};

export const resetSelectedMeshes = () => {
  const userMaterials = getUserMaterials();
  const firstMaterial = userMaterials[0] || {};

  newMetaDataEntry('selectedMaterials', []);

  if (!_.isEmpty(firstMaterial)) {
    const existingMaterial = scene.metadata?.materials?.find?.(mat => mat.materialId === firstMaterial.id);
    newMetaDataEntry('selectedMaterials', [ existingMaterial ]);
  }

  if (props.integration?.meshHighlighting !== false && highlightManager) {
    highlightManager.removeAllMeshes();
  }

  selectedMeshes = [];
  selectedMaterials = [];
  newMetaDataEntry('selectedMeshes', []);
  props.setSerializedData?.(serializeScene());
};

export const refreshHighlight = () => {
  if (props.integration?.meshHighlighting === false) return;

  highlightManager.removeAllMeshes();
  selectedMeshes.forEach(mesh => highlightManager.addMesh(mesh, highlightColor()));
};

const handleDragStart = e => {
  if (e.event.shiftKey) {
    const { event } = e;
    x1 = event.clientX - (event.clientX - event.offsetX);
    y1 = event.clientY - (event.clientY - event.offsetY);
    dragStarted = true;
    scene.activeCamera.inputs.attached.pointers.detachControl();
  }
};

const handleDrag = e => {
  if (dragStarted) {
    hasDragged = true;
    const { event } = e;
    const dragBox = guiTexture.getControlByName(GUI.dragSelectBox);
    x2 = event.clientX - (event.clientX - event.offsetX);
    y2 = event.clientY - (event.clientY - event.offsetY);

    dragBox.isVisible = true;
    dragBox.left = Math.min(x1, x2);
    dragBox.top = Math.min(y1, y2);
    dragBox.widthInPixels = Math.abs(x2 - x1);
    dragBox.heightInPixels = Math.abs(y2 - y1);
  }
};

const boundingBoxSelection = (meshes, start, end) => {
  meshes.forEach(mesh => {
    const meshRect = getClientRectFromMesh(mesh);
    const inTarget = isTargetIn(start, end, meshRect);
    const shouldBeSelected = mesh.isPickable && inTarget;
    if (shouldBeSelected) addNewMesh(mesh);
  });
};

const verticeSelection = (verticesByMesh, camera, start, end, debug) => {
  const worldPositionsByMesh = verticesByMesh.map(obj => {
    return {
      mesh: obj.mesh,
      worldPositions: getWorldPositionsByMesh(obj.mesh, obj.vertices),
    };
  });

  const worldToScaleGUIScreen = worldPositionsByMesh.map(obj => {
    const guiPositions = [];

    obj.worldPositions.forEach(vertex => {
      const guiPos = WorldToGUIPos(
        camera.position,
        camera.absoluteRotation,
        camera.fov,
        canvas.width,
        canvas.height,
        vertex,
      );
      guiPositions.push(guiPos);
    });

    return {
      mesh: obj.mesh,
      vertices: guiPositions,
    };
  });

  worldToScaleGUIScreen.forEach(obj => {
    if (debug) {
      obj.vertices.forEach(vertex => {
        const dot = new BABYLONGUI.Rectangle('item');
        dot.cornerRadius = 1;
        dot.thickness = 2;
        dot.alpha = 0.75;
        dot.color = theme.colors[$selectedTheme].accent;
        dot.background = `${theme.colors[$selectedTheme].accent}25`;
        dot.horizontalAlignment = BABYLONGUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
        dot.verticalAlignment = BABYLONGUI.Control.VERTICAL_ALIGNMENT_TOP;
        dot.leftInPixels = vertex.x * canvas.width;
        dot.topInPixels = vertex.y * canvas.height;
        dot.widthInPixels = 1;
        dot.heightInPixels = 1;

        guiTexture.addControl(dot);
      });
    }

    const shouldSelect = obj.vertices.some(vertex =>
      vertex.x <= end.x && vertex.x >= start.x && vertex.y <= end.y && vertex.y >= start.y,
    );
    if (shouldSelect && hasDragged) addNewMesh(scene.getMeshById(obj.mesh));
  });
};

const handleDragEnd = () => {
  const debug = false;

  if (dragStarted) {
    const dragBox = guiTexture.getControlByName(GUI.dragSelectBox);
    const camera = scene.activeCamera;
    const possibleMeshes = getMeshesInCameraView();
    const verticesByMesh = getMeshVertices(possibleMeshes);
    const minRangeX = dragBox.leftInPixels / canvas.width;
    const minRangeY = dragBox.topInPixels / canvas.height;
    const maxRangeX = (dragBox.leftInPixels + dragBox.widthInPixels) / canvas.width;
    const maxRangeY = (dragBox.topInPixels + dragBox.heightInPixels) / canvas.height;
    const manyMeshes = getUserMeshes().length > MESH_COUNT_THRESHOLD;
    const startPos = { x: minRangeX, y: minRangeY };
    const endPos = { x: maxRangeX, y: maxRangeY };
    const bbStartPos = { x: minRangeX * canvas.width, y: minRangeY * canvas.height };
    const bbEndPos = { x: maxRangeX * canvas.width, y: maxRangeY * canvas.height };

    manyMeshes
      ? boundingBoxSelection(possibleMeshes, bbStartPos, bbEndPos)
      : verticeSelection(verticesByMesh, camera, startPos, endPos, debug);

    if (selectedMeshes.length > 1) attachToSelectMeshesNode();

    dragBox.isVisible = false;
    x1 = null;
    y1 = null;
    x2 = null;
    y2 = null;
    dragStarted = false;
    hasDragged = false;

    scene.activeCamera.inputs.attached.pointers.attachControl();
    refreshHighlight();
  }
};

export const handleMousePointerTap = e => {
  const clickedOnMesh = e?._pickInfo?.hit;
  if (!clickedOnMesh && !dragStarted) {
    resetSelectedMeshes();
    resetManagerGizmos();
    attachToSelectMeshesNode();
  }
};

const handleMousePointerPick = e => {
  const meshName = e._pickInfo?.pickedMesh?.name;
  const selectedMesh = meshName && scene.getMeshByName(meshName);

  if (hasDragged) return;
  if (!e.event.shiftKey) resetSelectedMeshes();
  if (!e.event.shiftKey && selectedMesh) return addNewMesh(selectedMesh);

  selectedMeshes.includes(selectedMesh) ? removeSelectedMesh(selectedMesh) : addNewMesh(selectedMesh);
};

const handleMousePointerMove = () => {
  if (props.previewMode) return;
  requestRender();
  const ray = scene.createPickingRay(scene.pointerX, scene.pointerY, BABYLON.Matrix.Identity(), scene.activeCamera);
  const { hit, pickedMesh } = scene.pickWithRay(ray);

  !hit ? removeHoverMesh() : addHoverMesh(pickedMesh);
};

const handleSerializationUpdates = () => {
  const differentSelection = !_.isEqual(scene.metadata?.selectedMeshes, buildMeshIdArray());
  const deselecting = _.isEmpty(selectedMeshes) && hadSelection;

  if (differentSelection) {
    newMetaDataEntry('selectedMeshes', buildMeshIdArray());
    newMetaDataEntry('selectedMaterials', buildSelectedMaterialArray());
    props.setSerializedData?.(serializeScene());
    hadSelection = true;
  }

  if (deselecting) {
    hadSelection = false;
    resetSelectedMeshes();
  }

  if (gizmoManager.boundingBoxGizmoEnabled !== scene.metadata.boundingBoxEnabled) {
    newMetaDataEntry('boundingBoxEnabled', gizmoManager.boundingBoxGizmoEnabled);
    props.setSerializedData?.(serializeScene());
  }
};

export const handleMouseListeners = () => {
  scene.onPointerObservable.add(e => {
    switch (e.type) {
      case BABYLON.PointerEventTypes.POINTERMOVE:
        handleMousePointerMove(e);
        handleDrag(e);
        break;
      case BABYLON.PointerEventTypes.POINTERDOWN:
        renderState.interaction = true;
        requestRender();
        handleDragStart(e);
        break;
      case BABYLON.PointerEventTypes.POINTERPICK:
        requestRender();
        handleMousePointerPick(e);
        handleSerializationUpdates();
        break;
      case BABYLON.PointerEventTypes.POINTERUP:
        handleDragEnd(e);
        handleSerializationUpdates();
        renderState.interaction = false;
        requestRender();
        break;
      case BABYLON.PointerEventTypes.POINTERTAP:
        handleMousePointerTap(e);
        handleSerializationUpdates();
        renderState.interaction = false;
        requestRender();
        break;
      case BABYLON.PointerEventTypes.POINTERWHEEL:
        requestRender();
        break;

      default:
        break;
    }
  });
};

export const prepareAxes = () => {
  const xAxisTNode = new BABYLON.TransformNode('xAxis', scene);
  const yAxisTNode = new BABYLON.TransformNode('yAxis', scene);
  const zAxisTNode = new BABYLON.TransformNode('zAxis', scene);
  const { height, width } = groundOptions();
  const gridMax = Math.max(height, width);
  const unitBounds = 8;
  const axisLength = (1 / unitBounds) * gridMax;
  const xAxisLine = [ BABYLON.Vector3.Zero(), newVector(axisLength, 0, 0) ];
  const yAxisLine = [ BABYLON.Vector3.Zero(), newVector(0, axisLength, 0) ];
  const zAxisLine = [ BABYLON.Vector3.Zero(), newVector(0, 0, axisLength) ];

  const xAxisMesh = new BABYLON.MeshBuilder.CreateLines('xAxisMesh', { points: xAxisLine }, scene);
  const yAxisMesh = new BABYLON.MeshBuilder.CreateLines('yAxisMesh', { points: yAxisLine }, scene);
  const zAxisMesh = new BABYLON.MeshBuilder.CreateLines('zAxisMesh', { points: zAxisLine }, scene);

  const xAxisColor = newColor(AXIS_COLORS.xAxis);
  const yAxisColor = newColor(AXIS_COLORS.yAxis);
  const zAxisColor = newColor(AXIS_COLORS.zAxis);

  xAxisMesh.color = xAxisColor;
  yAxisMesh.color = yAxisColor;
  zAxisMesh.color = zAxisColor;

  xAxisMesh.rotation = newVector(0, toRadians(-90), 0);
  yAxisMesh.rotation = newVector(toRadians(90), 0, 0);

  xAxisMesh.isPickable = false;
  yAxisMesh.isPickable = false;
  zAxisMesh.isPickable = false;

  xAxisMesh.parent = xAxisTNode;
  yAxisMesh.parent = yAxisTNode;
  zAxisMesh.parent = zAxisTNode;

  new BABYLON.AxesViewer(scene, 1, null, xAxisTNode, yAxisTNode, zAxisTNode, 0.5);
};
