///////////////////////////////////////////////////////////////////////////////
// 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 { EventEmitter2 } from "@inweb/eventemitter2";

import { IHttpClient } from "./IHttpClient";
import { HttpClient } from "./HttpClient";
import { FetchError } from "./FetchError";
import { ClientEventMap } from "./ClientEvents";
import { Assembly } from "./Assembly";
import { File } from "./File";
import { Job } from "./Job";
import { Project } from "./Project";
import { User } from "./User";
import { OAuthClient } from "./OAuthClient";
import { ISharedLinkPermissions } from "./ISharedLink";
import { SharedLink } from "./SharedLink";
import { SharedFile } from "./SharedFile";
import { parseArgs } from "./Utils";

/**
 * Provides methods for managing Open Cloud Server resources such as users, files, assemblies, jobs,
 * projects, etc.
 */
export class Client extends EventEmitter2<ClientEventMap> {
  private _serverUrl = "";
  private _httpClient: IHttpClient = new HttpClient("");
  private _user: User | null = null;
  public eventEmitter: EventEmitter2 = this;

  /**
   * @param params - An object containing client configuration parameters.
   * @param params.serverUrl - Open Cloud REST API server URL.
   * @param params.url - Deprecated since `25.8`. Use `serverUrl` instead.
   */
  constructor(params: { serverUrl?: string; url?: string } = {}) {
    super();
    this.configure(params);
  }

  /**
   * Open Cloud REST API server URL. Use {@link configure | configure()} to change server URL.
   *
   * @readonly
   */
  get serverUrl(): string {
    return this._serverUrl;
  }

  /**
   * HTTP client instance used to send requests to the REST API server.
   *
   * @readonly
   */
  get httpClient(): IHttpClient {
    return this._httpClient;
  }

  /**
   * Deprecated since `25.3`. Use `Viewer.options()` instead to change `Viewer` parameters.
   *
   * @deprecated
   */
  get options(): any {
    console.warn(
      "Client.options has been deprecated since 25.3 and will be removed in a future release, use Viewer.options instead."
    );
    const data = {
      showWCS: true,
      cameraAnimation: true,
      antialiasing: true,
      groundShadow: false,
      shadows: false,
      cameraAxisXSpeed: 4,
      cameraAxisYSpeed: 1,
      ambientOcclusion: false,
      enableStreamingMode: true,
      enablePartialMode: false,
      memoryLimit: 3294967296,
      cuttingPlaneFillColor: { red: 0xff, green: 0x98, blue: 0x00 },
      edgesColor: { r: 0xff, g: 0x98, b: 0x00 },
      facesColor: { r: 0xff, g: 0x98, b: 0x00 },
      edgesVisibility: true,
      edgesOverlap: true,
      facesOverlap: false,
      facesTransparancy: 200,
      enableCustomHighlight: true,
      sceneGraph: false,
      edgeModel: true,
      reverseZoomWheel: false,
      enableZoomWheel: true,
      enableGestures: true,
    };
    return {
      ...data,
      data,
      defaults: () => data,
      resetToDefaults: () => {},
      saveToStorage: () => {},
      loadFromStorage: () => {},
    };
  }

  /**
   * Changes the client parameters.
   *
   * After changing the parameters, you must re-login.
   *
   * @param params - An object containing new parameters.
   * @param params.serverUrl - Open Cloud REST API server URL.
   */
  configure(params: { serverUrl?: string }): this {
    this._serverUrl = (params.serverUrl || "").replace(/\/+$/, "");
    this._httpClient = new HttpClient(this.serverUrl);
    this._user = null;
    return this;
  }

  /**
   * Returns client and server versions.
   *
   * No login is required to obtain the version.
   */
  version(): Promise<{ server: string; client: string; hash: string }> {
    return this.httpClient
      .get("/version")
      .then((response) => response.json())
      .then((data) => ({
        ...data,
        server: data.version,
        client: "CLIENT_JS_VERSION",
      }));
  }

