import * as THREE from 'three';
import { PCFSoftShadowMap, ACESFilmicToneMapping, sRGBEncoding, Raycaster, OrthographicCamera, PerspectiveCamera, Scene, Vector3, Vector2, Clock, Layers, Color, Texture, WebGLRenderer } from 'three';
import { createContext, useContext, useRef, useLayoutEffect, useEffect, useState, useMemo, createElement } from 'react';
import create from 'zustand';
import shallow from 'zustand/shallow';
import Reconciler from 'react-reconciler';
import { unstable_now, unstable_runWithPriority, unstable_IdlePriority } from 'scheduler';
import { useAsset } from 'use-asset';

var name = "react-three-fiber";
var version = "6.0.0-alpha.1";

const is = {
  obj: a => a === Object(a) && !is.arr(a),
  fun: a => typeof a === 'function',
  str: a => typeof a === 'string',
  num: a => typeof a === 'number',
  und: a => a === void 0,
  arr: a => Array.isArray(a),

  equ(a, b) {
    // Wrong type or one of the two undefined, doesn't match
    if (typeof a !== typeof b || !!a !== !!b) return false; // Atomic, just compare a against b

    if (is.str(a) || is.num(a) || is.obj(a)) return a === b; // Array, shallow compare first to see if it's a match

    if (is.arr(a) && a == b) return true; // Last resort, go through keys

    let i;

    for (i in a) if (!(i in b)) return false;

    for (i in b) if (a[i] !== b[i]) return false;

    return is.und(i) ? a === b : true;
  }

};

const isRenderer = def => def && !!def.render;
const isOrthographicCamera = def => def && def.isOrthographicCamera;
const context = /*#__PURE__*/createContext(null);

const createStore = (applyProps, invalidate, props) => {
  const {
    gl,
    size,
    vr = false,
    noninteractive = false,
    shadows = false,
    linear = false,
    orthographic = false,
    frameloop = true,
    updateCamera = true,
    pixelRatio = 1,
    raycaster: raycastOptions,
    camera: cameraOptions,
    onCreated,
    onPointerMissed
  } = props; // Set shadowmap

  if (shadows) {
    gl.shadowMap.enabled = true;
    if (typeof shadows === 'object') Object.assign(gl.shadowMap, shadows);else gl.shadowMap.type = PCFSoftShadowMap;
  } // Set color management


  if (!linear) {
    gl.toneMapping = ACESFilmicToneMapping;
    gl.outputEncoding = sRGBEncoding;
  } // Create custom raycaster


  const raycaster = new Raycaster();
  if (raycastOptions) applyProps(raycaster, raycastOptions, {}); // Create default camera

  const camera = orthographic ? new OrthographicCamera(0, 0, 0, 0, 0.1, 1000) : new PerspectiveCamera(75, 0, 0.1, 1000);
  camera.position.z = 5;
  if (cameraOptions) applyProps(camera, cameraOptions, {}); // Always look at [0, 0, 0]

  camera.lookAt(0, 0, 0);
  const scene = new Scene();
  scene.__r3f = {
    root: {},
    objects: []
  };

  function setPixelRatio(pixelRatio) {
    return Array.isArray(pixelRatio) ? Math.max(Math.min(pixelRatio[0], window.devicePixelRatio), pixelRatio[1]) : pixelRatio;
  }

  const rootState = create((set, get) => {
    const position = new Vector3();
    const defaultTarget = new Vector3();

    function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
      const {
        width,
        height
      } = size;
      const aspect = width / height;
      const distance = camera.getWorldPosition(position).distanceTo(target);

      if (isOrthographicCamera(camera)) {
        return {
          width: width / camera.zoom,
          height: height / camera.zoom,
          factor: 1,
          distance,
          aspect
        };
      } else {
        const fov = camera.fov * Math.PI / 180; // convert vertical fov to radians

        const h = 2 * Math.tan(fov / 2) * distance; // visible height

        const w = h * (width / height);
        return {
          width: w,
          height: h,
          factor: width / w,
          distance,
          aspect
        };
      }
    }

    return {
      gl,
      scene,
      camera,
      raycaster,
      mouse: new Vector2(),
      clock: new Clock(),
      vr,
      noninteractive,
      linear,
      frameloop,
      updateCamera,
      onCreated,
      onPointerMissed,
      size: {
        width: 0,
        height: 0
      },
      viewport: {
        pixelRatio: 1,
        width: 0,
        height: 0,
        aspect: 0,
        distance: 0,
        factor: 0,
        getCurrentViewport
      },
      invalidate: frames => invalidate(get(), frames),
      intersect: event => {},
      setSize: (width, height) => {
        const size = {
          width,
          height
        };
        set(state => ({
          size,
          viewport: { ...state.viewport,
            ...getCurrentViewport(camera, defaultTarget, size)
          }
        }));
      },
      setCamera: camera => {
        set({
          camera
        });
      },
      setPixelRatio: pixelRatio => {
        set(state => ({
          viewport: { ...state.viewport,
            pixelRatio: setPixelRatio(pixelRatio)
          }
        }));
      },
      internal: {
        manual: 0,
        frames: 0,
        lastProps: props,
        interaction: [],
        subscribers: [],
        captured: undefined,
        initialClick: [0, 0],
        initialHits: [],
        set,
        subscribe: (ref, priority = 0) => {
          const internal = get().internal; // If this subscription was given a priority, it takes rendering into its own hands
          // For that reason we switch off automatic rendering and increase the manual flag
          // As long as this flag is positive (there could be multiple render subscription)
          // ..there can be no internal rendering at all

          if (priority) internal.manual++;
          internal.subscribers.push({
            ref,
            priority: priority
          }); // Sort layers from lowest to highest, meaning, highest priority renders last (on top of the other frames)

          internal.subscribers = internal.subscribers.sort((a, b) => a.priority - b.priority);
          return () => {
            if (internal?.subscribers) {
              // Decrease manual flag if this subscription had a priority
              if (priority) internal.manual--;
              internal.subscribers = internal.subscribers.filter(s => s.ref !== ref);
            }
          };
        }
      }
    };
  }); // Resize camera and renderer on changes to size and pixelratio

  rootState.subscribe(() => {
    const {
      camera,
      size,
      viewport,
      updateCamera
    } = rootState.getState(); // https://github.com/pmndrs/react-three-fiber/issues/92
    // Sometimes automatic default camera adjustment isn't wanted behaviour

    if (updateCamera) {
      if (isOrthographicCamera(camera)) {
        camera.left = size.width / -2;
        camera.right = size.width / 2;
        camera.top = size.height / 2;
        camera.bottom = size.height / -2;
      } else {
        camera.aspect = size.width / size.height;
      }

      camera.updateProjectionMatrix(); // https://github.com/pmndrs/react-three-fiber/issues/178
      // Update matrix world since the renderer is a frame late

      camera.updateMatrixWorld();
    } // Update renderer


    gl.setPixelRatio(viewport.pixelRatio);
    gl.setSize(size.width, size.height); // Update state model
  }, state => [state.viewport.pixelRatio, state.size], shallow); // Invalidate on any change

  rootState.subscribe(state => invalidate(state));
  const state = rootState.getState(); // Update pixelratio

  if (pixelRatio) state.setPixelRatio(pixelRatio); // Update size

  if (size) state.setSize(size.width, size.height); // Return root state

  return rootState;
};

