///////////////////////////////////////////////////////////////////////////////
// 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 { Loader } from "@inweb/viewer-core";
import { Viewer } from "../Viewer";
import { ModelImpl } from "../Models/ModelImpl";

const PENDING_REQUESTS_SIZE = 50;
const PENDING_REQUESTS_TIMEOUT = 250;

export class VSFXCloudPartialLoader extends Loader {
  public viewer: Viewer;
  public abortControllerForRequestMap: Map<number, AbortController>;

  constructor(viewer: Viewer) {
    super();
    this.viewer = viewer;
    this.abortControllerForRequestMap = new Map();
  }

  override isSupport(file: any): boolean {
    return (
      typeof file === "object" &&
      typeof file.database === "string" &&
      typeof file.downloadResource === "function" &&
      typeof file.downloadResourceRange === "function" &&
      /.vsfx$/i.test(file.database) &&
      (this.viewer.options.enablePartialMode === true || /.rcs$/i.test(file.type))
    );
  }

  override async load(model: any): Promise<this> {
    if (!this.viewer.visualizeJs) return this;

    const visViewer = this.viewer.visViewer();
    visViewer.memoryLimit = this.viewer.options.memoryLimit;

    let servicePartAborted = false;

    const pendingRequestsMap = new Map();
    let pendingRequestsTimerId = 0;
    const pendingRequestsAbortHandler = () => clearTimeout(pendingRequestsTimerId);

    const pendingRequestsAbortController = new AbortController();
    this.abortControllerForRequestMap.set(0, pendingRequestsAbortController);

    const chunkLoadHandler = (progress: number, chunk: Uint8Array, requestId = 0) => {
      if (!this.viewer.visualizeJs) return;

      let isDatabaseChunk: boolean;
      try {
        isDatabaseChunk = visViewer.parseVsfxInPartialMode(requestId, chunk);
      } catch (error: any) {
        console.error("VSFX parse error.", error);
        throw error;
      }

      this.viewer.emitEvent({ type: "geometryprogress", data: progress, file: model.file, model });

      if (isDatabaseChunk) {
        const modelImpl = new ModelImpl();
        modelImpl.id = model.file.id;

        this.viewer.models.push(modelImpl);

        this.viewer.syncOptions();
        this.viewer.syncOverlay();

        this.viewer.emitEvent({ type: "databasechunk", data: chunk, file: model.file, model });
        this.viewer.update(true);
      } else {
        this.viewer.emitEvent({ type: "geometrychunk", data: chunk, file: model.file, model });
        this.viewer.update(500);
      }
    };

    const downloadResourceRange = async (dataId: string, requestId: number, ranges: any) => {
      const abortCtrl = new AbortController();
      this.abortControllerForRequestMap.set(requestId, abortCtrl);
      try {
        await model.downloadResourceRange(dataId, requestId, ranges, chunkLoadHandler, abortCtrl.signal);
      } catch (error: any) {
        this.viewer.emitEvent({ type: "geometryerror", data: error, file: model.file, model });
      } finally {
        ranges.forEach((range) => visViewer.onRequestResponseComplete(range.requestId));
        this.abortControllerForRequestMap.delete(requestId);
      }
    };

    const requestRecordsToRanges = (requestId: number, records: any): any => {
      const ranges = [];
      for (let i = 0; i < records.size(); i++) {
        const record = records.get(i);
        ranges.push({
          requestId,
          begin: Number(record.begin),
          end: Number(record.end) - 1, // <- Visualize fix
        });
        record.delete();
      }
      return ranges;
    };

    const objectHandler = {
      onServicePartReceived: (bHasIndex: boolean) => {
        if (bHasIndex) {
          servicePartAborted = true;
          this.abortController.abort();
        }
      },

      onRequest: (requestId: number, records: any) => {
        const ranges = requestRecordsToRanges(requestId, records);
        downloadResourceRange(model.database, requestId, ranges);
      },

      onFullLoaded: () => {
        this.viewer.update(true);
      },

      onRequestResponseParsed: (requestId: number) => {
        this.abortControllerForRequestMap.delete(requestId);
      },

      onRequestAborted: (requestId: number) => {
        const abortCtrl = this.abortControllerForRequestMap.get(requestId);
        if (abortCtrl) abortCtrl.abort();
      },

      onRequestResourceFile: (requestId: number, _: string, records: any) => {
        const dataId = `${model.fileId}${model.file.type}`;
        const ranges = requestRecordsToRanges(requestId, records);

        let pendingRanges = [];
        let requestNumber = 0;
        const pendingRequest = pendingRequestsMap.get(dataId);
        if (pendingRequest) {
          pendingRanges = pendingRequest.ranges;
          requestNumber = pendingRequest.number;
        }

        // first several records of each file are processed without grouping
        // (they usually require to be processed sequentially)
        if (requestNumber <= 5) {
          pendingRequestsMap.set(dataId, { ranges: [], number: requestNumber + 1 });
          downloadResourceRange(dataId, requestId, ranges);
          return;
        }

        pendingRanges = pendingRanges.concat(ranges);

        // group requests to each file to launch a combined server request
        if (pendingRanges.length >= PENDING_REQUESTS_SIZE) {
          if (pendingRequestsTimerId) {
            window.clearTimeout(pendingRequestsTimerId);
            pendingRequestsTimerId = 0;
          }

          pendingRequestsMap.set(dataId, { ranges: [], number: requestNumber + 1 });
          downloadResourceRange(dataId, requestId, pendingRanges);
          return;
        }

        pendingRequestsMap.set(dataId, { ranges: pendingRanges, number: requestNumber + 1 });

        // set timeout to wait for the new requests, after that process the remaining requests
        if (pendingRequestsTimerId === 0) {
          pendingRequestsTimerId = window.setTimeout(() => {
            pendingRequestsAbortController.signal.removeEventListener("abort", pendingRequestsAbortHandler);
            pendingRequestsTimerId = 0;

            pendingRequestsMap.forEach((request, dataId) => {
              if (request.ranges.length > 0) {
                pendingRequestsMap.set(dataId, { ranges: [], number: request.number + 1 });
                downloadResourceRange(dataId, requestId, request.ranges);
              }
            });
          }, PENDING_REQUESTS_TIMEOUT);

          pendingRequestsAbortController.signal.addEventListener("abort", pendingRequestsAbortHandler, { once: true });
        }
      },
    };

    visViewer.attachPartialResolver(objectHandler);

    try {
      await model.downloadResource(model.database, chunkLoadHandler, this.abortController.signal);
    } catch (error) {
      window.clearTimeout(pendingRequestsTimerId);
      if (!servicePartAborted) throw error;
    }

    return this;
  }

  override cancel(): void {
    super.cancel();
    this.abortControllerForRequestMap.forEach((controller) => controller.abort());
  }
}
