///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2025, Open Design Alliance (the "Alliance").
// All rights reserved.
//
// This software and its documentation and related materials are owned by
// the Alliance. The software may only be incorporated into application
// programs owned by members of the Alliance, subject to a signed
// Membership Agreement and Supplemental Software License Agreement with the
// Alliance. The structure and organization of this software are the valuable
// trade secrets of the Alliance and its suppliers. The software is also
// protected by copyright law and international treaty provisions. Application
// programs incorporating this software must include the following statement
// with their copyright notices:
//
//   This application incorporates Open Design Alliance software pursuant to a
//   license agreement with Open Design Alliance.
//   Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance.
//   All rights reserved.
//
// By use of this software, its documentation or related materials, you
// acknowledge and accept the above terms.
///////////////////////////////////////////////////////////////////////////////

import { IHttpClient } from "./IHttpClient";
import { Endpoint } from "./Endpoint";
import { ICdaNode, IFileStatus, IFileReferences, IFileVersionInfo, IGrantedTo } from "./IFile";
import { IShortUserDesc } from "./IUser";
import { Model } from "./Model";
import { Permission } from "./Permission";
import { Job } from "./Job";
import { SharedLink } from "./SharedLink";
import { ISharedLinkPermissions } from "./ISharedLink";
import { waitFor, parseArgs, userFullName, userInitials } from "./Utils";

/**
 * Provides properties and methods for obtaining information about a file on the Open Cloud Server and
 * managing its data and versions.
 */
export class File extends Endpoint {
  private _data: any;

  /**
   * @param data - Raw file data received from the server. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Files | Open Cloud Files API}.
   * @param httpClient - HTTP client instance used to send requests to the REST API server.
   */
  constructor(data: any, httpClient: IHttpClient) {
    super(`/files/${data.id}`, httpClient);
    this.data = data;
  }

  /**
   * Active version number of the file.
   *
   * @readonly
   */
  get activeVersion(): number {
    return this.data.activeVersion;
  }

  /**
   * File creation time (UTC) in the format specified in
   * {@link https://www.wikipedia.org/wiki/ISO_8601 | ISO 8601}.
   *
   * @readonly
   */
  get created(): string {
    return this.data.created;
  }

  /**
   * File custom fields object, to store custom data.
   */
  get customFields(): any {
    return this.data.customFields;
  }

  set customFields(value: any) {
    this.data.customFields = value;
  }

  /**
   * Raw file data received from the server. For more information, see
   * {@link https://cloud.opendesign.com/docs//pages/server/api.html#Files | Open Cloud Files API}.
   *
   * @readonly
   */
  get data(): any {
    return this._data;
  }

  set data(value: any) {
    this._data = value;
    this._data.previewUrl = value.preview
      ? `${this.httpClient.serverUrl}${this.path}/preview?updated=${value.updatedAt}`
      : "";
    // owner since 24.8
    if (typeof this._data.owner === "string") this._data.owner = { userId: this._data.owner };
    this._data.owner ??= {};
    this._data.owner.avatarUrl = `${this.httpClient.serverUrl}/users/${this._data.owner.userId}/avatar`;
    this._data.owner.fullName = userFullName(this._data.owner);
    this._data.owner.initials = userInitials(this._data.owner.fullName);
    // status since 24.9
    this._data.status ??= {};
    this._data.status.geometry ??= { state: this._data.geometryStatus ?? "none" };
    this._data.status.properties ??= { state: this._data.propertiesStatus ?? "none" };
    this._data.status.validation ??= { state: this._data.validationStatus ?? "none" };
    // updatedBy since 24.10
    this._data.updatedBy ??= {};
    this._data.updatedBy.avatarUrl = `${this.httpClient.serverUrl}/users/${this._data.updatedBy.userId}/avatar`;
    this._data.updatedBy.fullName = userFullName(this._data.updatedBy);
    this._data.updatedBy.initials = userInitials(this._data.updatedBy.fullName);
    // versions since 24.10
    this._data.versions ??= [{ ...value }];
    // geometryGltf status since 24.12
    this._data.status.geometryGltf ??= { state: "none" };
    // isFileDeleted since 25.7
    this._data.isFileDeleted ??= false;
    // sharedLinkToken since 26.0
    this._data.sharedLinkToken ??= null;
  }

