// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {NodeWorker, NodeWorkerType} from '../node/worker_threads';
import {isBrowser} from '../env-utils/globals';
import {assert} from '../env-utils/assert';
import {getLoadableWorkerURL} from '../worker-utils/get-loadable-worker-url';
import {getTransferList} from '../worker-utils/get-transfer-list';

const NOOP = () => {};

export type WorkerThreadProps = {
  name: string;
  source?: string;
  url?: string;
};

/**
 * Represents one worker thread
 */
export default class WorkerThread {
  readonly name: string;
  readonly source: string | undefined;
  readonly url: string | undefined;
  terminated: boolean = false;
  worker: Worker | NodeWorkerType;
  onMessage: (message: any) => void;
  onError: (error: Error) => void;

  private _loadableURL: string = '';

  /** Checks if workers are supported on this platform */
  static isSupported(): boolean {
    return (
      (typeof Worker !== 'undefined' && isBrowser) ||
      (typeof NodeWorker !== 'undefined' && !isBrowser)
    );
  }

  constructor(props: WorkerThreadProps) {
    const {name, source, url} = props;
    assert(source || url); // Either source or url must be defined
    this.name = name;
    this.source = source;
    this.url = url;
    this.onMessage = NOOP;
    this.onError = (error) => console.log(error); // eslint-disable-line

    this.worker = isBrowser ? this._createBrowserWorker() : this._createNodeWorker();
  }

  /**
   * Terminate this worker thread
   * @note Can free up significant memory
   */
  destroy(): void {
    this.onMessage = NOOP;
    this.onError = NOOP;
    this.worker.terminate(); // eslint-disable-line @typescript-eslint/no-floating-promises
    this.terminated = true;
  }

  get isRunning() {
    return Boolean(this.onMessage);
  }

  /**
   * Send a message to this worker thread
   * @param data any data structure, ideally consisting mostly of transferrable objects
   * @param transferList If not supplied, calculated automatically by traversing data
   */
  postMessage(data: any, transferList?: any[]): void {
    transferList = transferList || getTransferList(data);
    // @ts-ignore
    this.worker.postMessage(data, transferList);
  }

  // PRIVATE

  /**
   * Generate a standard Error from an ErrorEvent
   * @param event
   */
  _getErrorFromErrorEvent(event: ErrorEvent): Error {
    // Note Error object does not have the expected fields if loading failed completely
    // https://developer.mozilla.org/en-US/docs/Web/API/Worker#Event_handlers
    // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
    let message = 'Failed to load ';
    message += `worker ${this.name} from ${this.url}. `;
    if (event.message) {
      message += `${event.message} in `;
    }
    // const hasFilename = event.filename && !event.filename.startsWith('blob:');
    // message += hasFilename ? event.filename : this.source.slice(0, 100);
    if (event.lineno) {
      message += `:${event.lineno}:${event.colno}`;
    }
    return new Error(message);
  }

  /**
   * Creates a worker thread on the browser
   */
  _createBrowserWorker(): Worker {
    this._loadableURL = getLoadableWorkerURL({source: this.source, url: this.url});
    const worker = new Worker(this._loadableURL, {name: this.name});

    worker.onmessage = (event) => {
      if (!event.data) {
        this.onError(new Error('No data received'));
      } else {
        this.onMessage(event.data);
      }
    };
    // This callback represents an uncaught exception in the worker thread
    worker.onerror = (error: ErrorEvent): void => {
      this.onError(this._getErrorFromErrorEvent(error));
      this.terminated = true;
    };
    // TODO - not clear when this would be called, for now just log in case it happens
    worker.onmessageerror = (event) => console.error(event); // eslint-disable-line

    return worker;
  }

  /**
   * Creates a worker thread in node.js
   * @todo https://nodejs.org/api/async_hooks.html#async-resource-worker-pool
   */
  _createNodeWorker(): NodeWorkerType {
    let worker: NodeWorkerType;
    if (this.url) {
      // Make sure relative URLs start with './'
      const absolute = this.url.includes(':/') || this.url.startsWith('/');
      const url = absolute ? this.url : `./${this.url}`;
      const type = this.url.endsWith('.ts') || this.url.endsWith('.mjs') ? 'module' : 'commonjs';
      // console.log('Starting work from', url);
      // @ts-expect-error type is not known
      worker = new NodeWorker(url, {eval: false, type});
    } else if (this.source) {
      worker = new NodeWorker(this.source, {eval: true});
    } else {
      throw new Error('no worker');
    }
    worker.on('message', (data) => {
      // console.error('message', data);
      this.onMessage(data);
    });
    worker.on('error', (error) => {
      this.onError(error as Error);
    });
    worker.on('exit', (code) => {
      // console.error('exit', code);
    });
    return worker;
  }
}
