///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2026, 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-2026 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 { $fetch } from "./Fetch";
import { $xmlhttp } from "./XMLHttp";

export class HttpClient implements IHttpClient {
  public serverUrl: string;
  public headers: HeadersInit = {};
  public signInUserId = "";
  public signInUserIsAdmin = false;

  constructor(serverUrl: string) {
    this.serverUrl = serverUrl;
  }

  get(relativePath: string, init: RequestInit = {}): Promise<Response> {
    return $fetch(`${this.serverUrl}${relativePath}`, {
      ...init,
      method: "GET",
      headers: { ...this.headers, ...init.headers },
    });
  }

  post(relativePath: string, body?: BodyInit | object, init: RequestInit = {}): Promise<Response> {
    return $fetch(`${this.serverUrl}${relativePath}`, {
      ...init,
      method: "POST",
      headers: { ...this.headers, ...init.headers },
      body,
    });
  }

  put(relativePath: string, body?: BodyInit | object, init: RequestInit = {}): Promise<Response> {
    return $fetch(`${this.serverUrl}${relativePath}`, {
      ...init,
      method: "PUT",
      headers: { ...this.headers, ...init.headers },
      body,
    });
  }

  delete(relativePath: string, init: RequestInit = {}): Promise<Response> {
    return $fetch(`${this.serverUrl}${relativePath}`, {
      ...init,
      method: "DELETE",
      headers: { ...this.headers, ...init.headers },
    });
  }

  uploadFile(
    relativePath: string,
    file: File,
    onProgress?: (progress: number) => void,
    init: RequestInit = {}
  ): Promise<XMLHttpRequest> {
    const data = new FormData();
    data.append("file", file);
    return $xmlhttp(`${this.serverUrl}${relativePath}`, {
      method: "POST",
      headers: { ...this.headers, ...init.headers },
      body: data,
      uploadProgress: onProgress,
    });
  }

  async downloadFile(
    relativePath: string,
    onProgress?: (progress: number, chunk: Uint8Array) => void,
    init: RequestInit = {}
  ): Promise<Response> {
    const response = await this.get(relativePath, init);
    if (!onProgress) return response;

    const contentLength = response.headers.get("Content-Length");
    const total = parseInt(contentLength || "", 10) || 1;

    const stream = new ReadableStream({
      async start(controller) {
        const reader = response.body.getReader();
        let loaded = 0;
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          controller.enqueue(value);
          loaded += value.length;
          onProgress(loaded / total, value);
        }
        controller.close();
      },
    });

    return new Response(stream);
  }

  async downloadFileRange(
    relativePath: string,
    reserved: number | string,
    ranges: Array<{ begin: number; end: number; requestId: number }>,
    onProgress?: (progress: number, chunk: Uint8Array, requestId: number) => void,
    init: RequestInit = {}
  ): Promise<Response> {
    const headers = {
      ...init.headers,
      Range: "bytes=" + ranges.map((x) => `${x.begin}-${x.end}`).join(","),
    };
    const response = await this.get(relativePath, { ...init, headers });
    if (!onProgress) return response;

    const contentLength = response.headers.get("content-length");
    const total = parseInt(contentLength || "", 10) || 1;

    const stream = new ReadableStream({
      async start(controller) {
        const reader = response.body.getReader();
        let loaded = 0;
        let rangedIndex = 0;
        let rangePos = 0;
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          controller.enqueue(value);
          loaded += value.length;
          let chunkLeft = value.length;
          let chunkPos = 0;
          while (chunkLeft > 0) {
            const range = ranges[rangedIndex];
            const rangeLeft = range.end - range.begin + 1 - rangePos;
            if (chunkLeft < rangeLeft) {
              const chunk = value.subarray(chunkPos, chunkPos + chunkLeft);
              onProgress(loaded / total, chunk, range.requestId);
              rangePos += chunkLeft;
              chunkLeft = 0;
            } else {
              const chunk = value.subarray(chunkPos, chunkPos + rangeLeft);
              onProgress(loaded / total, chunk, range.requestId);
              chunkPos += rangeLeft;
              chunkLeft -= rangeLeft;
              rangedIndex++;
              rangePos = 0;
            }
          }
        }
        controller.close();
      },
    });

    return new Response(stream);
  }
}