  /**
   * Returns a list of file formats in which the active version of the file was exported.
   *
   * To export file to one of the supported formats create File Converter job using
   * {@link createJob | createJob()}. To download exported file use
   * {@link downloadResource | downloadResource()}.
   *
   * For an example of exporting files to other formats, see the {@link downloadResource} help.
   *
   * @readonly
   */
  get exports(): string[] {
    return this.data.exports;
  }

  /**
   * Geometry data type of the active file version. Can be one of:
   *
   * - `vsfx` - `VSFX` format, file can be opened in `VisualizeJS` viewer.
   * - `gltf` - `glTF` format, file can be opened in `Three.js` viewer.
   *
   * Returns an empty string if geometry data has not yet been converted. A files without geometry data
   * can be exported to other formas, but cannot be opened in viewer.
   */
  get geometryType(): string {
    if (this.status.geometryGltf.state === "done") return "gltf";
    else if (this.status.geometry.state === "done") return "vsfx";
    else return "";
  }

  /**
   * Unique file ID.
   *
   * @readonly
   */
  get id(): string {
    return this.data.id;
  }

  /**
   * Returns `true` if the source file of the active file version has been deleted.
   *
   * A files with deleted source file can be opened in the viewer, but cannot be exported to other
   * formats.
   *
   * @readonly
   */
  get isFileDeleted(): boolean {
    return this.data.isFileDeleted;
  }

  /**
   * File name, including the extension.
   */
  get name(): string {
    return this.data.name;
  }

  set name(value: string) {
    this.data.name = value;
  }

  /**
   * If the file is a version, then returns the ID of the original file. Otherwise, returns the file ID.
   *
   * @readonly
   */
  get originalFileId(): string {
    return this.data.originalFileId;
  }

  /**
   * File owner information.
   *
   * @readonly
   */
  get owner(): IShortUserDesc {
    return this.data.owner;
  }

  /**
   * File preview image URL or empty string if the file does not have a preview. Use
   * {@link setPreview | setPreview()} to change preview image.
   *
   * @readonly
   */
  get previewUrl(): string {
    return this.data.previewUrl;
  }

  /**
   * The size of the active version of the file in bytes.
   *
   * @readonly
   */
  get size(): number {
    return this.data.size;
  }

  /**
   * Total size of all versions of the file in bytes.
   *
   * @readonly
   */
  get sizeTotal(): number {
    return this.data.sizeTotal;
  }

  /**
   * File shared link token or `null` if file is not shared yet.
   *
   * @readonly
   */
  get sharedLinkToken(): string {
    return this.data.sharedLinkToken;
  }

  /**
   * Data status of the active version of the file. Contains:
   *
   * - `geometry` - status of geometry data of `vsfx` type.
   * - `geometryGltf` - status of geometry data of `gltf` type.
   * - `properties` - status of properties.
   * - `validation` - status of validation.
   *
   * Each status entity is a record with properties:
   *
   * - `state` - Data state. Can be `none`, `waiting`, `inprogress`, `done` or `failed`.
   * - `jobId` - Unique ID of the data job.
   *
   * @readonly
   */
  get status(): IFileStatus {
    return this.data.status;
  }

  /**
   * File type, matches the file extension.
   *
   * @readonly
   */
  get type(): string {
    return this.data.type;
  }

  /**
   * File last update time (UTC) in the format specified in
   * {@link https://www.wikipedia.org/wiki/ISO_8601 | ISO 8601}.
   *
   * @readonly
   */
  get updatedAt(): string {
    return this.data.updatedAt;
  }

  /**
   * Information about the user who made the last update.
   *
   * @readonly
   */
  get updatedBy(): IShortUserDesc {
    return this.data.updatedBy;
  }

  /**
   * Zero-based file version number for version files. The original file has version `0`.
   */

  get version(): number {
    return this.data.version;
  }

  /**
   * List of the file versions.
   *
   * @readonly
   */
  get versions(): IFileVersionInfo[] {
    return this.data.versions;
  }

