import * as BABYLON from 'babylonjs';

// GLTFFileLoader needs to be declared for the GLTF loader to work -- even if unused.
// eslint-disable-next-line no-unused-vars
import { GLTFFileLoader } from 'babylonjs-loaders';

import { reactProps as props } from '../Canvas';
import { DRAG_DROP_FORMATS } from '../enums';
import {
  canvas,
  engine,
  scene,
  initSceneFromFile,
  blobToGLB,
  getUserMaterials,
  getUserTextures,
  getUserMeshes,
  requestRender,
} from '../helpers';
import {
  MESSAGE_TYPES,
  TEXTURE_SIZE_THRESHOLD,
  MATERIAL_COUNT_THRESHOLD,
  MESH_COUNT_THRESHOLD,
} from '../constants';
import _ from 'lodash';
import { modelToOrigin } from '../actions';

const { REACT_APP_NAME: name } = process.env;
const inCanvasIDE = name === '@archvisioninc/canvas';
const clearExisting = true;

export const newEngine = async () => {
  const webGLSupported = () => {
    try {
      const canvas = document.createElement('canvas');
      return !!window.WebGLRenderingContext &&
        (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
    } catch(e) {
      return false;
    }
  };

  if (webGLSupported()) {
    const webGLEngine = () => new BABYLON.Engine(canvas, true);
    const enableWebGPU = false;

    try {
      const browserSupported = await BABYLON.WebGPUEngine.IsSupportedAsync;

      if (browserSupported && enableWebGPU) {
        const webGPUEngine = new BABYLON.WebGPUEngine(canvas);
        await webGPUEngine.initAsync(canvas);
        return webGPUEngine;
      }

      return webGLEngine();
    } catch (e) {
      console.error(e);
      return webGLEngine();
    }
  }

  return new BABYLON.NullEngine();
};

export const newScene = () => new BABYLON.Scene(engine);

export const checkForLargeTextures = () => {
  const userTextures = getUserTextures();

  const largeTexture = userTextures.find(texture => {
    const size = texture.getSize();
    const isLargeTexture = (
      size?.width >= TEXTURE_SIZE_THRESHOLD ||
      size?.height >= TEXTURE_SIZE_THRESHOLD
    );

    if (isLargeTexture) return texture;
  });

  return largeTexture;
};

export const warningChecks = () => {
  const hasLargeFile = checkForLargeTextures();
  const manyMaterials = getUserMaterials().length > MATERIAL_COUNT_THRESHOLD;
  const manyMeshes = getUserMeshes().length > MESH_COUNT_THRESHOLD;
  const messages = [];
  const config = {
    type: MESSAGE_TYPES.warning,
    closeButton: true,
    timer: 4,
  };

  if (hasLargeFile) {
    const message = `Large textures at or above ${TEXTURE_SIZE_THRESHOLD}, performance may be affected.`;
    messages.push({ message, config });
  }

  if (manyMaterials) {
    const message = 'High material count, performance may be affected.';
    messages.push({ message, config });
  }

  if (manyMeshes) {
    const message = 'High mesh count, shadows disabled for performance. Reenable in environments panel.';
    const directionalLight = scene.getLightById('directional');
    directionalLight.shadowEnabled = false;
    messages.push({ message, config });
  }

  props.addNotification?.(messages, clearExisting);
};

const checkSupportedFileTypes = files => {
  const config = { type: MESSAGE_TYPES.error };

  let hasPrimaryFile = false;

  if (files) {
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      const workingExtension = file.name.split('.').pop() || '';

      if (!hasPrimaryFile && DRAG_DROP_FORMATS.includes(workingExtension)) {
        hasPrimaryFile = true;
        break;
      }
    }
  }

  if (!hasPrimaryFile) {
    const message = 'Please upload a .gltf or .glb file.';
    props.addNotification?.([ { message, config } ], clearExisting);
  }
};

export const progressConfig = (currentStep, bytesLoaded, bytesTotal) => {
  const config = {
    type: MESSAGE_TYPES.loading,
    showModal: (inCanvasIDE || props.previewMode) && !props.integration ? false : true,
    currentStep,
    closeButton: false,
    totalSteps: 2,
    bytesLoaded,
    bytesTotal,
  };

  return config;
};

