() => {
    it('should trim lines passed.', () => {
        const result = preprocessLines([
            '    foo    ',
            '\t\tbar\t\t',
            '\r\rbatz\r\r',
            '\t\rhello\t\r',
            '\t\t    \r\t  world\t\r  \r\t  '
        ]);
        expect(result).to.be.deep.equal([
            'foo',
            'bar',
            'batz',
            'hello',
            'world'
        ]);
    });

    it('should discard comments.', () => {
        const result = preprocessLines([
            'foo',
            '# hello world'
        ]);
        expect(result).to.be.deep.equal([
            'foo'
        ]);
    });
}


() => {
        const result = preprocessLines([
            '    foo    ',
            '\t\tbar\t\t',
            '\r\rbatz\r\r',
            '\t\rhello\t\r',
            '\t\t    \r\t  world\t\r  \r\t  '
        ]);
        expect(result).to.be.deep.equal([
            'foo',
            'bar',
            'batz',
            'hello',
            'world'
        ]);
    }


() => {
        const result = preprocessLines([
            'foo',
            '# hello world'
        ]);
        expect(result).to.be.deep.equal([
            'foo'
        ]);
    }


() => this.settings.getServers(definition.private)


(x) => ({
                pattern: new RegExp(x.pattern),
                key: x.key,
                value: x.value
            })


(x) => x.pattern.test(file.filePath)


/**
 * Data that is provided to the templates to generate a link.
 */
interface UrlData {
    /**
     * The base URL of the server.
     */
    readonly base: string;

    /**
     * The path to the repository on the server.
     */
    readonly repository: string;

    /**
     * The type of link being generated.
     */
    readonly type: 'branch' | 'commit';

    /**
     * The Git ref to generate the link to. This will be a branch name or commit hash depending on the link type.
     */
    readonly ref: string;

    /**
     * The hash of the current commit.
     */
    readonly commit: string;

    /**
     * The file to generate the link for.
     */
    readonly file: string;

    /**
     * The one-based line number of the start of the selection, if a selection is being included in the link.
     */
    readonly startLine?: number;

    /**
     * The one-based column number of the start of the selection, if a selection is being included in the link.
     */
    readonly startColumn?: number;

    /**
     * The one-based line number of the end of the selection, if a selection is being included in the link.
     */
    readonly endLine?: number;

    /**
     * The one-based column number of the end of the selection, if a selection is being included in the link.
     */
    readonly endColumn?: number;

    /**
     * Additional handler-specific settings.
     */
    [settingsKey: string]: unknown;
}


interface FileData {
    readonly match: RegExpMatchArray;

    readonly http?: string;

    readonly ssh?: string;
}


/**
 * The parsed query modification for making modifications to the URL's query string.
 */
export interface ParsedQueryModification {
    /**
     * The regular expression to match against the file name.
     */
    readonly pattern: RegExp;

    /**
     * The key to add to the query string when the pattern matches.
     */
    readonly key: string;

    /**
     * The value to add to the query string when the pattern matches.
     */
    readonly value: string;
}


/**
 * The parsed settings for getting file information from a URL.
 */
interface ParsedReverseSettings {
    /**
     * The regular expression pattern to match against the URL.
     */
    readonly pattern: RegExp;

    /**
     * The template to produce a file name.
     */
    readonly file: ParsedTemplate;

    /**
     * The templates that provide the base remote URLs.
     */
    readonly server: ParsedTemplates<ReverseServerSettings>;

    /**
     * The templates that provide the selection range.
     */
    readonly selection: ParsedTemplates<ReverseSelectionSettings>;
}


/**
 * Parsed templates.
 */
type ParsedTemplates<T> = { [K in keyof T]: ParsedTemplate };


/**
     * Determines whether this handler can generate links for the given remote URL.
     *
     * @param remoteUrl The remote URL to check.
     * @returns True if this handler handles the given remote URL; otherwise, false.
     */
public isMatch(remoteUrl: string): boolean {
        return this.server.match(normalizeUrl(remoteUrl)) !== undefined;
    }


/**
     * Creates a link for the specified file.
     *
     * @param repository The repository that the file is in.
     * @param file The details of the file.
     * @param options The options for creating the link.
     * @returns The URL.
     */
public async createUrl(
        repository: RepositoryWithRemote,
        file: FileInfo,
        options: LinkOptions
    ): Promise<string> {
        let remote: string;
        let address: StaticServer;
        let type: LinkType;
        let url: string;
        let data: UrlData;

        // If a link type wasn't specified, then we'll use
        // the default type that's defined in the settings.
        type = options.type ?? this.settings.getDefaultLinkType();

        // Adjust the remote URL so that it's in a
        // standard format that we can manipulate.
        remote = normalizeUrl(repository.remote.url);

        address = this.getAddress(remote);

        data = {
            base: address.http,
            repository: this.getRepositoryPath(remote, address),
            ref: await this.getRef(type, repository),
            commit: await this.getRef('commit', repository),
            file: await this.getRelativePath(repository.root, file.filePath),
            type: type === 'commit' ? 'commit' : 'branch',
            ...file.selection
        };

        if (this.settingsKeys) {
            for (let key of this.settingsKeys) {
                data[key] = this.settings.getHandlerSetting(key);
            }
        }

        url = this.urlTemplate.render(data);

        if (file.selection) {
            url += this.selectionTemplate.render(data);
        }

        url = this.applyModifications(
            url,
            this.queryModifications.filter((x) => x.pattern.test(file.filePath))
        );

        return url;
    }


/**
     * Applies the given query string modifications to the URL.
     *
     * @param url The URL to modify.
     * @param modifications The modifications to apply.
     * @returns The modified URL.
     */
private applyModifications(url: string, modifications: ParsedQueryModification[]): string {
        if (modifications.length > 0) {
            let u: URL;

            u = new URL(url);

            for (let modification of modifications) {
                u.searchParams.append(modification.key, modification.value);
            }

            url = u.toString();
        }

        return url;
    }


/**
     * Gets the server address for the given remote URL.
     *
     * @param remote The remote URL.
     * @returns The server address.
     */
private getAddress(remote: string): StaticServer {
        let address: StaticServer | undefined;

        address = this.server.match(remote);

        if (!address) {
            throw new Error('Could not find a matching address.');
        }

        return this.normalizeServerUrls(address);
    }


/**
     * Normalizes the server URLs to make them consistent for use in the templates.
     *
     * @param address The server address to normalize.
     * @returns The normalized server URLs.
     */
private normalizeServerUrls(address: StaticServer): StaticServer {
        let http: string;
        let ssh: string | undefined;

        http = normalizeUrl(address.http);
        ssh = address.ssh ? normalizeUrl(address.ssh) : undefined;

        return { http, ssh };
    }


/**
     * Gets the path to the repository at the given server address.
     *
     * @param remoteUrl The remote URL of the repository.
     * @param address The address of the server.
     * @returns The path to the repository.
     */
private getRepositoryPath(remoteUrl: string, address: StaticServer): string {
        let repositoryPath: string;

        // Remove the server's address from the start of the URL.
        // Note that the remote URL and both URLs in the server
        // address have been normalized by this point.
        if (remoteUrl.startsWith(address.http)) {
            repositoryPath = remoteUrl.substring(address.http.length);
        } else {
            repositoryPath = address.ssh ? remoteUrl.substring(address.ssh.length) : '';
        }

        // The server address we matched against may not have ended
        // with a slash (for HTTPS paths) or a colon (for SSH paths),
        // which means the path might start with that. Trim that off now.
        if (repositoryPath.length > 0) {
            if (repositoryPath[0] === '/' || repositoryPath[0] === ':') {
                repositoryPath = repositoryPath.substring(1);
            }
        }

        if (repositoryPath.endsWith('.git')) {
            repositoryPath = repositoryPath.substring(0, repositoryPath.length - 4);
        }

        return repositoryPath;
    }


/**
     * Gets the ref to use when creating the link.
     *
     * @param type The type of ref to get.
     * @param repository The repository.
     * @returns The ref to use.
     */
private async getRef(type: LinkType, repository: RepositoryWithRemote): Promise<string> {
        switch (type) {
            case 'branch':
                return (
                    await git(
                        repository.root,
                        'rev-parse',
                        this.getRevParseOutputArgument(),
                        'HEAD'
                    )
                ).trim();
            case 'commit':
                return (await git(repository.root, 'rev-parse', 'HEAD')).trim();

            default:
                // Use the default branch if one is specified in the settings; otherwise find the
                // name of the default branch by getting the name of the "remote_name/HEAD" ref.
                return (
                    this.settings.getDefaultBranch() ||
                    (await this.getDefaultRemoteBranch(repository))
                );
        }
    }


/**
     * Gets the name of the default branch in the remote.
     *
     * @param repository The repository.
     * @returns The name of the default branch.
     */
private async getDefaultRemoteBranch(repository: RepositoryWithRemote): Promise<string> {
        let branch: string;

        try {
            branch = (
                await git(
                    repository.root,
                    'rev-parse',
                    this.getRevParseOutputArgument(),
                    `${repository.remote.name}/HEAD`
                )
            ).trim();
        } catch (ex) {
            throw new NoRemoteHeadError(getErrorMessage(ex));
        }

        switch (this.definition.branchRef) {
            case 'abbreviated':
                // The branch name will be "remote_name/branch_name",
                // but we only want the "branch_name" part.
                return branch.slice(repository.remote.name.length + 1);

            case 'symbolic':
                // The branch name will be "refs/remotes/remote_name/branch_name",
                // but we want it to be "refs/heads/branch_name".
                return branch.replace(
                    new RegExp(`^refs\\/remotes\\/${this.escapeRegExp(repository.remote.name)}\\/`),
                    'refs/heads/'
                );

            default:
                return branch;
        }
    }


/**
     * Escapes a value that can then be used in a Regular Expression.
     *
     * @param value The value to escape.
     * @returns The escaped value.
     */
private escapeRegExp(value: string): string {
        return value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
    }


/**
     * Gets the argument to use with `git rev-parse` to specify the output.
     *
     * @returns The argument to use.
     */
private getRevParseOutputArgument(): string {
        switch (this.definition.branchRef) {
            case 'symbolic':
                return '--symbolic-full-name';

            default:
                return '--abbrev-ref';
        }
    }


/**
     * Gets the relative path from the specified directory to the specified file.
     *
     * @param from The directory to get the relative path from.
     * @param to The file to get the relative path to.
     * @returns The relative path of the file.
     */
private async getRelativePath(from: string, to: string): Promise<string> {
        // If the file is a symbolic link, or is under a directory that's a
        // symbolic link, then we want to resolve the path to the real file
        // because the sybmolic link won't be in the Git repository.
        if (await this.isSymbolicLink(to, from)) {
            try {
                to = await fs.realpath(to);

                // Getting the real path of the file resolves all symbolic links,
                // which means if the repository is also under a symbolic link,
                // then the new file path may no longer be under the root directory.
                // We can fix this by also getting the real path of the root directory.
                from = await fs.realpath(from);
            } catch (ex) {
                // Provide a nicer error message that
                // explains what we were trying to do.
                throw new Error(
                    `Unable to resolve the symbolic link '${to}' to a real path.\n${getErrorMessage(
                        ex
                    )}`
                );
            }
        }

        // Get the relative path, then normalize
        // the separators to forward slashes.
        return path.relative(from, to).replace(/\\/g, '/');
    }


/**
     * Determines whether the specified file is a symbolic link.
     *
     * @param filePath The path of the file to check.
     * @param rootDirectory The path to the root of the repository.
     * @returns True if the specified file is a symbolic link within the repository; otherwise, false.
     */