  /**
   * Registers a new user on the server.
   *
   * No login is required to register a new user.
   *
   * @param email - User email. Cannot be empty. Must be unique within the server.
   * @param password - User password. Cannot be empty. Password can only contain letters (a-z, A-Z),
   *   numbers (0-9), and special characters (~!@#$%^&*()_-+={}[]<>|/'":;.,?).
   * @param userName - User name. Cannot be empty or blank if defined. this to `undefined` to use
   *   `username` from email.
   */
  registerUser(email: string, password: string, userName?: string): Promise<any> {
    return this.httpClient
      .post("/register", {
        email,
        password,
        userName: userName ?? (email + "").split("@").at(0),
      })
      .then((response) => response.json());
  }

  /**
   * Resends a Confirmation Email to the new user. If the user's email is already confirmed, an exception
   * will be thrown.
   *
   * @param email - User email.
   * @param password - User password.
   */
  resendConfirmationEmail(email: string, password: string): Promise<any> {
    return this.httpClient
      .post("/register/email-confirmation", { email, password })
      .then((response) => response.json());
  }

  /**
   * Marks the user's email address as confirmed. If the user's email is already confirmed, an exception
   * will be thrown.
   *
   * @param emailConfirmationId - Confirmation code from the Confirmation Email.
   */
  confirmUserEmail(emailConfirmationId: string): Promise<any> {
    return this.httpClient
      .get(`/register/email-confirmation/${emailConfirmationId}`)
      .then((response) => response.json());
  }

  /**
   * Log in an existing user using email or user name.
   *
   * @param email - An email or user name for authentication request.
   * @param password - Password for authentication request.
   */
  async signInWithEmail(email: string, password: string): Promise<User> {
    const credentials = btoa(unescape(encodeURIComponent(email + ":" + password)));
    this.httpClient.headers["Authorization"] = "Basic " + credentials;
    const response = await this.httpClient.get("/token");
    const data = await response.json();
    return this.setCurrentUser(data);
  }

  /**
   * Log in an existing user using access token (API Key).
   *
   * @param token - An access token for authentication request. See {@link User.token} for more details.
   */
  async signInWithToken(token: string): Promise<User> {
    this.httpClient.headers["Authorization"] = token;
    const response = await this.httpClient.get("/user");
    const data = await response.json();
    return this.setCurrentUser(data);
  }

  /**
   * Log out.
   *
   * You must log in again using {@link signInWithEmail} or {@link signInWithToken} to continue making
   * requests to the server
   */
  signOut(): void {
    this.clearCurrentUser();
  }

  // Save the current logged in user information for internal use.

  private setCurrentUser(data: any): User {
    this._user = new User(data, this.httpClient);
    this.httpClient.headers["Authorization"] = data.tokenInfo.token;
    this.httpClient.signInUserId = this._user.id;
    this.httpClient.signInUserIsAdmin = this._user.isAdmin;
    return this._user;
  }

  private clearCurrentUser(): void {
    this._user = null;
    delete this.httpClient.headers["Authorization"];
    this.httpClient.signInUserId = "";
    this.httpClient.signInUserIsAdmin = false;
  }

  /**
   * Returns the current logged in user. Returns `null` if the user is not logged in or the logged in
   * user has deleted themself.
   */
  getCurrentUser(): User | null {
    if (this._user && !this.httpClient.signInUserId) this._user = null;
    return this._user;
  }

  /**
   * Returns the list of server enabled indentity providers.
   */
  getIdentityProviders(): Promise<{ name: string; url: string }[]> {
    return this.httpClient.get("/identity").then((response) => response.json());
  }

  /**
   * Returns the current server settings.
   *
   * @returns Returns an object with server settings. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Settings | Open Cloud Settings API}.
   */
  getServerSettings(): Promise<any> {
    return this.httpClient.get("/settings").then((response) => response.json());
  }

  /**
   * Changes the server settings.
   *
   * Only administrators can change server settings. If the current logged in user is not an
   * administrator, an exception will be thrown.
   *
   * @param settings - An object with the new server settings or part of the settings. For more
   *   information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Settings | Open Cloud Settings API}.
   * @returns Returns an object with updated server settings.
   */
  updateServerSettings(settings: any): Promise<any> {
    return this.httpClient.put("/settings", settings).then((response) => response.json());
  }

