/*
 * Copyright (C) 2022 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {SCSearchRequest, SCSearchResponse} from '@openstapps/core';
import {Injectable} from '@angular/core';
import {DataProvider} from './data.provider';

/**
 * Delay execution for (at least) a set amount of time
 */
async function delay(ms: number): Promise<void> {
  return await new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Transforms an array to an object with the indices as keys
 *
 * ['a', 'b', 'c'] => {0: 'a', 1: 'b', 2: 'c'}
 */
export function arrayToIndexMap<T>(array: T[]): Record<number, T> {
  return array.reduce(
    (previous, current, index) => {
      previous[index] = current;
      return previous;
    },
    {} as Record<number, T>,
  );
}

interface OngoingQuery {
  request: SCSearchRequest;
  response?: Promise<SCSearchResponse>;
}

/**
 * Coordinated search request that bundles requests from multiple modules into a single one
 */
@Injectable({
  providedIn: 'root',
})
export class CoordinatedSearchProvider {
  constructor(readonly dataProvider: DataProvider) {}

  /**
   * Queue of ongoing queries
   */
  queue: OngoingQuery[] = [];

  /**
   * Start a coordinated search that merges requests across components
   *
   * This method collects the request, then:
   * 1. If the queue is full, dispatches all immediately
   * 2. If not, waits a set amount of time for other requests to come in
   */
  async coordinatedSearch(query: SCSearchRequest, latencyMs = 50): Promise<SCSearchResponse> {
    const ongoingQuery: OngoingQuery = {request: query};
    this.queue.push(ongoingQuery);

    if (this.queue.length < this.dataProvider.backendQueriesLimit) {
      await delay(latencyMs);
    }

    if (this.queue.length > 0) {
      // because we are guaranteed to have limited our queue size to be
      // <= to the backendQueriesLimite as of above, we can bypass the wrapper
      // in the data provider that usually would be responsible for splitting up the requests

      const responses = this.dataProvider.client.multiSearch(
        arrayToIndexMap(this.queue.map(it => it.request)),
      );

      for (const [index, request] of this.queue.entries()) {
        request.response = new Promise(resolve => responses.then(it => resolve(it[index])));
      }

      this.queue = [];
    }

    // Response is guaranteed to be defined here
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    return await ongoingQuery.response!;
  }
}
