Source: bids/types/tsv.js

import isPlainObject from 'lodash/isPlainObject'

import { BidsFile } from './file'
import { convertParsedTSVData, parseTSV } from '../tsvParser'
import { BidsSidecar } from './json'
import BidsHedTsvValidator from '../validator/tsvValidator'
import { IssueError } from '../../issues/issues'

/**
 * A BIDS TSV file.
 */
export class BidsTsvFile extends BidsFile {
  /**
   * This file's parsed TSV data.
   * @type {Map<string, string[]>}
   */
  parsedTsv

  /**
   * HED strings in the "HED" column of the TSV data.
   * @type {string[]}
   */
  hedColumnHedStrings

  /**
   * The pseudo-sidecar object representing the merged sidecar data.
   * @type {BidsSidecar}
   */
  mergedSidecar

  /**
   * Constructor.
   *
   * @param {string} name - The name of the TSV file.
   * @param {Object} file - The file object representing this file.
   * @param {{headers: string[], rows: string[][]}|Map|string} tsvData - This file's TSV data.
   * @param {Object} mergedDictionary - The merged sidecar data.
   * @param {DefinitionManager} defManager - The definition manager for this file.
   */
  constructor(name, file, tsvData, mergedDictionary = {}, defManager = undefined) {
    super(name, file, BidsHedTsvValidator)

    if (typeof tsvData === 'string') {
      this.parsedTsv = parseTSV(tsvData)
    } else if (tsvData instanceof Map) {
      this.parsedTsv = new Map(tsvData)
    } else if (isPlainObject(tsvData)) {
      this.parsedTsv = convertParsedTSVData(tsvData)
    } else {
      IssueError.generateAndThrowInternalError('parsedTsv has an invalid type')
    }

    this.mergedSidecar = new BidsSidecar(name, this.file, mergedDictionary, defManager)
    this._parseHedColumn()
  }

  _parseHedColumn() {
    const hedColumn = this.parsedTsv.get('HED')
    if (hedColumn === undefined) {
      this.hedColumnHedStrings = []
    } else {
      this.hedColumnHedStrings = hedColumn.map((hedCell) => (hedCell && hedCell !== 'n/a' ? hedCell : ''))
    }
  }

  /**
   * Determine whether this file has any HED data.
   *
   * @returns {boolean}
   */
  get hasHedData() {
    return this.parsedTsv.has('HED') || this.mergedSidecar.hasHedData
  }

  /**
   * Whether this TSV file is a timeline file.
   *
   * @returns {boolean}
   */
  get isTimelineFile() {
    return this.parsedTsv.has('onset')
  }
}

export class BidsTsvElement {
  /**
   * The string representation of this row
   * @type {string}
   */
  hedString

  /**
   * The ParsedHedString representation of this row
   * @type {ParsedHedString}
   */
  parsedHedString

  /**
   * The file this row belongs to (usually just the path).
   * @type {Object}
   */
  file

  /**
   * The onset represented by this row or a NaN.
   * @type {Number}
   */
  onset

  /**
   * The line number(s) (including the header) represented by this row.
   */
  tsvLine

  /**
   * Constructor.
   *
   * @param {string} hedString The HED string representing this row
   * @param {BidsTsvFile} tsvFile The file this row belongs to.
   * @param {number} onset - The onset for this element or undefined if none
   * @param {string} tsvLine The line number(s) (string) corresponding to the lines in {@link tsvFile} this line is located at.
   */
  constructor(hedString, tsvFile, onset, tsvLine) {
    this.hedString = hedString
    this.parsedHedString = null
    this.file = tsvFile.file
    this.fileName = tsvFile.name
    this.onset = onset
    this.tsvLine = tsvLine
  }

  /**
   * Override of {@link object.prototype.toString}.
   *
   * @returns {string}
   */
  toString() {
    const onsetString = this.onset ? ` with onset=${this.onset.toString()}` : ''
    return this.hedString + ` in TSV file "${this.fileName}" at line(s) ${this.tsvLine}` + onsetString
  }

  /**
   * Create a string list of a list of BidsTsvElement objects.
   * @param {BidsTsvElement[]} elements - A list of elements to construct line numbers from.
   * @returns {string} - A string with the list of line numbers for error messages.
   */
  static getTsvLines(elements) {
    return elements.map((element) => element.tsvLine).join(',')
  }
}

/**
 * A row in a BIDS TSV file.
 */
export class BidsTsvRow extends BidsTsvElement {
  /**
   * The map of column name to value for this row.
   * @type {Map}
   */
  rowCells

  /**
   * Constructor.
   *
   * @param {string} hedString The parsed string representing this row.
   * @param {Map} rowCells The column-to-value mapping for this row.
   * @param {BidsTsvFile} tsvFile The file this row belongs to.
   * @param {number} tsvLine The line number in {@link tsvFile} this line is located at.
   */
  constructor(hedString, tsvFile, tsvLine, rowCells) {
    const onset = rowCells.has('onset') ? rowCells.get('onset') : undefined
    super(hedString, tsvFile, onset, tsvLine.toString())
    this.rowCells = rowCells
  }
}