/**
 * @import {Program} from 'estree-jsx'
 * @import {Root} from 'mdast'
 * @import {Options as RehypeRecmaOptions} from 'rehype-recma'
 * @import {Options as RemarkRehypeOptions} from 'remark-rehype'
 * @import {SourceMapGenerator} from 'source-map'
 * @import {PluggableList, Processor} from 'unified'
 */

/**
 * @typedef ProcessorOptions
 *   Configuration for `createProcessor`.
 * @property {typeof SourceMapGenerator | null | undefined} [SourceMapGenerator]
 *   Add a source map (object form) as the `map` field on the resulting file
 *   (optional).
 * @property {URL | string | null | undefined} [baseUrl]
 *   Use this URL as `import.meta.url` and resolve `import` and `export … from`
 *   relative to it (optional, example: `import.meta.url`).
 * @property {boolean | null | undefined} [development=false]
 *   Whether to add extra info to error messages in generated code and use the
 *   development automatic JSX runtime (`Fragment` and `jsxDEV` from
 *   `/jsx-dev-runtime`) (default: `false`);
 *   when using the webpack loader (`@mdx-js/loader`) or the Rollup integration
 *   (`@mdx-js/rollup`) through Vite, this is automatically inferred from how
 *   you configure those tools.
 * @property {RehypeRecmaOptions['elementAttributeNameCase']} [elementAttributeNameCase='react']
 *   Casing to use for attribute names (default: `'react'`);
 *   HTML casing is for example `class`, `stroke-linecap`, `xml:lang`;
 *   React casing is for example `className`, `strokeLinecap`, `xmlLang`;
 *   for JSX components written in MDX, the author has to be aware of which
 *   framework they use and write code accordingly;
 *   for AST nodes generated by this project, this option configures it
 * @property {'md' | 'mdx' | null | undefined} [format='mdx']
 *   format of the file (default: `'mdx'`);
 *   `'md'` means treat as markdown and `'mdx'` means treat as MDX.
 * @property {boolean | null | undefined} [jsx=false]
 *   Whether to keep JSX (default: `false`);
 *   the default is to compile JSX away so that the resulting file is
 *   immediately runnable.
 * @property {string | null | undefined} [jsxImportSource='react']
 *   Place to import automatic JSX runtimes from (default: `'react'`);
 *   when in the `automatic` runtime, this is used to define an import for
 *   `Fragment`, `jsx`, `jsxDEV`, and `jsxs`.
 * @property {'automatic' | 'classic' | null | undefined} [jsxRuntime='automatic']
 *   JSX runtime to use (default: `'automatic'`);
 *   the automatic runtime compiles to `import _jsx from
 *   '$importSource/jsx-runtime'\n_jsx('p')`;
 *   the classic runtime compiles to calls such as `h('p')`.
 *
 *   > 👉 **Note**: support for the classic runtime is deprecated and will
 *   > likely be removed in the next major version.
 * @property {ReadonlyArray<string> | null | undefined} [mdExtensions]
 *   List of markdown extensions, with dot (default: `['.md', '.markdown', …]`);
 *   affects integrations.
 * @property {ReadonlyArray<string> | null | undefined} [mdxExtensions]
 *   List of MDX extensions, with dot (default: `['.mdx']`);
 *   affects integrations.
 * @property {'function-body' | 'program' | null | undefined} [outputFormat='program']
 *   Output format to generate (default: `'program'`);
 *   in most cases `'program'` should be used, it results in a whole program;
 *   internally `evaluate` uses `'function-body'` to compile to
 *   code that can be passed to `run`;
 *   in some cases, you might want what `evaluate` does in separate steps, such
 *   as when compiling on the server and running on the client.
 * @property {string | null | undefined} [pragma='React.createElement']
 *   Pragma for JSX, used in the classic runtime as an identifier for function
 *   calls: `<x />` to `React.createElement('x')` (default:
 *   `'React.createElement'`);
 *   when changing this, you should also define `pragmaFrag` and
 *   `pragmaImportSource` too.
 *
 *   > 👉 **Note**: support for the classic runtime is deprecated and will
 *   > likely be removed in the next major version.
 * @property {string | null | undefined} [pragmaFrag='React.Fragment']
 *   Pragma for fragment symbol, used in the classic runtime as an identifier
 *   for unnamed calls: `<>` to `React.createElement(React.Fragment)` (default:
 *   `'React.Fragment'`);
 *   when changing this, you should also define `pragma` and
 *   `pragmaImportSource` too.
 *
 *   > 👉 **Note**: support for the classic runtime is deprecated and will
 *   > likely be removed in the next major version.
 * @property {string | null | undefined} [pragmaImportSource='react']
 *   Where to import the identifier of `pragma` from, used in the classic
 *   runtime (default: `'react'`);
 *   to illustrate, when `pragma` is `'a.b'` and `pragmaImportSource` is `'c'`
 *   the following will be generated: `import a from 'c'` and things such as
 *   `a.b('h1', {})`.
 *   when changing this, you should also define `pragma` and `pragmaFrag` too.
 *
 *   > 👉 **Note**: support for the classic runtime is deprecated and will
 *   > likely be removed in the next major version.
 * @property {string | null | undefined} [providerImportSource]
 *   Place to import a provider from (optional, example: `'@mdx-js/react'`);
 *   normally it’s used for runtimes that support context (React, Preact), but
 *   it can be used to inject components into the compiled code;
 *   the module must export and identifier `useMDXComponents` which is called
 *   without arguments to get an object of components (`MDXComponents` from
 *   `mdx/types.js`).
 * @property {PluggableList | null | undefined} [recmaPlugins]
 *   List of recma plugins (optional).
 * @property {PluggableList | null | undefined} [remarkPlugins]
 *   List of remark plugins (optional).
 * @property {PluggableList | null | undefined} [rehypePlugins]
 *   List of rehype plugins (optional).
 * @property {Readonly<RemarkRehypeOptions> | null | undefined} [remarkRehypeOptions]
 *   Options to pass to `remark-rehype` (optional);
 *   in particular, you might want to pass configuration for footnotes if your
 *   content is not in English;
 *   the option `allowDangerousHtml` will always be set to `true` and the MDX
 *   nodes (see `nodeTypes`) are passed through.
 * @property {RehypeRecmaOptions['stylePropertyNameCase']} [stylePropertyNameCase='dom']
 *   Casing to use for property names in `style` objects (default: `'dom'`);
 *   CSS casing is for example `background-color` and `-webkit-line-clamp`;
 *   DOM casing is for example `backgroundColor` and `WebkitLineClamp`;
 *   for JSX components written in MDX, the author has to be aware of which
 *   framework they use and write code accordingly;
 *   for AST nodes generated by this project, this option configures it
 * @property {boolean | null | undefined} [tableCellAlignToStyle=true]
 *   Turn obsolete `align` properties on `td` and `th` into CSS `style`
 *   properties (default: `true`).
 */

