// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { assert } from 'chrome://resources/js/assert.js';
import { AsyncDirective, directive, html, PartType } from 'chrome://resources/lit/v3_0/lit.rollup.js';
// Directive to render some items in an array asynchronously. Initially
// renders `initialCount` items, and renders remaining items asynchronously
// in chunking mode, where each chunk is rendered on a subsequent animation
// frame. Chunk size is initialized to `initialCount` and increases by
// `initialCount` when frames render more quickly than the target, and halves if
// frames render more slowly than the target (20fps).
class AsyncMapDirective extends AsyncDirective {
    template = _item => html ``;
    initialCount = -1;
    items = [];
    chunkSize_ = -1;
    renderedItems_ = [];
    renderStartTime_ = 0;
    targetElapsedTime_ = 50; // 20fps
    requestId_ = null;
    timeout_ = null;
    constructor(partInfo) {
        super(partInfo);
        assert(partInfo.type === PartType.CHILD, 'asyncMap() can only be used in text expressions');
    }
    render(items, template, initialCount) {
        // Clear any outstanding timeout or animation frame.
        if (this.timeout_) {
            clearTimeout(this.timeout_);
            this.timeout_ = null;
        }
        if (this.requestId_) {
            cancelAnimationFrame(this.requestId_);
            this.requestId_ = null;
        }
        this.renderStartTime_ = 0;
        this.template = template;
        this.items = items;
        assert(initialCount > 0);
        this.initialCount = initialCount;
        if (this.chunkSize_ === -1) {
            this.chunkSize_ = this.initialCount;
        }
        // Don't unnecessarily fully remove items. This will create a larger
        // change in the template. Instead, update the number of items already
        // rendered + the current chunk size initially.
        const count = Math.min(items.length, this.renderedItems_.length + this.chunkSize_);
        this.renderedItems_ = this.items.slice(0, count);
        if (count < items.length) {
            this.timeout_ = setTimeout(() => this.renderInChunks_(), 0);
        }
        return this.renderItems_();
    }
    renderItems_() {
        return this.renderedItems_.map(item => this.template(item));
    }
    async renderInChunks_() {
        this.timeout_ = null;
        let length = this.renderedItems_.length;
        const arrayRef = this.items;
        while (length < arrayRef.length) {
            await new Promise((resolve) => {
                this.requestId_ = requestAnimationFrame(() => {
                    if (this.requestId_) {
                        cancelAnimationFrame(this.requestId_);
                        this.requestId_ = null;
                    }
                    resolve();
                });
            });
            if (this.items !== arrayRef) {
                return; // value updated, no longer our loop
            }
            // Adjust the chunk size if needed.
            if (this.renderStartTime_ > 0) {
                const elapsed = performance.now() - this.renderStartTime_;
                // Additive increase, multiplicative decrease
                if (elapsed < this.targetElapsedTime_) {
                    this.chunkSize_ += this.initialCount;
                }
                else {
                    this.chunkSize_ =
                        Math.max(this.initialCount, Math.floor(this.chunkSize_ / 2));
                }
            }
            const newLength = Math.min(length + this.chunkSize_, arrayRef.length);
            this.renderedItems_.push(...this.items.slice(length, newLength));
            length = newLength;
            this.renderStartTime_ = performance.now();
            this.setValue(this.renderItems_());
        }
    }
}
export const asyncMap = directive(AsyncMapDirective);
