/* @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.
 */
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { property } from 'lit/decorators.js';
import { Vector3 } from 'three';
import { $altDefaulted, $announceModelVisibility, $getModelIsVisible, $isElementInViewport, $progressTracker, $scene, $shouldAttemptPreload, $updateSource, $userInputElement, toVector3D } from '../model-viewer-base.js';
import { $loader, CachingGLTFLoader } from '../three-components/CachingGLTFLoader.js';
import { Renderer } from '../three-components/Renderer.js';
import { throttle } from '../utilities.js';
export const PROGRESS_BAR_UPDATE_THRESHOLD = 100;
const DEFAULT_DRACO_DECODER_LOCATION = 'https://www.gstatic.com/draco/versioned/decoders/1.5.6/';
const DEFAULT_KTX2_TRANSCODER_LOCATION = 'https://www.gstatic.com/basis-universal/versioned/2021-04-15-ba1c3e4/';
const DEFAULT_LOTTIE_LOADER_LOCATION = 'https://cdn.jsdelivr.net/npm/three@0.149.0/examples/jsm/loaders/LottieLoader.js';
const RevealStrategy = {
    AUTO: 'auto',
    MANUAL: 'manual'
};
const LoadingStrategy = {
    AUTO: 'auto',
    LAZY: 'lazy',
    EAGER: 'eager'
};
export const $defaultProgressBarElement = Symbol('defaultProgressBarElement');
export const $posterContainerElement = Symbol('posterContainerElement');
export const $defaultPosterElement = Symbol('defaultPosterElement');
const $shouldDismissPoster = Symbol('shouldDismissPoster');
const $hidePoster = Symbol('hidePoster');
const $modelIsRevealed = Symbol('modelIsRevealed');
const $updateProgressBar = Symbol('updateProgressBar');
const $ariaLabelCallToAction = Symbol('ariaLabelCallToAction');
const $onProgress = Symbol('onProgress');
/**
 * LoadingMixin implements features related to lazy loading, as well as
 * presentation details related to the pre-load / pre-render presentation of a
 * <model-viewer>
 *
 * This mixin implements support for models with DRACO-compressed meshes.
 * The DRACO decoder will be loaded on-demand if a glTF that uses the DRACO mesh
 * compression extension is encountered.
 *
 * By default, the DRACO decoder will be loaded from a Google CDN. It is
 * possible to customize where the decoder is loaded from by defining a global
 * configuration option for `<model-viewer>` like so:
 *
 * ```html
 * <script>
 * self.ModelViewerElement = self.ModelViewerElement || {};
 * self.ModelViewerElement.dracoDecoderLocation =
 *     'http://example.com/location/of/draco/decoder/files/';
 * </script>
 * ```
 *
 * Note that the above configuration strategy must be performed *before* the
 * first `<model-viewer>` element is created in the browser. The configuration
 * can be done anywhere, but the easiest way to ensure it is done at the right
 * time is to do it in the `<head>` of the HTML document. This is the
 * recommended way to set the location because it is most compatible with
 * scenarios where the `<model-viewer>` library is lazily loaded.
 *
 * If you absolutely have to set the DRACO decoder location *after* the first
 * `<model-viewer>` element is created, you can do it this way:
 *
 * ```html
 * <script>
 * const ModelViewerElement = customElements.get('model-viewer');
 * ModelViewerElement.dracoDecoderLocation =
 *     'http://example.com/location/of/draco/decoder/files/';
 * </script>
 * ```
 *
 * Note that the above configuration approach will not work until *after*
 * `<model-viewer>` is defined in the browser. Also note that this configuration
 * *must* be set *before* the first DRACO model is fully loaded.
 *
 * It is recommended that users who intend to take advantage of DRACO mesh
 * compression consider whether or not it is acceptable for their use case to
 * have code side-loaded from a Google CDN. If it is not acceptable, then the
 * location must be customized before loading any DRACO models in order to cause
 * the decoder to be loaded from an alternative, acceptable location.
 */