import {unreachable} from 'devlop'
import recmaBuildJsx from 'recma-build-jsx'
import recmaJsx from 'recma-jsx'
import recmaStringify from 'recma-stringify'
import rehypeRecma from 'rehype-recma'
import remarkMdx from 'remark-mdx'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import {unified} from 'unified'
import {recmaBuildJsxTransform} from './plugin/recma-build-jsx-transform.js'
import {recmaDocument} from './plugin/recma-document.js'
import {recmaJsxRewrite} from './plugin/recma-jsx-rewrite.js'
import {rehypeRemoveRaw} from './plugin/rehype-remove-raw.js'
import {remarkMarkAndUnravel} from './plugin/remark-mark-and-unravel.js'
import {nodeTypes} from './node-types.js'

const removedOptions = [
  'compilers',
  'filepath',
  'hastPlugins',
  'mdPlugins',
  'skipExport',
  'wrapExport'
]

let warned = false

/**
 * Create a processor to compile markdown or MDX to JavaScript.
 *
 * > **Note**: `format: 'detect'` is not allowed in `ProcessorOptions`.
 *
 * @param {Readonly<ProcessorOptions> | null | undefined} [options]
 *   Configuration (optional).
 * @return {Processor<Root, Program, Program, Program, string>}
 *   Processor.
 */
export function createProcessor(options) {
  const settings = options || {}
  let index = -1

  while (++index < removedOptions.length) {
    const key = removedOptions[index]
    if (key in settings) {
      unreachable(
        'Unexpected removed option `' +
          key +
          '`; see <https://mdxjs.com/migrating/v2/> on how to migrate'
      )
    }
  }

  // @ts-expect-error: throw an error for a runtime value which is not allowed
  // by the types.
  if (settings.format === 'detect') {
    unreachable(
      "Unexpected `format: 'detect'`, which is not supported by `createProcessor`, expected `'mdx'` or `'md'`"
    )
  }

  if (
    (settings.jsxRuntime === 'classic' ||
      settings.pragma ||
      settings.pragmaFrag ||
      settings.pragmaImportSource) &&
    !warned
  ) {
    warned = true
    console.warn(
      "Unexpected deprecated option `jsxRuntime: 'classic'`, `pragma`, `pragmaFrag`, or `pragmaImportSource`; see <https://mdxjs.com/migrating/v3/> on how to migrate"
    )
  }

  const pipeline = unified().use(remarkParse)

  if (settings.format !== 'md') {
    pipeline.use(remarkMdx)
  }

  const remarkRehypeOptions = settings.remarkRehypeOptions || {}

  pipeline
    .use(remarkMarkAndUnravel)
    .use(settings.remarkPlugins || [])
    .use(remarkRehype, {
      ...remarkRehypeOptions,
      allowDangerousHtml: true,
      passThrough: [...(remarkRehypeOptions.passThrough || []), ...nodeTypes]
    })
    .use(settings.rehypePlugins || [])

  if (settings.format === 'md') {
    pipeline.use(rehypeRemoveRaw)
  }

  pipeline
    // @ts-expect-error: `Program` is close enough to a `Node`,
    // but type inference has trouble with it and bridges.
    .use(rehypeRecma, settings)
    .use(recmaDocument, settings)
    .use(recmaJsxRewrite, settings)

  if (!settings.jsx) {
    pipeline.use(recmaBuildJsx, settings).use(recmaBuildJsxTransform, settings)
  }

  pipeline
    .use(recmaJsx)
    .use(recmaStringify, settings)
    .use(settings.recmaPlugins || [])

  // @ts-expect-error: TS doesn’t get the plugins we added with if-statements.
  return pipeline
}