  /**
   * Result for OAuth client list.
   *
   * @typedef {any} OAuthClientsResult
   * @property {OAuthClient[]} result - Result client list.
   * @property {number} start - The starting index in the client list in the request.
   * @property {number} limit - The maximum number of requested clients.
   * @property {number} allSize - Total number of OAuth clients on the server.
   * @property {number} size - The number of clients in the result list.
   */

  /**
   * Returns a list of OAuth clients of the server.
   *
   * Only administrators can get a list of OAuth clients. If the current logged in user is not an
   * administrator, an exception will be thrown.
   *
   * @param start - The starting index in the client list. Used for paging.
   * @param limit - The maximum number of clients that should be returned per request. Used for paging.
   */
  getOAuthClients(
    start?: number,
    limit?: number
  ): Promise<{
    result: OAuthClient[];
    start: number;
    limit: number;
    allSize: number;
    size: number;
  }> {
    const searchParams = new URLSearchParams();
    if (start > 0) searchParams.set("start", start.toString());
    if (limit > 0) searchParams.set("limit", limit.toString());

    let queryString = searchParams.toString();
    if (queryString) queryString = "?" + queryString;

    return this.httpClient
      .get(`/oauth/clients${queryString}`)
      .then((response) => response.json())
      .then((clients) => {
        return {
          ...clients,
          result: clients.result.map((data) => new OAuthClient(data, this.httpClient)),
        };
      });
  }

  /**
   * Returns information about the specified OAuth client.
   *
   * Only administrators can get OAuth clients. If the current logged in user is not an administrator, an
   * exception will be thrown.
   *
   * @param clientId - Client ID.
   */
  getOAuthClient(clientId: string): Promise<OAuthClient> {
    return this.httpClient
      .get(`/oauth/clients/${clientId}`)
      .then((response) => response.json())
      .then((data) => new OAuthClient(data, this.httpClient));
  }

  /**
   * Creates a new OAuth client on the server.
   *
   * Only administrators can create OAuth clients. If the current logged in user is not an administrator,
   * an exception will be thrown.
   *
   * @param name - Client name.
   * @param redirectUrl - Endpoint to which the OAuth 2.0 server sends the response.
   * @param description - Client description.
   */
  createOAuthClient(name: string, redirectUrl: string, description?: string): Promise<OAuthClient> {
    return this.httpClient
      .post("/oauth/clients", {
        name,
        redirectUrl,
        description,
      })
      .then((response) => response.json())
      .then((data) => new OAuthClient(data, this.httpClient));
  }

  /**
   * Deletes the specified OAuth client from the server.
   *
   * Only administrators can delete OAuth clients. If the current logged in user is not an administrator,
   * an exception will be thrown.
   *
   * @param clientId - Client ID.
   * @returns Returns the raw data of a deleted client. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#OAuthClient | Open Cloud OAuth Clients API}.
   */
  deleteOAuthClient(clientId: string): Promise<any> {
    return this.httpClient.delete(`/oauth/clients/${clientId}`).then((response) => response.json());
  }

  /**
   * Returns the list of server users.
   *
   * Only administrators can get a list of users. If the current logged in user is not an administrator,
   * an exception will be thrown.
   */
  getUsers(): Promise<User[]> {
    return this.httpClient
      .get("/users")
      .then((response) => response.json())
      .then((array) => array.map((data) => ({ id: data.id, ...data.userBrief })))
      .then((array) => array.map((data) => new User(data, this.httpClient)));
  }

  /**
   * Returns information about the specified user.
   *
   * Only administrators can get other users. If the current logged in user is not an administrator, they
   * can only get themselves, otherwise an exception will be thrown.
   *
   * @param userId - User ID.
   */
  getUser(userId: string): Promise<User> {
    if (this.httpClient.signInUserIsAdmin) {
      return this.httpClient
        .get(`/users/${userId}`)
        .then((response) => response.json())
        .then((data) => ({ id: data.id, ...data.userBrief }))
        .then((data) => new User(data, this.httpClient));
    } else if (userId === this.httpClient.signInUserId) {
      return this.httpClient
        .get("/user")
        .then((response) => response.json())
        .then((data) => ({ id: userId, ...data }))
        .then((data) => new User(data, this.httpClient));
    } else {
      return Promise.reject(new FetchError(403));
    }
  }