export const LoadingMixin = (ModelViewerElement) => {
    var _a, _b, _c, _d, _e, _f, _g, _h;
    class LoadingModelViewerElement extends ModelViewerElement {
        static set dracoDecoderLocation(value) {
            CachingGLTFLoader.setDRACODecoderLocation(value);
        }
        static get dracoDecoderLocation() {
            return CachingGLTFLoader.getDRACODecoderLocation();
        }
        static set ktx2TranscoderLocation(value) {
            CachingGLTFLoader.setKTX2TranscoderLocation(value);
        }
        static get ktx2TranscoderLocation() {
            return CachingGLTFLoader.getKTX2TranscoderLocation();
        }
        static set meshoptDecoderLocation(value) {
            CachingGLTFLoader.setMeshoptDecoderLocation(value);
        }
        static get meshoptDecoderLocation() {
            return CachingGLTFLoader.getMeshoptDecoderLocation();
        }
        static set lottieLoaderLocation(value) {
            Renderer.singleton.textureUtils.lottieLoaderUrl = value;
        }
        static get lottieLoaderLocation() {
            return Renderer.singleton.textureUtils.lottieLoaderUrl;
        }
        /**
         * If provided, the callback will be passed each resource URL before a
         * request is sent. The callback may return the original URL, or a new URL
         * to override loading behavior. This behavior can be used to load assets
         * from .ZIP files, drag-and-drop APIs, and Data URIs.
         */
        static mapURLs(callback) {
            Renderer.singleton.loader[$loader].manager.setURLModifier(callback);
        }
        /**
         * Dismisses the poster, causing the model to load and render if
         * necessary. This is currently effectively the same as interacting with
         * the poster via user input.
         */
        dismissPoster() {
            if (this.loaded) {
                this[$hidePoster]();
            }
            else {
                this[$shouldDismissPoster] = true;
                this[$updateSource]();
            }
        }
        /**
         * Displays the poster, hiding the 3D model. If this is called after the 3D
         * model has been revealed, then it must be dismissed by a call to
         * dismissPoster().
         */
        showPoster() {
            const posterContainerElement = this[$posterContainerElement];
            if (posterContainerElement.classList.contains('show')) {
                return;
            }
            posterContainerElement.classList.add('show');
            this[$userInputElement].classList.remove('show');
            const defaultPosterElement = this[$defaultPosterElement];
            defaultPosterElement.removeAttribute('tabindex');
            defaultPosterElement.removeAttribute('aria-hidden');
            const oldVisibility = this.modelIsVisible;
            this[$modelIsRevealed] = false;
            this[$announceModelVisibility](oldVisibility);
        }
        /**
         * Returns the model's bounding box dimensions in meters, independent of
         * turntable rotation.
         */
        getDimensions() {
            this[$scene].updateBoundingBoxAndShadowIfDirty();
            return toVector3D(this[$scene].size);
        }
        getBoundingBoxCenter() {
            this[$scene].updateBoundingBoxAndShadowIfDirty();
            return toVector3D(this[$scene].boundingBox.getCenter(new Vector3()));
        }
        constructor(...args) {
            super(...args);
            /**
             * A URL pointing to the image to use as a poster in scenarios where the
             * <model-viewer> is not ready to reveal a rendered model to the viewer.
             */
            this.poster = null;
            /**
             * An enumerable attribute describing under what conditions the
             * <model-viewer> should reveal a model to the viewer.
             *
             * The default value is "auto". The only supported alternative values is
             * "manual".
             */
            this.reveal = RevealStrategy.AUTO;
            /**
             * An enumerable attribute describing under what conditions the
             * <model-viewer> should preload a model.
             *
             * The default value is "auto". The only supported alternative values are
             * "lazy" and "eager". Auto is equivalent to lazy, which loads the model
             * when it is near the viewport for reveal = "auto", and when interacted
             * with for reveal = "interaction". Eager loads the model immediately.
             */
            this.loading = LoadingStrategy.AUTO;
            this[_a] = false;
            this[_b] = false;
            // TODO: Add this to the shadow root as part of this mixin's
            // implementation:
            this[_c] = this.shadowRoot.querySelector('.slot.poster');
            this[_d] = this.shadowRoot.querySelector('#default-poster');
            this[_e] = this.shadowRoot.querySelector('#default-progress-bar > .bar');
            this[_f] = this[$defaultPosterElement].getAttribute('aria-label');
            this[_g] = throttle((progress) => {
                const parentNode = this[$defaultProgressBarElement].parentNode;
                requestAnimationFrame(() => {
                    this[$defaultProgressBarElement].style.transform =
                        `scaleX(${progress})`;
                    if (progress === 0) {
                        // NOTE(cdata): We remove and re-append the progress bar in this
                        // condition so that the progress bar does not appear to
                        // transition backwards from the right when we reset to 0 (or
                        // otherwise <1) progress after having already reached 1 progress
                        // previously.
                        parentNode.removeChild(this[$defaultProgressBarElement]);
                        parentNode.appendChild(this[$defaultProgressBarElement]);
                    }
                    this[$defaultProgressBarElement].classList.toggle('hide', progress === 1.0);
                });
            }, PROGRESS_BAR_UPDATE_THRESHOLD);
            this[_h] = (event) => {
                const progress = event.detail.totalProgress;
                const reason = event.detail.reason;
                if (progress === 1.0) {
                    this[$updateProgressBar].flush();
                    if (this.loaded &&
                        (this[$shouldDismissPoster] ||
                            this.reveal === RevealStrategy.AUTO)) {
                        this[$hidePoster]();
                    }
                }
                this[$updateProgressBar](progress);
                this.dispatchEvent(new CustomEvent('progress', { detail: { totalProgress: progress, reason } }));
            };
            const ModelViewerElement = self.ModelViewerElement || {};
            const dracoDecoderLocation = ModelViewerElement.dracoDecoderLocation ||
                DEFAULT_DRACO_DECODER_LOCATION;
            CachingGLTFLoader.setDRACODecoderLocation(dracoDecoderLocation);
            const ktx2TranscoderLocation = ModelViewerElement.ktx2TranscoderLocation ||
                DEFAULT_KTX2_TRANSCODER_LOCATION;
            CachingGLTFLoader.setKTX2TranscoderLocation(ktx2TranscoderLocation);
            if (ModelViewerElement.meshoptDecoderLocation) {
                CachingGLTFLoader.setMeshoptDecoderLocation(ModelViewerElement.meshoptDecoderLocation);
            }
            const lottieLoaderLocation = ModelViewerElement.lottieLoaderLocation ||
                DEFAULT_LOTTIE_LOADER_LOCATION;
            if (Renderer.singleton.textureUtils != null) {
                Renderer.singleton.textureUtils.lottieLoaderUrl = lottieLoaderLocation;
            }
        }
        connectedCallback() {
            super.connectedCallback();
            if (!this.loaded) {
                this.showPoster();
            }
            this[$progressTracker].addEventListener('progress', this[$onProgress]);
        }
        disconnectedCallback() {
            super.disconnectedCallback();
            this[$progressTracker].removeEventListener('progress', this[$onProgress]);
        }
        async updated(changedProperties) {
            super.updated(changedProperties);
            if (changedProperties.has('poster') && this.poster != null) {
                this[$defaultPosterElement].style.backgroundImage =
                    `url(${this.poster})`;
            }
            if (changedProperties.has('alt')) {
                this[$defaultPosterElement].setAttribute('aria-label', this[$altDefaulted]);
            }
            if (changedProperties.has('reveal') || changedProperties.has('loading')) {
                this[$updateSource]();
            }
        }
        [(_a = $modelIsRevealed, _b = $shouldDismissPoster, _c = $posterContainerElement, _d = $defaultPosterElement, _e = $defaultProgressBarElement, _f = $ariaLabelCallToAction, _g = $updateProgressBar, _h = $onProgress, $shouldAttemptPreload)]() {
            const extraModels = Array.from(this.querySelectorAll('extra-model'));
            return !!(this.src || extraModels.length > 0) &&
                (this[$shouldDismissPoster] ||
                    this.loading === LoadingStrategy.EAGER ||
                    (this.reveal === RevealStrategy.AUTO && this[$isElementInViewport]));
        }
        [$hidePoster]() {
            this[$shouldDismissPoster] = false;
            const posterContainerElement = this[$posterContainerElement];
            if (!posterContainerElement.classList.contains('show')) {
                return;
            }
            posterContainerElement.classList.remove('show');
            this[$userInputElement].classList.add('show');
            const oldVisibility = this.modelIsVisible;
            this[$modelIsRevealed] = true;
            this[$announceModelVisibility](oldVisibility);
            const root = this.getRootNode();
            // If the <model-viewer> is still focused, forward the focus to
            // the canvas that has just been revealed
            if (root && root.activeElement === this) {
                this[$userInputElement].focus();
            }
            // Ensure that the poster is no longer focusable or visible to
            // screen readers
            const defaultPosterElement = this[$defaultPosterElement];
            defaultPosterElement.setAttribute('aria-hidden', 'true');
            defaultPosterElement.tabIndex = -1;
            this.dispatchEvent(new CustomEvent('poster-dismissed'));
        }
        [$getModelIsVisible]() {
            return super[$getModelIsVisible]() && this[$modelIsRevealed];
        }
    }
    __decorate([
        property({ type: String })
    ], LoadingModelViewerElement.prototype, "poster", void 0);
    __decorate([
        property({ type: String })
    ], LoadingModelViewerElement.prototype, "reveal", void 0);
    __decorate([
        property({ type: String })
    ], LoadingModelViewerElement.prototype, "loading", void 0);
    return LoadingModelViewerElement;
};
//# sourceMappingURL=loading.js.map