"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module ViewDefinitions
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ViewStore = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const CloudSqlite_1 = require("./CloudSqlite");
const SQLiteDb_1 = require("./SQLiteDb");
const Category_1 = require("./Category");
const Model_1 = require("./Model");
const Symbols_1 = require("./internal/Symbols");
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// cspell:ignore nocase rowid
/**
 * A ViewStore is a database that stores Views and related data. It is used to store and retrieve views for iTwin.js.
 * It can either be a local SQLite file, or a CloudSqlite database stored in a cloud container. To use a CloudSqlite
 * database, you must first create a container in Blob Storage and then call [[ViewStore.CloudAccess.initializeDb]].
 *
 * A ViewStore can hold:
 * - Views
 * - DisplayStyles
 * - CategorySelectors
 * - ModelSelectors
 * - RenderTimelines
 * - Searches
 * - Tags
 * - Thumbnails
 * - ViewGroups
 *
 * Views are added to a ViewStore via ViewDefinitionProps that may hold references to a DisplayStyle, CategorySelector, ModelSelector, or RenderTimeline.
 * Before storing a View, you must first add any referenced DisplayStyles, CategorySelectors, ModelSelectors, and RenderTimelines to the
 * ViewStore. The "add" methods return a string that uniquely identifies the object in the ViewStore.
 * You should set the ViewDefinitionProps's displayStyle, categorySelector, modelSelector, or renderTimeline member to the returned string.
 * When you load a ViewDefinition from the ViewStore, the member may be used to load the DisplayStyle, CategorySelector,
 * ModelSelector, RenderTimeline, etc.
 *
 * A IdString is a string that uniquely identifies a row in one of the ViewStore's internal tables. The string holds a base-36 integer
 * that starts with "@" (vs. "0x" for ElementIds). For example, if you store a DisplayStyle and it is assigned the ViewStore Id "@y1", then you
 * should set the ViewDefinitionProps's displayStyle member to "@y1". When you load the ViewDefinition from the ViewStore, the "@Y1" may be used to
 * alo load the DisplayStyle from the ViewStore.
 *
 * Views are organized into hierarchical ViewGroups (like file and folder hierarchies on a file system). A View is always stored "in" a ViewGroup, and
 * views must have a name that is unique within the ViewGroup. ViewGroups may each have a default ViewId.
 * The root ViewGroup is named "Root" and has a RowId of 1. The root ViewGroup can not be deleted.
 * View names and ViewGroup names may not contain either "/" or "@". ViewGroups are stored in the "viewGroups" table.
 *
 * Views may be "tagged" with one or more Tags. Tags are named with an arbitrary string that can be used to group Views. A Tag may
 * be associated with multiple Views, and a View may have multiple Tags. Tags are stored in the "tags" table.
 *
 * Views may optionally have a thumbnail, paired via the View's Id. Thumbnails are stored in the "thumbnails" table.
 *
 * Note: All ElementIds and ModelIds in ModelSelectors, CategorySelectors, DisplayStyles, Timelines, etc. are converted to guid-based identifiers when stored in the ViewStore.
 * They are then remapped back to their Ids when loaded from the ViewStore. This allows the ViewStore to be used with more than one iModel,
 * provided that the same Guids are used in each iModel. This is done by storing the set of unique Guids in the "guids" table, and then
 * creating a reference to the row in the "guids" table via the special Id prefix "^". For example, if a category selector contains the
 * Id "0x123", then the guid from element 0x123 is stored in the "guids" table, and the category selector is stored with the rowId of the entry
 * in the guid table (e.g. "^1w"). When the category selector is loaded from the ViewStore, the guid is looked up in the "guids" table and
 * the iModel is queried for the element with that guid. That element's Id (which may or may not be 0x123) is then returned in the category selector.
 *
 * @beta
 */