let emptyObject = {};
let catalogue = {};

function createRenderer(roots, invalidate) {
  function applyProps(instance, newProps, oldProps = {}, accumulative = false) {
    // Filter equals, events and reserved props
    const localState = instance?.__r3f ?? {};
    const root = localState.root;
    const rootState = root?.getState() ?? {};
    const sameProps = [];
    const handlers = [];
    let i;
    let keys = Object.keys(newProps);

    for (i = 0; i < keys.length; i++) {
      if (is.equ(newProps[keys[i]], oldProps[keys[i]])) sameProps.push(keys[i]); // Event-handlers ...
      //   are functions, that
      //   start with "on", and
      //   contain the name "Pointer", "Click", "ContextMenu", or "Wheel"

      if (is.fun(newProps[keys[i]]) && keys[i].startsWith('on')) {
        if (keys[i].includes('Pointer') || keys[i].includes('Click') || keys[i].includes('ContextMenu') || keys[i].includes('Wheel')) {
          handlers.push(keys[i]);
        }
      }
    }

    const leftOvers = [];
    keys = Object.keys(oldProps);

    if (accumulative) {
      for (i = 0; i < keys.length; i++) {
        if (newProps[keys[i]] === void 0) {
          leftOvers.push(keys[i]);
        }
      }
    }

    const toFilter = [...sameProps, 'children', 'key', 'ref']; // Instances use "object" as a reserved identifier

    if (localState.instance) toFilter.push('object');
    const filteredProps = { ...newProps
    }; // Removes sameProps and reserved props from newProps

    keys = Object.keys(filteredProps);

    for (i = 0; i < keys.length; i++) {
      if (toFilter.indexOf(keys[i]) > -1) {
        delete filteredProps[keys[i]];
      }
    } // Add left-overs as undefined props so they can be removed


    keys = Object.keys(leftOvers);

    for (i = 0; i < keys.length; i++) {
      if (keys[i] !== 'children') {
        filteredProps[keys[i]] = undefined;
      }
    }

    const filteredPropsEntries = Object.entries(filteredProps);

    if (filteredPropsEntries.length > 0) {
      filteredPropsEntries.forEach(([key, value]) => {
        if (!handlers.includes(key)) {
          let currentInstance = instance;
          let targetProp = currentInstance[key];

          if (key.includes('-')) {
            const entries = key.split('-');
            targetProp = entries.reduce((acc, key) => acc[key], instance); // If the target is atomic, it forces us to switch the root

            if (!(targetProp && targetProp.set)) {
              const [name, ...reverseEntries] = entries.reverse();
              currentInstance = reverseEntries.reverse().reduce((acc, key) => acc[key], instance);
              key = name;
            }
          } // Special treatment for objects with support for set/copy


          if (targetProp && targetProp.set && (targetProp.copy || targetProp instanceof Layers)) {
            // If value is an array it has got to be the set function
            if (Array.isArray(value)) {
              targetProp.set(...value);
            } // Test again target.copy(class) next ...
            else if (targetProp.copy && value && value.constructor && targetProp.constructor.name === value.constructor.name) {
                targetProp.copy(value);
              } // If nothing else fits, just set the single value, ignore undefined
              // https://github.com/react-spring/react-three-fiber/issues/274
              else if (value !== undefined) {
                  targetProp.set(value); // Auto-convert sRGB colors, for now ...
                  // https://github.com/react-spring/react-three-fiber/issues/344

                  if (!rootState.linear && targetProp instanceof Color) targetProp.convertSRGBToLinear();
                } // Else, just overwrite the value

          } else {
            currentInstance[key] = value; // Auto-convert sRGB textures, for now ...
            // https://github.com/react-spring/react-three-fiber/issues/344

            if (!rootState.linear && currentInstance[key] instanceof Texture) currentInstance[key].encoding = sRGBEncoding;
          }

          invalidateInstance(instance);
        }
      }); // Preemptively delete the instance from the containers interaction

      if (accumulative && root && instance.raycast && localState.handlers) {
        localState.handlers = undefined;
        const index = rootState.internal.interaction.indexOf(instance);
        if (index > -1) rootState.internal.interaction.splice(index, 1);
      } // Prep interaction handlers


      if (handlers.length) {
        if (accumulative && root && instance.raycast) rootState.internal.interaction.push(instance); // Add handlers to the instances handler-map

        localState.handlers = handlers.reduce((acc, key) => {
          // @ts-ignore
          acc[key.charAt(2).toLowerCase() + key.substr(3)] = newProps[key];
          return acc;
        }, {});
      } // Call the update lifecycle when it is being updated, but only when it is part of the scene


      if (instance.parent) updateInstance(instance);
    }
  }

  function invalidateInstance(instance) {
    invalidate(instance.__r3f?.root?.getState());
  }

  function updateInstance(instance) {
    if (instance.onUpdate) instance.onUpdate(instance);
  }

  function createInstance(type, {
    args = [],
    ...props
  }, root) {
    let name = `${type[0].toUpperCase()}${type.slice(1)}`;
    let instance; // Bind to the root container in case portals are being used
    // This is perhaps better for event management as we can keep them on a single instance

    /*while ((container as any).__r3f.root) {
      container = (container as any).__r3f.root
    }
     // TODO: https://github.com/facebook/react/issues/17147
    // If it's still not there it means the portal was created on a virtual node outside of react
    if (!roots.has(container)) {
      const fn = (node: Reconciler.Fiber): Container => {
        if (!node.return) return node.stateNode && node.stateNode.containerInfo
        else return fn(node.return)
      }
      container = fn(internalInstanceHandle)
    }*/

    if (type === 'primitive') {
      // Switch off dispose for primitive objects
      props = {
        dispose: null,
        ...props
      };
      instance = props.object;
      instance.__r3f = {
        root,
        objects: [],
        instance: true,
        dispose: instance.dispose
      };
    } else {
      const target = catalogue[name] || THREE[name];
      if (!target) throw `"${name}" is not part of the THREE namespace! Did you forget to extend it? See: https://github.com/pmndrs/react-three-fiber/blob/master/markdown/api.md#using-3rd-party-objects-declaratively`; // Instanciate new object, link it to the root

      instance = is.arr(args) ? new target(...args) : new target(args);
      instance.__r3f = {
        root,
        objects: []
      };
    } // Auto-attach geometries and materials


    if (name.endsWith('Geometry')) {
      props = {
        attach: 'geometry',
        ...props
      };
    } else if (name.endsWith('Material')) {
      props = {
        attach: 'material',
        ...props
      };
    } // It should NOT call onUpdate on object instanciation, because it hasn't been added to the
    // view yet. If the callback relies on references for instance, they won't be ready yet, this is
    // why it passes "true" here


    applyProps(instance, props, {});
    return instance;
  }

  function appendChild(parentInstance, child) {
    if (child) {
      if (child.isObject3D) {
        parentInstance.add(child);
      } else {
        parentInstance.__r3f.objects.push(child);

        child.parent = parentInstance; // The attach attribute implies that the object attaches itself on the parent

        if (child.attachArray) {
          if (!is.arr(parentInstance[child.attachArray])) parentInstance[child.attachArray] = [];
          parentInstance[child.attachArray].push(child);
        } else if (child.attachObject) {
          if (!is.obj(parentInstance[child.attachObject[0]])) parentInstance[child.attachObject[0]] = {};
          parentInstance[child.attachObject[0]][child.attachObject[1]] = child;
        } else if (child.attach) {
          parentInstance[child.attach] = child;
        }
      }

      updateInstance(child);
      invalidateInstance(child);
    }
  }

  function insertBefore(parentInstance, child, beforeChild) {
    if (child) {
      if (child.isObject3D) {
        child.parent = parentInstance;
        child.dispatchEvent({
          type: 'added'
        });
        const restSiblings = parentInstance.children.filter(sibling => sibling !== child); // TODO: the order is out of whack if data objects are present, has to be recalculated

        const index = restSiblings.indexOf(beforeChild);
        parentInstance.children = [...restSiblings.slice(0, index), child, ...restSiblings.slice(index)];
        updateInstance(child);
      } else {
        appendChild(parentInstance, child);
      } // TODO: order!!!


      invalidateInstance(child);
    }
  }

  function removeRecursive(array, parent, clone = false) {
    if (array) {
      // Three uses splice op's internally we may have to shallow-clone the array in order to safely remove items
      const target = clone ? [...array] : array;
      target.forEach(child => removeChild(parent, child));
    }
  }

  function removeChild(parentInstance, child) {
    if (child) {
      if (child.isObject3D) {
        parentInstance.remove(child);
      } else {
        child.parent = null;
        if (parentInstance.__r3f.objects) parentInstance.__r3f.objects = parentInstance.__r3f.objects.filter(x => x !== child); // Remove attachment

        if (child.attachArray) {
          parentInstance[child.attachArray] = parentInstance[child.attachArray].filter(x => x !== child);
        } else if (child.attachObject) {
          delete parentInstance[child.attachObject[0]][child.attachObject[1]];
        } else if (child.attach) {
          parentInstance[child.attach] = null;
        }
      } // Remove interactivity


      if (child.__r3f.root) {
        const rootState = child.__r3f.root.getState();

        rootState.internal.interaction = rootState.internal.interaction.filter(x => x !== child);
      }

      invalidateInstance(parentInstance); // Allow objects to bail out of recursive dispose alltogether by passing dispose={null}

      if (child.dispose !== null) {
        unstable_runWithPriority(unstable_IdlePriority, () => {
          // Remove nested child objects
          removeRecursive(child.__r3f.objects, child);
          removeRecursive(child.children, child, true); // Dispose item

          if (child.dispose && child.type !== 'Scene') child.dispose();else if (child.__r3f.dispose) child.__r3f.dispose(); // Remove references

          delete child.__r3f.root;
          delete child.__r3f.objects;
          delete child.__r3f.handlers;
          delete child.__r3f.dispose;
          delete child.__r3f;
        });
      }
    }
  }

  function switchInstance(instance, type, newProps, fiber) {
    const parent = instance.parent;
    if (!parent) return;
    const newInstance = createInstance(type, newProps, instance.__r3f.root);
    removeChild(parent, instance);
    appendChild(parent, newInstance) // This evil hack switches the react-internal fiber node
    // https://github.com/facebook/react/issues/14983
    // https://github.com/facebook/react/pull/15021
    ;
    [fiber, fiber.alternate].forEach(fiber => {
      if (fiber !== null) {
        fiber.stateNode = newInstance;

        if (fiber.ref) {
          if (typeof fiber.ref === 'function') fiber.ref(newInstance);else fiber.ref.current = newInstance;
        }
      }
    });
  }

  const reconciler = Reconciler({
    now: unstable_now,
    createInstance,
    removeChild,
    appendChild,
    appendInitialChild: appendChild,
    insertBefore,
    warnsIfNotActing: true,
    supportsMutation: true,
    isPrimaryRenderer: false,
    // @ts-ignore
    scheduleTimeout: is.fun(setTimeout) ? setTimeout : undefined,
    // @ts-ignore
    cancelTimeout: is.fun(clearTimeout) ? clearTimeout : undefined,
    // @ts-ignore
    setTimeout: is.fun(setTimeout) ? setTimeout : undefined,
    // @ts-ignore
    clearTimeout: is.fun(clearTimeout) ? clearTimeout : undefined,
    noTimeout: -1,
    appendChildToContainer: (parentInstance, child) => {
      const scene = parentInstance.getState().scene; // Link current root to the default scene

      scene.__r3f.root = parentInstance;
      appendChild(scene, child);
    },
    removeChildFromContainer: (parentInstance, child) => {
      removeChild(parentInstance.getState().scene, child);
    },
    insertInContainerBefore: (parentInstance, child, beforeChild) => {
      insertBefore(parentInstance.getState().scene, child, beforeChild);
    },

    commitUpdate(instance, updatePayload, type, oldProps, newProps, fiber) {
      if (instance.__instance && newProps.object && newProps.object !== instance) {
        // <instance object={...} /> where the object reference has changed
        switchInstance(instance, type, newProps, fiber);
      } else {
        // This is a data object, let's extract critical information about it
        const {
          args: argsNew = [],
          ...restNew
        } = newProps;
        const {
          args: argsOld = [],
          ...restOld
        } = oldProps; // If it has new props or arguments, then it needs to be re-instanciated

        const hasNewArgs = argsNew.some((value, index) => is.obj(value) ? Object.entries(value).some(([key, val]) => val !== argsOld[index][key]) : value !== argsOld[index]);

        if (hasNewArgs) {
          // Next we create a new instance and append it again
          switchInstance(instance, type, newProps, fiber);
        } else {
          // Otherwise just overwrite props
          applyProps(instance, restNew, restOld, true);
        }
      }
    },

    hideInstance(instance) {
      if (instance.isObject3D) {
        instance.visible = false;
        invalidateInstance(instance);
      }
    },

    // @ts-ignore
    unhideInstance(instance, props) {
      if (instance.isObject3D && props.visible == null || props.visible) {
        instance.visible = true;
        invalidateInstance(instance);
      }
    },

    hideTextInstance() {
      throw new Error('Text is not allowed in the react-three-fibre tree. You may have extraneous whitespace between components.');
    },

    getPublicInstance(instance) {
      return instance;
    },

    getRootHostContext() {
      return emptyObject;
    },

    getChildHostContext() {
      return emptyObject;
    },

    createTextInstance() {},

    finalizeInitialChildren(instance) {
      // https://github.com/facebook/react/issues/20271
      // Returning true will trigger commitMount
      return !!instance.__r3f.handlers;
    },

    commitMount(instance)
    /*, type, props*/
    {
      // https://github.com/facebook/react/issues/20271
      // This will make sure events are only added once to the central container
      if (instance.raycast && instance.__r3f.handlers) instance.__r3f.root.getState().internal.interaction.push(instance);
    },

    prepareUpdate() {
      return emptyObject;
    },

    shouldDeprioritizeSubtree() {
      return false;
    },

    prepareForCommit() {
      return null;
    },

    preparePortalMount() {
      return null;
    },

    resetAfterCommit() {},

    shouldSetTextContent() {
      return false;
    },

    clearContainer() {
      return false;
    }

  });
  return {
    reconciler,
    applyProps
  };
}