  /**
   * Creates a new user on the server.
   *
   * Only administrators can create users. If the current logged in user is not an administrator, an
   * exception will be thrown.
   *
   * @param email - User email. Cannot be empty. Must be unique within the server.
   * @param password - User password. Cannot be empty. Password can only contain latin letters (a-z,
   *   A-Z), numbers (0-9), and special characters (~!@#$%^&*()_-+={}[]<>|/'":;.,?).
   * @param params - Additional user data.
   * @param params.isAdmin - `true` if user is an administrator.
   * @param params.userName - User name. Cannot be empty or blank if defined. Specify `undefined` to use
   *   `username` from email.
   * @param params.firstName - First name.
   * @param params.lastName - Last name.
   * @param params.canCreateProject - `true` if user is allowed to create a project.
   * @param params.projectsLimit - The maximum number of projects that the user can create.
   * @param params.storageLimit - The size of the file storage available to the user in bytes.
   */
  createUser(
    email: string,
    password: string,
    params: {
      isAdmin?: boolean;
      userName?: string;
      firstName?: string;
      lastName?: string;
      canCreateProject?: boolean;
      projectsLimit?: number;
      storageLimit?: number;
    } = {}
  ): Promise<User> {
    const { isAdmin, userName, ...rest } = params;
    return this.httpClient
      .post("/users", {
        isAdmin,
        userBrief: {
          ...rest,
          email,
          userName: userName ?? (email + "").split("@").at(0),
        },
        password,
      })
      .then((response) => response.json())
      .then((data) => ({ id: data.id, ...data.userBrief }))
      .then((data) => new User(data, this.httpClient));
  }

  /**
   * Deletes the specified user from the server.
   *
   * Only administrators can delete users. If the current logged in user is not an administrator, an
   * exception will be thrown.
   *
   * Administrators can delete themselves or other administrators. An administrator can only delete
   * themself if they is not the last administrator.
   *
   * You need to re-login after deleting the current logged in user.
   *
   * @param userId - User ID.
   * @returns Returns the raw data of a deleted user. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Users | Open Cloud Users API}.
   */
  deleteUser(userId: string): Promise<any> {
    if (this.httpClient.signInUserIsAdmin) {
      return this.httpClient
        .delete(`/users/${userId}`)
        .then((response) => response.json())
        .then((data) => {
          if (userId === this.httpClient.signInUserId) {
            this.clearCurrentUser();
          }
          return data;
        });
    } else {
      return Promise.reject(new FetchError(403));
    }
  }

  /**
   * Result for file list.
   *
   * @typedef {any} FilesResult
   * @property {File[]} result - Result file list.
   * @property {number} start - The starting index in the file list in the request.
   * @property {number} limit - The maximum number of requested files.
   * @property {number} allSize - Total number of files the user has access to.
   * @property {number} size - The number of files in the result list.
   */