  /**
   * Reloads file data from the server.
   */
  async checkout(): Promise<this> {
    const response = await this.get("");
    this.data = await response.json();
    return this;
  }

  /**
   * Updates file data on the server.
   *
   * @param data - Raw file data. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Files | Open Cloud Files API}.
   */
  async update(data: any): Promise<this> {
    const response = await this.put("", data);
    this.data = await response.json();
    return this;
  }

  /**
   * Deletes a file and all its versions from the server.
   *
   * You cannot delete a version file using `delete()`, only the original file. To delete a version file
   * use {@link deleteVersion | deleteVersion()}.
   *
   * @returns Returns the raw data of a deleted file. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Files | Open Cloud Files API}.
   */
  override delete(): Promise<any> {
    return super.delete("").then((response) => response.json());
  }

  /**
   * Saves file properties changes to the server. Call this method to update file data on the server
   * after any property changes.
   */
  save(): Promise<this> {
    return this.update(this.data);
  }

  /**
   * Sets or removes the file preview.
   *
   * @param image - Preview image. Can be a
   *   {@link https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | Data URL} string,
   *   {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer | ArrayBuffer},
   *   {@link https://developer.mozilla.org/docs/Web/API/Blob/Blob | Blob} or
   *   {@link https://developer.mozilla.org/docs/Web/API/File | Web API File} object. Setting the `image`
   *   to `null` will remove the preview.
   */
  async setPreview(image?: BodyInit | null): Promise<this> {
    if (!image) {
      await this.deletePreview();
    } else {
      const response = await this.post("/preview", image);
      this.data = await response.json();
    }
    return this;
  }

  /**
   * Removes the file preview.
   */
  async deletePreview(): Promise<this> {
    const response = await super.delete("/preview");
    this.data = await response.json();
    return this;
  }

  /**
   * Returns a list of models of the active version of the file.
   */
  getModels(): Promise<Model[]> {
    return this.get("/geometry")
      .then((response) => response.json())
      .then((array) => array.map((data) => new Model(data, this)));
  }

  // File does not support model transformation.

  getModelTransformMatrix(handle: string): any {
    return undefined;
  }

  setModelTransformMatrix(handle: string, transform?: any): Promise<this> {
    console.warn("File does not support model transformation");
    return Promise.resolve(this);
  }

  /**
   * Object properties.
   *
   * @typedef {any} Properties
   * @property {string} handle - Object original handle.
   * @property {string | any} - Object property. Can be `any` for nested properties.
   */

  /**
   * Returns the properties for an objects in the active version of the file.
   *
   * @param handles - Object original handle or handles array. Specify `undefined` to get properties for
   *   all objects in the file.
   */
  getProperties(handles?: string | string[]): Promise<any[]> {
    const relativePath = handles !== undefined ? `/properties?handles=${handles}` : "/properties";
    return this.get(relativePath).then((response) => response.json());
  }

  /**
   * Search pattern.
   *
   * @typedef {any} SearchPattern
   * @property {string} key - Property name.
   * @property {string} value - Property value.
   */

  /**
   * Query operator. Operator name can be `$and`, `$or`, `$not`, `$eq`, `$regex`.
   *
   * @typedef {any} QueryOperator
   * @property {string | SearchPattern[] | QueryOperator[]} * - Array of the query values or patterns for
   *   operator.
   */

  /**
   * Returns the list of original handles for an objects in the active version of the file that match the
   * specified patterns. Search patterns may be combined using query operators.
   *
   * @example Simple search pattern.
   *
   * ```javascript
   * searchPattern = {
   *   key: "Category",
   *   value: "OST_Stairs",
   * };
   * ```
   *
   * @example Search patterns combination.
   *
   * ```javascript
   * searchPattern = {
   *   $or: [
   *     {
   *       $and: [
   *         { key: "Category", value: "OST_GenericModel" },
   *         { key: "Level", value: "03 - Floor" },
   *       ],
   *     },
   *     { key: "Category", value: "OST_Stairs" },
   *   ],
   * };
   * ```
   *
   * @param {SeacrhPattern | QueryOperator} searchPattern - Search pattern or combination of the
   *   patterns, see example below.
   * @returns {Promise<Properties[]>}
   */