let globalEffects = [];
let globalAfterEffects = [];
let globalTailEffects = [];
function render$1(state, timestamp, repeat = 0, runGlobalEffects = false) {
  let i; // Run global effects

  if (runGlobalEffects) {
    for (i = 0; i < globalEffects.length; i++) {
      globalEffects[i](timestamp);
      repeat++;
    }
  } // Run local effects


  const delta = state.clock.getDelta(); // Call subscribers (useFrame)

  for (i = 0; i < state.internal.subscribers.length; i++) state.internal.subscribers[i].ref.current(state, delta); // Decrease frame count


  state.internal.frames = Math.max(0, state.internal.frames - 1);
  repeat += state.frameloop ? 1 : state.internal.frames; // Render content

  if (!state.internal.manual && state.gl.render) state.gl.render(state.scene, state.camera); // Run global after-effects

  if (runGlobalEffects) for (i = 0; i < globalAfterEffects.length; i++) globalAfterEffects[i](timestamp);
  return repeat;
}

function createLoop(roots) {
  let running = false;

  function loop(timestamp) {
    running = true;
    let i;
    let repeat = 0; // Run global effects

    for (i = 0; i < globalEffects.length; i++) {
      globalEffects[i](timestamp);
      repeat++;
    }

    roots.forEach(root => {
      const state = root.store.getState(); // If the frameloop is invalidated, do not run another frame

      repeat = state.frameloop || state.internal.frames > 0 ? render$1(state, timestamp, repeat) : 0;
    }); // Run global after-effects

    for (i = 0; i < globalAfterEffects.length; i++) globalAfterEffects[i](timestamp); // Keep on looping if anything invalidates the frameloop


    if (repeat !== 0) return requestAnimationFrame(loop); // Tail call effects, they are called when rendering stops
    else for (i = 0; i < globalTailEffects.length; i++) globalTailEffects[i](timestamp); // Flag end of operation

    running = false;
  }

  function invalidate(state = true, frames = 1) {
    if (state === true) {
      roots.forEach(root => root.store.getState().internal.frames += frames);
    } else if (state) {
      if (state.vr) return;
      state.internal.frames += frames;
    }

    if (!running) {
      running = true;
      requestAnimationFrame(loop);
    }
  }

  return {
    loop,
    invalidate
  };
}

