Source: schema/schemaMerger.js

import { IssueError } from '../issues/issues'
import { SchemaTag, SchemaValueTag } from './entries'
import { PartneredSchema, Schema } from './containers'

export default class PartneredSchemaMerger {
  /**
   * The sources of data to be merged.
   * @type {Schema[]}
   */
  sourceSchemas

  /**
   * The current source of data to be merged.
   * @type {Schema}
   */
  currentSource

  /**
   * The destination of data to be merged.
   * @type {PartneredSchema}
   */
  destination

  /**
   * Constructor.
   *
   * @param {Schema[]} sourceSchemas The sources of data to be merged.
   */
  constructor(sourceSchemas) {
    this.sourceSchemas = sourceSchemas
    this.destination = new PartneredSchema(sourceSchemas)
    this._validate()
  }

  /**
   * Pre-validate the partnered schemas.
   * @private
   */
  _validate() {
    for (const schema of this.sourceSchemas.slice(1)) {
      if (schema.withStandard !== this.destination.withStandard) {
        IssueError.generateAndThrow('differentWithStandard', {
          first: schema.withStandard,
          second: this.destination.withStandard,
        })
      }
    }
  }

  /**
   * Merge the lazy partnered schemas.
   *
   * @returns {PartneredSchema} The merged partnered schema.
   */
  mergeSchemas() {
    for (const additionalSchema of this.sourceSchemas.slice(1)) {
      this.currentSource = additionalSchema
      this._mergeData()
    }
    return this.destination
  }

  /**
   * The source schema's tag collection.
   *
   * @return {SchemaEntryManager<SchemaTag>}
   */
  get sourceTags() {
    return this.currentSource.entries.tags
  }

  /**
   * The destination schema's tag collection.
   *
   * @returns {SchemaEntryManager<SchemaTag>}
   */
  get destinationTags() {
    return this.destination.entries.tags
  }

  /**
   * Merge two lazy partnered schemas.
   * @private
   */
  _mergeData() {
    this._mergeTags()
  }

  /**
   * Merge the tags from two lazy partnered schemas.
   * @private
   */
  _mergeTags() {
    for (const tag of this.sourceTags.values()) {
      this._mergeTag(tag)
    }
  }

  /**
   * Merge a tag from one schema to another.
   *
   * @param {SchemaTag} tag The tag to copy.
   * @private
   */
  _mergeTag(tag) {
    if (!tag.getAttributeValue('inLibrary')) {
      return
    }

    const shortName = tag.name
    if (this.destinationTags.hasEntry(shortName.toLowerCase())) {
      IssueError.generateAndThrow('lazyPartneredSchemasShareTag', { tag: shortName })
    }

    const rootedTagShortName = tag.getAttributeValue('rooted')
    if (rootedTagShortName) {
      const parentTag = tag.parent
      if (parentTag?.name?.toLowerCase() !== rootedTagShortName?.toLowerCase()) {
        IssueError.generateAndThrowInternalError(`Node ${shortName} is improperly rooted.`)
      }
    }

    this._copyTagToSchema(tag)
  }

  /**
   * Copy a tag from one schema to another.
   *
   * @param {SchemaTag} tag The tag to copy.
   * @private
   */
  _copyTagToSchema(tag) {
    const booleanAttributes = new Set()
    const valueAttributes = new Map()

    for (const attribute of tag.booleanAttributes) {
      booleanAttributes.add(this.destination.entries.attributes.getEntry(attribute.name) ?? attribute)
    }
    for (const [key, value] of tag.valueAttributes) {
      valueAttributes.set(this.destination.entries.attributes.getEntry(key.name) ?? key, value)
    }

    /**
     * @type {SchemaUnitClass[]}
     */
    const unitClasses = tag.unitClasses.map(
      (unitClass) => this.destination.entries.unitClasses.getEntry(unitClass.name) ?? unitClass,
    )

    let newTag
    if (tag instanceof SchemaValueTag) {
      newTag = new SchemaValueTag(tag.name, booleanAttributes, valueAttributes, unitClasses)
    } else {
      newTag = new SchemaTag(tag.name, booleanAttributes, valueAttributes, unitClasses)
    }
    const destinationParentTag = this.destinationTags.getEntry(tag.parent?.name?.toLowerCase())
    if (destinationParentTag) {
      newTag.parent = destinationParentTag
      if (newTag instanceof SchemaValueTag) {
        newTag.parent.valueTag = newTag
      }
    }

    this.destinationTags._definitions.set(newTag.name.toLowerCase(), newTag)
  }
}