function findLineStarts(src) {
  const ls = [0]
  let offset = src.indexOf('\n')
  while (offset !== -1) {
    offset += 1
    ls.push(offset)
    offset = src.indexOf('\n', offset)
  }
  return ls
}

function getSrcInfo(cst) {
  let lineStarts, src
  if (typeof cst === 'string') {
    lineStarts = findLineStarts(cst)
    src = cst
  } else {
    if (Array.isArray(cst)) cst = cst[0]
    if (cst && cst.context) {
      if (!cst.lineStarts) cst.lineStarts = findLineStarts(cst.context.src)
      lineStarts = cst.lineStarts
      src = cst.context.src
    }
  }
  return { lineStarts, src }
}

/**
 * @typedef {Object} LinePos - One-indexed position in the source
 * @property {number} line
 * @property {number} col
 */

/**
 * Determine the line/col position matching a character offset.
 *
 * Accepts a source string or a CST document as the second parameter. With
 * the latter, starting indices for lines are cached in the document as
 * `lineStarts: number[]`.
 *
 * Returns a one-indexed `{ line, col }` location if found, or
 * `undefined` otherwise.
 *
 * @param {number} offset
 * @param {string|Document|Document[]} cst
 * @returns {?LinePos}
 */
export function getLinePos(offset, cst) {
  if (typeof offset !== 'number' || offset < 0) return null
  const { lineStarts, src } = getSrcInfo(cst)
  if (!lineStarts || !src || offset > src.length) return null
  for (let i = 0; i < lineStarts.length; ++i) {
    const start = lineStarts[i]
    if (offset < start) {
      return { line: i, col: offset - lineStarts[i - 1] + 1 }
    }
    if (offset === start) return { line: i + 1, col: 1 }
  }
  const line = lineStarts.length
  return { line, col: offset - lineStarts[line - 1] + 1 }
}

/**
 * Get a specified line from the source.
 *
 * Accepts a source string or a CST document as the second parameter. With
 * the latter, starting indices for lines are cached in the document as
 * `lineStarts: number[]`.
 *
 * Returns the line as a string if found, or `null` otherwise.
 *
 * @param {number} line One-indexed line number
 * @param {string|Document|Document[]} cst
 * @returns {?string}
 */
export function getLine(line, cst) {
  const { lineStarts, src } = getSrcInfo(cst)
  if (!lineStarts || !(line >= 1) || line > lineStarts.length) return null
  const start = lineStarts[line - 1]
  let end = lineStarts[line] // undefined for last line; that's ok for slice()
  while (end && end > start && src[end - 1] === '\n') --end
  return src.slice(start, end)
}

/**
 * Pretty-print the starting line from the source indicated by the range `pos`
 *
 * Trims output to `maxWidth` chars while keeping the starting column visible,
 * using `…` at either end to indicate dropped characters.
 *
 * Returns a two-line string (or `null`) with `\n` as separator; the second line
 * will hold appropriately indented `^` marks indicating the column range.
 *
 * @param {Object} pos
 * @param {LinePos} pos.start
 * @param {LinePos} [pos.end]
 * @param {string|Document|Document[]*} cst
 * @param {number} [maxWidth=80]
 * @returns {?string}
 */
export function getPrettyContext({ start, end }, cst, maxWidth = 80) {
  let src = getLine(start.line, cst)
  if (!src) return null
  let { col } = start
  if (src.length > maxWidth) {
    if (col <= maxWidth - 10) {
      src = src.substr(0, maxWidth - 1) + '…'
    } else {
      const halfWidth = Math.round(maxWidth / 2)
      if (src.length > col + halfWidth)
        src = src.substr(0, col + halfWidth - 1) + '…'
      col -= src.length - maxWidth
      src = '…' + src.substr(1 - maxWidth)
    }
  }
  let errLen = 1
  let errEnd = ''
  if (end) {
    if (
      end.line === start.line &&
      col + (end.col - start.col) <= maxWidth + 1
    ) {
      errLen = end.col - start.col
    } else {
      errLen = Math.min(src.length + 1, maxWidth) - col
      errEnd = '…'
    }
  }
  const offset = col > 1 ? ' '.repeat(col - 1) : ''
  const err = '^'.repeat(errLen)
  return `${src}\n${offset}${err}${errEnd}`
}
