/**
 * @import {Directive, HtmlOptions} from 'micromark-extension-directive'
 * @import {CompileContext, Handle as MicromarkHandle, HtmlExtension} from 'micromark-util-types'
 */

import { parseEntities } from 'parse-entities';
const own = {}.hasOwnProperty;

/**
 * Create an extension for `micromark` to support directives when serializing
 * to HTML.
 *
 * @param {HtmlOptions | null | undefined} [options={}]
 *   Configuration (default: `{}`).
 * @returns {HtmlExtension}
 *   Extension for `micromark` that can be passed in `htmlExtensions`, to
 *   support directives when serializing to HTML.
 */
export function directiveHtml(options) {
  const options_ = options || {};
  return {
    enter: {
      directiveContainer() {
        enter.call(this, 'containerDirective');
      },
      directiveContainerAttributes: enterAttributes,
      directiveContainerLabel: enterLabel,
      directiveContainerContent() {
        this.buffer();
      },
      directiveLeaf() {
        enter.call(this, 'leafDirective');
      },
      directiveLeafAttributes: enterAttributes,
      directiveLeafLabel: enterLabel,
      directiveText() {
        enter.call(this, 'textDirective');
      },
      directiveTextAttributes: enterAttributes,
      directiveTextLabel: enterLabel
    },
    exit: {
      directiveContainer: exit,
      directiveContainerAttributeClassValue: exitAttributeClassValue,
      directiveContainerAttributeIdValue: exitAttributeIdValue,
      directiveContainerAttributeName: exitAttributeName,
      directiveContainerAttributeValue: exitAttributeValue,
      directiveContainerAttributes: exitAttributes,
      directiveContainerContent: exitContainerContent,
      directiveContainerFence: exitContainerFence,
      directiveContainerLabel: exitLabel,
      directiveContainerName: exitName,
      directiveLeaf: exit,
      directiveLeafAttributeClassValue: exitAttributeClassValue,
      directiveLeafAttributeIdValue: exitAttributeIdValue,
      directiveLeafAttributeName: exitAttributeName,
      directiveLeafAttributeValue: exitAttributeValue,
      directiveLeafAttributes: exitAttributes,
      directiveLeafLabel: exitLabel,
      directiveLeafName: exitName,
      directiveText: exit,
      directiveTextAttributeClassValue: exitAttributeClassValue,
      directiveTextAttributeIdValue: exitAttributeIdValue,
      directiveTextAttributeName: exitAttributeName,
      directiveTextAttributeValue: exitAttributeValue,
      directiveTextAttributes: exitAttributes,
      directiveTextLabel: exitLabel,
      directiveTextName: exitName
    }
  };

  /**
   * @this {CompileContext}
   * @param {Directive['type']} type
   */
  function enter(type) {
    let stack = this.getData('directiveStack');
    if (!stack) this.setData('directiveStack', stack = []);
    stack.push({
      type,
      name: ''
    });
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function exitName(token) {
    const stack = this.getData('directiveStack');
    stack[stack.length - 1].name = this.sliceSerialize(token);
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function enterLabel() {
    this.buffer();
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function exitLabel() {
    const data = this.resume();
    const stack = this.getData('directiveStack');
    stack[stack.length - 1].label = data;
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function enterAttributes() {
    this.buffer();
    this.setData('directiveAttributes', []);
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function exitAttributeIdValue(token) {
    const attributes = this.getData('directiveAttributes');
    attributes.push(['id', parseEntities(this.sliceSerialize(token), {
      attribute: true
    })]);
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function exitAttributeClassValue(token) {
    const attributes = this.getData('directiveAttributes');
    attributes.push(['class', parseEntities(this.sliceSerialize(token), {
      attribute: true
    })]);
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function exitAttributeName(token) {
    // Attribute names in CommonMark are significantly limited, so character
    // references can’t exist.
    const attributes = this.getData('directiveAttributes');
    attributes.push([this.sliceSerialize(token), '']);
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function exitAttributeValue(token) {
    const attributes = this.getData('directiveAttributes');
    attributes[attributes.length - 1][1] = parseEntities(this.sliceSerialize(token), {
      attribute: true
    });
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function exitAttributes() {
    const stack = this.getData('directiveStack');
    const attributes = this.getData('directiveAttributes');
    /** @type {Record<string, string>} */
    const cleaned = {};
    let index = -1;
    while (++index < attributes.length) {
      const attribute = attributes[index];
      if (attribute[0] === 'class' && cleaned.class) {
        cleaned.class += ' ' + attribute[1];
      } else {
        cleaned[attribute[0]] = attribute[1];
      }
    }
    this.resume();
    this.setData('directiveAttributes');
    stack[stack.length - 1].attributes = cleaned;
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function exitContainerContent() {
    const data = this.resume();
    const stack = this.getData('directiveStack');
    stack[stack.length - 1].content = data;
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function exitContainerFence() {
    const stack = this.getData('directiveStack');
    const directive = stack[stack.length - 1];
    if (!directive._fenceCount) directive._fenceCount = 0;
    directive._fenceCount++;
    if (directive._fenceCount === 1) this.setData('slurpOneLineEnding', true);
  }

  /**
   * @this {CompileContext}
   * @type {MicromarkHandle}
   */
  function exit() {
    const stack = this.getData('directiveStack');
    const directive = stack.pop();
    /** @type {boolean | undefined} */
    let found;
    /** @type {boolean | undefined} */
    let result;
    if (own.call(options_, directive.name)) {
      result = options_[directive.name].call(this, directive);
      found = result !== false;
    }
    if (!found && own.call(options_, '*')) {
      result = options_['*'].call(this, directive);
      found = result !== false;
    }
    if (!found && directive.type !== 'textDirective') {
      this.setData('slurpOneLineEnding', true);
    }
  }
}