  searchProperties(searchPattern: any): Promise<any[]> {
    return this.post("/properties/search", searchPattern).then((response) => response.json());
  }

  /**
   * Returns the CDA tree for an active version of the file.
   */
  getCdaTree(): Promise<ICdaNode[]> {
    return this.get(`/properties/tree`).then((response) => response.json());
  }

  /**
   * Returns a list of file viewpoints. For more information, see
   * {@link https://cloud.opendesign.com/docs//pages/server/api.html#FileViewpoints | Open Cloud File Viewpoints API}.
   */
  getViewpoints(): Promise<any[]> {
    return this.get("/viewpoints")
      .then((response) => response.json())
      .then((viewpoints) => viewpoints.result);
  }

  /**
   * Saves a new file viewpoint to the server. To create a viewpoint use `Viewer.createViewpoint()`.
   *
   * @param viewpoint - Viewpoint object. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#FileViewpoints | Open Cloud File Viewpoints API}.
   */
  saveViewpoint(viewpoint: any): Promise<any> {
    return this.post("/viewpoints", viewpoint).then((response) => response.json());
  }

  /**
   * Deletes the specified file viewpoint.
   *
   * @param guid - Viewpoint GUID.
   * @returns Returns the raw data of a deleted viewpoint. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#FileViewpoints | Open Cloud File Viewpoints API}.
   */
  deleteViewpoint(guid: string): Promise<any> {
    return super.delete(`/viewpoints/${guid}`).then((response) => response.json());
  }

  /**
   * Returns viewpoint snapshot as base64-encoded
   * {@link https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | Data URL}.
   *
   * @param guid - Viewpoint GUID.
   */
  getSnapshot(guid: string): Promise<string> {
    return this.get(`/viewpoints/${guid}/snapshot`).then((response) => response.text());
  }

  /**
   * Returns viewpoint snapshot data.
   *
   * @param guid - Viewpoint GUID.
   * @param bitmapGuid - Bitmap GUID.
   */
  getSnapshotData(guid: string, bitmapGuid: string): Promise<string> {
    return this.get(`/viewpoints/${guid}/bitmaps/${bitmapGuid}`).then((response) => response.text());
  }

  /**
   * Downloads the source file of active version of the file from the server.
   *
   * @param onProgress - Download progress callback.
   * @param signal - An
   *   {@link https://developer.mozilla.org/docs/Web/API/AbortController | AbortController} signal. Allows
   *   to communicate with a fetch request and abort it if desired.
   */
  download(onProgress?: (progress: number) => void, signal?: AbortSignal): Promise<ArrayBuffer> {
    return this.httpClient
      .downloadFile(this.getEndpointPath("/downloads"), onProgress, { signal, headers: this.headers })
      .then((response) => response.arrayBuffer());
  }

  /**
   * Downloads a resource file of the active version of the file. Resource files are files that contain
   * model scene descriptions, or geometry data, or exported files.
   *
   * @example Export file to DWG.
   *
   * ```javascript
   * const job = await file.crateJob("dwg");
   * await job.waitForDone();
   * const dwgFileName = file.exports.find((x) => x.endsWith(".dwg"));
   * const arrayBuffer = await file.downloadResource(dwgFileName);
   * const blob = new Blob([arrayBuffer]);
   * const fileName = file.name + ".dwg";
   * FileSaver.saveAs(blob, fileName);
   * ```
   *
   * @param dataId - Resource file name.
   * @param onProgress - Download progress callback.
   * @param signal - An
   *   {@link https://developer.mozilla.org/docs/Web/API/AbortController | AbortController} signal. Allows
   *   to communicate with a fetch request and abort it if desired.
   */
  downloadResource(
    dataId: string,
    onProgress?: (progress: number, chunk: Uint8Array) => void,
    signal?: AbortSignal
  ): Promise<ArrayBuffer> {
    return this.httpClient
      .downloadFile(this.getEndpointPath(`/downloads/${dataId}`), onProgress, { signal, headers: this.headers })
      .then((response) => response.arrayBuffer());
  }

