import {PortablePath}                 from '@yarnpkg/fslib';

import {FetchResult}                  from './Fetcher';
import {Report}                       from './Report';
import {Descriptor, Locator, Package} from './types';

export enum BuildDirectiveType {
  SCRIPT = 0,
  SHELLCODE = 1,
}

export type BuildDirective = {
  type: BuildDirectiveType;
  script: string;
};

export type BuildRequest = {
  skipped: false;
  directives: Array<BuildDirective>;
} | {
  skipped: true;
  explain: (report: Report) => void;
};

export type InstallStatus = {
  packageLocation: PortablePath | null;
  buildRequest: BuildRequest | null;
  installPromise?: Promise<void>;
};

export type FinalizeInstallStatus = {
  locator: Locator;
  buildLocations: Array<PortablePath>;
  buildRequest: BuildRequest;
};

export type FinalizeInstallData = {
  /**
   * A list of extra package instances that may have been installed on the
   * disk. While we usually recommend to avoid this feature (one package should
   * only be installed once in a project, virtual dependencies excluded), it
   * may be required to duplicate installs in some cases - for instance to
   * replicate the hoisting that would happen with the node-modules linking
   * strategy.
   */
  records?: Array<FinalizeInstallStatus>;

  /**
   * A set of data that are preserved from one install to the next. Linkers are
   * allowed to cache whatever they want (including ES-native data structures
   * like Map and Set) as long as they remember to follow basic rules:
   *
   * - They have to be prepared for no custom data to be passed at all; Yarn
   *   is allowed to clear the cache at will.
   *
   * - They have to cache only things that are unlikely to change. For instance
   *   caching the packages' `scripts` field is fine, but caching their
   *   dependencies isn't (first because dependencies are provided by the core
   *   itself, so caching wouldn't make sense, but also because users may
   *   change the dependencies of any package via the `resolutions` field).
   *
   * And of course, they have to manage their own migration.
   */
  customData?: any;
};

export type InstallPackageExtraApi = {
  /**
   * The core reclaims the virtual filesystem by default when the
   * `installPackage` function returns. This may be annoying when working on
   * parallel installers, since `installPackage` are guaranteed to work
   * sequentially (and thus no two packages could be installed at the same
   * time, since one's fs would be closed as soon as the second would start).
   *
   * To avoid that, you can call the `holdFetchResult` function from this extra
   * API to indicate to the core that it shouldn't reclaim the filesystem until
   * the API passed in parameter as finished executing. Note that this may lead
   * to higher memory consumption (since multiple packages may be kept in
   * memory), so you'll need to implement an upper bound to the number of
   * concurrent package installs.
   */
  holdFetchResult: (promise: Promise<void>) => void;
};

export interface Installer {
  /**
   * Only called if the installer has a custom data key matching one currently
   * stored. Will be called with whatever `finalizeInstall` returned in its
   * `customData` field.
   */
  attachCustomData(customData: unknown): void;

  /**
   * Install a package on the disk.
   *
   * Should return `null` if the package has no install steps, or an object
   * describing the various scripts that need to be run otherwise.
   *
   * Note that this function isn't called in any specific order. In particular,
   * this means that the order in which this function is called will not
   * necessarily match the order in which the packages will be built.
   *
   * This function is guaranteed to be called for all packages before the
   * dependencies start to be attached.
   *
   * @param pkg The package being installed
   * @param fetchResult The fetched information about the package
   * @param api An additional API one can use to interact with the core
   */
  installPackage(pkg: Package, fetchResult: FetchResult, api: InstallPackageExtraApi): Promise<InstallStatus>;

  /**
   * Link a package and its internal (same-linker) dependencies.
   *
   * This function is guaranteed to be called for all packages before the
   * install is finalized.
   *
   * @param locator The package itself
   * @param dependencies The package dependencies
   */
  attachInternalDependencies(locator: Locator, dependencies: Array<[Descriptor, Locator]>): Promise<void>;

  /**
   * Link a package to the location of the external packages that depend on
   * it (only the location is available, since two linkers should be generic
   * enough to not have to make custom integrations).
   *
   * Will never be called for packages supported by the same linker (they'll
   * be linked through the `attachInternalDependencies` hook instead).
   *
   * This function is guaranteed to be called for all packages before the
   * install is finalized.
   *
   * @param locator
   * @param locations
   */
  attachExternalDependents(locator: Locator, dependentPaths: Array<PortablePath>): Promise<void>;

  /**
   * Finalize the install by writing miscellaneous files to the disk.
   */
  finalizeInstall(): Promise<FinalizeInstallData | void | undefined>;
}