  /**
   * Returns a list of files that the current logged in user has uploaded to the server or has access to.
   *
   * @param start - The starting index in the file list. Used for paging.
   * @param limit - The maximum number of files that should be returned per request. Used for paging.
   * @param name - Filter the files by part of the name. Case sensitive.
   * @param ext - Filter the files by extension. Extension can be `dgn`, `dwf`, `dwg`, `dxf`, `ifc`,
   *   `ifczip`, `nwc`, `nwd`, `obj`, `rcs`, `rfa`, `rvt`, `step`, `stl`, `stp`, `vsf`, or any other file
   *   type extension.
   * @param ids - List of file IDs to return.
   * @param sortByDesc - Allows to specify the descending order of the result. By default, files are
   *   sorted by name in ascending order.
   * @param sortField - Allows to specify sort field.
   * @param shared - Returns shared files only.
   */
  getFiles(
    start?: number,
    limit?: number,
    name?: string,
    ext?: string | string[],
    ids?: string | string[],
    sortByDesc?: boolean,
    sortField?: string,
    shared?: boolean
  ): Promise<{
    result: File[];
    start: number;
    limit: number;
    allSize: number;
    size: number;
  }> {
    const searchParams = new URLSearchParams();
    if (start > 0) searchParams.set("start", start.toString());
    if (limit > 0) searchParams.set("limit", limit.toString());
    if (name) searchParams.set("name", name);
    if (ext) {
      if (Array.isArray(ext)) ext = ext.join("|");
      if (typeof ext === "string") ext = ext.toLowerCase();
      if (ext) searchParams.set("ext", ext);
    }
    if (ids) {
      if (Array.isArray(ids)) ids = ids.join("|");
      searchParams.set("id", ids);
    }
    if (sortByDesc !== undefined) searchParams.set("sortBy", sortByDesc ? "desc" : "asc");
    if (sortField) searchParams.set("sortField", sortField);
    if (shared) searchParams.set("shared", "true");

    let queryString = searchParams.toString();
    if (queryString) queryString = "?" + queryString;

    return this.httpClient
      .get(`/files${queryString}`)
      .then((response) => response.json())
      .then((files) => {
        return {
          ...files,
          result: files.result.map((data) => new File(data, this.httpClient)),
        };
      });
  }

  /**
   * Returns information about the specified file.
   *
   * @param fileId - File ID.
   */
  getFile(fileId: string): Promise<File> {
    return this.httpClient
      .get(`/files/${fileId}`)
      .then((response) => response.json())
      .then((data) => new File(data, this.httpClient));
  }

  /**
   * Upload a drawing or reference file to the server.
   *
   * Fires:
   *
   * - {@link UploadProgressEvent | uploadprogress}
   *
   * @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. Can be:
   *
   *   - `true` - Convert file geometry data to `VSFX` format to open the file in `VisualizeJS` viewer.
   *   - `vsfx` - Convert file geometry data to `VSFX` format to open the file in `VisualizeJS` viewer.
   *   - `gltf` - Convert file geometry data to `glTF` format to open the file in `Three.js` viewer.
   *
   * @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 uploadFile(
    file: globalThis.File,
    params: {
      geometry?: boolean | string;
      properties?: boolean;
      waitForDone?: boolean;
      timeout?: number;
      interval?: number;
      signal?: AbortSignal;
      onProgress?: (progress: number, file: globalThis.File) => void;
    } = {
      geometry: true,
      properties: false,
      waitForDone: false,
    }
  ): Promise<File> {
    const result = await this.httpClient
      .uploadFile("/files", file, (progress) => {
        this.emitEvent({ type: "uploadprogress", data: progress, file });
        params.onProgress?.(progress, file);
      })
      .then((xhr: XMLHttpRequest) => JSON.parse(xhr.responseText))
      .then((data) => new File(data, this.httpClient));

    const geometryType = typeof params.geometry === "string" ? params.geometry : "vsfx";

    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();

    return result;
  }

  /**
   * Deletes the specified file and all its versions from the server.
   *
   * You cannot delete a version file using `deleteFile()`, only the original file. To delete a version
   * file use {@link File.deleteVersion | File.deleteVersion()}.
   *
   * @param fileId - File ID.
   * @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}.
   */
  deleteFile(fileId: string): Promise<any> {
    return this.httpClient.delete(`/files/${fileId}`).then((response) => response.json());
  }

  /**
   * Downloads the specified file from the server.
   *
   * @param fileId - File ID.
   * @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.
   */
  downloadFile(fileId: string, onProgress?: (progress: number) => void, signal?: AbortSignal): Promise<ArrayBuffer> {
    return this.httpClient
      .downloadFile(`/files/${fileId}/downloads`, onProgress, { signal })
      .then((response) => response.arrayBuffer());
  }

  /**
   * Result for job list.
   *
   * @typedef {any} JobsResult
   * @property {Job[]} result - Result job list.
   * @property {number} start - The starting index in the job list in the request.
   * @property {number} limit - The maximum number of requested jobs.
   * @property {number} allSize - Total number of jobs created by the user.
   * @property {number} size - The number of jobs in the result list.
   */