  /**
   * Downloads a part of resource file of the active version of the file. Resource files are files that
   * contain model scene descriptions, or geometry data, or exported files.
   *
   * @param dataId - Resource file name.
   * @param ranges - A range of resource file contents to download.
   * @param requestId - Request ID for download progress callback.
   * @param onProgress - Download progress callback.
   * @param signal - An
   *   {@link https://developer.mozilla.org/docs/Web/API/AbortController | AbortController} signal. Allows
   *   to communicate with a fetch request and abort it if desired.
   */
  downloadResourceRange(
    dataId: string,
    requestId: number,
    ranges: Array<{ begin: number; end: number; requestId: number }>,
    onProgress?: (progress: number, chunk: Uint8Array, requestId: number) => void,
    signal?: AbortSignal
  ): Promise<ArrayBuffer> {
    return this.httpClient
      .downloadFileRange(
        this.getEndpointPath(`/downloads/${dataId}?requestId=${requestId}`),
        requestId,
        ranges,
        onProgress,
        { signal, headers: this.headers }
      )
      .then((response) => response.arrayBuffer());
  }

  /**
   * Deprecated since `25.3`. Use {@link downloadResource | downloadResource()} instead.
   *
   * @deprecated
   */
  partialDownloadResource(
    dataId: string,
    onProgress?: (progress: number, downloaded: Uint8Array) => void,
    signal?: AbortSignal
  ): Promise<ArrayBuffer> {
    console.warn(
      "File.partialDownloadResource() has been deprecated since 25.3 and will be removed in a future release, use File.downloadResource() instead."
    );
    return this.downloadResource(dataId, onProgress, signal);
  }

  /**
   * Deprecated since `25.3`. Use {@link downloadResourceRange | downloadResourceRange()} instead.
   *
   * @deprecated
   */
  async downloadFileRange(
    requestId: number,
    records: any | null,
    dataId: string,
    onProgress?: (progress: number, downloaded: Uint8Array, requestId: number) => void,
    signal?: AbortSignal
  ): Promise<void> {
    await this.downloadResourceRange(dataId, requestId, records, onProgress, signal);
  }

  /**
   * Returns a list of file references.
   *
   * References are images, fonts, or any other files to correct rendering of the file.
   *
   * @param signal - An
   *   {@link https://developer.mozilla.org/docs/Web/API/AbortController | AbortController} signal, which
   *   can be used to abort waiting as desired.
   */
  getReferences(signal?: AbortSignal): Promise<IFileReferences> {
    return this.get("/references", signal).then((response) => response.json());
  }

  /**
   * Sets the file references.
   *
   * References are images, fonts, or any other files to correct rendering of the file. Reference files
   * must be uploaded to the server before they can be assigned to the current file.
   *
   * @param references - File references.
   */
  setReferences(references: IFileReferences): Promise<IFileReferences> {
    return this.put("/references", references).then((response) => response.json());
  }

  /**
   * Runs a new job on the server for the active version of the file.
   *
   * @param outputFormat - The job type. Can be one of:
   *
   *   - `geometry` - Convert file geometry data to `VSFX` format suitable for `VisualizeJS` viewer.
   *   - `geometryGltf` - Convert file geometry data to `glTF` format suitable for `Three.js` viewer.
   *   - `properties` - Extract file properties.
   *   - `validation` - Validate the file. Only for `IFC` files.
   *   - `dwg`, `obj`, `gltf`, `glb`, `vsf`, `pdf`, `3dpdf` - Export file to the one of the supported format.
   *       Use {@link exports | exports()} to get the list of completed file exports. Use
   *       {@link downloadResource | downloadResource()} to download the exported file.
   *   - Other custom job name. Custom job runner must be registered in the job templates before creating a
   *       job.
   *
   * @param parameters - Parameters for the job runner. Can be given as command line arguments for the
   *   File Converter tool in form `--arg=value`.
   */
  createJob(outputFormat: string, parameters?: string | object): Promise<Job> {
    const jobs = new Endpoint("/jobs", this.httpClient, this.headers);
    return jobs
      .post(this.appendVersionParam(""), {
        fileId: this.id,
        outputFormat,
        parameters: parseArgs(parameters),
      })
      .then((response) => response.json())
      .then((data) => new Job(data, this.httpClient));
  }