private async isSymbolicLink(filePath: string, rootDirectory: string): Promise<boolean> {
        // Check if the file is a symbolic link. If it isn't, then walk up
        // the tree to see if an ancestor directory is a symbolic link. Keep
        // stepping up until we reach the root directory of the repository,
        // because we only need to resolve symbolic links within the repository.
        // If the entire repository is under a symbolic link, then we don't
        // want to resolve paths to somewhere outside the repository.
        while (filePath !== rootDirectory) {
            let stats: Stats;
            let parent: string;

            try {
                stats = await fs.lstat(filePath);
            } catch (ex) {
                // Assume that the path isn't a symbolic link.
                return false;
            }

            if (stats.isSymbolicLink()) {
                return true;
            }

            parent = path.dirname(filePath);

            if (parent === filePath) {
                // We can't go any higher, so the
                // path cannot be a symbolic link.
                return false;
            }

            filePath = parent;
        }

        return false;
    }


/**
     * Gets information about the given URL.
     *
     * @param url The URL to get the information from.
     * @param strict Whether to require the URL to match the server address of the handler.
     * @returns The URL information, or `undefined` if the information could not be determined.
     */
public getUrlInfo(url: string, strict: boolean): UrlInfo | undefined {
        let address: StaticServer | undefined;
        let match: RegExpExecArray | null;

        // See if the URL matches the server address for the handler.
        address = this.server.match(url);

        // If we are performing a strict match, then the
        // URL must match to this handler's server.
        if (strict && !address) {
            return undefined;
        }

        if (address) {
            address = this.normalizeServerUrls(address);
        }

        match = this.reverse.pattern.exec(url);

        if (match) {
            let data: FileData;
            let file: string;
            let server: StaticServer;
            let selection: Partial<SelectedRange>;

            data = {
                match,
                http: address?.http,
                ssh: address?.ssh
            };

            file = this.reverse.file.render(data);

            server = {
                http: this.reverse.server.http.render(data),
                ssh: this.reverse.server.ssh.render(data)
            };

            selection = {
                startLine: this.tryParseNumber(this.reverse.selection.startLine.render(data)),
                endLine: this.tryParseNumber(this.reverse.selection.endLine?.render(data)),
                startColumn: this.tryParseNumber(this.reverse.selection.startColumn?.render(data)),
                endColumn: this.tryParseNumber(this.reverse.selection.endColumn?.render(data))
            };

            return { filePath: file, server, selection };
        }

        return undefined;
    }


/**
     * Attempts to parse the given value to a number.
     *
     * @param value The value to parse.
     * @returns The value as a number, or `undefined` if the value could not be parsed.
     */
private tryParseNumber(value: string | undefined): number | undefined {
        if (value !== undefined) {
            let num: number;

            num = parseInt(value, 10);

            if (!isNaN(num)) {
                return num;
            }
        }

        return undefined;
    }


aPath => path.join(assetPath, aPath)


item => path.basename(item.path) !== '.DS_Store'


img => (img.prefix === prefix && img.suffix === suffix)


interface AlloyAutoCompleteRule {
	regExp: RegExp;
	getCompletions: (range?: Range) => Promise<CompletionItem[]>|CompletionItem[];
	requireRange?: boolean;
	rangeRegex?: RegExp;
}


async getCompletions () {
		const cfgPath = path.join(utils.getAlloyRootPath(), 'config.json');
		const completions: CompletionItem[] = [];
		if (utils.fileExists(cfgPath)) {
			const document = await workspace.openTextDocument(cfgPath);
			const cfgObj = JSON.parse(document.getText());
			const deconstructedConfig = {};

			for (const [ key, value ] of Object.entries(cfgObj)) {
				if (key === 'global' || key.startsWith('os:') || key.startsWith('env:')) {
					// Ignore and traverse
					Object.assign(deconstructedConfig, value);
				}
			}

			const allKeys = utils.getAllKeys(deconstructedConfig);
			for (const key of allKeys) {
				completions.push({
					label: key,
					kind: CompletionItemKind.Value
				});
			}
		}
		return completions;
	}


async getCompletions () {
		const defaultLang = ExtensionContainer.config.project.defaultI18nLanguage;
		const i18nPath = utils.getI18nPath();
		const completions: CompletionItem[] = [];
		if (utils.directoryExists(i18nPath)) {
			const i18nStringPath = path.join(i18nPath, defaultLang, 'strings.xml');
			if (utils.fileExists(i18nStringPath)) {
				const document = await workspace.openTextDocument(i18nStringPath);
				const result = await parseXmlString(document.getText()) as { resources: { string: { $: { name: string }; _: string }[] } };
				if (result && result.resources && result.resources.string) {
					for (const value of result.resources.string) {
						completions.push({
							label: value.$.name,
							kind: CompletionItemKind.Reference,
							detail: value._
						});
					}
				}
			}
		}
		return completions;
	}


getCompletions (range) {
		const alloyRootPath = utils.getAlloyRootPath();
		const assetPath = path.join(alloyRootPath, 'assets');
		const completions: CompletionItem[] = [];
		// limit search to these sub-directories
		let paths = [ 'images', 'iphone', 'android', 'windows' ];
		paths = paths.map(aPath => path.join(assetPath, aPath));

		for (const imgPath of paths) {
			if (!utils.directoryExists(imgPath)) {
				continue;
			}
			const files = walkSync(imgPath, {
				nodir: true,
				filter: item => path.basename(item.path) !== '.DS_Store'
			});
			const images = [];
			for (const file of files) {
				let prefix: string|undefined;
				let scale: string|undefined;
				let suffix: string|undefined;
				// test whether image is includes scaling factor (for iOS)
				let matches = file.path.match(/(^[\w\s\\/\-_():]+)(@[\w~]+)(.\w+$)/);
				if (matches && matches.length === 4) {
					prefix = matches[1];
					scale = matches[2];
					suffix = matches[3];
				} else {
					matches = file.path.match(/(^[\w\s/\\\-_():]+)(.\w+$)/);
					if (matches && matches.length === 3) {
						prefix = matches[1];
						scale = '@1x';
						suffix = matches[2];
					}
				}

				if (prefix && suffix && scale) {
					const image = images.find(img => (img.prefix === prefix && img.suffix === suffix));
					if (image) {
						image.scales.push(scale);
					} else {
						images.push({
							prefix,
							suffix,
							file: file.path,
							scales: [ scale ]
						});
					}
				}
			}

			for (const image of images) {
				let scales;
				if (!(image.scales.length === 1 && image.scales[0] === '@1x')) {
					scales = image.scales.join(', ');
				}
				// TODO: Is it possible to preview the image like the atom plugin? We do this elsewhere right now
				completions.push({
					label: utils.toUnixPath(`${image.prefix}${image.suffix}`.replace(assetPath, '')).replace(/^\/(iphone|android|windows)/, ''),
					kind: CompletionItemKind.File,
					range,
					detail: scales
				});
			}
		}
		return completions;
	}


@Component({
  tag: 'arcgis-expand',
  styleUrl: 'arcgis-expand.css',
  shadow: false,
})
export class ArcGISSearch {
  @Element() el: HTMLDivElement;

  @Prop() position: string;

  @Prop() view : __esri.MapView | __esri.SceneView;

  @Prop({ mutable: true }) widget: any;

  @Watch('view')
  validateView(value: __esri.MapView) {
    if (value) {
        this.widget.view = value;
        this.widget.view.ui.add(this.el, this.position);
    }
  }

  @Event() loaded: EventEmitter<boolean>;

  componentWillLoad() {
      const expand = new Expand({
          container: this.el
      });

      this.widget = expand;
      this.loaded.emit(true);
  }

  componentDidRender() {
    const elems = Array.from(this.el.children);
    for (let e of elems) {
      if (e.tagName.toLowerCase().includes('arcgis-')) {
        (e as any).view = this.view;
        this.widget.content = e;
        this.widget.expandIconClass = (e as any).widget.iconClass;
      }
    }
  }
}


@Watch('view')
  validateView(value: __esri.MapView) {
    if (value) {
        this.widget.view = value;
        this.widget.view.ui.add(this.el, this.position);
    }
  }


componentWillLoad() {
      const expand = new Expand({
          container: this.el
      });

      this.widget = expand;
      this.loaded.emit(true);
  }


componentDidRender() {
    const elems = Array.from(this.el.children);
    for (let e of elems) {
      if (e.tagName.toLowerCase().includes('arcgis-')) {
        (e as any).view = this.view;
        this.widget.content = e;
        this.widget.expandIconClass = (e as any).widget.iconClass;
      }
    }
  }


export interface IStrapiModelAttribute {
  unique?: boolean;
  required?: boolean;
  type?: StrapiType;
  default?: string | number | boolean;
  dominant?: boolean;
  collection?: string;
  model?: string;
  via?: string;
  plugin?: string;
  enum?: string[];
  component?: string;
  components?: string[];
  repeatable?: boolean;
}


export interface IStrapiModel {
  /** Not from Strapi, but is the filename on disk */
  _filename: string;
  _isComponent?: boolean;

  connection: string;
  collectionName: string;
  info: {
    name: string;
    description: string;
    icon?: string;
  };
  options?: {
    timestamps: boolean;
  };
  attributes: { [attr: string]: IStrapiModelAttribute };
}


export interface IStrapiModelExtended extends IStrapiModel {
  // use to output filename
  ouputFile: string;
  // interface name
  interfaceName: string;
  // model name extract from *.(settings|schema).json filename. Use to link model.
  modelName: string;
}


export interface IStrapi4ModelAttribute {
  type?: Strapi4AttributeType;
  relation?: Strapi4RelationType;
  target?: string;
  mappedBy?: string;
  inversedBy?: string;

  repeatable?: boolean;
  component?: string;
  components?: string[];

  required?: boolean;
  max?: number;
  min?: number;
  minLength?: number;
  maxLength?: number;
  private?: boolean;
  configurable?: boolean;

  targetField?: string;
  options?: string;

  default?: string | number | boolean;
  unique?: boolean;
  enum?: string[];
}


export interface IStrapi4Model {
  /** Not from Strapi, but is the filename on disk */
  _filename: string;
  _isComponent?: boolean;

  kind: 'collectionType' | 'singleType',
  tableName: string;
  info: {
    displayName: string;
    singularName: string;
    pluralName: string;
    description: string;
    icon: string;
  };
  options?: {
    privateAttributes: string[];
    populateCreatorFields: boolean;
    draftAndPublish: boolean;
  };
  attributes: { [attr: string]: IStrapi4ModelAttribute };
}


export interface IStrapi4ModelExtended extends IStrapi4Model {
  // use to output filename
  ouputFile: string;
  // interface name
  interfaceName: string;
  // model name extract from *.(settings|schema).json filename. Use to link model.
  modelName: string;
}


export type StrapiType = 'string' | 'number' | 'boolean' | 'text' | 'date' | 'email' | 'component' | 'enumeration' | 'dynamiczone';


export type Strapi4AttributeType = 'string' | 'text' | 'richtext' | 'enumeration' | 'email' | 'password' | 'uid' | 'date' | 'time' | 'datetime' | 'timestamp' | 'integer' | 'biginteger' | 'float' | 'decimal' | 'boolean' | 'array' | 'json' | 'media' | 'relation' | 'component' | 'dynamiczone' | 'locale' | 'localizations';


type Strapi4RelationType = 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany'


@Component({
  selector: 'app-async-observable-pipe',
  template: `
  <fieldset>

Async Observable :

      Count: {{counter$ | async}}

  </fieldset>
  `,
  styles: []
})
export class AsyncObservablePipeComponent {

  counter$: Observable<number>;

  constructor() {
    this.counter$ = Observable
      .interval(1000);
  }

}


/**
 * Start application
 */
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const port = app.get(ConfigService).get<number>("port");
  const ip = app.get(ConfigService).get<string>("ip");

  app.enableShutdownHooks();

  await app.listen(port, ip);
  Logger.log("Listening on " + ip + ":" + port.toString(), "NestApplication");
}


