///////////////////////////////////////////////////////////////////////////////
// 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 { BaseLoader } from "./BaseLoader";
import { UpdaterController, UpdateType } from "./UpdaterController";

const PENDING_REQUESTS_SIZE = 50;
const PENDING_REQUESTS_TIMEOUT = 250;

export class VsfXPartialLoader extends BaseLoader {
  override async load(): Promise<void> {
    if (!this.viewer.visualizeJs) return;

    const visLib = this.viewer.visLib();
    const visViewer = visLib.getViewer();
    const abortController = new AbortController();
    const abortControllerForRequestMap = new Map();
    let servicePartAborted = false;

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

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

    const updaterController = new UpdaterController();
    updaterController.initialize(this.viewer);

    this.viewer._abortController = abortController;
    this.viewer._abortControllerForRequestMap = abortControllerForRequestMap;

    visViewer.memoryLimit = this.options.memoryLimit;

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

      const state = visViewer.parseVsfxInPartialMode(requestId, chunk);
      updaterController.update(UpdateType.kDelay);

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

      if (state) {
        updaterController.update(UpdateType.kForce);

        this.viewer.syncOpenCloudVisualStyle(false);
        this.viewer.syncOptions();
        this.viewer.syncOverlay();
        this.viewer.resize();

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

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

    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),
        });
        record.delete();
      }
      return ranges;
    };

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

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

      onFullLoaded: () => {
        updaterController.update(UpdateType.kNormal);
      },

      onRequestResponseParsed: (requestId: number) => {
        abortControllerForRequestMap.delete(requestId);
        updaterController.update(UpdateType.kNormal);
      },

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

      onRequestResourceFile: (requestId: number, _: string, records: any) => {
        const dataId = `${this.model.fileId}${this.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 {
      this.viewer.emitEvent({ type: "geometrystart", model: this.model });

      await this.model.downloadResource(this.model.database, chunkLoadHandler, abortController.signal).catch((e) => {
        if (!servicePartAborted) throw e;
      });

      this.viewer.emitEvent({ type: "geometryend", model: this.model });
    } catch (e: any) {
      if (pendingRequestsTimerId) {
        window.clearTimeout(pendingRequestsTimerId);
        pendingRequestsTimerId = 0;
      }

      this.viewer.emitEvent({ type: "geometryerror", data: e, model: this.model });
      throw e;
    }
  }
}