  /**
   * Returns a list of jobs started by the current logged in user.
   *
   * @param status - Filter the jobs by status. Status can be `waiting`, `inpogress`, `done` or `failed`.
   * @param limit - The maximum number of jobs that should be returned per request. Used for paging.
   * @param start - The starting index in the job list. Used for paging.
   * @param sortByDesc - Allows to specify the descending order of the result. By default, jobs are
   *   sorted by creation time in ascending order.
   * @param {boolean} sortField - Allows to specify sort field.
   */
  getJobs(
    status?: string | string[],
    limit?: number,
    start?: number,
    sortByDesc?: boolean,
    sortField?: string
  ): Promise<{
    result: Job[];
    start: number;
    limit: number;
    allSize: number;
    size: number;
  }> {
    const searchParams = new URLSearchParams();
    if (start > 0) searchParams.set("start", start.toString());
    if (limit > 0) searchParams.set("limit", limit.toString());
    if (status) {
      if (Array.isArray(status)) status = status.join("|");
      if (typeof status === "string") status = status.trim().toLowerCase();
      if (status) searchParams.set("status", status);
    }
    if (sortByDesc !== undefined) searchParams.set("sortBy", sortByDesc ? "desc" : "asc");
    if (sortField) searchParams.set("sortField", sortField);

    let queryString = searchParams.toString();
    if (queryString) queryString = "?" + queryString;

    return this.httpClient
      .get(`/jobs${queryString}`)
      .then((response) => response.json())
      .then((jobs) => ({
        ...jobs,
        result: jobs.result.map((data) => new Job(data, this.httpClient)),
      }));
  }

  /**
   * Returns information about the specified job.
   *
   * @param jobId - Job ID.
   */
  getJob(jobId: string): Promise<Job> {
    return this.httpClient
      .get(`/jobs/${jobId}`)
      .then((response) => response.json())
      .then((data) => new Job(data, this.httpClient));
  }

  /**
   * Runs a new job on the server for the sepecified file.
   *
   * @param fileId - File ID.
   * @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.
   *   - Other custom job name. Custom job runner must be registered in the job templates table 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(fileId: string, outputFormat: string, parameters?: string | object): Promise<Job> {
    return this.httpClient
      .post("/jobs", {
        fileId,
        outputFormat,
        parameters: parseArgs(parameters),
      })
      .then((response) => response.json())
      .then((data) => new Job(data, this.httpClient));
  }

  /**
   * Deletes the specified job from the server job list. Jobs that are in progress or have already been
   * completed cannot be deleted.
   *
   * @param jobId - Job ID.
   * @returns Returns the raw data of a deleted job. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Jobs | Open Cloud Jobs API}.
   */
  deleteJob(jobId: string): Promise<any> {
    return this.httpClient.delete(`/jobs/${jobId}`).then((response) => response.json());
  }

  /**
   * Result for assembly list.
   *
   * @typedef {any} AssembliesResult
   * @property {Assembly[]} result - Result assembly list.
   * @property {number} start - The starting index in the assembly list in the request.
   * @property {number} limit - The maximum number of requested assemblies.
   * @property {number} allSize - Total number of assemblies the user has access to.
   * @property {number} size - The number of assemblies in the result list.
   */

  /**
   * Returns a list of assemblies created by the current logged in user.
   *
   * @param start - The starting index in the assembly list. Used for paging.
   * @param limit - The maximum number of assemblies that should be returned per request. Used for
   *   paging.
   * @param name - Filter the assemblies by part of the name. Case sensitive.
   * @param ids - List of assembly IDs to return.
   * @param sortByDesc - Allows to specify the descending order of the result. By default assemblies are
   *   sorted by name in ascending order.
   * @param sortField - Allows to specify sort field.
   */
  getAssemblies(
    start?: number,
    limit?: number,
    name?: string,
    ids?: string | string[],
    sortByDesc?: boolean,
    sortField?: string
  ): Promise<{
    result: Assembly[];
    start: number;
    limit: number;
    allSize: number;
    size: number;
  }> {
    const searchParams = new URLSearchParams();
    if (start > 0) searchParams.set("start", start.toString());
    if (limit > 0) searchParams.set("limit", limit.toString());
    if (name) searchParams.set("name", name);
    if (ids) {
      if (Array.isArray(ids)) ids = ids.join("|");
      if (typeof ids === "string") ids = ids.trim();
      if (ids) searchParams.set("id", ids);
    }
    if (sortByDesc !== undefined) searchParams.set("sortBy", sortByDesc ? "desc" : "asc");
    if (sortField) searchParams.set("sortField", sortField);

    let queryString = searchParams.toString();
    if (queryString) queryString = "?" + queryString;

    return this.httpClient
      .get(`/assemblies${queryString}`)
      .then((response) => response.json())
      .then((assemblies) => {
        return {
          ...assemblies,
          result: assemblies.result.map((data) => new Assembly(data, this.httpClient)),
        };
      });
  }

