/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import { JsonValue } from '@angular-devkit/core';
import { Job, JobDescription, JobHandler, JobHandlerContext, JobName, isJobHandler } from './api';
import { JobDoesNotExistException } from './exception';
import { Readwrite } from './types';

/**
 * A JobDispatcher can be used to dispatch between multiple jobs.
 */
export interface JobDispatcher<A extends JsonValue, I extends JsonValue, O extends JsonValue>
  extends JobHandler<A, I, O> {
  /**
   * Set the default job if all conditionals failed.
   * @param name The default name if all conditions are false.
   */
  setDefaultJob(name: JobName | null | JobHandler<JsonValue, JsonValue, JsonValue>): void;

  /**
   * Add a conditional job that will be selected if the input fits a predicate.
   * @param predicate
   * @param name
   */
  addConditionalJob(predicate: (args: A) => boolean, name: string): void;
}

/**
 * OnReady a dispatcher that can dispatch to a sub job, depending on conditions.
 * @param options
 */
export function createDispatcher<A extends JsonValue, I extends JsonValue, O extends JsonValue>(
  options: Partial<Readwrite<JobDescription>> = {},
): JobDispatcher<A, I, O> {
  let defaultDelegate: JobName | null = null;
  const conditionalDelegateList: [(args: JsonValue) => boolean, JobName][] = [];

  const job: JobHandler<JsonValue, JsonValue, JsonValue> = Object.assign(
    (argument: JsonValue, context: JobHandlerContext) => {
      const maybeDelegate = conditionalDelegateList.find(([predicate]) => predicate(argument));
      let delegate: Job<JsonValue, JsonValue, JsonValue> | null = null;

      if (maybeDelegate) {
        delegate = context.scheduler.schedule(maybeDelegate[1], argument);
      } else if (defaultDelegate) {
        delegate = context.scheduler.schedule(defaultDelegate, argument);
      } else {
        throw new JobDoesNotExistException('<null>');
      }

      context.inboundBus.subscribe(delegate.inboundBus);

      return delegate.outboundBus;
    },
    {
      jobDescription: options,
    },
  );

  return Object.assign(job, {
    setDefaultJob(name: JobName | null | JobHandler<JsonValue, JsonValue, JsonValue>) {
      if (isJobHandler(name)) {
        name = name.jobDescription.name === undefined ? null : name.jobDescription.name;
      }

      defaultDelegate = name;
    },
    addConditionalJob(predicate: (args: JsonValue) => boolean, name: JobName) {
      conditionalDelegateList.push([predicate, name]);
    },
    // TODO: Remove return-only generic from createDispatcher() API.
  }) as unknown as JobDispatcher<A, I, O>;
}
