import { Prism } from './prism-core.mjs';
import { h } from '../../../../hastscript@9.0.1/node_modules/hastscript/lib/index.mjs';
import { parseEntities } from '../../../../parse-entities@4.0.2/node_modules/parse-entities/lib/index.mjs';

/**
 * @import {Element, Root, Text} from 'hast'
 * @import {Grammar, Languages} from 'prismjs'
 */


// Inherit.
function Refractor() {}

Refractor.prototype = Prism;

/** @type {Refractor} */
// @ts-expect-error: TS is wrong.
const refractor = new Refractor();

// Create.
refractor.highlight = highlight;
refractor.register = register;
refractor.alias = alias;
refractor.registered = registered;
refractor.listLanguages = listLanguages;

// @ts-expect-error Overwrite Prism.
refractor.util.encode = encode;
// @ts-expect-error Overwrite Prism.
refractor.Token.stringify = stringify;

/**
 * Highlight `value` (code) as `language` (programming language).
 *
 * @param {string} value
 *   Code to highlight.
 * @param {Grammar | string} language
 *   Programming language name, alias, or grammar.
 * @returns {Root}
 *   Node representing highlighted code.
 */
function highlight(value, language) {
  if (typeof value !== 'string') {
    throw new TypeError('Expected `string` for `value`, got `' + value + '`')
  }

  /** @type {Grammar} */
  let grammar;
  /** @type {string | undefined} */
  let name;

  // `name` is a grammar object.
  // This was called internally by Prism.js before 1.28.0.
  /* c8 ignore next 2 */
  if (language && typeof language === 'object') {
    grammar = language;
  } else {
    name = language;

    if (typeof name !== 'string') {
      throw new TypeError('Expected `string` for `name`, got `' + name + '`')
    }

    if (Object.hasOwn(refractor.languages, name)) {
      grammar = refractor.languages[name];
    } else {
      throw new Error('Unknown language: `' + name + '` is not registered')
    }
  }

  return {
    type: 'root',
    // @ts-expect-error: we hacked Prism to accept and return the things we want.
    children: Prism.highlight.call(refractor, value, grammar, name)
  }
}

/**
 * Register a syntax.
 *
 * @param {Syntax} syntax
 *   Language function made for refractor, as in, the files in
 *   `refractor/lang/*.js`.
 * @returns {undefined}
 *   Nothing.
 */
function register(syntax) {
  if (typeof syntax !== 'function' || !syntax.displayName) {
    throw new Error('Expected `function` for `syntax`, got `' + syntax + '`')
  }

  // Do not duplicate registrations.
  if (!Object.hasOwn(refractor.languages, syntax.displayName)) {
    syntax(refractor);
  }
}

/**
 * Register aliases for already registered languages.
 *
 * @param {Record<string, ReadonlyArray<string> | string> | string} language
 *   Language to alias.
 * @param {ReadonlyArray<string> | string | null | undefined} [alias]
 *   Aliases.
 * @returns {undefined}
 *   Nothing.
 */
function alias(language, alias) {
  const languages = refractor.languages;
  /** @type {Record<string, ReadonlyArray<string> | string>} */
  let map = {};

  if (typeof language === 'string') {
    if (alias) {
      map[language] = alias;
    }
  } else {
    map = language;
  }

  /** @type {string} */
  let key;

  for (key in map) {
    if (Object.hasOwn(map, key)) {
      const value = map[key];
      const list = typeof value === 'string' ? [value] : value;
      let index = -1;

      while (++index < list.length) {
        languages[list[index]] = languages[key];
      }
    }
  }
}

/**
 * Check whether an `alias` or `language` is registered.
 *
 * @param {string} aliasOrLanguage
 *   Language or alias to check.
 * @returns {boolean}
 *   Whether the language is registered.
 */
function registered(aliasOrLanguage) {
  if (typeof aliasOrLanguage !== 'string') {
    throw new TypeError(
      'Expected `string` for `aliasOrLanguage`, got `' + aliasOrLanguage + '`'
    )
  }

  return Object.hasOwn(refractor.languages, aliasOrLanguage)
}

/**
 * List all registered languages (names and aliases).
 *
 * @returns {Array<string>}
 *   List of language names.
 */
function listLanguages() {
  const languages = refractor.languages;
  /** @type {Array<string>} */
  const list = [];
  /** @type {string} */
  let language;

  for (language in languages) {
    if (
      Object.hasOwn(languages, language) &&
      typeof languages[language] === 'object'
    ) {
      list.push(language);
    }
  }

  return list
}

/**
 * @param {Array<_Token | string> | _Token | string} value
 *   Token to stringify.
 * @param {string} language
 *   Language of the token.
 * @returns {Array<Element | Text> | Element | Text}
 *   Node representing the token.
 */
function stringify(value, language) {
  if (typeof value === 'string') {
    return {type: 'text', value}
  }

  if (Array.isArray(value)) {
    /** @type {Array<Element | Text>} */
    const result = [];
    let index = -1;

    while (++index < value.length) {
      if (
        value[index] !== null &&
        value[index] !== undefined &&
        value[index] !== ''
      ) {
        // Cast because we assume no sub-arrays.
        result.push(
          /** @type {Element | Text} */ (stringify(value[index], language))
        );
      }
    }

    return result
  }

  /** @type {_Env} */
  const env = {
    attributes: {},
    classes: ['token', value.type],
    content: stringify(value.content, language),
    language,
    tag: 'span',
    type: value.type
  };

  if (value.alias) {
    env.classes.push(
      ...(typeof value.alias === 'string' ? [value.alias] : value.alias)
    );
  }

  // @ts-expect-error Prism.
  refractor.hooks.run('wrap', env);

  return h(
    env.tag + '.' + env.classes.join('.'),
    attributes(env.attributes),
    env.content
  )
}

/**
 * @template {unknown} T
 *   Tokens.
 * @param {T} tokens
 *   Input.
 * @returns {T}
 *   Output, same as input.
 */
function encode(tokens) {
  return tokens
}

/**
 * @param {Record<string, string>} record
 *   Attributes.
 * @returns {Record<string, string>}
 *   Attributes.
 */
function attributes(record) {
  /** @type {string} */
  let key;

  for (key in record) {
    if (Object.hasOwn(record, key)) {
      record[key] = parseEntities(record[key]);
    }
  }

  return record
}

export { refractor };
