"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ECSqlReader = exports.PropertyMetaDataMap = void 0;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module iModels
 */
const Base64EncodedString_1 = require("./Base64EncodedString");
const ConcurrentQuery_1 = require("./ConcurrentQuery");
/** @public */
class PropertyMetaDataMap {
    constructor(properties) {
        this.properties = properties;
        this._byPropName = new Map();
        this._byJsonName = new Map();
        this._byNoCase = new Map();
        for (const property of this.properties) {
            property.extendType = property.extendedType !== undefined ? property.extendedType : ""; // eslint-disable-line @typescript-eslint/no-deprecated
            property.extendedType = property.extendedType === "" ? undefined : property.extendedType;
            this._byPropName.set(property.name, property.index);
            this._byJsonName.set(property.jsonName, property.index);
            this._byNoCase.set(property.name.toLowerCase(), property.index);
            this._byNoCase.set(property.jsonName.toLowerCase(), property.index);
        }
    }
    get length() {
        return this.properties.length;
    }
    [Symbol.iterator]() {
        return this.properties[Symbol.iterator]();
    }
    findByName(name) {
        const index = this._byPropName.get(name);
        if (typeof index === "number") {
            return this.properties[index];
        }
        return undefined;
    }
    findByJsonName(name) {
        const index = this._byJsonName.get(name);
        if (typeof index === "number") {
            return this.properties[index];
        }
        return undefined;
    }
    findByNoCase(name) {
        const index = this._byNoCase.get(name.toLowerCase());
        if (typeof index === "number") {
            return this.properties[index];
        }
        return undefined;
    }
}
exports.PropertyMetaDataMap = PropertyMetaDataMap;
/**
 * Execute ECSQL statements and read the results.
 *
 * The query results are returned one row at a time. The format of the row is dictated by the
 * [[QueryOptions.rowFormat]] specified in the `options` parameter of the constructed ECSqlReader object. Defaults to
 * [[QueryRowFormat.UseECSqlPropertyIndexes]] when no `rowFormat` is defined.
 *
 * There are three primary ways to interact with and read the results:
 * - Stream them using ECSqlReader as an asynchronous iterator.
 * - Iterator over them manually using [[ECSqlReader.step]].
 * - Capture all of the results at once in an array using [[QueryRowProxy.toArray]].
 *
 * @see
 * - [ECSQL Overview]($docs/learning/backend/ExecutingECSQL)
 * - [ECSQL Row Formats]($docs/learning/ECSQLRowFormat) for more details on how rows are formatted.
 * - [ECSQL Code Examples]($docs/learning/ECSQLCodeExamples#iterating-over-query-results) for examples of each
 *      of the above ways of interacting with ECSqlReader.
 *
 * @note When iterating over the results, the current row will be a [[QueryRowProxy]] object. To get the row as a basic
 *       JavaScript object, call [[QueryRowProxy.toRow]] on it.
 * @public
 */