  /**
   * Returns information about the specified assembly.
   *
   * @param assemblyId - Assembly ID.
   */
  getAssembly(assemblyId: string): Promise<Assembly> {
    return this.httpClient
      .get(`/assemblies/${assemblyId}`)
      .then((response) => response.json())
      .then((data) => new Assembly(data, this.httpClient));
  }

  /**
   * Creates a new assembly on the server.
   *
   * @param files - List of file IDs.
   * @param name - Assembly name.
   * @param params - Additional assembly creating parameters.
   * @param params.waitForDone - Wait for assembly to be created.
   * @param params.timeout - The time, in milliseconds, that the function should wait for the assembly to
   *   be created. If the assembly is not created within this time, a TimeoutError exception will be
   *   thrown.
   * @param params.interval - The time, in milliseconds, the function should delay in between checking
   *   assembly 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.
   */
  createAssembly(
    files: string[],
    name: string,
    params?: {
      waitForDone?: boolean;
      timeout?: number;
      interval?: number;
      signal?: AbortSignal;
      onCheckout?: (assembly: Assembly, ready: boolean) => boolean;
    }
  ): Promise<Assembly> {
    const { waitForDone } = params ?? {};
    return this.httpClient
      .post("/assemblies", { name, files })
      .then((response) => response.json())
      .then((data) => new Assembly(data, this.httpClient))
      .then((result) => (waitForDone ? result.waitForDone(params) : result));
  }

  /**
   * Deletes the specified assembly from the server.
   *
   * @param assemblyId - Assembly ID.
   * @returns Returns the raw data of a deleted assembly. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Assemblies | Open Cloud API}.
   */
  deleteAssembly(assemblyId: string): Promise<any> {
    return this.httpClient.delete(`/assemblies/${assemblyId}`).then((response) => response.json());
  }

  /**
   * Result for project list.
   *
   * @typedef {any} ProjectsResult
   * @property {Project[]} result - Result project list.
   * @property {number} start - The starting index in the project list in the request.
   * @property {number} limit - The maximum number of requested projects.
   * @property {number} allSize - Total number of projects the user has access to.
   * @property {number} size - The number of projects in the result list.
   */

  /**
   * Returns a list of projects that the currently logged in user has created or has access to.
   *
   * @param start - The starting index in the project list. Used for paging.
   * @param limit - The maximum number of projects that should be returned per request. Used for paging.
   * @param name - Filter the projects by part of the name. Case sensitive.
   * @param ids - List of project IDs to return.
   * @param sortByDesc - Allows to specify the descending order of the result. By default projects are
   *   sorted by name in ascending order.
   */
  getProjects(
    start?: number,
    limit?: number,
    name?: string,
    ids?: string | string[],
    sortByDesc?: boolean
  ): Promise<{
    result: Project[];
    start: number;
    limit: number;
    allSize: number;
    size: number;
  }> {
    const searchParams = new URLSearchParams();
    if (start > 0) searchParams.set("start", start.toString());
    if (limit > 0) searchParams.set("limit", limit.toString());
    if (name) searchParams.set("name", name);
    if (ids) {
      if (Array.isArray(ids)) ids = ids.join("|");
      if (typeof ids === "string") ids = ids.trim();
      if (ids) searchParams.set("id", ids);
    }
    if (sortByDesc !== undefined) searchParams.set("sortBy", sortByDesc ? "desc" : "asc");

    let queryString = searchParams.toString();
    if (queryString) queryString = "?" + queryString;
    return this.httpClient
      .get(`/projects${queryString}`)
      .then((response) => response.json())
      .then((projects) => {
        // fix for server 23.5 and below
        if (Array.isArray(projects)) {
          let result = projects;
          if (ids) result = result.filter((x) => ids.includes(x.id));
          if (name) result = result.filter((x) => x.name.includes(name));
          if (limit > 0) {
            const begin = start > 0 ? start : 0;
            result = result.slice(begin, begin + limit);
          }
          return {
            allSize: projects.length,
            start,
            limit,
            result,
            size: result.length,
          };
        }
        return projects;
      })
      .then((projects) => {
        return {
          ...projects,
          result: projects.result.map((data) => new Project(data, this.httpClient)),
        };
      });
  }