const onProgress = (e, args) => {
  const { addNotification } = args;

  const loaded = e.loaded;
  const total = e.total || loaded;

  const progress = (bytes, totalBytes) => {
    const conversionUnit = 1024;
    const unit = value => {
      const inKB = value / conversionUnit;
      const inMB = inKB / conversionUnit;
      const inGB = inMB / conversionUnit;
      const isGB = inMB >= conversionUnit;
      const raw = isGB ? inGB : inMB;
      const label = isGB ? 'GB' : 'MB';

      return { raw, label };
    };
    const label = unit(bytes).label;
    const total = parseFloat(totalBytes || 0);
    const stringOptions = { minimumFractionDigits: 2, maximumFractionDigits: 2 };
    const size = `${unit(bytes).raw?.toLocaleString('en-US', stringOptions)} ${label}`;
    const percent = `${(Math.floor((bytes / total) * 100))}%`;
    const finalSize = `${unit(total).raw.toLocaleString('en-US', stringOptions)} ${unit(total).label}`;
    const both = `${finalSize} ${percent}`;

    return { size, percent, both };
  };

  const message = {
    message: props.previewMode ? 'Building preview...' : `Loading... ${progress(loaded, total).both}`,
    config: progressConfig(2, loaded, total),
  };

  addNotification?.([ message ], clearExisting);
};

export const loadFromGuidURL = url => {
  try {
    if (!url) throw new Error('No URL provided');
    const sceneLoader = BABYLON.SceneLoader;
    const handleProgress = e => onProgress(e, { addNotification: props.addNotification });

    sceneLoader.ShowLoadingScreen = false;

    sceneLoader.LoadAsync(url, null, engine, handleProgress)
      .then(sceneData => {
        props.clearNotifications?.();
        initSceneFromFile(sceneData);
      });
  } catch (e) {
    console.error(e);
    props.addNotification?.(
      [ { message: e, config: { type: MESSAGE_TYPES.error } } ],
      clearExisting,
    );
  }
};

export const loadFromBlob = blob => {
  try {
    if (!blob) throw new Error('No file provided');
    const file = blobToGLB(blob, 'downloadedModel');
    const sceneLoader = BABYLON.SceneLoader;
    const handleProgress = e => onProgress(e, { addNotification: props.addNotification });

    sceneLoader.ShowLoadingScreen = false;

    sceneLoader.LoadAsync('file:', file, engine, handleProgress)
      .then(sceneData => {
        props.clearNotifications?.();
        initSceneFromFile(sceneData);
      });
  } catch (e) {
    console.error(e);
    props.addNotification?.(
      [ { message: e, config: { type: MESSAGE_TYPES.error } } ],
      clearExisting,
    );
  }
};

let filesInput;

export const loadFromDragDrop = () => {
  const handleCloseLoading = () => props.clearNotifications?.();

  const sceneLoaded = (file, newScene) => {
    const fileName = file.name.split('.').shift();
    initSceneFromFile(newScene, fileName);
    modelToOrigin();
  };

  const onProgress = null;
  const additionalRenderLoopLogic = null;
  const textureLoading = null;
  const startingProcessingFiles = () => {
    const hasGuid = !_.isEmpty(props.guidURL);

    if (hasGuid) {
      // eslint-disable-next-line max-len
      const message = "This model has published data that can't be updated from a drag and drop operation. Use Create from gLTF to start a new file.";
      const config = { type: MESSAGE_TYPES.warning };

      props.addNotification?.([ { message, config } ], clearExisting);
      throw new Error(message);
    }

    checkSupportedFileTypes();
  };
  const onReload = null;

  const onError = (file, scene, error) => {
    const missingBin = error.includes('.bin');
    const missingTextures = error.includes('textures/');
    const missingMessage = `Missing ${missingTextures ? 'textures folder' : '.bin file'}.`;
    const message = missingBin || missingTextures ? missingMessage : error;
    const config = { type: MESSAGE_TYPES.error };

    handleCloseLoading();
    requestRender();
    props.addNotification?.([ { message, config } ], clearExisting);
  };

  const handleDisplayLoading = () => {
    const message = 'Importing...';
    const config = {
      type: MESSAGE_TYPES.loading,
      showModal: inCanvasIDE || props.previewMode ? false : true,
    };

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

  BABYLON.DefaultLoadingScreen.prototype.displayLoadingUI = handleDisplayLoading;
  BABYLON.DefaultLoadingScreen.prototype.hideLoadingUI = _.noop;

  if (filesInput) filesInput.dispose();

  filesInput = new BABYLON.FilesInput(
    engine,
    scene,
    sceneLoaded,
    onProgress,
    additionalRenderLoopLogic,
    textureLoading,
    startingProcessingFiles,
    onReload,
    onError,
    false,
    true,
  );

  filesInput.monitorElementForDragNDrop(canvas);
};
