/**
 * @typedef {import('mdast').Root|import('mdast').Parent['children'][number]} MdastNode
 * @typedef {import('hast').Root|import('hast').Parent['children'][number]} HastNode
 * @typedef {import('mdast').Parent} Parent
 * @typedef {import('mdast').Definition} Definition
 * @typedef {import('mdast').FootnoteDefinition} FootnoteDefinition
 * @typedef {import('hast').Properties} Properties
 * @typedef {import('hast').Element} Element
 * @typedef {import('hast').ElementContent} Content
 * @typedef {import('unist-util-position').PositionLike} PositionLike
 *
 * @typedef EmbeddedHastFields
 * @property {string} [hName]
 *   Defines the tag name of an element.
 * @property {Properties} [hProperties]
 *   Defines the properties of an element.
 * @property {Array<Content>} [hChildren]
 *   Defines the (hast) children of an element.
 *
 * @typedef {Record<string, unknown> & EmbeddedHastFields} Data
 *   unist data with embedded hast fields.
 *
 * @typedef {MdastNode & {data?: Data}} NodeWithData
 *   unist node with embedded hast data.
 *
 * @callback Handler
 *   Handle a node.
 * @param {H} h
 *   Handle context.
 * @param {any} node
 *   mdast node to handle.
 * @param {Parent|null} parent
 *   Parent of `node`.
 * @returns {Content|Array<Content>|null|undefined}
 *   hast node.
 *
 * @callback HFunctionProps
 * @param {MdastNode|PositionLike|null|undefined} node
 *   mdast node or unist position.
 * @param {string} tagName
 *   HTML tag name.
 * @param {Properties} props
 *   Properties.
 * @param {Array<Content>?} [children]
 *   hast content.
 * @returns {Element}
 *   Compiled element.
 *
 * @callback HFunctionNoProps
 * @param {MdastNode|PositionLike|null|undefined} node
 *   mdast node or unist position.
 * @param {string} tagName
 *   HTML tag name.
 * @param {Array<Content>?} [children]
 *   hast content
 * @returns {Element}
 *   Compiled element.
 *
 * @typedef HFields
 * @property {boolean} dangerous
 *   Whether HTML is allowed.
 * @property {string} clobberPrefix
 *   Prefix to use to prevent DOM clobbering.
 * @property {string} footnoteLabel
 *   Label to use to introduce the footnote section.
 * @property {string} footnoteLabelTagName
 *   HTML used for the footnote label.
 * @property {Properties} footnoteLabelProperties
 *   Properties on the HTML tag used for the footnote label.
 * @property {string} footnoteBackLabel
 *   Label to use to go back to a footnote call from the footnote section.
 * @property {(identifier: string) => Definition|null} definition
 *   Definition cache.
 * @property {Record<string, FootnoteDefinition>} footnoteById
 *   Footnote cache.
 * @property {Array<string>} footnoteOrder
 *   Order in which footnotes occur.
 * @property {Record<string, number>} footnoteCounts
 *   Counts the same footnote was used.
 * @property {Handlers} handlers
 *   Applied handlers.
 * @property {Handler} unknownHandler
 *   Handler for any none not in `passThrough` or otherwise handled.
 * @property {(left: NodeWithData|PositionLike|null|undefined, right: Content) => Content} augment
 *   Like `h` but lower-level and usable on non-elements.
 * @property {Array<string>} passThrough
 *   List of node types to pass through untouched (except for their children).
 *
 * @typedef Options
 *   Configuration (optional).
 * @property {boolean} [allowDangerousHtml=false]
 *   Whether to allow `html` nodes and inject them as `raw` HTML.
 * @property {string} [clobberPrefix='user-content-']
 *   Prefix to use before the `id` attribute to prevent it from *clobbering*.
 *   attributes.
 *   DOM clobbering is this:
 *
 *   ```html
 *   <p id=x></p>
 *   <script>alert(x)</script>
 *   ```
 *
 *   Elements by their ID are made available in browsers on the `window` object.
 *   Using a prefix prevents this from being a problem.
 * @property {string} [footnoteLabel='Footnotes']
 *   Label to use for the footnotes section.
 *   Affects screen reader users.
 *   Change it if you’re authoring in a different language.
 * @property {string} [footnoteLabelTagName='h2']
 *   HTML tag to use for the footnote label.
 *   Can be changed to match your document structure and play well with your choice of css.
 * @property {Properties} [footnoteLabelProperties={className: ['sr-only']}]
 *   Properties to use on the footnote label.
 *   A 'sr-only' class is added by default to hide this from sighted users.
 *   Change it to make the label visible, or add classes for other purposes.
 * @property {string} [footnoteBackLabel='Back to content']
 *   Label to use from backreferences back to their footnote call.
 *   Affects screen reader users.
 *   Change it if you’re authoring in a different language.
 * @property {Handlers} [handlers]
 *   Object mapping mdast nodes to functions handling them
 * @property {Array<string>} [passThrough]
 *   List of custom mdast node types to pass through (keep) in hast
 * @property {Handler} [unknownHandler]
 *   Handler for all unknown nodes.
 *
 * @typedef {Record<string, Handler>} Handlers
 *   Map of node types to handlers
 * @typedef {HFunctionProps & HFunctionNoProps & HFields} H
 *   Handle context
 */