const makeId = event => (event.eventObject || event.object).uuid + '/' + event.index;

function createEvents(store) {
  const hovered = new Map();
  const temp = new Vector3();
  /** Sets up defaultRaycaster */

  function prepareRay(event) {
    const state = store.getState();
    const {
      raycaster,
      mouse,
      camera,
      size,
      internal
    } = state; // https://github.com/pmndrs/react-three-fiber/pull/782
    // Events trigger outside of canvas when moved

    const offsets = internal.lastProps.raycaster?.computeOffsets?.(event, state) || event;

    if (offsets) {
      const {
        offsetX,
        offsetY
      } = offsets;
      const {
        width,
        height
      } = size;
      mouse.set(offsetX / width * 2 - 1, -(offsetY / height) * 2 + 1);
      raycaster.setFromCamera(mouse, camera);
    }
  }
  /** Intersects interaction objects using the event input */


  function intersect(filter) {
    const state = store.getState();
    const {
      raycaster,
      noninteractive,
      internal
    } = state; // Skip event handling when noEvents is set

    if (noninteractive) return [];
    const seen = new Set();
    const hits = []; // Allow callers to eliminate event objects

    const eventsObjects = filter ? filter(internal.interaction) : internal.interaction; // Intersect known handler objects and filter against duplicates

    let intersects = raycaster.intersectObjects(eventsObjects, true).filter(item => {
      const id = makeId(item);
      if (seen.has(id)) return false;
      seen.add(id);
      return true;
    }); // https://github.com/mrdoob/three.js/issues/16031
    // Allow custom userland intersect sort order

    if (internal.lastProps?.raycaster?.filter) intersects = internal.lastProps.raycaster.filter(intersects, state);

    for (const intersect of intersects) {
      let eventObject = intersect.object; // Bubble event up

      while (eventObject) {
        const handlers = eventObject.__r3f.handlers;
        if (handlers) hits.push({ ...intersect,
          eventObject
        });
        eventObject = eventObject.parent;
      }
    }

    return hits;
  }
  /**  Calculates click deltas */


  function calculateDistance(event) {
    const {
      internal
    } = store.getState();
    const dx = event.offsetX - internal.initialClick[0];
    const dy = event.offsetY - internal.initialClick[1];
    return Math.round(Math.sqrt(dx * dx + dy * dy));
  }

  function handlePointerCancel(event, hits, prepare = true) {
    if (prepare) prepareRay(event);
    Array.from(hovered.values()).forEach(hoveredObj => {
      // When no objects were hit or the the hovered object wasn't found underneath the cursor
      // we call onPointerOut and delete the object from the hovered-elements map
      if (hits && (!hits.length || !hits.find(hit => hit.object === hoveredObj.object && hit.index === hoveredObj.index))) {
        const eventObject = hoveredObj.eventObject;
        const handlers = eventObject.__r3f.handlers;
        hovered.delete(makeId(hoveredObj));

        if (handlers) {
          // Clear out intersects, they are outdated by now
          const data = { ...hoveredObj,
            intersections: hits || []
          };
          if (handlers.pointerOut) handlers.pointerOut({ ...data,
            type: 'pointerout'
          });
          if (handlers.pointerLeave) handlers.pointerLeave({ ...data,
            type: 'pointerleave'
          });
        }
      }
    });
  }
  /**  Creates filtered intersects and returns an array of positive hits */


  function getIntersects(event, filter) {
    const {
      internal
    } = store.getState(); // Get fresh intersects

    const intersections = intersect(filter); // If the interaction is captured take that into account, the captured event has to be part of the intersects

    if (internal.captured && event.type !== 'click' && event.type !== 'wheel') {
      internal.captured.forEach(captured => {
        if (!intersections.find(hit => hit.eventObject === captured.eventObject)) intersections.push(captured);
      });
    }

    return intersections;
  }
  /**  Handles intersections by forwarding them to handlers */


  function handleIntersects(intersections, event, fn) {
    const {
      raycaster,
      mouse,
      camera,
      internal
    } = store.getState(); // If anything has been found, forward it to the event listeners

    if (intersections.length) {
      const unprojectedPoint = temp.set(mouse.x, mouse.y, 0).unproject(camera);
      const delta = event.type === 'click' ? calculateDistance(event) : 0;

      const releasePointerCapture = id => event.target.releasePointerCapture(id);

      const localState = {
        stopped: false,
        captured: false
      };

      for (const hit of intersections) {
        const setPointerCapture = id => {
          // If the hit is going to be captured flag that we're in captured state
          if (!localState.captured) {
            localState.captured = true; // The captured hit array is reset to collect hits

            internal.captured = [];
          } // Push hits to the array


          if (internal.captured) internal.captured.push(hit) // Call the original event now
          ;
          event.target.setPointerCapture(id);
        };

        const raycastEvent = { ...event,
          ...hit,
          intersections,
          stopped: localState.stopped,
          delta,
          unprojectedPoint,
          ray: raycaster.ray,
          camera: camera,
          // Hijack stopPropagation, which just sets a flag
          stopPropagation: () => {
            // https://github.com/react-spring/react-three-fiber/issues/596
            // Events are not allowed to stop propagation if the pointer has been captured
            const cap = internal.captured;

            if (!cap || cap.find(h => h.eventObject.id === hit.eventObject.id)) {
              raycastEvent.stopped = localState.stopped = true; // Propagation is stopped, remove all other hover records
              // An event handler is only allowed to flush other handlers if it is hovered itself

              if (hovered.size && Array.from(hovered.values()).find(i => i.eventObject === hit.eventObject)) {
                // Objects cannot flush out higher up objects that have already caught the event
                const higher = intersections.slice(0, intersections.indexOf(hit));
                handlePointerCancel(raycastEvent, [...higher, hit]);
              }
            }
          },
          target: { ...event.target,
            setPointerCapture,
            releasePointerCapture
          },
          currentTarget: { ...event.currentTarget,
            setPointerCapture,
            releasePointerCapture
          },
          sourceEvent: event
        };
        fn(raycastEvent); // Event bubbling may be interrupted by stopPropagation

        if (localState.stopped === true) break;
      }
    }

    return intersections;
  }

  function handlePointerMove(event, prepare = true) {
    if (prepare) prepareRay(event);
    const hits = getIntersects(event, // This is onPointerMove, we're only interested in events that exhibit this particular event
    objects => objects.filter(obj => ['Move', 'Over', 'Enter', 'Out', 'Leave'].some(name => obj.__r3f.handlers['pointer' + name]))); // Take care of unhover

    handlePointerCancel(event, hits);
    handleIntersects(hits, event, data => {
      const eventObject = data.eventObject;
      const handlers = eventObject.__r3f.handlers; // Check presence of handlers

      if (!handlers) return; // Check if mouse enter or out is present

      if (handlers.pointerOver || handlers.pointerEnter || handlers.pointerOut || handlers.pointerLeave) {
        const id = makeId(data);
        const hoveredItem = hovered.get(id);

        if (!hoveredItem) {
          // If the object wasn't previously hovered, book it and call its handler
          hovered.set(id, data);
          if (handlers.pointerOver) handlers.pointerOver({ ...data,
            type: 'pointerover'
          });
          if (handlers.pointerEnter) handlers.pointerEnter({ ...data,
            type: 'pointerenter'
          });
        } else if (hoveredItem.stopped) {
          // If the object was previously hovered and stopped, we shouldn't allow other items to proceed
          data.stopPropagation();
        }
      } // Call mouse move


      if (handlers.pointerMove) handlers.pointerMove(data);
    });
    return hits;
  }

  function pointerMissed(event, objects, filter = object => true) {
    objects.filter(filter).forEach(object => object.__r3f.handlers.pointerMissed?.(event));
  }

  const handlePointer = name => (event, prepare = true) => {
    const {
      onPointerMissed,
      internal
    } = store.getState();
    if (prepare) prepareRay(event);
    const hits = getIntersects(event);
    handleIntersects(hits, event, data => {
      const eventObject = data.eventObject;
      const handlers = eventObject.__r3f.handlers;

      if (handlers && handlers[name]) {
        // Forward all events back to their respective handlers with the exception of click events,
        // which must use the initial target
        if (name !== 'click' && name !== 'contextMenu' && name !== 'doubleClick' || internal.initialHits.includes(eventObject)) {
          handlers[name](data);
          pointerMissed(event, internal.interaction, object => object !== eventObject);
        }
      }
    }); // If a click yields no results, pass it back to the user as a miss

    if (name === 'pointerDown') {
      internal.initialClick = [event.offsetX, event.offsetY];
      internal.initialHits = hits.map(hit => hit.eventObject);
    }

    if ((name === 'click' || name === 'contextMenu' || name === 'doubleClick') && !hits.length) {
      if (calculateDistance(event) <= 2) {
        pointerMissed(event, internal.interaction);
        if (onPointerMissed) onPointerMissed();
      }
    }
  };

  return {
    onClick: handlePointer('click'),
    onContextMenu: handlePointer('contextMenu'),
    onDoubleClick: handlePointer('doubleClick'),
    onWheel: handlePointer('wheel'),
    onPointerDown: handlePointer('pointerDown'),
    onPointerUp: handlePointer('pointerUp'),
    onPointerLeave: e => handlePointerCancel(e, []),
    onPointerMove: handlePointerMove,
    onLostPointerCapture: e => (store.getState().internal.captured = undefined, handlePointerCancel(e))
  };
}