var ViewStore;
(function (ViewStore) {
    ViewStore.tableName = {
        categorySelectors: "categorySelectors",
        displayStyles: "displayStyles",
        viewGroups: "viewGroups",
        guids: "guids",
        modelSelectors: "modelSelectors",
        taggedViews: "taggedViews",
        tags: "tags",
        thumbnails: "thumbnails",
        timelines: "timelines",
        searches: "searches",
        views: "views",
    };
    /** convert a RowId to a RowString (base-36 integer with a leading "@") */
    ViewStore.fromRowId = (rowId) => {
        return `@${rowId.toString(36)}`;
    };
    /** convert a guid RowId to a GuidRowString (base-36 integer with a leading "^") */
    const guidRowToString = (rowId) => {
        return `^${rowId.toString(36)}`;
    };
    /** determine if a string is a guid row string (base-36 integer with a leading "^") */
    const isGuidRowString = (id) => true === id?.startsWith("^");
    /** @internal */
    ViewStore.toRowId = (id) => {
        if (typeof id === "number")
            return id;
        if (!core_common_1.ViewStoreRpc.isViewStoreId(id) && !isGuidRowString(id))
            core_common_1.ViewStoreError.throwError("invalid-value", { message: `invalid value: ${id}` });
        return parseInt(id.slice(1), 36);
    };
    const maybeToRowId = (id) => undefined === id ? undefined : ViewStore.toRowId(id);
    const cloneProps = (from) => JSON.parse(JSON.stringify(from));
    const blankElementProps = (from, classFullName, idString, name) => {
        from.id = ViewStore.fromRowId(ViewStore.toRowId(idString));
        from.classFullName = classFullName;
        from.model = core_common_1.IModel.dictionaryId;
        from.code = { spec: "0x1", scope: "0x1", value: name };
        return from;
    };
    const validateName = (name, msg) => {
        if (name.trim().length === 0 || (/[@^#<>:"/\\"`'|?*\u0000-\u001F]/g.test(name)))
            core_common_1.ViewStoreError.throwError("invalid-value", { message: `illegal ${msg} name "${name}"` });
    };
    ViewStore.defaultViewGroupId = 1;
    class ViewDb extends SQLiteDb_1.VersionedSqliteDb {
        myVersion = "4.0.0";
        _iModel;
        _guidMap;
        get guidMap() { return this._guidMap; }
        set guidMap(guidMap) { this._guidMap = guidMap; }
        get iModel() { return this._iModel; }
        set iModel(iModel) { this._iModel = iModel; }
        constructor(arg) {
            super();
            this._iModel = arg?.iModel;
            this._guidMap = arg?.guidMap ?? this._iModel?.elements; // this is only so tests can mock guids
        }
        /** create all the tables for a new ViewDb */
        createDDL() {
            const baseCols = "Id INTEGER PRIMARY KEY AUTOINCREMENT,json TEXT,owner TEXT";
            this.createTable({
                tableName: ViewStore.tableName.views,
                columns: `${baseCols},name TEXT NOT NULL COLLATE NOCASE,className TEXT NOT NULL,private BOOLEAN NOT NULL,` +
                    `groupId INTEGER NOT NULL REFERENCES ${ViewStore.tableName.viewGroups}(Id) ON DELETE CASCADE, ` +
                    `modelSel INTEGER REFERENCES ${ViewStore.tableName.modelSelectors}(Id), ` +
                    `categorySel INTEGER  NOT NULL REFERENCES ${ViewStore.tableName.categorySelectors}(Id), ` +
                    `displayStyle INTEGER NOT NULL REFERENCES ${ViewStore.tableName.displayStyles}(Id)`,
                constraints: "UNIQUE(groupId,name)",
                addTimestamp: true,
            });
            this.createTable({
                tableName: ViewStore.tableName.viewGroups,
                columns: `${baseCols},name TEXT NOT NULL COLLATE NOCASE,parent INTEGER NOT NULL REFERENCES ${ViewStore.tableName.viewGroups}(Id) ON DELETE CASCADE` +
                    `,defaultViewId INTEGER REFERENCES ${ViewStore.tableName.views}(Id)`,
                constraints: "UNIQUE(parent,name)",
                addTimestamp: true,
            });
            // for tables that have a "name" column, we want to enforce case-insensitive uniqueness. Names may be null.
            const makeTable = (table, extra) => {
                this.createTable({ tableName: table, columns: `${baseCols},name TEXT UNIQUE COLLATE NOCASE${extra ?? ""}`, addTimestamp: true });
            };
            makeTable(ViewStore.tableName.modelSelectors);
            makeTable(ViewStore.tableName.categorySelectors);
            makeTable(ViewStore.tableName.displayStyles);
            makeTable(ViewStore.tableName.timelines);
            makeTable(ViewStore.tableName.tags);
            makeTable(ViewStore.tableName.searches);
            this.createTable({ tableName: ViewStore.tableName.thumbnails, columns: `Id INTEGER PRIMARY KEY REFERENCES ${ViewStore.tableName.views} (Id) ON DELETE CASCADE,json,owner,data BLOB NOT NULL` });
            this.createTable({
                tableName: ViewStore.tableName.taggedViews, columns: `viewId INTEGER NOT NULL REFERENCES ${ViewStore.tableName.views} (Id) ON DELETE CASCADE,` +
                    `tagId INTEGER NOT NULL REFERENCES ${ViewStore.tableName.tags} (Id) ON DELETE CASCADE`,
                constraints: `UNIQUE(tagId,viewId)`,
            });
            this.createTable({ tableName: ViewStore.tableName.guids, columns: `guid BLOB NOT NULL UNIQUE` });
            this.addViewGroupRow({ name: "Root", json: JSON.stringify({}) });
        }
        /** get the row in the "guids" table for a given guid. If the guid is not present, return 0 */
        getGuidRow(guid) {
            return this.withPreparedSqliteStatement(`SELECT rowId FROM ${ViewStore.tableName.guids} WHERE guid=?`, (stmt) => {
                stmt.bindGuid(1, guid);
                return !stmt.nextRow() ? 0 : stmt.getValueInteger(0);
            });
        }
        /** @internal */
        getGuid(rowid) {
            return this.withSqliteStatement(`SELECT guid FROM ${ViewStore.tableName.guids} WHERE rowId=?`, (stmt) => {
                stmt.bindInteger(1, rowid);
                return !stmt.nextRow() ? undefined : stmt.getValueGuid(0);
            });
        }
        /** @internal */
        iterateGuids(rowIds, fn) {
            this.withSqliteStatement(`SELECT guid FROM ${ViewStore.tableName.guids} WHERE rowId=?`, (stmt) => {
                for (const rowId of rowIds) {
                    stmt.reset();
                    stmt.bindInteger(1, rowId);
                    if (stmt.nextRow())
                        fn(stmt.getValueGuid(0), rowId);
                }
            });
        }
        /** @internal */
        addGuid(guid) {
            const existing = this.getGuidRow(guid);
            return existing !== 0 ? existing : this.withPreparedSqliteStatement(`INSERT INTO ${ViewStore.tableName.guids} (guid) VALUES(?)`, (stmt) => {
                stmt.bindGuid(1, guid);
                stmt.stepForWrite();
                return this[Symbols_1._nativeDb].getLastInsertRowId();
            });
        }
        /** @internal */
        addViewRow(args) {
            validateName(args.name, "view");
            return this.withSqliteStatement(`INSERT INTO ${ViewStore.tableName.views} (className,name,json,owner,private,groupId,modelSel,categorySel,displayStyle) VALUES(?,?,?,?,?,?,?,?,?)`, (stmt) => {
                stmt.bindString(1, args.className);
                stmt.bindString(2, args.name);
                stmt.bindString(3, args.json);
                stmt.maybeBindString(4, args.owner);
                stmt.bindBoolean(5, args.isPrivate ?? false);
                stmt.bindInteger(6, args.groupId ?? 1);
                stmt.maybeBindInteger(7, args.modelSel);
                stmt.bindInteger(8, args.categorySel);
                stmt.bindInteger(9, args.displayStyle);
                stmt.stepForWrite();
                return this[Symbols_1._nativeDb].getLastInsertRowId();
            });
        }
        /** @internal */
        addViewGroupRow(args) {
            validateName(args.name, "group");
            return this.withSqliteStatement(`INSERT INTO ${ViewStore.tableName.viewGroups} (name,owner,parent,json) VALUES(?,?,?,?)`, (stmt) => {
                stmt.bindString(1, args.name);
                stmt.maybeBindString(2, args.owner);
                stmt.bindInteger(3, args.parentId ?? 1);
                stmt.bindString(4, args.json);
                stmt.stepForWrite();
                return this[Symbols_1._nativeDb].getLastInsertRowId();
            });
        }
        addTableRow(table, args) {
            return this.withSqliteStatement(`INSERT INTO ${table} (name,json,owner) VALUES(?,?,?)`, (stmt) => {
                stmt.maybeBindString(1, args.name);
                stmt.bindString(2, args.json);
                stmt.maybeBindString(3, args.owner);
                stmt.stepForWrite();
                return this[Symbols_1._nativeDb].getLastInsertRowId();
            });
        }
        /** add a row to the "modelSelectors" table, return the RowId
         * @internal
         */
        addModelSelectorRow(args) {
            return this.addTableRow(ViewStore.tableName.modelSelectors, args);
        }
        /** add a row to the "categorySelectors" table, return the RowId
         * @internal
         */
        addCategorySelectorRow(args) {
            return this.addTableRow(ViewStore.tableName.categorySelectors, args);
        }
        /** add a row to the "displayStyles" table, return the RowId
         * @internal
         */
        addDisplayStyleRow(args) {
            return this.addTableRow(ViewStore.tableName.displayStyles, args);
        }
        /** add a row to the "timelines" table, return the RowId
         * @internal
         */
        addTimelineRow(args) {
            return this.addTableRow(ViewStore.tableName.timelines, args);
        }
        /** add a row to the "tags" table, return the RowId
         * @internal
         */
        addTag(args) {
            return this.addTableRow(ViewStore.tableName.tags, args);
        }
        /** add a row to the "searches" table, return the RowId
         * @internal
         */
        async addSearch(args) {
            return this.addTableRow(ViewStore.tableName.searches, args);
        }
        /** add or update a row in the "thumbnails" table, return the RowId
         * @internal
         */
        addOrReplaceThumbnailRow(args) {
            return this.withSqliteStatement(`INSERT OR REPLACE INTO ${ViewStore.tableName.thumbnails} (Id,json,owner,data) VALUES(?,?,?,?)`, (stmt) => {
                stmt.bindInteger(1, args.viewId);
                stmt.bindString(2, JSON.stringify(args.format));
                stmt.maybeBindString(3, args.owner);
                stmt.bindBlob(4, args.data);
                stmt.stepForWrite();
                return this[Symbols_1._nativeDb].getLastInsertRowId();
            });
        }
        deleteFromTable(table, id) {
            this.withSqliteStatement(`DELETE FROM ${table} WHERE Id=?`, (stmt) => {
                stmt.bindInteger(1, ViewStore.toRowId(id));
                stmt.stepForWrite();
            });
        }
        /** @internal */
        deleteViewRow(id) {
            return this.deleteFromTable(ViewStore.tableName.views, id);
        }
        async deleteViewGroup(args) {
            const rowId = this.findViewGroup(args.name);
            if (rowId === 1)
                core_common_1.ViewStoreError.throwError("group-error", { message: "Cannot delete root group" });
            return this.deleteFromTable(ViewStore.tableName.viewGroups, rowId);
        }
        deleteModelSelectorSync(id) {
            return this.deleteFromTable(ViewStore.tableName.modelSelectors, id);
        }
        async deleteModelSelector(args) {
            return this.deleteModelSelectorSync(args.id);
        }
        deleteCategorySelectorSync(id) {
            return this.deleteFromTable(ViewStore.tableName.categorySelectors, id);
        }
        deleteDisplayStyleSync(id) {
            return this.deleteFromTable(ViewStore.tableName.displayStyles, id);
        }
        deleteTimelineSync(id) {
            return this.deleteFromTable(ViewStore.tableName.timelines, id);
        }
        deleteTagSync(arg) {
            const id = this.findTagByName(arg.name);
            return this.deleteFromTable(ViewStore.tableName.tags, id);
        }
        async deleteTag(arg) {
            return this.deleteTagSync(arg);
        }
        async deleteCategorySelector(args) {
            return this.deleteCategorySelectorSync(args.id);
        }
        async deleteDisplayStyle(args) {
            return this.deleteDisplayStyleSync(args.id);
        }
        async deleteTimeline(args) {
            return this.deleteTimelineSync(args.id);
        }
        deleteSearch(id) {
            return this.deleteFromTable(ViewStore.tableName.searches, id);
        }
        deleteThumbnailSync(id) {
            return this.deleteFromTable(ViewStore.tableName.thumbnails, ViewStore.toRowId(id));
        }
        async deleteThumbnail(arg) {
            return this.deleteThumbnailSync(arg.viewId);
        }
        /** get the data for a view from the database
         * @internal
         */
        getViewRow(viewId) {
            return this.withSqliteStatement(`SELECT className,name,json,owner,private,groupId,modelSel,categorySel,displayStyle FROM ${ViewStore.tableName.views} WHERE Id=?`, (stmt) => {
                stmt.bindInteger(1, viewId);
                return !stmt.nextRow() ? undefined : {
                    className: stmt.getValueString(0),
                    name: stmt.getValueString(1),
                    json: stmt.getValueString(2),
                    owner: stmt.getValueStringMaybe(3),
                    isPrivate: stmt.getValueBoolean(4),
                    groupId: stmt.getValueInteger(5),
                    modelSel: stmt.getValueIntegerMaybe(6),
                    categorySel: stmt.getValueInteger(7),
                    displayStyle: stmt.getValueInteger(8),
                };
            });
        }
        /** @internal */
        getThumbnailRow(viewId) {
            return this.withSqliteStatement(`SELECT json,owner,data FROM ${ViewStore.tableName.thumbnails} WHERE Id=?`, (stmt) => {
                stmt.bindInteger(1, viewId);
                return !stmt.nextRow() ? undefined : {
                    viewId,
                    format: JSON.parse(stmt.getValueString(0)),
                    owner: stmt.getValueStringMaybe(1),
                    data: stmt.getValueBlob(2),
                };
            });
        }
        /** @internal */
        getViewGroup(id) {
            return this.withSqliteStatement(`SELECT name,owner,json,parent,defaultViewId FROM ${ViewStore.tableName.viewGroups} WHERE Id=?`, (stmt) => {
                stmt.bindInteger(1, id);
                return !stmt.nextRow() ? undefined : {
                    name: stmt.getValueString(0),
                    owner: stmt.getValueStringMaybe(1),
                    json: stmt.getValueString(2),
                    parentId: stmt.getValueInteger(3),
                    defaultViewId: stmt.getValueIntegerMaybe(4),
                };
            });
        }
        getTableRow(table, id) {
            return this.withSqliteStatement(`SELECT name,json,owner FROM ${table} WHERE Id=?`, (stmt) => {
                stmt.bindInteger(1, id);
                return !stmt.nextRow() ? undefined : {
                    name: stmt.getValueStringMaybe(0),
                    json: stmt.getValueString(1),
                    owner: stmt.getValueStringMaybe(2),
                };
            });
        }
        /** read a ModelSelector given a rowId
         * @internal
         */
        getModelSelectorRow(id) {
            return this.getTableRow(ViewStore.tableName.modelSelectors, id);
        }
        /** read a CategorySelector given a rowId
         * @internal
         */
        getCategorySelectorRow(id) {
            return this.getTableRow(ViewStore.tableName.categorySelectors, id);
        }
        /** read a DisplayStyle given a rowId
         * @internal
         */
        getDisplayStyleRow(id) {
            return this.getTableRow(ViewStore.tableName.displayStyles, id);
        }
        /** @internal */
        getTimelineRow(id) {
            return this.getTableRow(ViewStore.tableName.timelines, id);
        }
        /** @internal */
        getTag(id) {
            return this.getTableRow(ViewStore.tableName.tags, id);
        }
        /** @internal */
        getSearch(id) {
            return this.getTableRow(ViewStore.tableName.searches, id);
        }
        updateJson(table, id, json) {
            this.withSqliteStatement(`UPDATE ${table} SET json=? WHERE Id=?`, (stmt) => {
                stmt.bindString(1, json);
                stmt.bindInteger(2, ViewStore.toRowId(id));
                stmt.stepForWrite();
            });
        }
        async updateViewShared(arg) {
            if (!arg.isShared && arg.owner === undefined)
                core_common_1.ViewStoreError.throwError("no-owner", { message: "owner must be defined for private views" });
            this.withSqliteStatement(`UPDATE ${ViewStore.tableName.views} SET private=?,owner=? WHERE Id=?`, (stmt) => {
                stmt.bindBoolean(1, !arg.isShared);
                stmt.maybeBindString(2, arg.owner);
                stmt.bindInteger(3, ViewStore.toRowId(arg.viewId));
                stmt.stepForWrite();
            });
        }
        /** @internal */
        updateViewGroupJson(groupId, json) {
            return this.updateJson(ViewStore.tableName.viewGroups, groupId, json);
        }
        /** @internal */
        updateModelSelectorJson(modelSelectorId, json) {
            return this.updateJson(ViewStore.tableName.modelSelectors, modelSelectorId, json);
        }
        /** @internal */
        updateCategorySelectorJson(categorySelectorId, json) {
            return this.updateJson(ViewStore.tableName.categorySelectors, categorySelectorId, json);
        }
        /** @internal */
        updateDisplayStyleJson(styleId, json) {
            return this.updateJson(ViewStore.tableName.displayStyles, styleId, json);
        }
        /** @internal */
        updateTimelineJson(timelineId, json) {
            return this.updateJson(ViewStore.tableName.timelines, timelineId, json);
        }
        /** @internal */
        updateSearchJson(searchId, json) {
            return this.updateJson(ViewStore.tableName.searches, searchId, json);
        }
        updateName(table, id, name) {
            this.withSqliteStatement(`UPDATE ${table} SET name=? WHERE Id=?`, (stmt) => {
                stmt.maybeBindString(1, name);
                stmt.bindInteger(2, ViewStore.toRowId(id));
                stmt.stepForWrite();
            });
        }
        async renameView(args) {
            return this.updateName(ViewStore.tableName.views, args.viewId, args.name);
        }
        async renameViewGroup(args) {
            return this.updateName(ViewStore.tableName.viewGroups, args.groupId, args.name);
        }
        async renameModelSelector(args) {
            return this.updateName(ViewStore.tableName.modelSelectors, args.id, args.name);
        }
        async renameCategorySelector(args) {
            return this.updateName(ViewStore.tableName.categorySelectors, args.id, args.name);
        }
        async renameDisplayStyle(args) {
            return this.updateName(ViewStore.tableName.displayStyles, args.id, args.name);
        }
        async renameTimeline(args) {
            return this.updateName(ViewStore.tableName.timelines, args.id, args.name);
        }
        async renameSearch(args) {
            return this.updateName(ViewStore.tableName.searches, args.id, args.name);
        }
        async renameTag(args) {
            this.withSqliteStatement(`UPDATE ${ViewStore.tableName.tags} SET name=? WHERE name=?`, (stmt) => {
                stmt.bindString(1, args.newName);
                stmt.bindString(2, args.oldName);
                stmt.stepForWrite();
            });
        }
        /** @internal */
        addTagToView(args) {
            this.withSqliteStatement(`INSERT OR IGNORE INTO ${ViewStore.tableName.taggedViews} (viewId,tagId) VALUES(?,?)`, (stmt) => {
                stmt.bindInteger(1, args.viewId);
                stmt.bindInteger(2, args.tagId);
                stmt.stepForWrite();
            });
        }
        deleteViewTag(args) {
            this.withSqliteStatement(`DELETE FROM ${ViewStore.tableName.taggedViews} WHERE viewId=? AND tagId=?`, (stmt) => {
                stmt.bindInteger(1, args.viewId);
                stmt.bindInteger(2, args.tagId);
                stmt.stepForWrite();
            });
        }
        /** @internal */
        findViewsForTag(tagId) {
            return this.withSqliteStatement(`SELECT viewId FROM ${ViewStore.tableName.taggedViews} WHERE tagId=?`, (stmt) => {
                stmt.bindInteger(1, tagId);
                const list = [];
                while (stmt.nextRow())
                    list.push(stmt.getValueInteger(0));
                return list;
            });
        }
        findByName(table, name) {
            return this.withSqliteStatement(`SELECT Id FROM ${table} WHERE name=?`, (stmt) => {
                stmt.bindString(1, name);
                return !stmt.nextRow() ? 0 : stmt.getValueInteger(0);
            });
        }
        /** @internal */
        getViewGroupByName(name, parentId) {
            return this.withSqliteStatement(`SELECT Id FROM ${ViewStore.tableName.viewGroups} WHERE name=? AND parent=?`, (stmt) => {
                stmt.bindString(1, name);
                stmt.bindInteger(2, parentId);
                return !stmt.nextRow() ? 0 : stmt.getValueInteger(0);
            });
        }
        /** @internal */
        findModelSelectorByName(name) {
            return this.findByName(ViewStore.tableName.modelSelectors, name);
        }
        /** @internal */
        findCategorySelectorByName(name) {
            return this.findByName(ViewStore.tableName.categorySelectors, name);
        }
        /** @internal */
        findDisplayStyleByName(name) {
            return this.findByName(ViewStore.tableName.displayStyles, name);
        }
        /** @internal */
        findTagByName(name) {
            return this.findByName(ViewStore.tableName.tags, name);
        }
        /** @internal */
        findTimelineByName(name) {
            return this.findByName(ViewStore.tableName.timelines, name);
        }
        /** @internal */
        findSearchByName(name) {
            return this.findByName(ViewStore.tableName.searches, name);
        }
        getViewInfoSync(id) {
            const maybeId = (rowId) => rowId ? ViewStore.fromRowId(rowId) : undefined;
            return this.withPreparedSqliteStatement(`SELECT owner,className,name,private,groupId,modelSel,categorySel,displayStyle FROM ${ViewStore.tableName.views} WHERE id=?`, (stmt) => {
                const viewId = ViewStore.toRowId(id);
                stmt.bindInteger(1, viewId);
                return stmt.nextRow() ? {
                    id: ViewStore.fromRowId(viewId),
                    owner: stmt.getValueString(0),
                    className: stmt.getValueString(1),
                    name: stmt.getValueStringMaybe(2),
                    isPrivate: stmt.getValueBoolean(3),
                    groupId: ViewStore.fromRowId(stmt.getValueInteger(4)),
                    modelSelectorId: maybeId(stmt.getValueInteger(5)),
                    categorySelectorId: ViewStore.fromRowId(stmt.getValueInteger(6)),
                    displayStyleId: ViewStore.fromRowId(stmt.getValueInteger(7)),
                    tags: this.getTagsForView(viewId),
                } : undefined;
            });
        }
        async getViewInfo(args) {
            return this.getViewInfoSync(args.viewId);
        }
        async findViewsByOwner(args) {
            const list = [];
            this.withSqliteStatement(`SELECT Id FROM ${ViewStore.tableName.views} WHERE owner=? ORDER BY Id ASC`, (stmt) => {
                stmt.bindString(1, args.owner);
                while (stmt.nextRow()) {
                    const info = this.getViewInfoSync(stmt.getValueInteger(0));
                    if (info)
                        list.push(info);
                }
            });
            return list;
        }
        /** @internal */
        findTagIdsForView(viewId) {
            return this.withSqliteStatement(`SELECT tagId FROM ${ViewStore.tableName.taggedViews} WHERE viewId=?`, (stmt) => {
                stmt.bindInteger(1, viewId);
                const list = [];
                while (stmt.nextRow())
                    list.push(stmt.getValueInteger(0));
                return list;
            });
        }
        toGuidRow(id) {
            if (undefined === id)
                return undefined;
            const fedGuid = this.guidMap.getFederationGuidFromId(id);
            return fedGuid ? this.addGuid(fedGuid) : undefined;
        }
        toCompressedGuidRows(ids) {
            const result = new Set();
            for (const id of (typeof ids === "string" ? core_bentley_1.CompressedId64Set.iterable(ids) : ids)) {
                const guidRow = this.toGuidRow(id);
                if (undefined !== guidRow)
                    result.add(core_bentley_1.Id64.fromLocalAndBriefcaseIds(guidRow, 0));
            }
            return core_bentley_1.CompressedId64Set.compressSet(result);
        }
        fromGuidRow(guidRow) {
            return this.guidMap.getIdFromFederationGuid(this.getGuid(guidRow));
        }
        fromGuidRowString(id) {
            return (typeof id !== "string" || !isGuidRowString(id)) ? id : this.fromGuidRow(ViewStore.toRowId(id));
        }
        iterateCompressedGuidRows(guidRows, callback) {
            if (typeof guidRows !== "string")
                return;
            for (const rowId64String of core_bentley_1.CompressedId64Set.iterable(guidRows)) {
                const elId = this.fromGuidRow(core_bentley_1.Id64.getLocalId(rowId64String));
                if (undefined !== elId)
                    callback(elId);
            }
        }
        fromCompressedGuidRows(guidRows) {
            const result = new Set();
            this.iterateCompressedGuidRows(guidRows, (id) => result.add(id));
            return core_bentley_1.CompressedId64Set.compressSet(result);
        }
        toGuidRowMember(base, memberName) {
            const id = base?.[memberName];
            if (id === undefined)
                return;
            if (typeof id === "string") {
                if (isGuidRowString(id)) {
                    // member is already a guid row. Make sure it exists.
                    if (undefined === this.getGuid(ViewStore.toRowId(id)))
                        core_common_1.ViewStoreError.throwError("invalid-member", { message: `${memberName} id does not exist` });
                    return;
                }
                const guidRow = this.toGuidRow(id);
                if (undefined !== guidRow) {
                    base[memberName] = guidRowToString(guidRow);
                    return;
                }
            }
            core_common_1.ViewStoreError.throwError("invalid-member", { message: `invalid ${memberName}: ${id} ` });
        }
        fromGuidRowMember(base, memberName) {
            const id = base?.[memberName];
            if (id === undefined)
                return;
            if (typeof id === "string" && isGuidRowString(id)) {
                const elId = this.fromGuidRow(ViewStore.toRowId(id));
                if (undefined !== elId) {
                    base[memberName] = elId;
                    return;
                }
            }
            core_common_1.ViewStoreError.throwError("invalid-member", { message: `invalid ${memberName}: ${id} ` });
        }
        verifyRowId(table, rowIdString) {
            try {
                const rowId = ViewStore.toRowId(rowIdString);
                this.withSqliteStatement(`SELECT 1 FROM ${table} WHERE Id=?`, (stmt) => {
                    stmt.bindInteger(1, rowId);
                    if (!stmt.nextRow())
                        core_common_1.ViewStoreError.throwError("not-found", { message: `missing: ${rowIdString} ` });
                });
                return rowId;
            }
            catch (err) {
                core_common_1.ViewStoreError.throwError("invalid-value", { message: `invalid Id for ${table}: ${err.message} ` });
            }
        }
        scriptToGuids(script) {
            const scriptProps = [];
            for (const model of script) {
                const modelGuidRow = this.toGuidRow(model.modelId);
                if (modelGuidRow) {
                    model.modelId = guidRowToString(modelGuidRow);
                    scriptProps.push(model);
                    for (const batch of model.elementTimelines)
                        batch.elementIds = this.toCompressedGuidRows(batch.elementIds);
                }
            }
            return scriptProps;
        }
        scriptFromGuids(script, omitElementIds) {
            const scriptProps = [];
            for (const model of script) {
                const modelId = this.fromGuidRow(ViewStore.toRowId(model.modelId));
                if (modelId) {
                    model.modelId = modelId;
                    scriptProps.push(model);
                    for (const batch of model.elementTimelines) {
                        if (undefined !== batch.elementIds)
                            batch.elementIds = omitElementIds ? "" : this.fromCompressedGuidRows(batch.elementIds);
                    }
                }
            }
            return scriptProps;
        }
        async addViewGroup(args) {
            const parentId = args.parentId ? this.findViewGroup(args.parentId) : ViewStore.defaultViewGroupId;
            const json = JSON.stringify({});
            return ViewStore.fromRowId(this.addViewGroupRow({ name: args.name, parentId, json, owner: args.owner }));
        }
        async getViewGroups(args) {
            const parentIdRow = args.parent ? this.findViewGroup(args.parent) : ViewStore.defaultViewGroupId;
            const groups = [];
            this.withSqliteStatement(`SELECT Id,Name FROM ${ViewStore.tableName.viewGroups} WHERE parent=?`, (stmt) => {
                stmt.bindInteger(1, parentIdRow);
                while (stmt.nextRow()) {
                    const id = stmt.getValueInteger(0);
                    if (id !== ViewStore.defaultViewGroupId) // don't include root group
                        groups.push({ id: ViewStore.fromRowId(id), name: stmt.getValueString(1) });
                }
            });
            return groups;
        }
        makeSelectorJson(props, entity) {
            const selector = { ...props }; // shallow copy
            if (selector.query) {
                selector.query = { ...selector.query }; // shallow copy
                selector.query.from = selector.query.from.toLowerCase().replace(".", ":").replace("bis:", "biscore:");
                if (!this.iModel.getJsClass(selector.query.from).is(entity))
                    core_common_1.ViewStoreError.throwError("invalid-value", { message: `query must select from ${entity.classFullName}` });
                if (selector.query.adds)
                    selector.query.adds = this.toCompressedGuidRows(selector.query.adds);
                if (selector.query.removes)
                    selector.query.removes = this.toCompressedGuidRows(selector.query.removes);
            }
            else {
                if (!(selector.ids.length))
                    core_common_1.ViewStoreError.throwError("invalid-value", { message: `Selector must specify at least one ${entity.className}` });
                selector.ids = this.toCompressedGuidRows(selector.ids);
            }
            return JSON.stringify(selector);
        }
        querySelectorValues(json, bindings) {
            if (typeof json !== "object")
                core_common_1.ViewStoreError.throwError("invalid-value", { message: "invalid selector" });
            const props = json;
            if (!props.query) {
                // there's no query, so the ids are the list of elements
                return (typeof props.ids === "string") ? core_bentley_1.CompressedId64Set.decompressArray(this.fromCompressedGuidRows(props.ids)) : [];
            }
            const query = props.query;
            const sql = `SELECT ECInstanceId FROM ${query.only ? "ONLY " : ""}${query.from}${query.where ? ` WHERE ${query.where}` : ""}`;
            const ids = new Set();
            try {
                // eslint-disable-next-line @typescript-eslint/no-deprecated
                this.iModel.withPreparedStatement(sql, (stmt) => {
                    if (bindings)
                        stmt.bindValues(bindings);
                    for (const el of stmt) {
                        if (typeof el.id === "string")
                            ids.add(el.id);
                    }
                });
                this.iterateCompressedGuidRows(props.query.adds, (id) => ids.add(id));
                this.iterateCompressedGuidRows(props.query.removes, (id) => ids.delete(id)); // removes take precedence over adds
            }
            catch (err) {
                core_bentley_1.Logger.logError("ViewStore", `querySelectorValues: ${err.message}`);
            }
            return [...ids];
        }
        async addCategorySelector(args) {
            const json = this.makeSelectorJson(args.selector, Category_1.Category);
            return ViewStore.fromRowId(this.addCategorySelectorRow({ name: args.name, owner: args.owner, json }));
        }
        async updateCategorySelector(args) {
            const rowId = this.getRowId(ViewStore.tableName.categorySelectors, args);
            const json = this.makeSelectorJson(args.selector, Category_1.Category);
            return this.updateCategorySelectorJson(rowId, json);
        }
        getRowId(table, arg) {
            return undefined !== arg.name ? this.findByName(table, arg.name) : ViewStore.toRowId(arg.id);
        }
        /** @internal */
        getCategorySelectorSync(args) {
            const rowId = this.getRowId(ViewStore.tableName.categorySelectors, args);
            const row = this.getCategorySelectorRow(rowId);
            if (undefined === row)
                core_common_1.ViewStoreError.throwError("not-found", { message: "CategorySelector not found" });
            const props = blankElementProps({}, "BisCore:CategorySelector", rowId, row.name);
            props.categories = this.querySelectorValues(JSON.parse(row.json), args.bindings);
            return props;
        }
        async getCategorySelector(args) {
            return this.getCategorySelectorSync(args);
        }
        async addModelSelector(args) {
            const json = this.makeSelectorJson(args.selector, Model_1.Model);
            return ViewStore.fromRowId(this.addModelSelectorRow({ name: args.name, owner: args.owner, json }));
        }
        async updateModelSelector(args) {
            const rowId = this.getRowId(ViewStore.tableName.modelSelectors, args);
            const json = this.makeSelectorJson(args.selector, Model_1.Model);
            return this.updateModelSelectorJson(rowId, json);
        }
        getModelSelectorSync(args) {
            const rowId = this.getRowId(ViewStore.tableName.modelSelectors, args);
            const row = this.getModelSelectorRow(rowId);
            if (undefined === row)
                core_common_1.ViewStoreError.throwError("not-found", { message: "ModelSelector not found" });
            const props = blankElementProps({}, "BisCore:ModelSelector", rowId, row?.name);
            props.models = this.querySelectorValues(JSON.parse(row.json), args.bindings);
            return props;
        }
        async getModelSelector(args) {
            return this.getModelSelectorSync(args);
        }
        makeTimelineJson(timeline) {
            timeline = cloneProps(timeline);
            if (!Array.isArray(timeline))
                core_common_1.ViewStoreError.throwError("not-found", { message: "Timeline has no entries" });
            return JSON.stringify(this.scriptToGuids(timeline));
        }
        async addTimeline(args) {
            const json = this.makeTimelineJson(args.timeline);
            return ViewStore.fromRowId(this.addTimelineRow({ name: args.name, owner: args.owner, json }));
        }
        async updateTimeline(args) {
            const rowId = this.getRowId(ViewStore.tableName.timelines, args);
            const json = this.makeTimelineJson(args.timeline);
            return this.updateTimelineJson(rowId, json);
        }
        getTimelineSync(args) {
            const rowId = this.getRowId(ViewStore.tableName.timelines, args);
            const row = this.getTimelineRow(rowId);
            if (undefined === row)
                core_common_1.ViewStoreError.throwError("not-found", { message: "Timeline not found" });
            const props = blankElementProps({}, "BisCore:RenderTimeline", rowId, row?.name);
            props.script = JSON.stringify(this.scriptFromGuids(JSON.parse(row.json), false));
            return props;
        }
        async getTimeline(args) {
            return this.getTimelineSync(args);
        }
        /** make a JSON string for a DisplayStyle */
        makeDisplayStyleJson(args) {
            const settings = cloneProps(args.settings); // don't modify input
            if (settings.subCategoryOvr) {
                const outOvr = [];
                for (const ovr of settings.subCategoryOvr) {
                    const subCategoryGuidRow = this.toGuidRow(ovr.subCategory);
                    if (subCategoryGuidRow) {
                        ovr.subCategory = guidRowToString(subCategoryGuidRow);
                        outOvr.push(ovr);
                    }
                }
                settings.subCategoryOvr = outOvr;
            }
            if (settings.excludedElements)
                settings.excludedElements = this.toCompressedGuidRows(settings.excludedElements);
            const settings3d = settings;
            if (settings3d.planProjections) {
                const planProjections = {};
                for (const entry of Object.entries(settings3d.planProjections)) {
                    const modelGuidRow = this.toGuidRow(entry[0]);
                    if (modelGuidRow)
                        planProjections[guidRowToString(modelGuidRow)] = entry[1];
                }
                settings3d.planProjections = planProjections;
            }
            if (settings.renderTimeline) {
                if (!core_common_1.ViewStoreRpc.isViewStoreId(settings.renderTimeline))
                    this.toGuidRowMember(settings, "renderTimeline");
                delete settings.scheduleScript;
            }
            else if (settings.scheduleScript) {
                const scriptProps = this.scriptToGuids(settings.scheduleScript);
                if (scriptProps.length > 0)
                    settings.scheduleScript = scriptProps;
            }
            return JSON.stringify({ settings, className: args.className });
        }
        async addDisplayStyle(args) {
            const json = this.makeDisplayStyleJson(args);
            return ViewStore.fromRowId(this.addDisplayStyleRow({ name: args.name, owner: args.owner, json }));
        }
        async updateDisplayStyle(args) {
            const rowId = this.getRowId(ViewStore.tableName.displayStyles, args);
            const json = this.makeDisplayStyleJson(args);
            return this.updateDisplayStyleJson(rowId, json);
        }
        getDisplayStyleSync(args) {
            const rowId = this.getRowId(ViewStore.tableName.displayStyles, args);
            const row = this.getDisplayStyleRow(rowId);
            if (undefined === row)
                core_common_1.ViewStoreError.throwError("not-found", { message: "DisplayStyle not found" });
            const val = JSON.parse(row.json);
            const props = blankElementProps({}, val.className, rowId, row.name);
            props.jsonProperties = { styles: val.settings };
            const settings = val.settings;
            if (settings.subCategoryOvr) {
                const subCatOvr = [];
                for (const ovr of settings.subCategoryOvr) {
                    const id = this.fromGuidRowString(ovr.subCategory);
                    if (undefined !== id) {
                        ovr.subCategory = id;
                        subCatOvr.push(ovr);
                    }
                }
                settings.subCategoryOvr = subCatOvr;
            }
            if (settings.excludedElements)
                settings.excludedElements = this.fromCompressedGuidRows(settings.excludedElements);
            const settings3d = settings;
            if (settings3d.planProjections) {
                const planProjections = {};
                for (const entry of Object.entries(settings3d.planProjections)) {
                    const modelId = this.fromGuidRowString(entry[0]);
                    if (undefined !== modelId)
                        planProjections[modelId] = entry[1];
                }
                settings3d.planProjections = planProjections;
            }
            if (isGuidRowString(settings.renderTimeline))
                settings.renderTimeline = this.fromGuidRowString(settings.renderTimeline);
            if (undefined !== settings.renderTimeline) {
                delete settings.scheduleScript;
            }
            else if (settings.scheduleScript) {
                delete settings.renderTimeline;
                settings.scheduleScript = this.scriptFromGuids(settings.scheduleScript, args.opts?.omitScheduleScriptElementIds === true);
            }
            return props;
        }
        async getDisplayStyle(args) {
            return this.getDisplayStyleSync(args);
        }
        makeViewDefinitionProps(viewDefinition) {
            const viewDef = cloneProps(viewDefinition); // don't modify input
            this.verifyRowId(ViewStore.tableName.categorySelectors, viewDef.categorySelectorId);
            this.verifyRowId(ViewStore.tableName.displayStyles, viewDef.displayStyleId);
            if (viewDef.modelSelectorId)
                this.verifyRowId(ViewStore.tableName.modelSelectors, viewDef.modelSelectorId);
            this.toGuidRowMember(viewDef, "baseModelId");
            this.toGuidRowMember(viewDef.jsonProperties?.viewDetails, "acs");
            const props = viewDef;
            delete props.id;
            delete props.federationGuid;
            delete props.parent;
            delete props.code;
            delete props.model;
            return viewDef;
        }
        addViewDefinition(args) {
            const name = args.viewDefinition.code.value;
            if (name === undefined)
                core_common_1.ViewStoreError.throwError("not-found", { message: "ViewDefinition must have a name" });
            const groupId = args.group ? this.findViewGroup(args.group) : ViewStore.defaultViewGroupId;
            const maybeRow = (rowString) => rowString ? ViewStore.toRowId(rowString) : undefined;
            const viewDef = this.makeViewDefinitionProps(args.viewDefinition);
            try {
                return this.addViewRow({
                    name,
                    className: viewDef.classFullName,
                    owner: args.owner,
                    groupId,
                    isPrivate: args.isPrivate,
                    json: JSON.stringify(viewDef),
                    modelSel: maybeRow(viewDef.modelSelectorId),
                    categorySel: ViewStore.toRowId(viewDef.categorySelectorId),
                    displayStyle: ViewStore.toRowId(viewDef.displayStyleId),
                });
            }
            catch (e) {
                const err = e;
                if (err.errorId === "DuplicateValue")
                    err.message = `View "${name}" already exists`;
                throw e;
            }
        }
        async updateViewDefinition(args) {
            const maybeRow = (rowString) => rowString ? ViewStore.toRowId(rowString) : undefined;
            const viewDef = this.makeViewDefinitionProps(args.viewDefinition);
            this.withSqliteStatement(`UPDATE ${ViewStore.tableName.views} SET json=?,modelSel=?,categorySel=?,displayStyle=? WHERE Id=?`, (stmt) => {
                stmt.bindString(1, JSON.stringify(viewDef));
                stmt.maybeBindInteger(2, maybeRow(viewDef.modelSelectorId));
                stmt.bindInteger(3, ViewStore.toRowId(viewDef.categorySelectorId));
                stmt.bindInteger(4, ViewStore.toRowId(viewDef.displayStyleId));
                stmt.bindInteger(5, ViewStore.toRowId(args.viewId));
                stmt.stepForWrite();
            });
        }
        getViewDefinitionSync(args) {
            const viewId = ViewStore.toRowId(args.viewId);
            const row = this.getViewRow(viewId);
            if (undefined === row)
                core_common_1.ViewStoreError.throwError("not-found", { message: "View not found" });
            const props = blankElementProps(JSON.parse(row.json), row.className, viewId, row.name);
            this.fromGuidRowMember(props, "baseModelId");
            this.fromGuidRowMember(props.jsonProperties?.viewDetails, "acs");
            return props;
        }
        async getViewDefinition(args) {
            return this.getViewDefinitionSync(args);
        }
        async addOrReplaceThumbnail(args) {
            const viewRow = this.getViewRow(ViewStore.toRowId(args.viewId));
            if (viewRow === undefined)
                core_common_1.ViewStoreError.throwError("not-found", { message: "View not found" });
            const format = { format: args.thumbnail.format, height: args.thumbnail.height, width: args.thumbnail.width };
            this.addOrReplaceThumbnailRow({ data: args.thumbnail.image, viewId: ViewStore.toRowId(args.viewId), format, owner: args.owner });
        }
        getThumbnailSync(args) {
            const row = this.getThumbnailRow(ViewStore.toRowId(args.viewId));
            return row ? { image: row.data, format: row.format.format, height: row.format.height, width: row.format.width } : undefined;
        }
        async getThumbnail(args) {
            return this.getThumbnailSync(args);
        }
        /** find a group with the specified name using path syntax (e.g., "group1/design/issues"). If the group does not exist, return 0.
         * If groupName starts with "@", then it is considered to be a group id and this function verifies that it exists and throws if it does not.
         */
        findViewGroup(groupName) {
            // if it starts with "@", then it is a group id
            if (groupName.startsWith("@"))
                return this.verifyRowId(ViewStore.tableName.viewGroups, groupName);
            // split the name into parts using "/" as the separator
            const names = groupName.split("/");
            let groupId = 1; // start at root group
            for (const name of names) {
                if (name.length !== 0) {
                    groupId = this.getViewGroupByName(name, groupId);
                    if (groupId === 0)
                        return 0;
                }
            }
            return groupId;
        }
        /**
         * find a view by name using path syntax (e.g., "group1/design/issues/issue113"). If the view does not exist, return 0.
         * If a groupId is specified, then the view is searched for in that group and the name should not contain a path.
         * @internal
         */
        findViewIdByName(arg) {
            let name = arg.name;
            let groupId = arg.groupId;
            if (groupId === undefined) {
                // find last "/" in name
                const slash = name.lastIndexOf("/");
                if (slash !== -1) {
                    groupId = this.findViewGroup(name.slice(0, slash));
                    name = name.slice(slash + 1);
                }
            }
            return this.withSqliteStatement(`SELECT Id FROM ${ViewStore.tableName.views} WHERE name=? AND groupId=?`, (stmt) => {
                stmt.bindString(1, name);
                stmt.bindInteger(2, maybeToRowId(groupId) ?? ViewStore.defaultViewGroupId);
                return !stmt.nextRow() ? 0 : stmt.getValueInteger(0);
            });
        }
        getViewByNameSync(arg) {
            const id = this.findViewIdByName(arg);
            return id ? this.getViewInfoSync(id) : undefined;
        }
        async getViewByName(arg) {
            return this.getViewByNameSync(arg);
        }
        async getViewGroupInfo(args) {
            const groupId = args.groupId ? this.findViewGroup(args.groupId) : ViewStore.defaultViewGroupId;
            const groupRow = groupId ? this.getViewGroup(groupId) : undefined;
            if (groupRow === undefined)
                return undefined;
            const info = {
                id: ViewStore.fromRowId(groupId),
                name: groupRow.name,
                defaultView: groupRow.defaultViewId ? ViewStore.fromRowId(groupRow.defaultViewId) : undefined,
                parent: ViewStore.fromRowId(groupRow.parentId),
            };
            return info;
        }
        async changeDefaultViewId(args) {
            const groupId = args.group ? this.findViewGroup(args.group) : ViewStore.defaultViewGroupId;
            const viewRow = this.getViewRow(ViewStore.toRowId(args.defaultView));
            if (viewRow === undefined)
                core_common_1.ViewStoreError.throwError("not-found", { message: "View not found" });
            if (viewRow.groupId !== groupId)
                core_common_1.ViewStoreError.throwError("not-found", { message: "View is not in the specified group" });
            const groupRow = this.getViewGroup(groupId);
            if (groupRow === undefined)
                core_common_1.ViewStoreError.throwError("not-found", { message: "View group not found" });
            this.withSqliteStatement(`UPDATE ${ViewStore.tableName.viewGroups} SET defaultViewId=? WHERE Id=?`, (stmt) => {
                stmt.bindInteger(1, groupId);
                stmt.bindInteger(2, ViewStore.toRowId(args.defaultView));
                stmt.stepForWrite();
            });
        }
        /** get the array of tags for the specified view. Returns undefined if the view has no tags. */
        getTagsForView(viewId) {
            const tags = [];
            this.withPreparedSqliteStatement(`SELECT t.name FROM ${ViewStore.tableName.tags} t JOIN ${ViewStore.tableName.taggedViews} v ON t.Id = v.tagId WHERE v.viewId=?`, (stmt) => {
                stmt.bindInteger(1, ViewStore.toRowId(viewId));
                while (stmt.nextRow())
                    tags.push(stmt.getValueString(0));
            });
            return tags.length === 0 ? undefined : tags;
        }
        iterateViewQuery(queryParams, callback) {
            const groupId = queryParams.group ? this.findViewGroup(queryParams.group) : ViewStore.defaultViewGroupId;
            let sql = `SELECT Id,className,name,owner,private FROM ${ViewStore.tableName.views} WHERE groupId=? ${queryParams.owner ? " AND (owner=@owner OR private!=1)" : " AND private!=1"}`;
            if (queryParams.classNames)
                sql += ` AND className IN(${queryParams.classNames.map((className) => `'${className}'`).join(",")})`;
            if (queryParams.nameSearch)
                sql += ` AND name ${queryParams.nameCompare ?? "="} @name`;
            if (queryParams.tags)
                sql += ` AND Id IN(SELECT viewId FROM ${ViewStore.tableName.taggedViews} WHERE tagId IN(SELECT Id FROM ${ViewStore.tableName.tags} WHERE name IN(${queryParams.tags.map((tag) => `'${tag}'`).join(",")})))`;
            sql += " ORDER BY name";
            if (queryParams.limit)
                sql += ` LIMIT ${queryParams.limit} `;
            if (queryParams.offset)
                sql += ` OFFSET ${queryParams.offset} `;
            this.withSqliteStatement(sql, (stmt) => {
                stmt.bindInteger(1, groupId);
                if (queryParams.nameSearch)
                    stmt.bindString("@name", queryParams.nameSearch);
                if (queryParams.owner)
                    stmt.bindString("@owner", queryParams.owner);
                while (stmt.nextRow())
                    callback(stmt.getValueInteger(0));
            });
        }
        queryViewsSync(queryParams) {
            const entries = [];
            this.iterateViewQuery(queryParams, (rowId) => {
                const view = this.getViewInfoSync(rowId);
                if (view !== undefined)
                    entries.push(view);
            });
            return entries;
        }
        async queryViews(queryParams) {
            return this.queryViewsSync(queryParams);
        }
        async addTagsToView(args) {
            const viewId = ViewStore.toRowId(args.viewId);
            for (const tag of args.tags) {
                let tagId = this.findTagByName(tag);
                if (tagId === 0)
                    tagId = this.addTag({ name: tag, owner: args.owner, json: "{}" });
                this.addTagToView({ viewId, tagId });
            }
        }
        async removeTagFromView(args) {
            const viewId = ViewStore.toRowId(args.viewId);
            const tagId = this.findTagByName(args.tag);
            if (tagId !== 0)
                this.deleteViewTag({ viewId, tagId });
        }
        async addView(args) {
            const owner = args.owner;
            if (core_common_1.ViewStoreRpc.isViewStoreId(args.viewDefinition.categorySelectorId)) {
                this.verifyRowId(ViewStore.tableName.categorySelectors, args.viewDefinition.categorySelectorId);
            }
            else {
                if (args.categorySelectorProps === undefined)
                    core_common_1.ViewStoreError.throwError("not-found", { message: "Must supply categorySelector" });
                args.viewDefinition.categorySelectorId = await this.addCategorySelector({ selector: { ids: args.categorySelectorProps.categories }, owner });
            }
            const spatialDef = args.viewDefinition;
            if (core_common_1.ViewStoreRpc.isViewStoreId(spatialDef.modelSelectorId)) {
                this.verifyRowId(ViewStore.tableName.modelSelectors, spatialDef.modelSelectorId);
            }
            else if (args.modelSelectorProps) {
                spatialDef.modelSelectorId = await this.addModelSelector({ selector: { ids: args.modelSelectorProps.models }, owner });
            }
            else if (args.viewDefinition.classFullName === "BisCore:SpatialViewDefinition") {
                core_common_1.ViewStoreError.throwError("not-found", { message: "Must supply modelSelector for Spatial views" });
            }
            if (core_common_1.ViewStoreRpc.isViewStoreId(spatialDef.displayStyleId)) {
                this.verifyRowId(ViewStore.tableName.displayStyles, spatialDef.displayStyleId);
            }
            else {
                if (args.displayStyleProps === undefined || args.displayStyleProps.jsonProperties?.styles === undefined)
                    core_common_1.ViewStoreError.throwError("not-found", { message: "Must supply valid displayStyle" });
                spatialDef.displayStyleId = await this.addDisplayStyle({ className: args.displayStyleProps.classFullName, settings: args.displayStyleProps.jsonProperties.styles, owner });
            }
            const viewId = this.addViewDefinition(args);
            if (args.tags)
                await this.addTagsToView({ viewId, tags: args.tags, owner });
            if (args.thumbnail)
                await this.addOrReplaceThumbnail({ viewId, thumbnail: args.thumbnail, owner });
            return ViewStore.fromRowId(viewId);
        }
        async deleteView(arg) {
            const rowId = ViewStore.toRowId(arg.viewId);
            const viewRow = this.getViewRow(rowId);
            if (viewRow === undefined)
                core_common_1.ViewStoreError.throwError("not-found", { message: "View not found" });
            this.deleteViewRow(rowId);
            const hasName = (table, nameRow) => {
                const name = this.withSqliteStatement(`SELECT name FROM ${table} WHERE Id=?`, (stmt) => {
                    stmt.bindInteger(1, nameRow);
                    return stmt.nextRow() ? stmt.getValueString(0) : undefined;
                });
                return name !== undefined && name?.length > 0;
            };
            const tryDelete = (table, id) => {
                if (id !== undefined && !hasName(table, id)) { // only delete if the row has no name
                    try {
                        this.deleteFromTable(table, id);
                    }
                    catch {
                        // ignore constraint error if the row is still referenced by another view
                    }
                }
            };
            // delete any selectors or display styles that are no longer referenced
            tryDelete(ViewStore.tableName.categorySelectors, viewRow.categorySel);
            tryDelete(ViewStore.tableName.modelSelectors, viewRow.modelSel);
            tryDelete(ViewStore.tableName.displayStyles, viewRow.displayStyle);
        }
    }
    ViewStore.ViewDb = ViewDb;
    const viewDbName = "ViewDb";
    /** Provides access to a cloud-based `ViewDb` */
    class CloudAccess extends CloudSqlite_1.CloudSqlite.DbAccess {
        constructor(props) {
            super({ dbType: ViewDb, props, dbName: viewDbName });
        }
        /**
         * Initialize a cloud container for use as a ViewDb. This method is called by [[createNewContainer]].
         * It is only necessary to convert an existing container to a ViewStore container.
         * @note this deletes any existing content in the container.
         * @internal
         */
        static async initializeDb(props) {
            return super._initializeDb({ props, dbType: ViewDb, dbName: viewDbName });
        }
        /** Create and initialize a new BlobContainer to hold a ViewStore
         * @note the current user must have administrator rights to create containers.
         */
        static async createNewContainer(args) {
            const props = await this.createBlobContainer({ scope: args.scope, metadata: { ...args.metadata, containerType: "viewstore" } });
            await this.initializeDb(props);
            return props;
        }
    }
    ViewStore.CloudAccess = CloudAccess;
})(ViewStore || (exports.ViewStore = ViewStore = {}));
//# sourceMappingURL=ViewStore.js.map