// @flow

import path from "path";
import Plugin from "./plugin";
import { mergeOptions } from "./util";
import { createItemFromDescriptor } from "./item";
import { buildRootChain, type ConfigContext } from "./config-chain";
import { getEnv } from "./helpers/environment";
import { validate, type ValidatedOptions } from "./validation/options";

import type { ConfigFile, IgnoreFile } from "./files";

export default function loadPrivatePartialConfig(
  inputOpts: mixed,
): {
  options: ValidatedOptions,
  context: ConfigContext,
  ignore: IgnoreFile | void,
  babelrc: ConfigFile | void,
  config: ConfigFile | void,
} | null {
  if (
    inputOpts != null &&
    (typeof inputOpts !== "object" || Array.isArray(inputOpts))
  ) {
    throw new Error("Babel options must be an object, null, or undefined");
  }

  const args = inputOpts ? validate("arguments", inputOpts) : {};

  const { envName = getEnv(), cwd = ".", root: rootDir = "." } = args;
  const absoluteCwd = path.resolve(cwd);
  const absoluteRootDir = path.resolve(absoluteCwd, rootDir);

  const context: ConfigContext = {
    filename:
      typeof args.filename === "string"
        ? path.resolve(cwd, args.filename)
        : undefined,
    cwd: absoluteCwd,
    root: absoluteRootDir,
    envName,
  };

  const configChain = buildRootChain(args, context);
  if (!configChain) return null;

  const options = {};
  configChain.options.forEach(opts => {
    mergeOptions(options, opts);
  });

  // Tack the passes onto the object itself so that, if this object is
  // passed back to Babel a second time, it will be in the right structure
  // to not change behavior.
  options.babelrc = false;
  options.configFile = false;
  options.passPerPreset = false;
  options.envName = context.envName;
  options.cwd = context.cwd;
  options.root = context.root;
  options.filename =
    typeof context.filename === "string" ? context.filename : undefined;

  options.plugins = configChain.plugins.map(descriptor =>
    createItemFromDescriptor(descriptor),
  );
  options.presets = configChain.presets.map(descriptor =>
    createItemFromDescriptor(descriptor),
  );

  return {
    options,
    context,
    ignore: configChain.ignore,
    babelrc: configChain.babelrc,
    config: configChain.config,
  };
}

export function loadPartialConfig(inputOpts: mixed): PartialConfig | null {
  const result = loadPrivatePartialConfig(inputOpts);
  if (!result) return null;

  const { options, babelrc, ignore, config } = result;

  (options.plugins || []).forEach(item => {
    if (item.value instanceof Plugin) {
      throw new Error(
        "Passing cached plugin instances is not supported in " +
          "babel.loadPartialConfig()",
      );
    }
  });

  return new PartialConfig(
    options,
    babelrc ? babelrc.filepath : undefined,
    ignore ? ignore.filepath : undefined,
    config ? config.filepath : undefined,
  );
}

export type { PartialConfig };

class PartialConfig {
  /**
   * These properties are public, so any changes to them should be considered
   * a breaking change to Babel's API.
   */
  options: ValidatedOptions;
  babelrc: string | void;
  babelignore: string | void;
  config: string | void;

  constructor(
    options: ValidatedOptions,
    babelrc: string | void,
    ignore: string | void,
    config: string | void,
  ) {
    this.options = options;
    this.babelignore = ignore;
    this.babelrc = babelrc;
    this.config = config;

    // Freeze since this is a public API and it should be extremely obvious that
    // reassigning properties on here does nothing.
    Object.freeze(this);
  }

  /**
   * Returns true if their is a config file in the filesystem for this config.
   *
   * While this only means .babelrc(.mjs)?/package.json#babel right now, it
   * may well expand in the future, so using this is recommended vs checking
   * this.babelrc directly.
   */
  hasFilesystemConfig(): boolean {
    return this.babelrc !== undefined || this.config !== undefined;
  }
}
Object.freeze(PartialConfig.prototype);