function Canvas() {
  return null;
}

function useThree(selector, equalityFn) {
  return useContext(context)(selector, equalityFn);
}
function useFrame(callback, renderPriority = 0) {
  const {
    subscribe
  } = useContext(context).getState().internal; // Update ref

  const ref = useRef(callback);
  useLayoutEffect(() => void (ref.current = callback), [callback]); // Subscribe/unsub

  useEffect(() => {
    const unsubscribe = subscribe(ref, renderPriority);
    return () => unsubscribe();
  }, [renderPriority, subscribe]);
  return null;
}
function useUpdate(callback, dependents, optionalRef) {
  const {
    invalidate
  } = useContext(context).getState();
  const localRef = useRef();
  const ref = optionalRef ? optionalRef : localRef;
  useLayoutEffect(() => {
    if (ref.current) {
      callback(ref.current);
      invalidate();
    }
  }, dependents); // eslint-disable-line react-hooks/exhaustive-deps

  return ref;
}
function useResource(optionalRef) {
  const [_, forceUpdate] = useState(false);
  const localRef = useRef(undefined);
  const ref = optionalRef ? optionalRef : localRef;
  useLayoutEffect(() => void forceUpdate(i => !i), []);
  return ref;
}

function buildGraph(object) {
  const data = {
    nodes: {},
    materials: {}
  };

  if (object) {
    object.traverse(obj => {
      if (obj.name) data.nodes[obj.name] = obj;
      if (obj.material && !data.materials[obj.material.name]) data.materials[obj.material.name] = obj.material;
    });
  }

  return data;
}