  /**
   * Returns information about the specified project.
   *
   * @param projectId - Project ID.
   */
  getProject(projectId: string): Promise<Project> {
    return this.httpClient
      .get(`/projects/${projectId}`)
      .then((response) => response.json())
      .then((data) => new Project(data, this.httpClient));
  }

  /**
   * Creates a new project on the server.
   *
   * @param name - Project name.
   * @param description - Project description.
   * @param startDate - Project start date.
   * @param endDate - Project end date.
   */
  createProject(
    name: string,
    description?: string,
    startDate?: Date | string,
    endDate?: Date | string
  ): Promise<Project> {
    return this.httpClient
      .post("/projects", {
        name,
        description,
        startDate: startDate instanceof Date ? startDate.toISOString() : startDate,
        endDate: endDate instanceof Date ? endDate.toISOString() : endDate,
      })
      .then((response) => response.json())
      .then((data) => new Project(data, this.httpClient));
  }

  /**
   * Deletes the specified project from the server.
   *
   * @param projectId - Project ID.
   * @returns Returns the raw data of a deleted project. For more information, see
   *   {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
   */
  deleteProject(projectId: string): Promise<any> {
    return this.httpClient
      .delete(`/projects/${projectId}`)
      .then((response) => response.text())
      .then((text) => {
        // fix for server 23.5 and below
        try {
          return JSON.parse(text);
        } catch {
          return { id: projectId };
        }
      });
  }

  /**
   * Returns information about the specified file shared link.
   *
   * @param token - Shared link token.
   */
  getSharedLink(token: string): Promise<SharedLink> {
    return this.httpClient
      .get(`/shares/${token}`)
      .then((response) => response.json())
      .then((data) => new SharedLink(data, this.httpClient));
  }

  /**
   * Creates a shared link for the specified file.
   *
   * @param fileId - File ID.
   * @param permissions - Share permissions.
   */
  createSharedLink(fileId: string, permissions?: ISharedLinkPermissions): Promise<SharedLink> {
    return this.httpClient
      .post("/shares", {
        fileId,
        permissions,
      })
      .then((response) => response.json())
      .then((data) => new SharedLink(data, this.httpClient));
  }

  /**
   * Deletes the specified shared link.
   *
   * Only file owner can delete shared link. If the current logged in user is not a file owner, an
   * exception will be thrown.
   *
   * @param token - Shared link token.
   * @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}.
   */
  deleteSharedLink(token: string): Promise<any> {
    return this.httpClient.delete(`/shares/${token}`).then((response) => response.json());
  }

  /**
   * Returns information about a file from a shared link.
   *
   * Some file features are not available via shared link:
   *
   * - Updating file properties, preview, and viewpoints
   * - Running file jobs
   * - Managing file permissions
   * - Managing file versions
   * - Deleting file
   *
   * @param token - Shared link token.
   * @param password - Password to get access to the file.
   */
  getSharedFile(token: string, password?: string): Promise<File> {
    return this.httpClient
      .get(`/shares/${token}/info`, { headers: { "InWeb-Password": password } })
      .then((response) => response.json())
      .then((data) => new SharedFile(data, password, this.httpClient));
  }
}
