import { WorkerRunMessage, WorkerRunResponse, WorkerOperation } from './worker-common';
import { AppStorage } from 'src/app/shared/app-storage';
import { AdvancedSettingsEnum } from 'src/app/components/advanced-settings/advanced-settings.component';

export interface WorkerProcess {
  worker: Worker;
  operations: number;
  runningPromises: { [key: number]: WorkerProcessRunningPromises };
}

export interface WorkerProcessRunningPromises {
  resolve: (value: any) => void;
  reject: (reason?: any) => void;
}

export class WorkerHandler {
  private processes: WorkerProcess[] = [];
  private maxProcess = 1;
  private lastOperationId = 0;

  public constructor() {}

  public setMaxProcess(maxProcess: number) {
    if (maxProcess >= 1) {
      this.maxProcess = maxProcess;
    } else {
      console.warn('Can not set 0 or less process number');
    }

    for (let i = this.processes.length; i > maxProcess - 1; i--) {
      this.processes.splice(i, 1);
    }
  }

  /**
   * for debug and develop purpose
   */
  public getProcesses() {
    return this.processes;
  }

  public getMaxProcess(): number {
    return this.maxProcess;
  }

  /**
   * Every function call use an unique id, so we can identify which response should we give back
   * @returns
   */
  private getOperationId(): number {
    return ++this.lastOperationId;
  }

  /**
   * Run a process on a dedicated worker. Mostly for performance tests.
   */
  public runProcess(processIndex: number, operation: WorkerOperation, ...variables) {
    let workerProcess = this.processes[processIndex];

    if (!workerProcess) {
      workerProcess = this.getWorker();
    }

    workerProcess.operations++;

    let operationId = this.getOperationId();

    let message: WorkerRunMessage = {
      operation,
      variables,
      operationId,
    };
    return new Promise((resolve, reject) => {
      workerProcess.runningPromises[operationId] = { resolve, reject };
      workerProcess.worker.postMessage(message);
    });
  }

  /**
   * Run the operation
   * @param {WorkerOperation} operation - reference for the function
   * @param {any[]} variables - variables in an array inorder
   * @returns {Promise<any>} the response
   */
  public run(operation: WorkerOperation, ...variables): Promise<any> {
    let workerProcess = this.getWorker();

    workerProcess.operations++;

    let operationId = this.getOperationId();

    let message: WorkerRunMessage = {
      operation,
      variables,
      operationId,
    };
    return new Promise((resolve, reject) => {
      workerProcess.runningPromises[operationId] = { resolve, reject };
      workerProcess.worker.postMessage(message);
    });
  }

  /**
   * Get a free or a less working Worker
   */
  private getWorker(): WorkerProcess {
    let sortedProcesses = this.processes.sort(this.sortByOperation);

    // it is a free worker
    if (sortedProcesses.length > 0 && sortedProcesses[0].operations == 0) {
      return sortedProcesses[0];
    } else {
      // we dont have free workers, check if we can create a new one
      if (this.maxProcess > this.processes.length) {
        let workerProcess = this.createNewWorker();
        this.processes.push(workerProcess);
        return workerProcess;
      } else {
        // we can not create new, so give back the less working
        return sortedProcesses[0];
      }
    }
  }

  /**
   * We need to get a free or a less working Worker
   */
  private sortByOperation(a: WorkerProcess, b: WorkerProcess) {
    return a.operations - b.operations;
  }

  /**
   * If we does not reach the paralell workers limits, we can create a new one
   * @returns
   */
  private createNewWorker(): WorkerProcess {
    let worker = new Worker(new URL('./crypto.worker', import.meta.url), {
      type: 'module',
    });

    console.log('create worker', worker);
    let process: WorkerProcess = {
      worker,
      operations: 0,
      runningPromises: {},
    };

    worker.addEventListener('message', (event: MessageEvent<WorkerRunResponse>) => {
      if (event.data.operationId in process.runningPromises) {
        let promise = process.runningPromises[event.data.operationId];
        delete process.runningPromises[event.data.operationId];
        process.operations--;

        if (!event.data.error) {
          promise.resolve(event.data.response);
        } else {
          promise.reject(event.data.response);
        }

        if (this.maxProcess < this.processes.indexOf(process) + 1) {
          this.processes.splice(this.processes.indexOf(process), 1);
        }
      }
    });

    return process;
  }
}

export let workerHandler = new WorkerHandler();
workerHandler.setMaxProcess(parseInt(AppStorage.getItem(AdvancedSettingsEnum.WORKERS)) || 1);
console.log('worker', workerHandler);
