import { EventDispatcher, WebGLRenderer, ACESFilmicToneMapping, Vector2 } from "../../../../../../three@0.154.0/node_modules/three/build/three.module.js";
import { $updateEnvironment } from "../features/environment.js";
import { $canvas, $updateSize, $tick } from "../model-viewer-base.js";
import { isDebugMode, resolveDpr, clamp } from "../utilities.js";
import { ARRenderer } from "./ARRenderer.js";
import { CachingGLTFLoader } from "./CachingGLTFLoader.js";
import { ModelViewerGLTFInstance } from "./gltf-instance/ModelViewerGLTFInstance.js";
import TextureUtils from "./TextureUtils.js";
/* @license
 * Copyright 2019 Google LLC. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
const DURATION_DECAY = 0.2;
const LOW_FRAME_DURATION_MS = 40;
const HIGH_FRAME_DURATION_MS = 60;
const MAX_AVG_CHANGE_MS = 5;
const SCALE_STEPS = [1, 0.79, 0.62, 0.5, 0.4, 0.31, 0.25];
const DEFAULT_LAST_STEP = 3;
const DEFAULT_POWER_PREFERENCE = "high-performance";
class Renderer extends EventDispatcher {
  constructor(options) {
    super();
    this.loader = new CachingGLTFLoader(ModelViewerGLTFInstance);
    this.width = 0;
    this.height = 0;
    this.dpr = 1;
    this.scenes = /* @__PURE__ */ new Set();
    this.multipleScenesVisible = false;
    this.lastTick = performance.now();
    this.renderedLastFrame = false;
    this.scaleStep = 0;
    this.lastStep = DEFAULT_LAST_STEP;
    this.avgFrameDuration = (HIGH_FRAME_DURATION_MS + LOW_FRAME_DURATION_MS) / 2;
    this.onWebGLContextLost = (event) => {
      this.dispatchEvent({ type: "contextlost", sourceEvent: event });
    };
    this.onWebGLContextRestored = () => {
      var _a;
      (_a = this.textureUtils) === null || _a === void 0 ? void 0 : _a.dispose();
      this.textureUtils = new TextureUtils(this.threeRenderer);
      for (const scene of this.scenes) {
        scene.element[$updateEnvironment]();
      }
    };
    this.dpr = resolveDpr();
    this.canvas3D = document.createElement("canvas");
    this.canvas3D.id = "webgl-canvas";
    this.canvas3D.classList.add("show");
    try {
      this.threeRenderer = new WebGLRenderer({
        canvas: this.canvas3D,
        alpha: true,
        antialias: true,
        powerPreference: options.powerPreference,
        preserveDrawingBuffer: true
      });
      this.threeRenderer.autoClear = true;
      this.threeRenderer.useLegacyLights = false;
      this.threeRenderer.setPixelRatio(1);
      this.threeRenderer.debug = {
        checkShaderErrors: !!options.debug,
        onShaderError: null
      };
      this.threeRenderer.toneMapping = ACESFilmicToneMapping;
    } catch (error) {
      console.warn(error);
    }
    this.arRenderer = new ARRenderer(this);
    this.textureUtils = this.canRender ? new TextureUtils(this.threeRenderer) : null;
    CachingGLTFLoader.initializeKTX2Loader(this.threeRenderer);
    this.canvas3D.addEventListener("webglcontextlost", this.onWebGLContextLost);
    this.canvas3D.addEventListener("webglcontextrestored", this.onWebGLContextRestored);
    this.updateRendererSize();
  }
  static get singleton() {
    return this._singleton;
  }
  static resetSingleton() {
    const elements = this._singleton.dispose();
    for (const element of elements) {
      element.disconnectedCallback();
    }
    this._singleton = new Renderer({
      powerPreference: (self.ModelViewerElement || {}).powerPreference || DEFAULT_POWER_PREFERENCE,
      debug: isDebugMode()
    });
    for (const element of elements) {
      element.connectedCallback();
    }
  }
  get canRender() {
    return this.threeRenderer != null;
  }
  get scaleFactor() {
    return SCALE_STEPS[this.scaleStep];
  }
  set minScale(scale) {
    let i = 1;
    while (i < SCALE_STEPS.length) {
      if (SCALE_STEPS[i] < scale) {
        break;
      }
      ++i;
    }
    this.lastStep = i - 1;
  }
  registerScene(scene) {
    this.scenes.add(scene);
    scene.forceRescale();
    const size = new Vector2();
    this.threeRenderer.getSize(size);
    scene.canvas.width = size.x;
    scene.canvas.height = size.y;
    if (this.canRender && this.scenes.size > 0) {
      this.threeRenderer.setAnimationLoop((time, frame) => this.render(time, frame));
    }
  }
  unregisterScene(scene) {
    this.scenes.delete(scene);
    if (this.canvas3D.parentElement === scene.canvas.parentElement) {
      scene.canvas.parentElement.removeChild(this.canvas3D);
    }
    if (this.canRender && this.scenes.size === 0) {
      this.threeRenderer.setAnimationLoop(null);
    }
  }
  displayCanvas(scene) {
    return this.multipleScenesVisible ? scene.element[$canvas] : this.canvas3D;
  }
  /**
   * The function enables an optimization, where when there is only a single
   * <model-viewer> element, we can use the renderer's 3D canvas directly for
   * display. Otherwise we need to use the element's 2D canvas and copy the
   * renderer's result into it.
   */
  countVisibleScenes() {
    const { canvas3D } = this;
    let visibleScenes = 0;
    let canvas3DScene = null;
    for (const scene of this.scenes) {
      const { element } = scene;
      if (element.modelIsVisible && scene.externalRenderer == null) {
        ++visibleScenes;
      }
      if (canvas3D.parentElement === scene.canvas.parentElement) {
        canvas3DScene = scene;
      }
    }
    const multipleScenesVisible = visibleScenes > 1;
    if (canvas3DScene != null) {
      const newlyMultiple = multipleScenesVisible && !this.multipleScenesVisible;
      const disappearing = !canvas3DScene.element.modelIsVisible;
      if (newlyMultiple || disappearing) {
        const { width, height } = this.sceneSize(canvas3DScene);
        this.copyPixels(canvas3DScene, width, height);
        canvas3D.parentElement.removeChild(canvas3D);
      }
    }
    this.multipleScenesVisible = multipleScenesVisible;
  }
  /**
   * Updates the renderer's size based on the largest scene and any changes to
   * device pixel ratio.
   */
  updateRendererSize() {
    var _a;
    const dpr = resolveDpr();
    if (dpr !== this.dpr) {
      for (const scene of this.scenes) {
        const { element } = scene;
        element[$updateSize](element.getBoundingClientRect());
      }
    }
    let width = 0;
    let height = 0;
    for (const scene of this.scenes) {
      width = Math.max(width, scene.width);
      height = Math.max(height, scene.height);
    }
    if (width === this.width && height === this.height && dpr === this.dpr) {
      return;
    }
    this.width = width;
    this.height = height;
    this.dpr = dpr;
    width = Math.ceil(width * dpr);
    height = Math.ceil(height * dpr);
    if (this.canRender) {
      this.threeRenderer.setSize(width, height, false);
    }
    for (const scene of this.scenes) {
      const { canvas } = scene;
      canvas.width = width;
      canvas.height = height;
      scene.forceRescale();
      (_a = scene.effectRenderer) === null || _a === void 0 ? void 0 : _a.setSize(width, height);
    }
  }
  updateRendererScale(delta) {
    const scaleStep = this.scaleStep;
    this.avgFrameDuration += clamp(DURATION_DECAY * (delta - this.avgFrameDuration), -MAX_AVG_CHANGE_MS, MAX_AVG_CHANGE_MS);
    if (this.avgFrameDuration > HIGH_FRAME_DURATION_MS) {
      ++this.scaleStep;
    } else if (this.avgFrameDuration < LOW_FRAME_DURATION_MS && this.scaleStep > 0) {
      --this.scaleStep;
    }
    this.scaleStep = Math.min(this.scaleStep, this.lastStep);
    if (scaleStep !== this.scaleStep) {
      this.avgFrameDuration = (HIGH_FRAME_DURATION_MS + LOW_FRAME_DURATION_MS) / 2;
    }
  }
  shouldRender(scene) {
    if (!scene.shouldRender()) {
      if (scene.scaleStep != 0) {
        scene.scaleStep = 0;
        this.rescaleCanvas(scene);
      } else {
        return false;
      }
    } else if (scene.scaleStep != this.scaleStep) {
      scene.scaleStep = this.scaleStep;
      this.rescaleCanvas(scene);
    }
    return true;
  }
  rescaleCanvas(scene) {
    const scale = SCALE_STEPS[scene.scaleStep];
    const width = Math.ceil(this.width / scale);
    const height = Math.ceil(this.height / scale);
    const { style } = scene.canvas;
    style.width = `${width}px`;
    style.height = `${height}px`;
    this.canvas3D.style.width = `${width}px`;
    this.canvas3D.style.height = `${height}px`;
    const renderedDpr = this.dpr * scale;
    const reason = scale < 1 ? "GPU throttling" : this.dpr !== window.devicePixelRatio ? "No meta viewport tag" : "";
    scene.element.dispatchEvent(new CustomEvent("render-scale", {
      detail: {
        reportedDpr: window.devicePixelRatio,
        renderedDpr,
        minimumDpr: this.dpr * SCALE_STEPS[this.lastStep],
        pixelWidth: Math.ceil(scene.width * renderedDpr),
        pixelHeight: Math.ceil(scene.height * renderedDpr),
        reason
      }
    }));
  }
  sceneSize(scene) {
    const { dpr } = this;
    const scaleFactor = SCALE_STEPS[scene.scaleStep];
    const width = Math.min(Math.ceil(scene.width * scaleFactor * dpr), this.canvas3D.width);
    const height = Math.min(Math.ceil(scene.height * scaleFactor * dpr), this.canvas3D.height);
    return { width, height };
  }
  copyPixels(scene, width, height) {
    const context2D = scene.context;
    if (context2D == null) {
      console.log("could not acquire 2d context");
      return;
    }
    context2D.clearRect(0, 0, width, height);
    context2D.drawImage(this.canvas3D, 0, 0, width, height, 0, 0, width, height);
    scene.canvas.classList.add("show");
  }
  /**
   * Returns an array version of this.scenes where the non-visible ones are
   * first. This allows eager scenes to be rendered before they are visible,
   * without needing the multi-canvas render path.
   */
  orderedScenes() {
    const scenes = [];
    for (const visible of [false, true]) {
      for (const scene of this.scenes) {
        if (scene.element.modelIsVisible === visible) {
          scenes.push(scene);
        }
      }
    }
    return scenes;
  }
  get isPresenting() {
    return this.arRenderer.isPresenting;
  }
  /**
   * This method takes care of updating the element and renderer state based on
   * the time that has passed since the last rendered frame.
   */
  preRender(scene, t, delta) {
    const { element, exposure } = scene;
    element[$tick](t, delta);
    const exposureIsNumber = typeof exposure === "number" && !Number.isNaN(exposure);
    this.threeRenderer.toneMappingExposure = exposureIsNumber ? exposure : 1;
  }
  render(t, frame) {
    if (frame != null) {
      this.arRenderer.onWebXRFrame(t, frame);
      return;
    }
    const delta = t - this.lastTick;
    this.lastTick = t;
    if (!this.canRender || this.isPresenting) {
      return;
    }
    this.countVisibleScenes();
    this.updateRendererSize();
    if (this.renderedLastFrame) {
      this.updateRendererScale(delta);
      this.renderedLastFrame = false;
    }
    const { canvas3D } = this;
    for (const scene of this.orderedScenes()) {
      const { element } = scene;
      if (!element.loaded || !element.modelIsVisible && scene.renderCount > 0) {
        continue;
      }
      this.preRender(scene, t, delta);
      if (!this.shouldRender(scene)) {
        continue;
      }
      if (scene.externalRenderer != null) {
        const camera = scene.getCamera();
        camera.updateMatrix();
        const { matrix, projectionMatrix } = camera;
        const viewMatrix = matrix.elements.slice();
        const target = scene.getTarget();
        viewMatrix[12] += target.x;
        viewMatrix[13] += target.y;
        viewMatrix[14] += target.z;
        scene.externalRenderer.render({
          viewMatrix,
          projectionMatrix: projectionMatrix.elements
        });
        continue;
      }
      if (!element.modelIsVisible && !this.multipleScenesVisible) {
        for (const visibleScene of this.scenes) {
          if (visibleScene.element.modelIsVisible) {
            visibleScene.queueRender();
          }
        }
      }
      const { width, height } = this.sceneSize(scene);
      scene.renderShadow(this.threeRenderer);
      this.threeRenderer.setRenderTarget(null);
      this.threeRenderer.setViewport(0, Math.ceil(this.height * this.dpr) - height, width, height);
      if (scene.effectRenderer != null) {
        scene.effectRenderer.render(delta);
      } else {
        this.threeRenderer.autoClear = true;
        this.threeRenderer.render(scene, scene.camera);
      }
      if (this.multipleScenesVisible || !scene.element.modelIsVisible && scene.renderCount === 0) {
        this.copyPixels(scene, width, height);
      } else if (canvas3D.parentElement !== scene.canvas.parentElement) {
        scene.canvas.parentElement.appendChild(canvas3D);
        scene.canvas.classList.remove("show");
      }
      scene.hasRendered();
      ++scene.renderCount;
      this.renderedLastFrame = true;
    }
  }
  dispose() {
    if (this.textureUtils != null) {
      this.textureUtils.dispose();
    }
    if (this.threeRenderer != null) {
      this.threeRenderer.dispose();
    }
    this.textureUtils = null;
    this.threeRenderer = null;
    const elements = [];
    for (const scene of this.scenes) {
      elements.push(scene.element);
    }
    this.canvas3D.removeEventListener("webglcontextlost", this.onWebGLContextLost);
    this.canvas3D.removeEventListener("webglcontextrestored", this.onWebGLContextRestored);
    return elements;
  }
}
Renderer._singleton = new Renderer({
  powerPreference: (self.ModelViewerElement || {}).powerPreference || DEFAULT_POWER_PREFERENCE,
  debug: isDebugMode()
});
export {
  DEFAULT_POWER_PREFERENCE,
  Renderer
};
//# sourceMappingURL=Renderer.js.map