function useGraph(object) {
  return useMemo(() => buildGraph(object), [object]);
}

function loadingFn(extensions, onProgress) {
  return function (Proto, ...input) {
    // Construct new loader and run extensions
    const loader = new Proto();
    if (extensions) extensions(loader); // Go through the urls and load them

    return Promise.all(input.map(input => new Promise((res, reject) => loader.load(input, data => {
      if (data.scene) Object.assign(data, buildGraph(data.scene));
      res(data);
    }, onProgress, error => reject(error.message ?? `failure loading ${input}`)))));
  };
}

function useLoader(Proto, input, extensions, onProgress) {
  // Use suspense to load async assets
  const keys = Array.isArray(input) ? input : [input];
  const results = useAsset(loadingFn(extensions, onProgress), Proto, ...keys); // Return the object/s

  return Array.isArray(input) ? results : results[0];
}

useLoader.preload = function (Proto, input, extensions) {
  const keys = Array.isArray(input) ? input : [input];
  return useAsset.preload(loadingFn(extensions), Proto, ...keys);
};

const roots = new Map();
const {
  invalidate
} = createLoop(roots);
const {
  reconciler,
  applyProps
} = createRenderer(roots, invalidate);

const createRendererInstance = (gl, canvas) => isRenderer(gl) ? gl : new WebGLRenderer({
  powerPreference: 'high-performance',
  canvas,
  antialias: true,
  alpha: true,
  ...gl
});

