import {
  WorkerRunMessage,
  WorkerRunResponse,
  WorkerOperation,
} from "./worker-common";

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(localStorage.getItem("workers")) || 1);
console.log("worker", workerHandler);