  /**
   * Runs a job to convert geometry data of active version of the file. This is alias to
   * {@link createJob | createJob("geometry")}.
   *
   * @param type - Geometry data type. Can be one of:
   *
   *   - `vsfx` - `VSFX` format (default), for opening a file in `VisualizeJS` viewer.
   *   - `gltf` - `glTF` format, for opening a file in `Three.js` viewer.
   *
   * @param parameters - Parameters for the job runner. Can be given as command line arguments for the
   *   File Converter tool in form `--arg=value`.
   */
  extractGeometry(type?: string, parameters?: string | object): Promise<Job> {
    return this.createJob(type === "gltf" ? "geometryGltf" : "geometry", parameters);
  }

  /**
   * Runs a job to extract properties of the active version of the file. This is alias to
   * {@link createJob | createJob("properties")}.
   *
   * @param parameters - Parameters for the job runner. Can be given as command line arguments for the
   *   File Converter tool in form `--arg=value`.
   */
  extractProperties(parameters?: string | object): Promise<Job> {
    return this.createJob("properties", parameters);
  }

  /**
   * Runs a job to validate the active version of the file. This is alias to
   * {@link createJob | createJob("validation")}.
   *
   * To get validation report use {@link downloadResource | downloadResource("validation_report.json")}.
   *
   * @param parameters - Parameters for the job runner. Can be given as command line arguments for the
   *   File Converter tool in form `--arg=value`.
   */
  validate(parameters?: string | object): Promise<Job> {
    return this.createJob("validation", parameters);
  }

  /**
   * Waits for jobs of the active version of the file to be done. Job is done when it changes to `none`,
   * `done` or `failed` status.
   *
   * @param jobs - Job or job array to wait on. Can be `geometry`, `geometryGltf`, `properties`,
   *   `validation`, `dwg`, `obj`, `gltf`, `glb`, `vsf`, `pdf`, `3dpdf` or custom job name.
   * @param waitAll - If this parameter is `true`, the function returns when all the specified jobs have
   *   done. If `false`, the function returns when any one of the jobs are done.
   * @param params - An object containing waiting parameters.
   * @param params.timeout - The time, in milliseconds that the function should wait jobs. If no one jobs
   *   are done during this time, the `TimeoutError` exception will be thrown.
   * @param params.interval - The time, in milliseconds, the function should delay in between checking
   *   jobs status.
   * @param params.signal - An
   *   {@link https://developer.mozilla.org/docs/Web/API/AbortController | AbortController} signal, which
   *   can be used to abort waiting as desired.
   * @param params.onCheckout - Waiting progress callback. Return `true` to cancel waiting.
   */
  waitForDone(
    jobs: string | string[],
    waitAll?: boolean,
    params?: {
      timeout?: number;
      interval?: number;
      signal?: AbortSignal;
      onCheckout?: (file: File, ready: boolean) => boolean;
    }
  ): Promise<this> {
    const waitJobs = Array.isArray(jobs) ? jobs : [jobs];
    if (waitAll === undefined) waitAll = true;

    const checkDone = () =>
      this.checkout().then((file) => {
        const readyJobs = waitJobs.filter((job: string) => {
          const jobStatus = file.status[job] || {};
          return ["none", "done", "failed"].includes(jobStatus.state || "none");
        });
        const ready = waitAll ? readyJobs.length === waitJobs.length : readyJobs.length > 0;
        const cancel = params?.onCheckout?.(file, ready);
        return cancel || ready;
      });

    return waitFor(checkDone, params).then(() => this);
  }

  /**
   * Returns a list of file permissions.
   */
  getPermissions(): Promise<Permission[]> {
    return this.get("/permissions")
      .then((response) => response.json())
      .then((array) => array.map((data) => new Permission(data, this.id, this.httpClient)));
  }

  /**
   * Returns information about specified file permission.
   *
   * @param permissionId - Permission ID.
   */
  getPermission(permissionId: string): Promise<Permission> {
    return this.get(`/permissions/${permissionId}`)
      .then((response) => response.json())
      .then((data) => new Permission(data, this.id, this.httpClient));
  }