function render(element, canvas, {
  gl,
  size,
  concurrent,
  ...props
}) {
  let root = roots.get(canvas);
  let fiber = root?.fiber;
  let store = root?.store;
  let state = store?.getState();

  if (fiber && state) {
    const lastProps = state.internal.lastProps; // When a root was found, see if any fundamental props must be changed or exchanged
    // Check pixelratio

    if (props.pixelRatio !== undefined && !is.equ(lastProps.pixelRatio, props.pixelRatio)) state.setPixelRatio(props.pixelRatio); // Check size

    if (size !== undefined && !is.equ(lastProps.size, size)) state.setSize(size.width, size.height); // For some props we want to reset the entire root
    // Changes to the color-space

    const linearChanged = props.linear !== lastProps.linear;

    if (linearChanged) {
      unmountComponentAtNode(canvas);
      fiber = undefined;
    }
  }

  if (!fiber) {
    // If no root has been found, make one
    store = createStore(applyProps, invalidate, {
      gl: createRendererInstance(gl, canvas),
      size,
      ...props
    });
    const events = createEvents(store);
    canvas.addEventListener('pointermove', e => events.onPointerMove(e), {
      passive: true
    });
    canvas.addEventListener('pointerdown', e => events.onPointerDown(e), {
      passive: true
    });
    canvas.addEventListener('pointerup', e => events.onPointerUp(e), {
      passive: true
    });
    canvas.addEventListener('pointerleave', e => events.onPointerLeave(e), {
      passive: true
    });
    canvas.addEventListener('click', e => events.onClick(e), {
      passive: true
    });
    fiber = reconciler.createContainer(store, concurrent ? 2 : 0, false, null); // Map it

    roots.set(canvas, {
      fiber,
      store
    }); // Kick off render loop

    store.getState().invalidate();
  }

  if (store && fiber) {
    reconciler.updateContainer( /*#__PURE__*/createElement(context.Provider, {
      value: store,
      children: element
    }), fiber, null, () => undefined);
    return reconciler.getPublicRootInstance(fiber);
  } else {
    throw 'R3F: Error creating fiber-root!';
  }
}

