import { getLineTerminator } from "./util";

/**
 * All Recast API functions take second parameter with configuration options,
 * documented in options.js
 */
export interface Options extends DeprecatedOptions {
  /**
   * If you want to use a different branch of esprima, or any other module
   * that supports a .parse function, pass that module object to
   * recast.parse as options.parser (legacy synonym: options.esprima).
   * @default require("recast/parsers/esprima")
   */
  parser?: any;

  /**
   * Number of spaces the pretty-printer should use per tab for
   * indentation. If you do not pass this option explicitly, it will be
   * (quite reliably!) inferred from the original code.
   * @default 4
   */
  tabWidth?: number;

  /**
   * If you really want the pretty-printer to use tabs instead of spaces,
   * make this option true.
   * @default false
   */
  useTabs?: boolean;

  /**
   * The reprinting code leaves leading whitespace untouched unless it has
   * to reindent a line, or you pass false for this option.
   * @default true
   */
  reuseWhitespace?: boolean;

  /**
   * Override this option to use a different line terminator, e.g. \r\n.
   * @default require("os").EOL || "\n"
   */
  lineTerminator?: string;

  /**
   * Some of the pretty-printer code (such as that for printing function
   * parameter lists) makes a valiant attempt to prevent really long
   * lines. You can adjust the limit by changing this option; however,
   * there is no guarantee that line length will fit inside this limit.
   * @default 74
   */
  wrapColumn?: number;

  /**
   * Pass a string as options.sourceFileName to recast.parse to tell the
   * reprinter to keep track of reused code so that it can construct a
   * source map automatically.
   * @default null
   */
  sourceFileName?: string | null;

  /**
   * Pass a string as options.sourceMapName to recast.print, and (provided
   * you passed options.sourceFileName earlier) the PrintResult of
   * recast.print will have a .map property for the generated source map.
   * @default null
   */
  sourceMapName?: string | null;

  /**
   * If provided, this option will be passed along to the source map
   * generator as a root directory for relative source file paths.
   * @default null
   */
  sourceRoot?: string | null;

  /**
   * If you provide a source map that was generated from a previous call
   * to recast.print as options.inputSourceMap, the old source map will be
   * composed with the new source map.
   * @default null
   */
  inputSourceMap?: string | null;

  /**
   * If you want esprima to generate .range information (recast only uses
   * .loc internally), pass true for this option.
   * @default false
   */
  range?: boolean;

  /**
   * If you want esprima not to throw exceptions when it encounters
   * non-fatal errors, keep this option true.
   * @default true
   */
  tolerant?: boolean;

  /**
   * If you want to override the quotes used in string literals, specify
   * either "single", "double", or "auto" here ("auto" will select the one
   * which results in the shorter literal) Otherwise, use double quotes.
   * @default null
   */
  quote?: "single" | "double" | "auto" | null;

  /**
   * Controls the printing of trailing commas in object literals, array
   * expressions and function parameters.
   *
   * This option could either be:
   * * Boolean - enable/disable in all contexts (objects, arrays and function params).
   * * Object - enable/disable per context.
   *
   * Example:
   * trailingComma: {
   *   objects: true,
   *   arrays: true,
   *   parameters: false,
   * }
   *
   * @default false
   */
  trailingComma?: boolean;

  /**
   * Controls the printing of spaces inside array brackets.
   * See: http://eslint.org/docs/rules/array-bracket-spacing
   * @default false
   */
  arrayBracketSpacing?: boolean;

  /**
   * Controls the printing of spaces inside object literals,
   * destructuring assignments, and import/export specifiers.
   * See: http://eslint.org/docs/rules/object-curly-spacing
   * @default true
   */
  objectCurlySpacing?: boolean;

  /**
   * If you want parenthesis to wrap single-argument arrow function
   * parameter lists, pass true for this option.
   * @default false
   */
  arrowParensAlways?: boolean;

  /**
   * There are 2 supported syntaxes (`,` and `;`) in Flow Object Types;
   * The use of commas is in line with the more popular style and matches
   * how objects are defined in JS, making it a bit more natural to write.
   * @default true
   */
  flowObjectCommas?: boolean;

  /**
   * Whether to return an array of .tokens on the root AST node.
   * @default true
   */
  tokens?: boolean;
}

interface DeprecatedOptions {
  /** @deprecated */
  esprima?: any;
}

const defaults: Options = {
  parser: require("../parsers/esprima"),
  tabWidth: 4,
  useTabs: false,
  reuseWhitespace: true,
  lineTerminator: getLineTerminator(),
  wrapColumn: 74, // Aspirational for now.
  sourceFileName: null,
  sourceMapName: null,
  sourceRoot: null,
  inputSourceMap: null,
  range: false,
  tolerant: true,
  quote: null,
  trailingComma: false,
  arrayBracketSpacing: false,
  objectCurlySpacing: true,
  arrowParensAlways: false,
  flowObjectCommas: true,
  tokens: true,
};
const hasOwn = defaults.hasOwnProperty;

export type NormalizedOptions = Required<
  Omit<Options, keyof DeprecatedOptions>
>;

// Copy options and fill in default values.
export function normalize(opts?: Options): NormalizedOptions {
  const options = opts || defaults;

  function get(key: keyof Options) {
    return hasOwn.call(options, key) ? options[key] : defaults[key];
  }

  return {
    tabWidth: +get("tabWidth"),
    useTabs: !!get("useTabs"),
    reuseWhitespace: !!get("reuseWhitespace"),
    lineTerminator: get("lineTerminator"),
    wrapColumn: Math.max(get("wrapColumn"), 0),
    sourceFileName: get("sourceFileName"),
    sourceMapName: get("sourceMapName"),
    sourceRoot: get("sourceRoot"),
    inputSourceMap: get("inputSourceMap"),
    parser: get("esprima") || get("parser"),
    range: get("range"),
    tolerant: get("tolerant"),
    quote: get("quote"),
    trailingComma: get("trailingComma"),
    arrayBracketSpacing: get("arrayBracketSpacing"),
    objectCurlySpacing: get("objectCurlySpacing"),
    arrowParensAlways: get("arrowParensAlways"),
    flowObjectCommas: get("flowObjectCommas"),
    tokens: !!get("tokens"),
  };
}