  /**
   * Creates a new file permission for a user, project, or group.
   *
   * @example Grant the specified user permission to "update" the file.
   *
   * ```javascript
   * const action = "update";
   * const grantedTo = [{ user: { id: myUser.id, email: myUser.email } }];
   * await file.createPermission(action, grantedTo);
   * ```
   *
   * @example Add a file to the specified project in "read-only" mode.
   *
   * ```javascript
   * const actions = ["read", "readSourceFile"];
   * const grantedTo = [{ project: { id: myProject.id, name: myProject.name } }];
   * await file.createPermission(actions, grantedTo);
   * ```
   *
   * @param actions - Actions are allowed to be performed on a file with this permission:
   *
   *   - `read` - The ability to read file description, geometry data and properties.
   *   - `readSourceFile` - The ability to download source file.
   *   - `write` - The ability to modify file name, description and references.
   *   - `readViewpoint` - The ability to read file viewpoints.
   *   - `createViewpoint` - The ability to create file viewpoints.
   *
   * @param grantedTo - A list of entities that will get access to the file.
   * @param _public - Specifies whether all users have access to the file or not.
   */
  createPermission(actions: string | string[], grantedTo: IGrantedTo[], _public: boolean): Promise<Permission> {
    return this.post("/permissions", {
      actions: Array.isArray(actions) ? actions : [actions],
      grantedTo,
      public: _public,
    })
      .then((response) => response.json())
      .then((data) => new Permission(data, this.id, this.httpClient));
  }

  /**
   * Removes the specified permission from the file.
   *
   * @param permissionId - Permission ID.
   * @returns Returns the raw data of a deleted permission. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Permission | Open Cloud File Permissions API}.
   */
  deletePermission(permissionId: string): Promise<any> {
    return super.delete(`/permissions/${permissionId}`).then((response) => response.json());
  }

  /**
   * Uploads the new version of the file to the server, convert the geometry data and extract properties
   * as needed.
   *
   * @param file - {@link https://developer.mozilla.org/docs/Web/API/File | Web API File} object are
   *   generally retrieved from a {@link https://developer.mozilla.org/docs/Web/API/FileList | FileList}
   *   object returned as a result of a user selecting files using the HTML `<input>` element.
   * @param params - An object containing upload parameters.
   * @param params.geometry - Create job to convert file geometry data. The geometry data type is the
   *   same as the original file.
   * @param params.properties - Create job to extract file properties.
   * @param params.waitForDone - Wait for geometry and properties jobs to complete.
   * @param params.timeout - The time, in milliseconds that the function should wait jobs. If no one jobs
   *   are done during this time, the `TimeoutError` exception will be thrown.
   * @param params.interval - The time, in milliseconds, the function should delay in between checking
   *   jobs status.
   * @param params.signal - An
   *   {@link https://developer.mozilla.org/docs/Web/API/AbortController | AbortController} signal, which
   *   can be used to abort waiting as desired.
   * @param params.onProgress - Upload progress callback.
   */

  async uploadVersion(
    file: globalThis.File,
    params: {
      geometry?: boolean;
      properties?: boolean;
      waitForDone?: boolean;
      timeout?: number;
      interval?: number;
      signal?: AbortSignal;
      onProgress?: (progress: number, file: globalThis.File) => void;
    } = {
      waitForDone: false,
    }
  ): Promise<File> {
    const result = await this.httpClient
      .uploadFile(this.getEndpointPath("/versions"), file, (progress) => params.onProgress?.(progress, file), {
        headers: this.headers,
      })
      .then((xhr: XMLHttpRequest) => JSON.parse(xhr.responseText))
      .then((data) => new File(data, this.httpClient));

    let geometryType = "";
    if (this.versions[0].status.geometryGltf.state !== "none") geometryType = "gltf";
    if (this.versions[0].status.geometry.state !== "none") geometryType = "vsfx";

    params = { ...params };
    if (params.geometry === undefined) params.geometry = geometryType !== "";
    if (params.properties === undefined) params.properties = this.versions[0].status.properties.state !== "none";

    const jobs: string[] = [];
    if (params.geometry) jobs.push((await result.extractGeometry(geometryType)).outputFormat);
    if (params.properties) jobs.push((await result.extractProperties()).outputFormat);
    if (jobs.length > 0)
      if (params.waitForDone) await result.waitForDone(jobs, true, params);
      else await result.checkout();

    await this.checkout();

    return result;
  }