class ECSqlReader {
    /**
     * @internal
     */
    constructor(_executor, query, param, options) {
        this._executor = _executor;
        this.query = query;
        this._localRows = [];
        this._localOffset = 0;
        this._globalOffset = -1;
        this._globalCount = -1;
        this._done = false;
        this._globalDone = false;
        this._props = new PropertyMetaDataMap([]);
        this._param = new ConcurrentQuery_1.QueryBinder().serialize();
        this._lockArgs = false;
        this._stats = { backendCpuTime: 0, backendTotalTime: 0, backendMemUsed: 0, backendRowsReturned: 0, totalTime: 0, retryCount: 0, prepareTime: 0 };
        this._options = new ConcurrentQuery_1.QueryOptionsBuilder().getOptions();
        this._rowProxy = new Proxy(this, {
            get: (target, key) => {
                if (typeof key === "string") {
                    const idx = Number.parseInt(key, 10);
                    if (!Number.isNaN(idx)) {
                        return target.getRowInternal()[idx];
                    }
                    const prop = target._props.findByNoCase(key);
                    if (prop) {
                        return target.getRowInternal()[prop.index];
                    }
                    if (key === "getMetaData") {
                        return () => target._props.properties;
                    }
                    if (key === "toRow") {
                        return () => target.formatCurrentRow(true);
                    }
                    if (key === "toArray") {
                        return () => this.getRowInternal();
                    }
                }
                return undefined;
            },
            has: (target, p) => {
                return !target._props.findByNoCase(p);
            },
            ownKeys: (target) => {
                const keys = [];
                for (const prop of target._props) {
                    keys.push(prop.name);
                }
                return keys;
            },
        });
        if (query.trim().length === 0) {
            throw new Error("expecting non-empty ecsql statement");
        }
        if (param) {
            this._param = param.serialize();
        }
        this.reset(options);
    }
    static replaceBase64WithUint8Array(row) {
        for (const key of Object.keys(row)) {
            const val = row[key];
            if (typeof val === "string") {
                if (Base64EncodedString_1.Base64EncodedString.hasPrefix(val)) {
                    row[key] = Base64EncodedString_1.Base64EncodedString.toUint8Array(val);
                }
            }
            else if (typeof val === "object" && val !== null) {
                this.replaceBase64WithUint8Array(val);
            }
        }
    }
    setParams(param) {
        if (this._lockArgs) {
            throw new Error("call resetBindings() before setting or changing parameters");
        }
        this._param = param.serialize();
    }
    reset(options) {
        if (options) {
            this._options = options;
        }
        this._props = new PropertyMetaDataMap([]);
        this._localRows = [];
        this._globalDone = false;
        this._globalOffset = 0;
        this._globalCount = -1;
        if (typeof this._options.rowFormat === "undefined")
            this._options.rowFormat = ConcurrentQuery_1.QueryRowFormat.UseECSqlPropertyIndexes;
        if (this._options.limit) {
            if (typeof this._options.limit.offset === "number" && this._options.limit.offset > 0)
                this._globalOffset = this._options.limit.offset;
            if (typeof this._options.limit.count === "number" && this._options.limit.count > 0)
                this._globalCount = this._options.limit.count;
        }
        this._done = false;
    }
    /**
     * Get the current row from the query result. The current row is the one most recently stepped-to
     * (by step() or during iteration).
     *
     * Each value from the row can be accessed by index or by name.
     *
     * The format of the row is dictated by the [[QueryOptions.rowFormat]] specified in the `options` parameter of the
     * constructed ECSqlReader object.
     *
     * @see
     * - [[QueryRowFormat]]
     * - [ECSQL Row Formats]($docs/learning/ECSQLRowFormat)
     *
     * @note The current row is be a [[QueryRowProxy]] object. To get the row as a basic JavaScript object, call
     *       [[QueryRowProxy.toRow]] on it.
     *
     * @example
     * ```ts
     * const reader = iModel.createQueryReader("SELECT ECInstanceId FROM bis.Element");
     * while (await reader.step()) {
     *   // Both lines below print the same value
     *   console.log(reader.current[0]);
     *   console.log(reader.current.ecinstanceid);
     * }
     * ```
     *
     * @return The current row as a [[QueryRowProxy]].
     */
    get current() {
        return this._rowProxy;
    }
    /**
     * Clear all bindings.
     */
    resetBindings() {
        this._param = new ConcurrentQuery_1.QueryBinder().serialize();
        this._lockArgs = false;
    }
    /**
     * Returns if there are more rows available.
     *
     * @returns `true` if all rows have been stepped through already.<br/>
     *          `false` if there are any yet unaccessed rows.
     */
    get done() {
        return this._done;
    }
    /**
     * @internal
     */
    getRowInternal() {
        if (this._localRows.length <= this._localOffset)
            throw new Error("no current row");
        return this._localRows[this._localOffset];
    }
    /**
     * Get performance-related statistics for the current query.
     */
    get stats() {
        return this._stats;
    }
    /**
     *
     */
    async readRows() {
        if (this._globalDone) {
            return [];
        }
        this._lockArgs = true;
        this._globalOffset += this._localRows.length;
        this._globalCount -= this._localRows.length;
        if (this._globalCount === 0) {
            return [];
        }
        const valueFormat = this._options.rowFormat === ConcurrentQuery_1.QueryRowFormat.UseJsPropertyNames ? ConcurrentQuery_1.DbValueFormat.JsNames : ConcurrentQuery_1.DbValueFormat.ECSqlNames;
        const request = {
            ...this._options,
            kind: ConcurrentQuery_1.DbRequestKind.ECSql,
            valueFormat,
            query: this.query,
            args: this._param,
        };
        request.includeMetaData = this._props.length > 0 ? false : true;
        request.limit = { offset: this._globalOffset, count: this._globalCount < 1 ? -1 : this._globalCount };
        const resp = await this.runWithRetry(request);
        this._globalDone = resp.status === ConcurrentQuery_1.DbResponseStatus.Done;
        if (this._props.length === 0 && resp.meta.length > 0) {
            this._props = new PropertyMetaDataMap(resp.meta);
        }
        for (const row of resp.data) {
            ECSqlReader.replaceBase64WithUint8Array(row);
        }
        return resp.data;
    }
    /**
     * @internal
     */
    async runWithRetry(request) {
        const needRetry = (rs) => (rs.status === ConcurrentQuery_1.DbResponseStatus.Partial || rs.status === ConcurrentQuery_1.DbResponseStatus.QueueFull || rs.status === ConcurrentQuery_1.DbResponseStatus.Timeout || rs.status === ConcurrentQuery_1.DbResponseStatus.ShuttingDown) && (rs.data === undefined || rs.data.length === 0);
        const updateStats = (rs) => {
            this._stats.backendCpuTime += rs.stats.cpuTime;
            this._stats.backendTotalTime += rs.stats.totalTime;
            this._stats.backendMemUsed += rs.stats.memUsed;
            this._stats.prepareTime += rs.stats.prepareTime;
            this._stats.backendRowsReturned += (rs.data === undefined) ? 0 : rs.data.length;
        };
        const execQuery = async (req) => {
            const startTime = Date.now();
            const rs = await this._executor.execute(req);
            this.stats.totalTime += (Date.now() - startTime);
            return rs;
        };
        let retry = ECSqlReader._maxRetryCount;
        let resp = await execQuery(request);
        ConcurrentQuery_1.DbQueryError.throwIfError(resp, request);
        while (--retry > 0 && needRetry(resp)) {
            resp = await execQuery(request);
            this._stats.retryCount += 1;
            if (needRetry(resp)) {
                updateStats(resp);
            }
        }
        if (retry === 0 && needRetry(resp)) {
            throw new Error("query too long to execute or server is too busy");
        }
        updateStats(resp);
        return resp;
    }
    /**
     * @internal
     */
    formatCurrentRow(onlyReturnObject = false) {
        if (!onlyReturnObject && this._options.rowFormat === ConcurrentQuery_1.QueryRowFormat.UseECSqlPropertyIndexes) {
            return this.getRowInternal();
        }
        const formattedRow = {};
        for (const prop of this._props) {
            const propName = this._options.rowFormat === ConcurrentQuery_1.QueryRowFormat.UseJsPropertyNames ? prop.jsonName : prop.name;
            const val = this.getRowInternal()[prop.index];
            if (typeof val !== "undefined" && val !== null) {
                Object.defineProperty(formattedRow, propName, {
                    value: val,
                    enumerable: true,
                });
            }
        }
        return formattedRow;
    }
    /**
     * Get the metadata for each column in the query result.
     *
     * @returns An array of [[QueryPropertyMetaData]].
     */
    async getMetaData() {
        if (this._props.length === 0) {
            await this.fetchRows();
        }
        return this._props.properties;
    }
    /**
     *
     */
    async fetchRows() {
        this._localOffset = -1;
        this._localRows = await this.readRows();
        if (this._localRows.length === 0) {
            this._done = true;
        }
    }
    /**
     * Step to the next row of the query result.
     *
     * @returns `true` if a row can be read from `current`.<br/>
     *          `false` if there are no more rows; i.e., all rows have been stepped through already.
     */
    async step() {
        if (this._done) {
            return false;
        }
        const cachedRows = this._localRows.length;
        if (this._localOffset < cachedRows - 1) {
            ++this._localOffset;
        }
        else {
            await this.fetchRows();
            this._localOffset = 0;
            return !this._done;
        }
        return true;
    }
    /**
     * Get all remaining rows from the query result.
     *
     * @returns An array of all remaining rows from the query result.
     */
    async toArray() {
        const rows = [];
        while (await this.step()) {
            rows.push(this.formatCurrentRow());
        }
        return rows;
    }
    /**
     * Accessor for using ECSqlReader as an asynchronous iterator.
     *
     * @returns An asynchronous iterator over the rows returned by the executed ECSQL query.
     */
    [Symbol.asyncIterator]() {
        return this;
    }
    /**
     * Calls step when called as an iterator.
     *
     * Returns the row alongside a `done` boolean to indicate if there are any more rows for an iterator to step to.
     *
     * @returns An object with the keys: `value` which contains the row and `done` which contains a boolean.
     */
    async next() {
        if (await this.step()) {
            return {
                done: false,
                value: this.current,
            };
        }
        else {
            return {
                done: true,
                value: this.current,
            };
        }
    }
}
exports.ECSqlReader = ECSqlReader;
ECSqlReader._maxRetryCount = 10;
//# sourceMappingURL=ECSqlReader.js.map