// @flow

import type { PluginTarget, PluginOptions } from "./validation/options";

import path from "path";
import {
  createDescriptor,
  type UnloadedDescriptor,
} from "./config-descriptors";

export function createItemFromDescriptor(desc: UnloadedDescriptor): ConfigItem {
  return new ConfigItem(desc);
}

/**
 * Create a config item using the same value format used in Babel's config
 * files. Items returned from this function should be cached by the caller
 * ideally, as recreating the config item will mean re-resolving the item
 * and re-evaluating the plugin/preset function.
 */
export function createConfigItem(
  value:
    | PluginTarget
    | [PluginTarget, PluginOptions]
    | [PluginTarget, PluginOptions, string | void],
  {
    dirname = ".",
    type,
  }: {
    dirname?: string,
    type?: "preset" | "plugin",
  } = {},
): ConfigItem {
  const descriptor = createDescriptor(value, path.resolve(dirname), {
    type,
    alias: "programmatic item",
  });

  return createItemFromDescriptor(descriptor);
}

export function getItemDescriptor(item: mixed): UnloadedDescriptor | void {
  if (item instanceof ConfigItem) {
    return item._descriptor;
  }

  return undefined;
}

export type { ConfigItem };

/**
 * A public representation of a plugin/preset that will _eventually_ be load.
 * Users can use this to interact with the results of a loaded Babel
 * configuration.
 *
 * Any changes to public properties of this class should be considered a
 * breaking change to Babel's API.
 */
class ConfigItem {
  /**
   * The private underlying descriptor that Babel actually cares about.
   * If you access this, you are a bad person.
   */
  _descriptor: UnloadedDescriptor;

  /**
   * The resolved value of the item itself.
   */
  value: {} | Function;

  /**
   * The options, if any, that were passed to the item.
   * Mutating this will lead to undefined behavior. If you need
   */
  options: {} | void;

  /**
   * The directory that the options for this item are relative to.
   */
  dirname: string;

  /**
   * Get the name of the plugin, if the user gave it one.
   */
  name: string | void;

  /**
   * Data about the file that the item was loaded from, if Babel knows it.
   */
  file: {
    // The requested path, e.g. "@babel/env".
    request: string,

    // The resolved absolute path of the file.
    resolved: string,
  } | void;

  constructor(descriptor: UnloadedDescriptor) {
    // Make people less likely to stumble onto this if they are exploring
    // programmatically, and also make sure that if people happen to
    // pass the item through JSON.stringify, it doesn't show up.
    this._descriptor = descriptor;
    Object.defineProperty(this, "_descriptor", ({ enumerable: false }: any));

    if (this._descriptor.options === false) {
      throw new Error("Assertion failure - unexpected false options");
    }

    this.value = this._descriptor.value;
    this.options = this._descriptor.options;
    this.dirname = this._descriptor.dirname;
    this.name = this._descriptor.name;
    this.file = this._descriptor.file
      ? {
          request: this._descriptor.file.request,
          resolved: this._descriptor.file.resolved,
        }
      : undefined;

    // Freeze the object to make it clear that people shouldn't expect mutating
    // this object to do anything. A new item should be created if they want
    // to change something.
    Object.freeze(this);
  }
}

Object.freeze(ConfigItem.prototype);