  /**
   * Returns a list of version files.
   */
  getVersions(): Promise<File[]> {
    return this.get("/versions")
      .then((response) => response.json())
      .then((files) => files.map((data) => new File(data, this.httpClient)))
      .then((files) => files.map((file) => (file.id == file.originalFileId ? file.useVersion(0) : file)));
  }

  /**
   * Returns information about the specified version file.
   *
   * @param version - Desired version.
   */
  getVersion(version: number): Promise<File> {
    return this.get(`/versions/${version}`)
      .then((response) => response.json())
      .then((data) => new File(data, this.httpClient))
      .then((file) => (file.id == file.originalFileId ? file.useVersion(0) : file));
  }

  /**
   * Deletes the specified version file.
   *
   * @param version - Version to delete.
   * @returns Returns the raw data of a deleted version file. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Files | Open Cloud Files API}.
   */
  async deleteVersion(version: number): Promise<any> {
    const response = await super.delete(`/versions/${version}`);
    const data = await response.json();
    await this.checkout();
    return data;
  }

  /**
   * Replaces the active version of the file with the selected version.
   *
   * @param version - Desired active version.
   */

  setActiveVersion(version: number): Promise<this> {
    return this.update({ activeVersion: version });
  }

  /**
   * Makes the given version active on client side. Does not change the active file version on the
   * server.
   *
   * This version change will affect the result:
   *
   * - {@link getModels | getModels()}
   * - {@link getProperties | getProperties()}
   * - {@link searchProperties | searchProperties()}
   * - {@link getCdaTree | getCdaTree()}
   * - {@link download | download()}
   * - {@link downloadResource | downloadResource()}
   * - {@link createJob | createJob()}
   * - {@link extractGeometry | extractGeometry()}
   * - {@link extractProperties | extractProperties()}
   * - {@link validate | validate()}
   * - {@link waitForDone | waitForDone()}
   * - Viewer.open()
   *
   * Other clients will still continue to use the current active version of the file. Use `undefined` to
   * revert back to the active version.
   *
   * You need to reload the file data using {@link checkout | checkout()} to match the size and status
   * fields to the version you selected.
   */
  override useVersion(version?: number): this {
    return super.useVersion(version);
  }

  /**
   * Deletes the source file of the active file version from the server.
   */
  async deleteSource(): Promise<this> {
    const response = await super.delete("/source");
    this.data = await response.json();
    return this;
  }

  /**
   * Creates a file shared link.
   *
   * @param permissions - Share permissions.
   */
  async createSharedLink(permissions?: ISharedLinkPermissions): Promise<SharedLink> {
    const shares = new Endpoint("/shares", this.httpClient, this.headers);
    const response = await shares.post("", { fileId: this.id, permissions });
    const data = await response.json();
    await this.checkout();
    return new SharedLink(data, this.httpClient);
  }

  /**
   * Returns information about the file shared link or `undefined` if file is not shared.
   */
  async getSharedLink(): Promise<SharedLink> {
    if (!this.sharedLinkToken) return Promise.resolve(undefined);
    const shares = new Endpoint("/shares", this.httpClient, this.headers);
    const response = await shares.get(`/${this.sharedLinkToken}`);
    const data = await response.json();
    return new SharedLink(data, this.httpClient);
  }

  /**
   * Deletes the file shared link.
   *
   * @returns Returns the raw data of a deleted shared link. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#ShareLinks | Open Cloud SharedLinks API}.
   */
  async deleteSharedLink(): Promise<any> {
    const shares = new Endpoint("/shares", this.httpClient, this.headers);
    const response = await shares.delete(`/${this.sharedLinkToken}`);
    const data = await response.json();
    await this.checkout();
    return data;
  }
}
