Source: core/src/plugins/input/type.js

import logger from '../../logger.js'
import { dataTypeOf } from './dataType.js'

// register
const types = {}
const dataTypes = {}

// extensions not registered as such
const unregExts = {}

/**
 * Hard-coded, for reasons
 *
 * @access private
 * @memberof module:@citation-js/core.plugins.input
 *
 * @param {module:@citation-js/core~InputData} input
 * @param {module:@citation-js/core.plugins.input~dataType} dataType
 * @return {module:@citation-js/core.plugins.input~format} native format
 */
function parseNativeTypes (input, dataType) {
  switch (dataType) {
    case 'Array':
      if (input.length === 0 || input.every(entry => type(entry) === '@csl/object')) {
        return '@csl/list+object'
      } else {
        return '@else/list+object'
      }

    case 'SimpleObject':
    case 'ComplexObject':
      // might, of course, be something completely else, but this is how the parser works
      return '@csl/object'

    default:
      return '@invalid'
  }
}

/**
 * @access private
 * @memberof module:@citation-js/core.plugins.input
 *
 * @param {Array<module:@citation-js/core.plugins.input~format>} [typeList=[]]
 * @param {module:@citation-js/core~InputData} data
 *
 * @return {module:@citation-js/core.plugins.input~format} native format
 */
function matchType (typeList = [], data) {
  for (const type of typeList) {
    if (types[type].predicate(data)) {
      return matchType(types[type].extensions, data) || type
    }
  }
}

/**
 * @access public
 * @method type
 * @memberof module:@citation-js/core.plugins.input
 *
 * @param {module:@citation-js/core~InputData} input
 *
 * @return {module:@citation-js/core.plugins.input~format} type
 */
export function type (input) {
  const dataType = dataTypeOf(input)

  // Empty array should be @csl/list+object too
  if (dataType === 'Array' && input.length === 0) {
    // Off-load to parseNativeTypes() to not repeat the name
    // '@csl/list+object' here as well, as it might change
    return parseNativeTypes(input, dataType)
  }

  const match = matchType(dataTypes[dataType], input)

  // If no matching formats found, test if native format,
  // else invalid input.
  return match || parseNativeTypes(input, dataType)
}

/**
 * @access public
 * @method addTypeParser
 * @memberof module:@citation-js/core.plugins.input
 *
 * @param {module:@citation-js/core.plugins.input~format} format
 * @param {module:@citation-js/core.plugins.input.util.TypeParser} typeParser
 */
export function addTypeParser (format, { dataType, predicate, extends: extend }) {
  // 1. check if any subclass formats are waiting for this format
  let extensions = []
  if (format in unregExts) {
    extensions = unregExts[format]
    delete unregExts[format]
    logger.debug('[core]', `Subclasses "${extensions}" finally registered to parent type "${format}"`)
  }

  // 2. create object with parser info
  const object = { predicate, extensions }
  types[format] = object

  // 3. determine which type lists the type should be added to
  if (extend) {
    // 3.1. if format is subclass, check if parent type is registered
    const parentTypeParser = types[extend]

    if (parentTypeParser) {
      // 3.1.1. if it is, add the type parser
      parentTypeParser.extensions.push(format)
    } else {
      // 3.1.2. if it isn't, register type as waiting
      if (!unregExts[extend]) {
        unregExts[extend] = []
      }
      unregExts[extend].push(format)
      logger.debug('[core]', `Subclass "${format}" is waiting on parent type "${extend}"`)
    }
  } else {
    // 3.2. else, add
    const typeList = dataTypes[dataType] || (dataTypes[dataType] = [])
    typeList.push(format)
  }
}

/**
 * @access public
 * @method hasTypeParser
 * @memberof module:@citation-js/core.plugins.input
 *
 * @param {module:@citation-js/core.plugins.input~format} type
 *
 * @return {Boolean} type parser is registered
 */
export function hasTypeParser (type) {
  return Object.prototype.hasOwnProperty.call(types, type)
}

/**
 * @access public
 * @method removeTypeParser
 * @memberof module:@citation-js/core.plugins.input
 *
 * @param {module:@citation-js/core.plugins.input~format} type
 */
export function removeTypeParser (type) {
  delete types[type]

  // Removing orphaned type refs
  const typeLists = [
    ...Object.keys(dataTypes).map(key => dataTypes[key]),
    ...Object.keys(types).map(type => types[type].extensions).filter(list => list.length > 0)
  ]
  typeLists.forEach(typeList => {
    const index = typeList.indexOf(type)
    if (index > -1) {
      typeList.splice(index, 1)
    }
  })
}

/**
 * @access public
 * @method listTypeParser
 * @memberof module:@citation-js/core.plugins.input
 *
 * @return {Array<module:@citation-js/core.plugins.input~format>} list of registered type parsers
 */
export function listTypeParser () {
  return Object.keys(types)
}

/**
 * @access public
 * @method treeTypeParser
 * @memberof module:@citation-js/core.plugins.input
 *
 * @return {Object} tree structure
 */
/* istanbul ignore next: debugging */
export function treeTypeParser () {
  const attachNode = name => ({ name, children: types[name].extensions.map(attachNode) })
  return {
    name: 'Type tree',
    children: Object.keys(dataTypes).map(name => ({
      name,
      children: dataTypes[name].map(attachNode)
    }))
  }
}

/**
 * Validate and parse the format name
 *
 * @access public
 * @method typeMatcher
 * @memberof module:@citation-js/core.plugins.input
 * @type {RegExp}
 */
export const typeMatcher = /^(?:@(.+?))(?:\/(?:(.+?)\+)?(?:(.+)))?$/