import {u} from 'unist-builder'
import {visit} from 'unist-util-visit'
import {pointStart, pointEnd} from 'unist-util-position'
import {generated} from 'unist-util-generated'
import {definitions} from 'mdast-util-definitions'
import {one} from './traverse.js'
import {footer} from './footer.js'
import {handlers} from './handlers/index.js'

const own = {}.hasOwnProperty

/**
 * Turn mdast into hast.
 *
 * @param {MdastNode} tree
 *   mdast node.
 * @param {Options} [options]
 *   Configuration (optional).
 * @returns {H}
 *   `h` function.
 */
function factory(tree, options) {
  const settings = options || {}
  const dangerous = settings.allowDangerousHtml || false
  /** @type {Record<string, FootnoteDefinition>} */
  const footnoteById = {}

  h.dangerous = dangerous
  h.clobberPrefix =
    settings.clobberPrefix === undefined || settings.clobberPrefix === null
      ? 'user-content-'
      : settings.clobberPrefix
  h.footnoteLabel = settings.footnoteLabel || 'Footnotes'
  h.footnoteLabelTagName = settings.footnoteLabelTagName || 'h2'
  h.footnoteLabelProperties = settings.footnoteLabelProperties || {
    className: ['sr-only']
  }
  h.footnoteBackLabel = settings.footnoteBackLabel || 'Back to content'
  h.definition = definitions(tree)
  h.footnoteById = footnoteById
  /** @type {Array<string>} */
  h.footnoteOrder = []
  /** @type {Record<string, number>} */
  h.footnoteCounts = {}
  h.augment = augment
  h.handlers = {...handlers, ...settings.handlers}
  h.unknownHandler = settings.unknownHandler
  h.passThrough = settings.passThrough

  visit(tree, 'footnoteDefinition', (definition) => {
    const id = String(definition.identifier).toUpperCase()

    // Mimick CM behavior of link definitions.
    // See: <https://github.com/syntax-tree/mdast-util-definitions/blob/8290999/index.js#L26>.
    if (!own.call(footnoteById, id)) {
      footnoteById[id] = definition
    }
  })

  // @ts-expect-error Hush, it’s fine!
  return h

  /**
   * Finalise the created `right`, a hast node, from `left`, an mdast node.
   *
   * @param {(NodeWithData|PositionLike)?} left
   * @param {Content} right
   * @returns {Content}
   */
  function augment(left, right) {
    // Handle `data.hName`, `data.hProperties, `data.hChildren`.
    if (left && 'data' in left && left.data) {
      /** @type {Data} */
      const data = left.data

      if (data.hName) {
        if (right.type !== 'element') {
          right = {
            type: 'element',
            tagName: '',
            properties: {},
            children: []
          }
        }

        right.tagName = data.hName
      }

      if (right.type === 'element' && data.hProperties) {
        right.properties = {...right.properties, ...data.hProperties}
      }

      if ('children' in right && right.children && data.hChildren) {
        right.children = data.hChildren
      }
    }

    if (left) {
      const ctx = 'type' in left ? left : {position: left}

      if (!generated(ctx)) {
        // @ts-expect-error: fine.
        right.position = {start: pointStart(ctx), end: pointEnd(ctx)}
      }
    }

    return right
  }

  /**
   * Create an element for `node`.
   *
   * @type {HFunctionProps}
   */
  function h(node, tagName, props, children) {
    if (Array.isArray(props)) {
      children = props
      props = {}
    }

    // @ts-expect-error augmenting an element yields an element.
    return augment(node, {
      type: 'element',
      tagName,
      properties: props || {},
      children: children || []
    })
  }
}

/**
 * Transform `tree` (an mdast node) to a hast node.
 *
 * @param {MdastNode} tree mdast node
 * @param {Options} [options] Configuration
 * @returns {HastNode|null|undefined} hast node
 */
export function toHast(tree, options) {
  const h = factory(tree, options)
  const node = one(h, tree, null)
  const foot = footer(h)

  if (foot) {
    // @ts-expect-error If there’s a footer, there were definitions, meaning block
    // content.
    // So assume `node` is a parent node.
    node.children.push(u('text', '\n'), foot)
  }

  return Array.isArray(node) ? {type: 'root', children: node} : node
}

export {handlers as defaultHandlers} from './handlers/index.js'