(ctor: any, superCtor: any) => {
  if (superCtor) {
    ctor.super_ = superCtor;
    ctor.prototype = Object.create(superCtor.prototype, {
      constructor: {
        value: ctor,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
  }
}


(...args: any[]) =>
  args.forEach(arg => console.log(arg))


arg => console.log(arg)


(fn: Function): () => Promise<any> => {
  if (typeof (fn as any)[promisify.custom] === 'function') {
    // https://nodejs.org/api/util.html#util_custom_promisified_functions
    return function (...args: any[]) {
      return (fn as any)[promisify.custom].apply(this, args);
    }
  }

  return function (...args: any[]) {
    return new Promise((resolve, reject) => {
      args.push((err: any, result: any) => {
        if (err != null) {
          reject(err);
        } else {
          resolve(result);
        }
      });
      fn.apply(this, args);
    });
  };
}


(resolve, reject) => {
      args.push((err: any, result: any) => {
        if (err != null) {
          reject(err);
        } else {
          resolve(result);
        }
      });
      fn.apply(this, args);
    }


(err: any, result: any) => {
        if (err != null) {
          reject(err);
        } else {
          resolve(result);
        }
      }


speed => {
      this.playbackRate = speed;
    }


/**
 * References the content to go into the animation menu.
 * Also handles the dialog closing.
 */
@Component({
  host: {
    'class': 'mat-typography',
  },
  selector: 'animation-menu',
  styleUrls: ['./animation_menu.scss'],
  templateUrl: './animation_menu.ng.html',
})
export class AnimationMenuComponent {
  playbackRate!: number;

  constructor(
      private animationService: AnimationMenuService,
      public dialogRef: MatDialogRef<AnimationMenuComponent>) {
    this.animationService.getAnimationSpeed().subscribe(speed => {
      this.playbackRate = speed;
    });
  }

  closeDialog() {
    this.animationService.setAnimationSpeed(this.playbackRate);
    this.dialogRef.close();
  }
}


closeDialog() {
    this.animationService.setAnimationSpeed(this.playbackRate);
    this.dialogRef.close();
  }


@Injectable({
  providedIn: "root"
})
export class EventCourseTypeCommunicationService {
  private viewEventCourseTypeSubject = new Subject<EventCourseType>();
  private addEventCourseTypeSubject = new Subject<EventCourseType>();

  constructor() {}

  sendAddEventCourseTypeEvent(eventCourseType: EventCourseType) {
    this.addEventCourseTypeSubject.next(eventCourseType);
  }

  getAddEventCourseType(): Observable<EventCourseType> {
    return this.addEventCourseTypeSubject.asObservable();
  }

  sendViewEventCourseTypeEvent(eventCourseTypes: EventCourseType) {
    this.viewEventCourseTypeSubject.next(eventCourseTypes);
  }

  getViewEventCourseType(): Observable<EventCourseType> {
    return this.viewEventCourseTypeSubject.asObservable();
  }

}


sendAddEventCourseTypeEvent(eventCourseType: EventCourseType) {
    this.addEventCourseTypeSubject.next(eventCourseType);
  }


getAddEventCourseType(): Observable<EventCourseType> {
    return this.addEventCourseTypeSubject.asObservable();
  }


sendViewEventCourseTypeEvent(eventCourseTypes: EventCourseType) {
    this.viewEventCourseTypeSubject.next(eventCourseTypes);
  }


getViewEventCourseType(): Observable<EventCourseType> {
    return this.viewEventCourseTypeSubject.asObservable();
  }


() => ({ contextId, iModelId, ...openParams })


() => {
            timedOut = true;
          }


() => ({ token: requestContext.accessToken.toTokenString(), contextId, iModelId, ...openParams })


() => ({ errorStatus: error.status, errorMessage: error.Message, iModelToken: imodelDb.iModelToken })


() => ({ name: this.name, ...this.iModelToken })


() => {
      if (stmt.isShared)
        this._statementCache.release(stmt);
      else
        stmt.dispose();
    }


(_name: string, value: any) => {
      if (typeof value === "string") {
        if (value.length >= base64Header.length && value.startsWith(base64Header)) {
          const out = value.substr(base64Header.length);
          const buffer = Buffer.from(out, "base64");
          return new Uint8Array(buffer);
        }
      }
      return value;
    }


(_name: string, value: any) => {
      if (value && value.constructor === Uint8Array) {
        const buffer = Buffer.from(value);
        return base64Header + buffer.toString("base64");
      }
      return value;
    }


(resolve) => {
      const postrc = this.nativeDb.postConcurrentQuery(ecsql, JSON.stringify(bindings, replacer), limit!, quota!, priority!);
      if (postrc.status !== PostStatus.Done)
        resolve({ status: QueryResponseStatus.PostError, rows: [] });

      const poll = () => {
        const pollrc = this.nativeDb.pollConcurrentQuery(postrc.taskId);
        if (pollrc.status === PollStatus.Done)
          resolve({ status: QueryResponseStatus.Done, rows: JSON.parse(pollrc.result, reviver) });
        else if (pollrc.status === PollStatus.Partial)
          resolve({ status: QueryResponseStatus.Partial, rows: JSON.parse(pollrc.result, reviver) });
        else if (pollrc.status === PollStatus.Timeout)
          resolve({ status: QueryResponseStatus.Timeout, rows: [] });
        else if (pollrc.status === PollStatus.Pending)
          setTimeout(() => { poll(); }, IModelHost.configuration!.concurrentQuery.pollInterval);
        else
          resolve({ status: QueryResponseStatus.Error, rows: [pollrc.result] });
      };
      setTimeout(() => { poll(); }, IModelHost.configuration!.concurrentQuery.pollInterval);
    }


() => {
        const pollrc = this.nativeDb.pollConcurrentQuery(postrc.taskId);
        if (pollrc.status === PollStatus.Done)
          resolve({ status: QueryResponseStatus.Done, rows: JSON.parse(pollrc.result, reviver) });
        else if (pollrc.status === PollStatus.Partial)
          resolve({ status: QueryResponseStatus.Partial, rows: JSON.parse(pollrc.result, reviver) });
        else if (pollrc.status === PollStatus.Timeout)
          resolve({ status: QueryResponseStatus.Timeout, rows: [] });
        else if (pollrc.status === PollStatus.Pending)
          setTimeout(() => { poll(); }, IModelHost.configuration!.concurrentQuery.pollInterval);
        else
          resolve({ status: QueryResponseStatus.Error, rows: [pollrc.result] });
      }


() => { poll(); }


(stmt: ECSqlStatement) => {
      if (bindings)
        stmt.bindValues(bindings);
      const rows: any[] = [];
      while (DbResult.BE_SQLITE_ROW === stmt.step()) {
        rows.push(stmt.getRow());
        if (rows.length > IModelDb.maxLimit) {
          throw new IModelError(IModelStatus.BadRequest, "Max LIMIT exceeded in SELECT statement", Logger.logError, loggerCategory);
        }
      }
      return rows;
    }


() => {
      if (stmt.isShared)
        this._sqliteStatementCache.release(stmt);
      else
        stmt.dispose();
    }


(stmt) => {
      for (const row of stmt) {
        if (row.id !== undefined) {
          ids.add(row.id);
          if (ids.size > IModelDb.maxLimit) {
            throw new IModelError(IModelStatus.BadRequest, "Max LIMIT exceeded in SELECT statement", Logger.logError, loggerCategory);
          }
        }
      }
    }


() => ({ schemaFileName })


() => iModelToken


(baseClass) => this.forEachMetaData(iModel, baseClass, true, func, includeCustom)


() => ({ ...this._token, classFullName })


(baseClassName: string) => this.loadMetaData(baseClassName)


(resolve, reject) => {
      request!.doSnap(this.nativeDb, JsonUtils.toObject(props), (ret: IModelJsNative.ErrorStatusOrResult<IModelStatus, SnapResponseProps>) => {
        this._snaps.delete(sessionId);
        if (ret.error !== undefined)
          reject(new Error(ret.error.message));
        else
          resolve(ret.result);
      });
    }


(ret: IModelJsNative.ErrorStatusOrResult<IModelStatus, SnapResponseProps>) => {
        this._snaps.delete(sessionId);
        if (ret.error !== undefined)
          reject(new Error(ret.error.message));
        else
          resolve(ret.result);
      }


(id) => {
        const props = this.getModelProps(id);
        const jsClass = this._iModel.getJsClass<typeof Model>(props.classFullName);
        if (IModelStatus.Success !== jsClass.onDelete(props))
          return;

        const error = this._iModel.nativeDb.deleteModel(id);
        if (error !== IModelStatus.Success)
          throw new IModelError(error, "", Logger.logWarning, loggerCategory);

        jsClass.onDeleted(props);
      }


(stmt: ECSqlStatement) => {
        stmt.bindId(1, code.spec);
        stmt.bindId(2, Id64.fromString(code.scope));
        stmt.bindString(3, code.value!);
        if (DbResult.BE_SQLITE_ROW !== stmt.step())
          return undefined;

        return Id64.fromJSON(stmt.getRow().id);
      }


(id) => {
        const props = this.getElementProps(id);
        const jsClass = iModel.getJsClass<typeof Element>(props.classFullName);
        if (IModelStatus.Success !== jsClass.onDelete(props, iModel))
          return;

        const error = iModel.nativeDb.deleteElement(id);
        if (error !== IModelStatus.Success)
          throw new IModelError(error, "", Logger.logWarning, loggerCategory);

        jsClass.onDeleted(props, iModel);
      }


() => ({ aspectInstanceId, aspectClassName })


(id) => {
        const status = this._iModel.nativeDb.deleteElementAspect(id);
        if (status !== IModelStatus.Success)
          throw new IModelError(status, "Error deleting ElementAspect", Logger.logWarning, loggerCategory);
      }


(id) => {
        try {
          props.push(imodel.elements.getElementProps<ViewDefinitionProps>(id));
        } catch (err) { }
      }


(resolve, reject) => {
        requestContext.enter();
        this._iModel.nativeDb.getTileTree(id, (ret: IModelJsNative.ErrorStatusOrResult<IModelStatus, any>) => {
          if (undefined !== ret.error)
            reject(new IModelError(ret.error.status, "TreeId=" + id));
          else
            resolve(ret.result! as TileTreeProps);
        });
      }


(ret: IModelJsNative.ErrorStatusOrResult<IModelStatus, any>) => {
          if (undefined !== ret.error)
            reject(new IModelError(ret.error.status, "TreeId=" + id));
          else
            resolve(ret.result! as TileTreeProps);
        }


() => ({ tileSize, tileSizeThreshold, treeId, tileId, iModelGuid })


() => ({ loadTime, loadTimeThreshold, treeId, tileId, iModelGuid })


() => this.pollTileContent(resolve, reject, treeId, tileId, requestContext)


(resolve, reject) => {
        this.pollTileContent(resolve, reject, treeId, tileId, requestContext);
      }


/** Parameters to open an IModelDb
 * @public
 */
export class OpenParams {
  // Constructor
  public constructor(
    /** Mode to Open the IModelDb */
    public readonly openMode: OpenMode,

    /** Operations allowed when synchronizing changes between the IModelDb and IModelHub */
    public readonly syncMode?: SyncMode,

    /** Timeout (in milliseconds) before the open operations throws a RpcPendingResponse exception
     * after queuing up the open. If undefined, waits for the open to complete.
     */
    public timeout?: number,
  ) {
    this.validate();
  }

  /** Returns true if the open params open a snapshot Db */
  public get isStandalone(): boolean { return this.syncMode === undefined; }

  private validate() {
    if (this.isStandalone && this.syncMode !== undefined)
      throw new IModelError(BentleyStatus.ERROR, "Invalid parameters - only openMode can be defined if opening a standalone Db");

    if (this.openMode === OpenMode.Readonly && this.syncMode === SyncMode.PullAndPush) {
      throw new IModelError(BentleyStatus.ERROR, "Cannot pull or push changes into/from a ReadOnly IModel");
    }
  }

  /** Create parameters to open the Db as of a fixed version in a readonly mode */
  public static fixedVersion(): OpenParams { return new OpenParams(OpenMode.Readonly, SyncMode.FixedVersion); }

  /** Create parameters to open the Db to make edits and push changes to the Hub */
  public static pullAndPush(): OpenParams { return new OpenParams(OpenMode.ReadWrite, SyncMode.PullAndPush); }

  /** Create parameters to open a standalone Db
   * @deprecated The confusing concept of *standalone* is being replaced by the more strict concept of a read-only iModel *snapshot*.
   */
  public static standalone(openMode: OpenMode) { return new OpenParams(openMode); }
  /** Returns true if equal and false otherwise */
  public equals(other: OpenParams) {
    return other.openMode === this.openMode && other.syncMode === this.syncMode;
  }
}


/** The collection of models in an [[IModelDb]].
   * @public
   */
export class Models {
    /** @internal */
    public constructor(private _iModel: IModelDb) { }

    /** Get the Model with the specified identifier.
     * @param modelId The Model identifier.
     * @throws [[IModelError]]
     */
    public getModelProps<T extends ModelProps>(modelId: Id64String): T {
      const json = this.getModelJson(JSON.stringify({ id: modelId.toString() }));
      return JSON.parse(json) as T;
    }

    /** Get the Model with the specified identifier.
     * @param modelId The Model identifier.
     * @throws [[IModelError]]
     */
    public getModel<T extends Model>(modelId: Id64String): T { return this._iModel.constructEntity<T>(this.getModelProps(modelId)); }

    /**
     * Read the properties for a Model as a json string.
     * @param modelIdArg a json string with the identity of the model to load. Must have either "id" or "code".
     * @return a json string with the properties of the model.
     */
    public getModelJson(modelIdArg: string): string {
      if (!this._iModel.briefcase) throw this._iModel.newNotOpenError();
      const val = this._iModel.nativeDb.getModel(modelIdArg);
      if (val.error)
        throw new IModelError(val.error.status, "Model=" + modelIdArg);
      return val.result!;
    }

    /** Get the sub-model of the specified Element.
     * See [[IModelDb.Elements.queryElementIdByCode]] for more on how to find an element by Code.
     * @param modeledElementId Identifies the modeled element.
     * @throws [[IModelError]]
     */
    public getSubModel<T extends Model>(modeledElementId: Id64String | GuidString | Code): T {
      const modeledElement = this._iModel.elements.getElement(modeledElementId);
      if (modeledElement.id === IModel.rootSubjectId)
        throw new IModelError(IModelStatus.NotFound, "Root subject does not have a sub-model", Logger.logWarning, loggerCategory);

      return this.getModel<T>(modeledElement.id);
    }

    /** Create a new model in memory.
     * See the example in [[InformationPartitionElement]].
     * @param modelProps The properties to use when creating the model.
     * @throws [[IModelError]] if there is a problem creating the model.
     */
    public createModel<T extends Model>(modelProps: ModelProps): T { return this._iModel.constructEntity<T>(modelProps); }

    /** Insert a new model.
     * @param props The data for the new model.
     * @returns The newly inserted model's Id.
     * @throws [[IModelError]] if unable to insert the model.
     */
    public insertModel(props: ModelProps): Id64String {
      if (props.isPrivate === undefined) // temporarily work around bug in addon
        props.isPrivate = false;

      const jsClass = this._iModel.getJsClass<typeof Model>(props.classFullName);
      if (IModelStatus.Success !== jsClass.onInsert(props))
        return Id64.invalid;

      const val = this._iModel.nativeDb.insertModel(JSON.stringify(props));
      if (val.error)
        throw new IModelError(val.error.status, "inserting model", Logger.logWarning, loggerCategory);

      props.id = Id64.fromJSON(JSON.parse(val.result!).id);
      jsClass.onInserted(props.id);
      return props.id;
    }

    /** Update an existing model.
     * @param props the properties of the model to change
     * @throws [[IModelError]] if unable to update the model.
     */
    public updateModel(props: ModelProps): void {
      const jsClass = this._iModel.getJsClass<typeof Model>(props.classFullName);
      if (IModelStatus.Success !== jsClass.onUpdate(props))
        return;

      const error = this._iModel.nativeDb.updateModel(JSON.stringify(props));
      if (error !== IModelStatus.Success)
        throw new IModelError(error, "updating model id=" + props.id, Logger.logWarning, loggerCategory);

      jsClass.onUpdated(props);
    }

    /** Delete one or more existing models.
     * @param ids The Ids of the models to be deleted
     * @throws [[IModelError]]
     */
    public deleteModel(ids: Id64Arg): void {
      Id64.toIdSet(ids).forEach((id) => {
        const props = this.getModelProps(id);
        const jsClass = this._iModel.getJsClass<typeof Model>(props.classFullName);
        if (IModelStatus.Success !== jsClass.onDelete(props))
          return;

        const error = this._iModel.nativeDb.deleteModel(id);
        if (error !== IModelStatus.Success)
          throw new IModelError(error, "", Logger.logWarning, loggerCategory);

        jsClass.onDeleted(props);
      });
    }
  }


/** The collection of views in an [[IModelDb]].
   * @public
   */
export class Views {
    /** @internal */
    public constructor(private _iModel: IModelDb) { }

    /** Query for the array of ViewDefinitionProps of the specified class and matching the specified IsPrivate setting.
     * @param className Query for view definitions of this class.
     * @param wantPrivate If true, include private view definitions.
     */
    public queryViewDefinitionProps(className: string = "BisCore.ViewDefinition", limit = IModelDb.defaultLimit, offset = 0, wantPrivate: boolean = false): ViewDefinitionProps[] {
      const where: string = (wantPrivate === false) ? "IsPrivate=FALSE" : "";
      const ids = this._iModel.queryEntityIds({ from: className, limit, offset, where });

      const props: ViewDefinitionProps[] = [];
      const imodel = this._iModel;
      ids.forEach((id) => {
        try {
          props.push(imodel.elements.getElementProps<ViewDefinitionProps>(id));
        } catch (err) { }
      });

      return props;
    }

    /** Default parameters for iterating/querying ViewDefinitions. Includes all subclasses of ViewDefinition, excluding only those marked 'private'. */
    public static readonly defaultQueryParams: ViewQueryParams = { from: "BisCore.ViewDefinition", where: "IsPrivate=FALSE" };

    /** Iterate all ViewDefinitions matching the supplied query.
     * @param params Specifies the query by which views are selected.
     * @param callback Function invoked for each ViewDefinition matching the query. Return false to terminate iteration, true to continue.
     * @return true if all views were iterated, false if iteration was terminated early due to callback returning false.
     *
     * **Example: Finding all views of a specific DrawingModel**
     * ``` ts
     * [[include:IModelDb.Views.iterateViews]]
     * ```
     */
    public iterateViews(params: ViewQueryParams, callback: (view: ViewDefinition) => boolean): boolean {
      const ids = this._iModel.queryEntityIds(params);
      let finished = true;
      for (const id of ids) {
        try {
          const view = this._iModel.elements.getElement(id);
          if (undefined !== view && view instanceof ViewDefinition) {
            finished = callback(view);
            if (!finished)
              break;
          }
        } catch (err) { }
      }

      return finished;
    }

    public getViewStateData(viewDefinitionId: string): ViewStateProps {
      const viewStateData: ViewStateProps = {} as any;
      const elements = this._iModel.elements;
      const viewDefinitionElement = elements.getElement<ViewDefinition>(viewDefinitionId);
      viewStateData.viewDefinitionProps = viewDefinitionElement.toJSON();
      viewStateData.categorySelectorProps = elements.getElementProps<CategorySelectorProps>(viewStateData.viewDefinitionProps.categorySelectorId);
      viewStateData.displayStyleProps = elements.getElementProps<DisplayStyleProps>(viewStateData.viewDefinitionProps.displayStyleId);
      if (viewStateData.viewDefinitionProps.modelSelectorId !== undefined)
        viewStateData.modelSelectorProps = elements.getElementProps<ModelSelectorProps>(viewStateData.viewDefinitionProps.modelSelectorId);
      else if (viewDefinitionElement instanceof SheetViewDefinition) {
        viewStateData.sheetProps = elements.getElementProps<SheetProps>(viewDefinitionElement.baseModelId);
        viewStateData.sheetAttachments = Array.from(this._iModel.queryEntityIds({
          from: "BisCore.ViewAttachment",
          where: "Model.Id=" + viewDefinitionElement.baseModelId,
        }));
      }
      return viewStateData;
    }

    private getViewThumbnailArg(viewDefinitionId: Id64String): string {
      const viewProps: FilePropertyProps = { namespace: "dgn_View", name: "Thumbnail", id: viewDefinitionId };
      return JSON.stringify(viewProps);
    }

    /** Get the thumbnail for a view.
     * @param viewDefinitionId The Id of the view for thumbnail
     * @return the ThumbnailProps, or undefined if no thumbnail exists.
     */
    public getThumbnail(viewDefinitionId: Id64String): ThumbnailProps | undefined {
      const viewArg = this.getViewThumbnailArg(viewDefinitionId);
      const sizeProps = this._iModel.nativeDb.queryFileProperty(viewArg, true) as string;
      if (undefined === sizeProps)
        return undefined;

      const out = JSON.parse(sizeProps) as ThumbnailProps;
      out.image = this._iModel.nativeDb.queryFileProperty(viewArg, false) as Uint8Array;
      return out;
    }

    /** Save a thumbnail for a view.
     * @param viewDefinitionId The Id of the view for thumbnail
     * @param thumbnail The thumbnail data.
     * @returns 0 if successful
     */
    public saveThumbnail(viewDefinitionId: Id64String, thumbnail: ThumbnailProps): number {
      const viewArg = this.getViewThumbnailArg(viewDefinitionId);
      const props = { format: thumbnail.format, height: thumbnail.height, width: thumbnail.width };
      return this._iModel.nativeDb.saveFileProperty(viewArg, JSON.stringify(props), thumbnail.image);
    }

    /** Set the default view property the iModel
     * @param viewId The Id of the ViewDefinition to use as the default
     */
    public setDefaultViewId(viewId: Id64String): void {
      const spec = { namespace: "dgn_View", name: "DefaultView" };
      const blob32 = new Uint32Array(2);
      blob32[0] = Id64.getLowerUint32(viewId);
      blob32[1] = Id64.getUpperUint32(viewId);
      const blob8 = new Uint8Array(blob32.buffer);
      this._iModel.saveFileProperty(spec, undefined, blob8);
    }
  }


/** @internal */
export class Tiles {
    /** @internal */
    public constructor(private _iModel: IModelDb) { }

    /** @internal */
    public async requestTileTreeProps(requestContext: ClientRequestContext, id: string): Promise<TileTreeProps> {
      requestContext.enter();
      if (!this._iModel.briefcase)
        throw this._iModel.newNotOpenError();

      return new Promise<TileTreeProps>((resolve, reject) => {
        requestContext.enter();
        this._iModel.nativeDb.getTileTree(id, (ret: IModelJsNative.ErrorStatusOrResult<IModelStatus, any>) => {
          if (undefined !== ret.error)
            reject(new IModelError(ret.error.status, "TreeId=" + id));
          else
            resolve(ret.result! as TileTreeProps);
        });
      });
    }

    private pollTileContent(resolve: (arg0: Uint8Array) => void, reject: (err: Error) => void, treeId: string, tileId: string, requestContext: ClientRequestContext) {
      requestContext.enter();
      if (!this._iModel.briefcase) {
        reject(this._iModel.newNotOpenError());
        return;
      }

      const ret = this._iModel.nativeDb.pollTileContent(treeId, tileId);
      if (undefined !== ret.error) {
        reject(new IModelError(ret.error.status, "TreeId=" + treeId + " TileId=" + tileId));
      } else if (typeof ret.result !== "number") { // if type is not a number, it's the TileContent interface
        const res = ret.result as IModelJsNative.TileContent;
        const iModelGuid = this._iModel.getGuid();

        const tileSizeThreshold = IModelHost.logTileSizeThreshold;
        const tileSize = res.content.length;
        if (tileSize > tileSizeThreshold) {
          Logger.logWarning(loggerCategory, "Tile size (in bytes) larger than specified threshold", () => ({ tileSize, tileSizeThreshold, treeId, tileId, iModelGuid }));
        }

        const loadTimeThreshold = IModelHost.logTileLoadTimeThreshold;
        const loadTime = res.elapsedSeconds;
        if (loadTime > loadTimeThreshold) {
          Logger.logWarning(loggerCategory, "Tile load time (in seconds) greater than specified threshold", () => ({ loadTime, loadTimeThreshold, treeId, tileId, iModelGuid }));
        }

        resolve(res.content);
      } else { // if the type is a number, it's the TileContentState enum
        // ###TODO: Decide appropriate timeout interval. May want to switch on state (new vs loading vs pending)
        setTimeout(() => this.pollTileContent(resolve, reject, treeId, tileId, requestContext), 10);
      }
    }

    /** @internal */
    public async requestTileContent(requestContext: ClientRequestContext, treeId: string, tileId: string): Promise<Uint8Array> {
      requestContext.enter();
      if (!this._iModel.briefcase)
        throw this._iModel.newNotOpenError();

      return new Promise<Uint8Array>((resolve, reject) => {
        this.pollTileContent(resolve, reject, treeId, tileId, requestContext);
      });
    }
  }


/** An error generated during dependency validation.
 * @beta
 */
export interface ValidationError {
  /** If true, txn is aborted. */
  fatal: boolean;
  /** The type of error. */
  errorType: string;
  /** Optional description of what went wrong. */
  message?: string;
}


/** Operations allowed when synchronizing changes between the IModelDb and the iModel Hub
 * @public
 */
export enum SyncMode { FixedVersion = 1, PullAndPush = 2 }


/** Represents the current state of a pollable tile content request.
   * Note: lack of a "completed" state because polling a completed request returns the content as a Uint8Array.
   * @internal
   */
export enum TileContentState {
    New, // Request was just created and enqueued.
    Pending, // Request is enqueued but not yet being processed.
    Loading, // Request is being actively processed.
  }


/** @public */
export enum TxnAction { None = 0, Commit = 1, Abandon = 2, Reverse = 3, Reinstate = 4, Merge = 5 }


/** A string that identifies a Txn.
 * @public
 */
export type TxnIdString = string;


/** The signature of a function that can supply a description of local Txns in the specified briefcase up to and including the specified endTxnId.
 * @public
 */
export type ChangeSetDescriber = (endTxnId: TxnIdString) => string;


private validate() {
    if (this.isStandalone && this.syncMode !== undefined)
      throw new IModelError(BentleyStatus.ERROR, "Invalid parameters - only openMode can be defined if opening a standalone Db");

    if (this.openMode === OpenMode.Readonly && this.syncMode === SyncMode.PullAndPush) {
      throw new IModelError(BentleyStatus.ERROR, "Cannot pull or push changes into/from a ReadOnly IModel");
    }
  }


/** Create parameters to open the Db as of a fixed version in a readonly mode */
public static fixedVersion(): OpenParams { return new OpenParams(OpenMode.Readonly, SyncMode.FixedVersion); }


/** Create parameters to open the Db to make edits and push changes to the Hub */
public static pullAndPush(): OpenParams { return new OpenParams(OpenMode.ReadWrite, SyncMode.PullAndPush); }


/** Create parameters to open a standalone Db
   * @deprecated The confusing concept of *standalone* is being replaced by the more strict concept of a read-only iModel *snapshot*.
   */
public static standalone(openMode: OpenMode) { return new OpenParams(openMode); }


/** Returns true if equal and false otherwise */
public equals(other: OpenParams) {
    return other.openMode === this.openMode && other.syncMode === this.syncMode;
  }


public readFontJson(): string { return this.nativeDb.readFontMap(); }


public embedFont(prop: FontProps): FontProps { this._fontMap = undefined; return JSON.parse(this.nativeDb.embedFont(JSON.stringify(prop))) as FontProps; }


private initializeIModelDb() {
    const props = JSON.parse(this.nativeDb.getIModelProps()) as IModelProps;
    const name = props.rootSubject ? props.rootSubject.name : path.basename(this.briefcase.pathname);
    super.initialize(name, props);
  }


private static constructIModelDb(briefcaseEntry: BriefcaseEntry, openParams: OpenParams, contextId?: string): IModelDb {
    if (briefcaseEntry.iModelDb)
      return briefcaseEntry.iModelDb; // If there's an IModelDb already associated with the briefcase, that should be reused.
    const iModelToken = new IModelToken(briefcaseEntry.getKey(), contextId, briefcaseEntry.iModelId, briefcaseEntry.currentChangeSetId, openParams.openMode);
    return new IModelDb(briefcaseEntry, iModelToken, openParams);
  }


/** Create an *empty* local iModel *snapshot* file. Snapshots are disconnected from iModelHub so do not have a change timeline. Snapshots are typically used for archival or data transfer purposes.
   * > Note: A *snapshot* cannot be modified after [[closeSnapshot]] is called.
   * @param snapshotFile The file that will contain the new iModel *snapshot*
   * @param args The parameters that define the new iModel *snapshot*
   * @beta
   */
public static createSnapshot(snapshotFile: string, args: CreateIModelProps): IModelDb {
    const briefcaseEntry: BriefcaseEntry = BriefcaseManager.createStandalone(snapshotFile, args);
    return IModelDb.constructIModelDb(briefcaseEntry, OpenParams.standalone(briefcaseEntry.openParams.openMode));
  }


/** Create a local iModel *snapshot* file using this iModel as a *seed* or starting point.
   * Snapshots are disconnected from iModelHub so do not have a change timeline. Snapshots are typically used for archival or data transfer purposes.
   * > Note: A *snapshot* cannot be modified after [[closeSnapshot]] is called.
   * @param snapshotFile The file that will contain the new iModel *snapshot*
   * @returns A writeable IModelDb
   * @beta
   */
public createSnapshot(snapshotFile: string): IModelDb {
    IModelJsFs.copySync(this.briefcase.pathname, snapshotFile);
    const briefcaseEntry: BriefcaseEntry = BriefcaseManager.openStandalone(snapshotFile, OpenMode.ReadWrite, false);
    const isSeedFileMaster: boolean = BriefcaseId.Master === briefcaseEntry.briefcaseId;
    const isSeedFileSnapshot: boolean = BriefcaseId.Snapshot === briefcaseEntry.briefcaseId;
    // Replace iModelId if seedFile is a master or snapshot, preserve iModelId if seedFile is an iModelHub-managed briefcase
    if ((isSeedFileMaster) || (isSeedFileSnapshot)) {
      briefcaseEntry.iModelId = Guid.createValue();
    }
    briefcaseEntry.briefcaseId = BriefcaseId.Snapshot;
    const snapshotDb: IModelDb = IModelDb.constructIModelDb(briefcaseEntry, OpenParams.standalone(OpenMode.ReadWrite)); // WIP: clean up copied file on error?
    if (isSeedFileMaster) {
      snapshotDb.setGuid(briefcaseEntry.iModelId);
    } else {
      snapshotDb.setAsMaster(briefcaseEntry.iModelId);
    }
    snapshotDb.nativeDb.setBriefcaseId(briefcaseEntry.briefcaseId);
    return snapshotDb;
  }


/** Create an iModel on iModelHub */
public static async create(requestContext: AuthorizedClientRequestContext, contextId: string, iModelName: string, args: CreateIModelProps): Promise<IModelDb> {
    requestContext.enter();
    IModelDb.onCreate.raiseEvent(requestContext, contextId, args);
    const iModelId: string = await BriefcaseManager.create(requestContext, contextId, iModelName, args);
    requestContext.enter();
    return IModelDb.open(requestContext, contextId, iModelId);
  }


/** Open an iModel from a local file.
   * @param pathname The pathname of the iModel
   * @param openMode Open mode for database
   * @param enableTransactions Enable tracking of transactions in this standalone iModel
   * @throws [[IModelError]]
   * @see [[open]], [[openSnapshot]]
   * @deprecated iModelHub manages the change history of an iModel, so writing changes to a local/unmanaged file doesn't make sense. Callers should migrate to [[openSnapshot]] or [[open]] instead.
   * @internal
   */
public static openStandalone(pathname: string, openMode: OpenMode = OpenMode.ReadWrite, enableTransactions: boolean = false): IModelDb {
    const briefcaseEntry: BriefcaseEntry = BriefcaseManager.openStandalone(pathname, openMode, enableTransactions);
    return IModelDb.constructIModelDb(briefcaseEntry, OpenParams.standalone(openMode));
  }


/** Open a local iModel *snapshot*. Once created, *snapshots* are read-only and are typically used for archival or data transfer purposes.
   * @see [[open]]
   * @throws [IModelError]($common) If the file is not found or is not a valid *snapshot*.
   * @beta
   */
public static openSnapshot(filePath: string): IModelDb {
    const iModelDb: IModelDb = this.openStandalone(filePath, OpenMode.Readonly, false);
    const briefcaseId: number = iModelDb.getBriefcaseId().value;
    if ((BriefcaseId.Snapshot === briefcaseId) || (BriefcaseId.Master === briefcaseId)) {
      return iModelDb;
    }
    throw new IModelError(IModelStatus.BadRequest, "IModelDb.openSnapshot cannot be used to open a briefcase", Logger.logError, loggerCategory);
  }


/** Open an iModel from iModelHub. IModelDb files are cached locally. The requested version may be downloaded from iModelHub to the
   * cache, or a previously downloaded version re-used from the cache - this behavior can optionally be configured through OpenParams.
   * Every open call must be matched with a call to close the IModelDb.
   * @param requestContext The client request context.
   * @param contextId Id of the Connect Project or Asset containing the iModel
   * @param iModelId Id of the iModel
   * @param version Version of the iModel to open
   * @param openParams Parameters to open the iModel
   */
public static async open(requestContext: AuthorizedClientRequestContext, contextId: string, iModelId: string, openParams: OpenParams = OpenParams.pullAndPush(), version: IModelVersion = IModelVersion.latest()): Promise<IModelDb> {
    requestContext.enter();
    const perfLogger = new PerfLogger("Opening iModel", () => ({ contextId, iModelId, ...openParams }));

    IModelDb.onOpen.raiseEvent(requestContext, contextId, iModelId, openParams, version);

    let briefcaseEntry: BriefcaseEntry;
    let timedOut = false;
    try {
      briefcaseEntry = await BriefcaseManager.open(requestContext, contextId, iModelId, openParams, version);
      requestContext.enter();

      if (briefcaseEntry.isPending) {
        if (typeof openParams.timeout === "undefined")
          await briefcaseEntry.isPending;
        else {
          const waitPromise = BeDuration.wait(openParams.timeout).then(() => {
            timedOut = true;
          });
          await Promise.race([briefcaseEntry.isPending, waitPromise]);
        }
      }
    } catch (error) {
      requestContext.enter();
      Logger.logError(loggerCategory, "Failed IModelDb.open", () => ({ token: requestContext.accessToken.toTokenString(), contextId, iModelId, ...openParams }));
      throw error;
    }

    if (timedOut) {
      Logger.logTrace(loggerCategory, "Issuing pending status in IModelDb.open", () => ({ contextId, iModelId, ...openParams }));
      throw new RpcPendingResponse();
    }

    const imodelDb = IModelDb.constructIModelDb(briefcaseEntry, openParams, contextId);
    try {
      const client = new UlasClient();
      const ulasEntry = new UsageLogEntry(os.hostname(), UsageType.Trial, contextId);
      await client.logUsage(requestContext, ulasEntry);
      requestContext.enter();
    } catch (error) {
      requestContext.enter();
      Logger.logError(loggerCategory, "Could not log usage information", () => ({ errorStatus: error.status, errorMessage: error.Message, iModelToken: imodelDb.iModelToken }));
    }
    IModelDb.onOpened.raiseEvent(requestContext, imodelDb);

    perfLogger.dispose();
    return imodelDb;
  }


/** Close this standalone iModel, if it is currently open
   * @throws IModelError if the iModel is not open, or is not standalone
   * @see [[closeSnapshot]]
   * @deprecated The confusing concept of *standalone* is being replaced by the more strict concept of a read-only iModel *snapshot*. Callers should migrate to [[closeSnapshot]].
   * @internal
   */
public closeStandalone(): void {
    if (!this.briefcase)
      throw this.newNotOpenError();
    if (!this.isStandalone)
      throw new IModelError(BentleyStatus.ERROR, "Cannot use to close a managed iModel. Use IModelDb.close() instead");
    BriefcaseManager.closeStandalone(this.briefcase);
    this.clearBriefcaseEntry();
  }


/** Close this local read-only iModel *snapshot*, if it is currently open.
   * > Note: A *snapshot* cannot be modified after this function is called.
   * @throws IModelError if the iModel is not open, or is not a *snapshot*.
   * @beta
   */
public closeSnapshot(): void {
    this.closeStandalone();
  }


/** Close this iModel, if it is currently open.
   * @param requestContext The client request context.
   * @param keepBriefcase Hint to discard or keep the briefcase for potential future use.
   * @throws IModelError if the iModel is not open, or is really a snapshot iModel
   */
public async close(requestContext: AuthorizedClientRequestContext, keepBriefcase: KeepBriefcase = KeepBriefcase.Yes): Promise<void> {
    requestContext.enter();
    if (!this.briefcase)
      throw this.newNotOpenError();
    if (this.isStandalone)
      throw new IModelError(BentleyStatus.ERROR, "Cannot use IModelDb.close() to close a snapshot iModel. Use IModelDb.closeSnapshot() instead");

    try {
      await BriefcaseManager.close(requestContext, this.briefcase, keepBriefcase);
    } catch (error) {
      throw error;
    } finally {
      requestContext.enter();
      this.clearBriefcaseEntry();
    }
  }


private forwardChangesetApplied() { this.onChangesetApplied.raiseEvent(); }


private setupBriefcaseEntry(briefcaseEntry: BriefcaseEntry) {
    briefcaseEntry.iModelDb = this;
    briefcaseEntry.onBeforeClose.addListener(this.onBriefcaseCloseHandler, this);
    briefcaseEntry.onBeforeVersionUpdate.addListener(this.onBriefcaseVersionUpdatedHandler, this);
    briefcaseEntry.onChangesetApplied.addListener(this.forwardChangesetApplied, this);
    this._briefcase = briefcaseEntry;
  }


private clearBriefcaseEntry(): void {
    const briefcaseEntry = this.briefcase;
    briefcaseEntry.onBeforeClose.removeListener(this.onBriefcaseCloseHandler, this);
    briefcaseEntry.onBeforeVersionUpdate.removeListener(this.onBriefcaseVersionUpdatedHandler, this);
    briefcaseEntry.onChangesetApplied.removeListener(this.forwardChangesetApplied, this);
    briefcaseEntry.iModelDb = undefined;
    this._briefcase = undefined;
  }


private onBriefcaseCloseHandler() {
    this.onBeforeClose.raiseEvent();
    this.clearStatementCache();
    this.clearSqliteStatementCache();
  }


private onBriefcaseVersionUpdatedHandler() { this.iModelToken.changeSetId = this.briefcase.currentChangeSetId; }


/** Get the briefcase Id of this iModel */
public getBriefcaseId(): BriefcaseId { return new BriefcaseId(this.briefcase === undefined ? BriefcaseId.Illegal : this.briefcase.briefcaseId); }


/** Returns a new IModelError with errorNumber, message, and meta-data set properly for a *not open* error.
   * @internal
   */
public newNotOpenError() {
    return new IModelError(IModelStatus.NotOpen, "IModelDb not open", Logger.logError, loggerCategory, () => ({ name: this.name, ...this.iModelToken }));
  }


/** Get a prepared ECSQL statement - may require preparing the statement, if not found in the cache.
   * @param ecsql The ECSQL statement to prepare
   * @returns the prepared statement
   * @throws IModelError if the statement cannot be prepared. Normally, prepare fails due to ECSQL syntax errors or references to tables or properties that do not exist. The error.message property will describe the property.
   */
private getPreparedStatement(ecsql: string): ECSqlStatement {
    const cachedStatement = this._statementCache.find(ecsql);
    if (cachedStatement !== undefined && cachedStatement.useCount === 0) {  // we can only recycle a previously cached statement if nobody is currently using it.
      cachedStatement.useCount++;
      return cachedStatement.statement;
    }

    this._statementCache.removeUnusedStatementsIfNecessary();
    const stmt = this.prepareStatement(ecsql);
    if (cachedStatement)
      this._statementCache.replace(ecsql, stmt);
    else
      this._statementCache.add(ecsql, stmt);

    return stmt;
  }


/** Use a prepared ECSQL statement. This function takes care of preparing the statement and then releasing it.
   *
   * As preparing statements can be costly, they get cached. When calling this method again with the same ECSQL,
   * the already prepared statement from the cache will be reused.
   *
   * See also:
   * - [ECSQL Overview]($docs/learning/backend/ExecutingECSQL)
   * - [Code Examples]($docs/learning/backend/ECSQLCodeExamples)
   *
   * @param ecsql The ECSQL statement to execute
   * @param callback the callback to invoke on the prepared statement
   * @returns the value returned by cb
   */
public withPreparedStatement<T>(ecsql: string, callback: (stmt: ECSqlStatement) => T): T {
    const stmt = this.getPreparedStatement(ecsql);
    const release = () => {
      if (stmt.isShared)
        this._statementCache.release(stmt);
      else
        stmt.dispose();
    };

    try {
      const val: T = callback(stmt);
      if (val instanceof Promise) {
        val.then(release, release);
      } else {
        release();
      }
      return val;
    } catch (err) {
      release();
      Logger.logError(loggerCategory, err.toString());
      throw err;
    }
  }


/** Compute number of rows that would be returned by the ECSQL.
   *
   * See also:
   * - [ECSQL Overview]($docs/learning/backend/ExecutingECSQL)
   * - [Code Examples]($docs/learning/backend/ECSQLCodeExamples)
   *
   * @param ecsql The ECSQL statement to execute
   * @param bindings The values to bind to the parameters (if the ECSQL has any).
   * Pass an *array* of values if the parameters are *positional*.
   * Pass an *object of the values keyed on the parameter name* for *named parameters*.
   * The values in either the array or object must match the respective types of the parameters.
   * See "[iModel.js Types used in ECSQL Parameter Bindings]($docs/learning/ECSQLParameterTypes)" for details.
   * @returns Return row count.
   * @throws [IModelError]($common) If the statement is invalid
   */
public async queryRowCount(ecsql: string, bindings?: any[] | object): Promise<number> {
    for await (const row of this.query(`select count(*) nRows from (${ecsql})`, bindings)) {
      return row.nRows;
    }
    throw new IModelError(DbResult.BE_SQLITE_ERROR, "Failed to get row count");
  }


/** Execute a query against this ECDb but restricted by quota and limit settings. This is intended to be used internally
   * The result of the query is returned as an array of JavaScript objects where every array element represents an
   * [ECSQL row]($docs/learning/ECSQLRowFormat).
   *
   * See also:
   * - [ECSQL Overview]($docs/learning/backend/ExecutingECSQL)
   * - [Code Examples]($docs/learning/backend/ECSQLCodeExamples)
   *
   * @param ecsql The ECSQL statement to execute
   * @param bindings The values to bind to the parameters (if the ECSQL has any).
   * Pass an *array* of values if the parameters are *positional*.
   * Pass an *object of the values keyed on the parameter name* for *named parameters*.
   * The values in either the array or object must match the respective types of the parameters.
   * See "[iModel.js Types used in ECSQL Parameter Bindings]($docs/learning/ECSQLParameterTypes)" for details.
   * @param limitRows Specify upper limit for rows that can be returned by the query.
   * @param quota Specify non binding quota. These values are constrained by global setting
   * but never the less can be specified to narrow down the quota constraint for the query but staying under global settings.
   * @param priority Specify non binding priority for the query. It can help user to adjust
   * priority of query in queue so that small and quicker queries can be prioritized over others.
   * @returns Returns structure containing rows and status.
   * See [ECSQL row format]($docs/learning/ECSQLRowFormat) for details about the format of the returned rows.
   * @internal
   */
public async queryRows(ecsql: string, bindings?: any[] | object, limit?: QueryLimit, quota?: QueryQuota, priority?: QueryPriority): Promise<QueryResponse> {
    if (!this._concurrentQueryInitialized) {
      this._concurrentQueryInitialized = this.nativeDb.concurrentQueryInit(IModelHost.configuration!.concurrentQuery);
    }
    if (!bindings) bindings = [];
    if (!limit) limit = {};
    if (!quota) quota = {};
    if (!priority) priority = QueryPriority.Normal;
    const base64Header = "encoding=base64;";
    // handle binary type
    const reviver = (_name: string, value: any) => {
      if (typeof value === "string") {
        if (value.length >= base64Header.length && value.startsWith(base64Header)) {
          const out = value.substr(base64Header.length);
          const buffer = Buffer.from(out, "base64");
          return new Uint8Array(buffer);
        }
      }
      return value;
    };
    // handle binary type
    const replacer = (_name: string, value: any) => {
      if (value && value.constructor === Uint8Array) {
        const buffer = Buffer.from(value);
        return base64Header + buffer.toString("base64");
      }
      return value;
    };
    return new Promise<QueryResponse>((resolve) => {
      const postrc = this.nativeDb.postConcurrentQuery(ecsql, JSON.stringify(bindings, replacer), limit!, quota!, priority!);
      if (postrc.status !== PostStatus.Done)
        resolve({ status: QueryResponseStatus.PostError, rows: [] });

      const poll = () => {
        const pollrc = this.nativeDb.pollConcurrentQuery(postrc.taskId);
        if (pollrc.status === PollStatus.Done)
          resolve({ status: QueryResponseStatus.Done, rows: JSON.parse(pollrc.result, reviver) });
        else if (pollrc.status === PollStatus.Partial)
          resolve({ status: QueryResponseStatus.Partial, rows: JSON.parse(pollrc.result, reviver) });
        else if (pollrc.status === PollStatus.Timeout)
          resolve({ status: QueryResponseStatus.Timeout, rows: [] });
        else if (pollrc.status === PollStatus.Pending)
          setTimeout(() => { poll(); }, IModelHost.configuration!.concurrentQuery.pollInterval);
        else
          resolve({ status: QueryResponseStatus.Error, rows: [pollrc.result] });
      };
      setTimeout(() => { poll(); }, IModelHost.configuration!.concurrentQuery.pollInterval);
    });
  }


/** Execute a query and stream its results
   * The result of the query is async iterator over the rows. The iterator will get next page automatically once rows in current page has been read.
   * [ECSQL row]($docs/learning/ECSQLRowFormat).
   *
   * See also:
   * - [ECSQL Overview]($docs/learning/backend/ExecutingECSQL)
   * - [Code Examples]($docs/learning/backend/ECSQLCodeExamples)
   *
   * @param ecsql The ECSQL statement to execute
   * @param bindings The values to bind to the parameters (if the ECSQL has any).
   * Pass an *array* of values if the parameters are *positional*.
   * Pass an *object of the values keyed on the parameter name* for *named parameters*.
   * The values in either the array or object must match the respective types of the parameters.
   * See "[iModel.js Types used in ECSQL Parameter Bindings]($docs/learning/ECSQLParameterTypes)" for details.
   * @param limitRows Specify upper limit for rows that can be returned by the query.
   * @param quota Specify non binding quota. These values are constrained by global setting
   * but never the less can be specified to narrow down the quota constraint for the query but staying under global settings.
   * @param priority Specify non binding priority for the query. It can help user to adjust
   * priority of query in queue so that small and quicker queries can be prioritized over others.
   * @returns Returns the query result as an *AsyncIterableIterator<any>*  which lazy load result as needed
   * See [ECSQL row format]($docs/learning/ECSQLRowFormat) for details about the format of the returned rows.
   * @throws [IModelError]($common) If there was any error while submitting, preparing or stepping into query
   */
public async * query(ecsql: string, bindings?: any[] | object, limitRows?: number, quota?: QueryQuota, priority?: QueryPriority): AsyncIterableIterator<any> {
    let result: QueryResponse;
    let offset: number = 0;
    let rowsToGet = limitRows ? limitRows : -1;
    do {
      result = await this.queryRows(ecsql, bindings, { maxRowAllowed: rowsToGet, startRowOffset: offset }, quota, priority);
      while (result.status === QueryResponseStatus.Timeout) {
        result = await this.queryRows(ecsql, bindings, { maxRowAllowed: rowsToGet, startRowOffset: offset }, quota, priority);
      }

      if (result.status === QueryResponseStatus.Error)
        throw new IModelError(DbResult.BE_SQLITE_ERROR, result.rows.length > 0 ? result.rows[0] : "Failed to execute ECSQL");

      if (rowsToGet > 0) {
        rowsToGet -= result.rows.length;
      }
      offset += result.rows.length;

      for (const row of result.rows)
        yield row;

    } while (result.status !== QueryResponseStatus.Done);
  }


/** Execute a query against this IModelDb.
   * The result of the query is returned as an array of JavaScript objects where every array element represents an
   * [ECSQL row]($docs/learning/ECSQLRowFormat).
   *
   * See also:
   * - [ECSQL Overview]($docs/learning/backend/ExecutingECSQL)
   * - [Code Examples]($docs/learning/backend/ECSQLCodeExamples)
   *
   * @param ecsql The ECSQL SELECT statement to execute
   * @param bindings The values to bind to the parameters (if the ECSQL has any).
   * Pass an *array* of values if the parameters are *positional*.
   * Pass an *object of the values keyed on the parameter name* for *named parameters*.
   * The values in either the array or object must match the respective types of the parameters.
   * See "[iModel.js Types used in ECSQL Parameter Bindings]($docs/learning/ECSQLParameterTypes)" for details.
   * @returns Returns the query result as an array of the resulting rows or an empty array if the query has returned no rows.
   * See [ECSQL row format]($docs/learning/ECSQLRowFormat) for details about the format of the returned rows.
   * @throws [IModelError]($common) If the statement is invalid or [IModelDb.maxLimit]($backend) exceeded when collecting ids.
   * @deprecated use withPreparedStatement or query or queryPage instead
   */
public executeQuery(ecsql: string, bindings?: any[] | object): any[] {
    return this.withPreparedStatement(ecsql, (stmt: ECSqlStatement) => {
      if (bindings)
        stmt.bindValues(bindings);
      const rows: any[] = [];
      while (DbResult.BE_SQLITE_ROW === stmt.step()) {
        rows.push(stmt.getRow());
        if (rows.length > IModelDb.maxLimit) {
          throw new IModelError(IModelStatus.BadRequest, "Max LIMIT exceeded in SELECT statement", Logger.logError, loggerCategory);
        }
      }
      return rows;
    });
  }


/** Use a prepared SQLite SQL statement. This function takes care of preparing the statement and then releasing it.
   * As preparing statements can be costly, they get cached. When calling this method again with the same ECSQL,
   * the already prepared statement from the cache will be reused.
   * @param sql The SQLite SQL statement to execute
   * @param callback the callback to invoke on the prepared statement
   * @returns the value returned by cb
   * @internal
   */
public withPreparedSqliteStatement<T>(sql: string, callback: (stmt: SqliteStatement) => T): T {
    const stmt = this.getPreparedSqlStatement(sql);
    const release = () => {
      if (stmt.isShared)
        this._sqliteStatementCache.release(stmt);
      else
        stmt.dispose();
    };
    try {
      const val: T = callback(stmt);
      if (val instanceof Promise) {
        val.then(release, release);
      } else {
        release();
      }
      return val;
    } catch (err) {
      release();
      Logger.logError(loggerCategory, err.toString());
      throw err;
    }
  }


/** Prepare an SQLite SQL statement.
   * @param sql The SQLite SQL statement to prepare
   * @throws [[IModelError]] if there is a problem preparing the statement.
   * @internal
   */
public prepareSqliteStatement(sql: string): SqliteStatement {
    const stmt = new SqliteStatement();
    stmt.prepare(this.briefcase.nativeDb, sql);
    return stmt;
  }


/** Get a prepared SQLite SQL statement - may require preparing the statement, if not found in the cache.
   * @param sql The SQLite SQL statement to prepare
   * @returns the prepared statement
   * @throws IModelError if the statement cannot be prepared. Normally, prepare fails due to SQL syntax errors or references to tables or properties that do not exist. The error.message property will describe the property.
   */
private getPreparedSqlStatement(sql: string): SqliteStatement {
    const cachedStatement: CachedSqliteStatement | undefined = this._sqliteStatementCache.find(sql);
    if (cachedStatement !== undefined && cachedStatement.useCount === 0) {  // we can only recycle a previously cached statement if nobody is currently using it.
      cachedStatement.useCount++;
      return cachedStatement.statement;
    }

    this._sqliteStatementCache.removeUnusedStatementsIfNecessary();
    const stmt: SqliteStatement = this.prepareSqliteStatement(sql);
    if (cachedStatement)
      this._sqliteStatementCache.replace(sql, stmt);
    else
      this._sqliteStatementCache.add(sql, stmt);
    return stmt;
  }


/** Query for a set of entity ids, given an EntityQueryParams
   * @param params The query parameters. The `limit` and `offset` members should be used to page results.
   * @returns an Id64Set with results of query
   * @throws [IModelError]($common) if the generated statement is invalid or [IModelDb.maxLimit]($backend) exceeded when collecting ids.
   *
   * *Example:*
   * ``` ts
   * [[include:ECSQL-backend-queries.select-element-by-code-value-using-queryEntityIds]]
   * ```
   */
public queryEntityIds(params: EntityQueryParams): Id64Set {
    let sql = "SELECT ECInstanceId FROM ";
    if (params.only)
      sql += "ONLY ";
    sql += params.from;
    if (params.where) sql += " WHERE " + params.where;
    if (typeof params.limit === "number" && params.limit > 0) sql += " LIMIT " + params.limit;
    if (typeof params.offset === "number" && params.offset > 0) sql += " OFFSET " + params.offset;
    if (params.orderBy) sql += " ORDER BY " + params.orderBy;

    const ids = new Set<string>();
    this.withPreparedStatement(sql, (stmt) => {
      for (const row of stmt) {
        if (row.id !== undefined) {
          ids.add(row.id);
          if (ids.size > IModelDb.maxLimit) {
            throw new IModelError(IModelStatus.BadRequest, "Max LIMIT exceeded in SELECT statement", Logger.logError, loggerCategory);
          }
        }
      }
    });
    return ids;
  }


/** Empty the [ECSqlStatementCache]($backend) for this iModel. */
public clearStatementCache(): void { this._statementCache.clear(); }


/** Empty the [SqliteStatementCache]($backend) for this iModel. */
public clearSqliteStatementCache(): void { this._sqliteStatementCache.clear(); }


/** Get the GUID of this iModel.  */
public getGuid(): GuidString { return this.nativeDb.getDbGuid(); }


/** Set the GUID of this iModel. */
public setGuid(guid: GuidString): DbResult { return this.nativeDb.setDbGuid(guid); }


/** Update the project extents for this iModel.
   *
Example:
   * ``` ts
   * [[include:IModelDb.updateProjectExtents]]
   * ```
   */
public updateProjectExtents(newExtents: AxisAlignedBox3d) {
    this.projectExtents = newExtents;
    this.updateIModelProps();
  }


/** Update the [EcefLocation]($docs/learning/glossary#eceflocation) of this iModel.  */
public updateEcefLocation(ecef: EcefLocation) {
    this.setEcefLocation(ecef);
    this.updateIModelProps();
  }


/** Update the IModelProps of this iModel in the database. */
public updateIModelProps() { this.nativeDb.updateIModelProps(JSON.stringify(this.toJSON())); }


/**
   * Commit pending changes to this iModel.
   * @note If this IModelDb is connected to an iModel, then you must call [[ConcurrencyControl.request]] before attempting to save changes.
   * @param _description Optional description of the changes
   * @throws [[IModelError]] if there is a problem saving changes or if there are pending, un-processed lock or code requests.
   */
public saveChanges(description?: string) {
    if (this.openParams.openMode === OpenMode.Readonly)
      throw new IModelError(IModelStatus.ReadOnly, "IModelDb was opened read-only", Logger.logError, loggerCategory);

    // TODO: this.Txns.onSaveChanges => validation, rules, indirect changes, etc.
    this.concurrencyControl.onSaveChanges();

    const stat = this.nativeDb.saveChanges(description);
    if (DbResult.BE_SQLITE_OK !== stat)
      throw new IModelError(stat, "Problem saving changes", Logger.logError, loggerCategory);

    this.concurrencyControl.onSavedChanges();
  }


/** Abandon pending changes in this iModel */
public abandonChanges() {
    this.concurrencyControl.abandonRequest();
    this.nativeDb.abandonChanges();
  }


/** Pull and Merge changes from iModelHub
   * @param requestContext The client request context.
   * @param version Version to pull and merge to.
   * @throws [[IModelError]] If the pull and merge fails.
   * @beta
   */
public async pullAndMergeChanges(requestContext: AuthorizedClientRequestContext, version: IModelVersion = IModelVersion.latest()): Promise<void> {
    requestContext.enter();
    this.concurrencyControl.onMergeChanges();
    await BriefcaseManager.pullAndMergeChanges(requestContext, this.briefcase, version);
    requestContext.enter();
    this.concurrencyControl.onMergedChanges();
    this._token.changeSetId = this.briefcase.currentChangeSetId;
    this.initializeIModelDb();
  }


/** Push changes to iModelHub
   * @param requestContext The client request context.
   * @param describer A function that returns a description of the changeset. Defaults to the combination of the descriptions of all local Txns.
   * @throws [[IModelError]] If the pull and merge fails.
   * @beta
   */
public async pushChanges(requestContext: AuthorizedClientRequestContext, describer?: ChangeSetDescriber): Promise<void> {
    requestContext.enter();
    const description = describer ? describer(this.txns.getCurrentTxnId()) : this.txns.describeChangeSet();
    await BriefcaseManager.pushChanges(requestContext, this.briefcase, description);
    requestContext.enter();
    this._token.changeSetId = this.briefcase.currentChangeSetId;
    this.initializeIModelDb();
  }


/** Reverse a previously merged set of changes
   * @param requestContext The client request context.
   * @param version Version to reverse changes to.
   * @throws [[IModelError]] If the reversal fails.
   * @beta
   */
public async reverseChanges(requestContext: AuthorizedClientRequestContext, version: IModelVersion = IModelVersion.latest()): Promise<void> {
    requestContext.enter();
    await BriefcaseManager.reverseChanges(requestContext, this.briefcase, version);
    requestContext.enter();
    this.initializeIModelDb();
  }


/** Reinstate a previously reversed set of changes
   * @param requestContext The client request context.
   * @param version Version to reinstate changes to.
   * @throws [[IModelError]] If the reinstate fails.
   * @beta
   */
public async reinstateChanges(requestContext: AuthorizedClientRequestContext, version: IModelVersion = IModelVersion.latest()): Promise<void> {
    requestContext.enter();
    await BriefcaseManager.reinstateChanges(requestContext, this.briefcase, version);
    requestContext.enter();
    this.initializeIModelDb();
  }


/** Set iModel as Master copy.
   * @param guid Optionally provide db guid. If its not provided the method would generate one.
   */
public setAsMaster(guid?: GuidString): void {
    if (guid === undefined) {
      if (DbResult.BE_SQLITE_OK !== this.nativeDb.setAsMaster())
        throw new IModelError(IModelStatus.SQLiteError, "", Logger.logWarning, loggerCategory);
    } else {
      if (DbResult.BE_SQLITE_OK !== this.nativeDb.setAsMaster(guid!))
        throw new IModelError(IModelStatus.SQLiteError, "", Logger.logWarning, loggerCategory);
    }
  }


/** Import an ECSchema. On success, the schema definition is stored in the iModel.
   * This method is asynchronous (must be awaited) because, in the case where this IModelId is a briefcase,
   * this method must first obtain the schema lock from the IModel server.
   * You must import a schema into an iModel before you can insert instances of the classes in that schema. See [[Element]]
   * @param requestContext The client request context
   * @param schemaFileName  Full path to an ECSchema.xml file that is to be imported.
   * @throws IModelError if the schema lock cannot be obtained.
   * @see containsClass
   */
public async importSchema(requestContext: ClientRequestContext | AuthorizedClientRequestContext, schemaFileName: string): Promise<void> {
    requestContext.enter();
    if (!this.briefcase)
      throw this.newNotOpenError();

    if (this.isStandalone) {
      const status = this.briefcase.nativeDb.importSchema(schemaFileName);
      if (DbResult.BE_SQLITE_OK !== status)
        throw new IModelError(status, "Error importing schema", Logger.logError, loggerCategory, () => ({ schemaFileName }));
      return;
    }

    if (!(requestContext instanceof AuthorizedClientRequestContext))
      throw new IModelError(AuthStatus.Error, "Importing the schema requires an AuthorizedClientRequestContext");
    await this.concurrencyControl.lockSchema(requestContext);
    requestContext.enter();

    const stat = this.briefcase.nativeDb.importSchema(schemaFileName);
    if (DbResult.BE_SQLITE_OK !== stat) {
      throw new IModelError(stat, "Error importing schema", Logger.logError, loggerCategory, () => ({ schemaFileName }));
    }

    try {
      // The schema import logic and/or imported Domains may have created new elements and models.
      // Make sure we have the supporting locks and codes.
      if (!(requestContext instanceof AuthorizedClientRequestContext))
        throw new IModelError(AuthStatus.Error, "Importing the schema requires an AuthorizedClientRequestContext");
      await this.concurrencyControl.request(requestContext);
      requestContext.enter();
    } catch (err) {
      requestContext.enter();
      this.abandonChanges();
      throw err;
    }
  }


/** Find an already open IModelDb. Used by the remoting logic.
   * @throws [[IModelError]] if an open IModelDb matching the token is not found.
   */
public static find(iModelToken: IModelToken): IModelDb {
    const briefcaseEntry = BriefcaseManager.findBriefcaseByToken(iModelToken);
    if (!briefcaseEntry || !briefcaseEntry.iModelDb) {
      Logger.logError(loggerCategory, "IModelDb not found in briefcase cache", () => iModelToken);
      throw new IModelNotFoundResponse();
    }
    Logger.logTrace(loggerCategory, "IModelDb found in briefcase cache", () => iModelToken);
    return briefcaseEntry.iModelDb;
  }


/** @internal */
public insertCodeSpec(codeSpec: CodeSpec): Id64String {
    if (!this.briefcase) throw this.newNotOpenError();
    const { error, result } = this.nativeDb.insertCodeSpec(codeSpec.name, codeSpec.specScopeType, codeSpec.scopeReq);
    if (error) throw new IModelError(error.status, "inserting CodeSpec" + codeSpec, Logger.logWarning, loggerCategory);
    return Id64.fromJSON(result);
  }


/** Prepare an ECSQL statement.
   * @param sql The ECSQL statement to prepare
   * @throws [[IModelError]] if there is a problem preparing the statement.
   */
public prepareStatement(sql: string): ECSqlStatement {
    if (!this.briefcase)
      throw this.newNotOpenError();
    const stmt = new ECSqlStatement();
    stmt.prepare(this.nativeDb, sql);
    return stmt;
  }


/** Construct an entity (Element or Model) from an iModel.
   * @throws [[IModelError]] if the entity cannot be constructed.
   */
public constructEntity<T extends Entity>(props: EntityProps): T {
    const jsClass = this.getJsClass(props.classFullName);
    return new jsClass(props, this) as T;
  }


/** Get the JavaScript class that handles a given entity class.  */
public getJsClass<T extends typeof Entity>(classFullName: string): T {
    try {
      return ClassRegistry.getClass(classFullName, this) as T;
    } catch (err) {
      if (!ClassRegistry.isNotFoundError(err)) {
        Logger.logError(loggerCategory, err.toString());
        throw err;
      }

      this.loadMetaData(classFullName);
      return ClassRegistry.getClass(classFullName, this) as T;
    }
  }


/** Get metadata for a class. This method will load the metadata from the iModel into the cache as a side-effect, if necessary.
   * @throws [IModelError]($common) if the metadata cannot be found nor loaded.
   */
public getMetaData(classFullName: string): EntityMetaData {
    let metadata = this.classMetaDataRegistry.find(classFullName);
    if (metadata === undefined) {
      this.loadMetaData(classFullName);
      metadata = this.classMetaDataRegistry.find(classFullName);
      if (metadata === undefined)
        throw ClassRegistry.makeMetaDataNotFoundError(classFullName); // do not log
    }
    return metadata;
  }


/**
   * Invoke a callback on each property of the specified class, optionally including superclass properties.
   * @param iModel  The IModel that contains the schema
   * @param classFullName The full class name to load the metadata, if necessary
   * @param wantSuper If true, superclass properties will also be processed
   * @param func The callback to be invoked on each property
   * @param includeCustom If true, include custom-handled properties in the iteration. Otherwise, skip custom-handled properties.
   */
public static forEachMetaData(iModel: IModelDb, classFullName: string, wantSuper: boolean, func: PropertyCallback, includeCustom: boolean) {
    const meta = iModel.getMetaData(classFullName); // will load if necessary
    for (const propName in meta.properties) {
      if (propName) {
        const propMeta = meta.properties[propName];
        if (includeCustom || !propMeta.isCustomHandled || propMeta.isCustomHandledOrphan)
          func(propName, propMeta);
      }
    }

    if (wantSuper && meta.baseClasses && meta.baseClasses.length > 0)
      meta.baseClasses.forEach((baseClass) => this.forEachMetaData(iModel, baseClass, true, func, includeCustom));
  }


/*** @internal */
private loadMetaData(classFullName: string) {
    if (this.classMetaDataRegistry.find(classFullName))
      return;

    const className = classFullName.split(":");
    if (className.length !== 2)
      throw new IModelError(IModelStatus.BadArg, "Invalid classFullName", Logger.logError, loggerCategory, () => ({ ...this._token, classFullName }));

    const val = this.nativeDb.getECClassMetaData(className[0], className[1]);
    if (val.error)
      throw new IModelError(val.error.status, "Error getting class meta data for: " + classFullName, Logger.logError, loggerCategory, () => ({ ...this._token, classFullName }));

    const metaData = new EntityMetaData(JSON.parse(val.result!));
    this.classMetaDataRegistry.add(classFullName, metaData);

    // Recursive, to make sure that base classes are cached.
    if (metaData.baseClasses !== undefined && metaData.baseClasses.length > 0)
      metaData.baseClasses.forEach((baseClassName: string) => this.loadMetaData(baseClassName));
  }


/** Query if this iModel contains the definition of the specified class.
   * @param classFullName The full name of the class, for example, SomeSchema:SomeClass
   * @returns true if the iModel contains the class definition or false if not.
   * @see importSchema
   */
public containsClass(classFullName: string): boolean {
    const className = classFullName.split(":");
    return className.length === 2 && this.nativeDb.getECClassMetaData(className[0], className[1]).error === undefined;
  }


/** Query a "file property" from this iModel, as a string.
   * @returns the property string or undefined if the property is not present.
   */
public queryFilePropertyString(prop: FilePropertyProps): string | undefined { return this.nativeDb.queryFileProperty(JSON.stringify(prop), true) as string | undefined; }


/** Query a "file property" from this iModel, as a blob.
   * @returns the property blob or undefined if the property is not present.
   */
public queryFilePropertyBlob(prop: FilePropertyProps): Uint8Array | undefined { return this.nativeDb.queryFileProperty(JSON.stringify(prop), false) as Uint8Array | undefined; }


/** Save a "file property" to this iModel
   * @param prop the FilePropertyProps that describes the new property
   * @param value either a string or a blob to save as the file property
   * @returns 0 if successful, status otherwise
   */
public saveFileProperty(prop: FilePropertyProps, strValue: string | undefined, blobVal?: Uint8Array): DbResult { return this.nativeDb.saveFileProperty(JSON.stringify(prop), strValue, blobVal); }


/** delete a "file property" from this iModel
   * @param prop the FilePropertyProps that describes the property
   * @returns 0 if successful, status otherwise
   */
public deleteFileProperty(prop: FilePropertyProps): DbResult { return this.nativeDb.saveFileProperty(JSON.stringify(prop), undefined, undefined); }


/** Query for the next available major id for a "file property" from this iModel.
   * @param prop the FilePropertyProps that describes the property
   * @returns the next available (that is, an unused) id for prop. If none are present, will return 0.
   */
public queryNextAvailableFileProperty(prop: FilePropertyProps) { return this.nativeDb.queryNextAvailableFileProperty(JSON.stringify(prop)); }


public async requestSnap(requestContext: ClientRequestContext, sessionId: string, props: SnapRequestProps): Promise<SnapResponseProps> {
    requestContext.enter();
    let request = this._snaps.get(sessionId);
    if (undefined === request) {
      request = new IModelHost.platform.SnapRequest();
      this._snaps.set(sessionId, request);
    } else
      request.cancelSnap();

    return new Promise<SnapResponseProps>((resolve, reject) => {
      request!.doSnap(this.nativeDb, JsonUtils.toObject(props), (ret: IModelJsNative.ErrorStatusOrResult<IModelStatus, SnapResponseProps>) => {
        this._snaps.delete(sessionId);
        if (ret.error !== undefined)
          reject(new Error(ret.error.message));
        else
          resolve(ret.result);
      });
    });
  }


/** Cancel a previously requested snap. */
public cancelSnap(sessionId: string): void {
    const request = this._snaps.get(sessionId);
    if (undefined !== request) {
      request.cancelSnap();
      this._snaps.delete(sessionId);
    }
  }


/** Get the IModel coordinate corresponding to each GeoCoordinate point in the input */
public async getIModelCoordinatesFromGeoCoordinates(requestContext: ClientRequestContext, props: string): Promise<IModelCoordinatesResponseProps> {
    requestContext.enter();
    const resultString: string = this.nativeDb.getIModelCoordinatesFromGeoCoordinates(props);
    return JSON.parse(resultString) as IModelCoordinatesResponseProps;
  }


/** Get the GeoCoordinate (longitude, latitude, elevation) corresponding to each IModel Coordinate point in the input */
public async getGeoCoordinatesFromIModelCoordinates(requestContext: ClientRequestContext, props: string): Promise<GeoCoordinatesResponseProps> {
    requestContext.enter();
    const resultString: string = this.nativeDb.getGeoCoordinatesFromIModelCoordinates(props);
    return JSON.parse(resultString) as GeoCoordinatesResponseProps;
  }