function unmountComponentAtNode(canvas, callback) {
  const root = roots.get(canvas);
  const fiber = root?.fiber;

  if (fiber) {
    reconciler.updateContainer(null, fiber, null, () => {
      const state = root?.store.getState();

      if (state) {
        if (state.gl.renderLists) state.gl.renderLists.dispose();
        if (state.gl.forceContextLoss) state.gl.forceContextLoss();
        dispose(state.gl);
        dispose(state.raycaster);
        dispose(state.camera);
        dispose(state);
      }

      roots.delete(canvas);
      if (callback) callback(canvas);
    });
  }
}

function dispose(obj) {
  if (obj.dispose && obj.type !== 'Scene') obj.dispose();

  for (const p in obj) {
    if (typeof p === 'object' && p.dispose) p.dispose();
    delete obj[p];
  }
}

const hasSymbol = is.fun(Symbol) && Symbol.for;
const REACT_PORTAL_TYPE = hasSymbol ? Symbol.for('react.portal') : 0xeaca;

function createPortal(children, container, impl, key = null) {
  if (!container.__objects) container.__objects = [];
  return {
    $$typeof: REACT_PORTAL_TYPE,
    key: key == null ? null : '' + key,
    children,
    container,
    impl
  };
}

reconciler.injectIntoDevTools({
  bundleType: process.env.NODE_ENV === 'production' ? 0 : 1,
  rendererPackageName: name,
  version
});

export { Canvas, applyProps, context, createPortal, invalidate, reconciler, render, unmountComponentAtNode, useFrame, useGraph, useLoader, useResource, useThree, useUpdate };
