const fullWidthRanges: Array<[number, number]> = [
  [0x1100, 0x115F],
  [0x2329, 0x232A],
  [0x2E80, 0xA4CF],
  [0xAC00, 0xD7A3],
  [0xF900, 0xFAFF],
  [0xFE10, 0xFE19],
  [0xFE30, 0xFE6F],
  [0xFF00, 0xFF60],
  [0xFFE0, 0xFFE6],
]

function isFullWidthCodePoint(codePoint: number): boolean {
  // Quick reject: below first or above last full-width range
  if (codePoint < 0x1100 || codePoint > 0xFFE6)
    return false
  // Ranges are sorted — exit as soon as we pass the candidate
  for (const [start, end] of fullWidthRanges) {
    if (codePoint < start)
      return false
    if (codePoint <= end)
      return true
  }
  return false
}

function isEmojiLike(codePoint: number): boolean {
  return (
    (codePoint >= 0x1F300 && codePoint <= 0x1FAFF)
    || (codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF)
  )
}

const extendedPictographicRe = /\p{Extended_Pictographic}/u
const emojiPresentationRe = /\p{Emoji_Presentation}/u
const emojiRe = /\p{Emoji}/u

function charCellWidth(text: string): 1 | 2 {
  if (!text)
    return 1

  // Fast path: single BMP character below all full-width / emoji ranges.
  if (text.length === 1) {
    const code = text.charCodeAt(0)
    if (code < 0x1100)
      return 1
  }

  const codePoint = text.codePointAt(0)
  if (codePoint == null)
    return 1

  const hasVs16 = text.includes('\uFE0F')
  if (isFullWidthCodePoint(codePoint) || isEmojiLike(codePoint))
    return 2
  if (emojiPresentationRe?.test(text))
    return 2
  if (text.includes('\u20E3'))
    return 2
  if (hasVs16 && emojiRe?.test(text))
    return 2
  if (extendedPictographicRe?.test(text))
    return codePoint <= 0xFFFF ? (hasVs16 ? 2 : 1) : 2
  return 1
}

interface GraphemeSegment {
  segment: string
  index: number
}

let graphemeSegmenter: Intl.Segmenter | null = null
try {
  graphemeSegmenter
    = typeof Intl !== 'undefined' && 'Segmenter' in Intl
      ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
      : null
}
catch {
  graphemeSegmenter = null
}

function forEachGraphemeWithIndex(
  text: string,
  cb: (seg: GraphemeSegment) => void,
): void {
  if (!text)
    return
  if (graphemeSegmenter) {
    for (const part of graphemeSegmenter.segment(
      text,
    ) as any as Iterable<GraphemeSegment>)
      cb(part)
    return
  }

  // Fallback: code point iteration with best-effort indices.
  let i = 0
  while (i < text.length) {
    const cp = text.codePointAt(i) ?? 0
    const ch = String.fromCodePoint(cp)
    cb({ segment: ch, index: i })
    i += ch.length
  }
}

export function textCellWidth(text: string): number {
  let cells = 0
  forEachGraphemeWithIndex(text, (g) => {
    cells += charCellWidth(g.segment)
  })
  return cells
}

export function takeByCells(
  text: string,
  maxCells: number,
): { chunk: string, rest: string } {
  maxCells = Math.max(0, Math.floor(maxCells))
  if (maxCells <= 0)
    return { chunk: '', rest: text }
  let cells = 0
  let end = 0
  const graphemes: GraphemeSegment[] = []
  forEachGraphemeWithIndex(text, (g) => {
    graphemes.push({ segment: g.segment, index: g.index })
  })

  for (let gi = 0; gi < graphemes.length; gi++) {
    const { segment, index } = graphemes[gi]!
    const nextIndex = index + segment.length
    const w = charCellWidth(segment)
    if (cells + w > maxCells)
      break
    cells += w
    end = nextIndex
    if (cells >= maxCells)
      break
  }
  return { chunk: text.slice(0, end), rest: text.slice(end) }
}

export function sliceByCells(text: string, maxCells: number): string {
  return takeByCells(text, maxCells).chunk
}

export function padEndByCells(text: string, width: number): string {
  width = Math.max(0, Math.floor(width))
  const cells = textCellWidth(text)
  if (cells >= width)
    return sliceByCells(text, width)
  return `${text}${' '.repeat(width - cells)}`
}

export function wrapLineByCells(text: string, width: number): string[] {
  width = Math.max(1, Math.floor(width))
  if (!text)
    return ['']
  const out: string[] = []
  let remaining = text
  while (true) {
    const { chunk, rest } = takeByCells(remaining, width)
    out.push(chunk)
    if (!rest)
      break
    remaining = rest
  }
  return out
}
