Source: app/utils/compactJsonInStatusMessage.js

/*
 * Copyright 2026 The Ray Optics Simulation authors and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 */

import escapeHtml from 'escape-html'

/** Inner length (between `{` and `}`) above which we collapse the segment in the UI. */
export const JSON_INNER_COLLAPSE_THRESHOLD = 48

/**
 * HTML ignores newline characters in text; convert them to line breaks for status display.
 * @param {string} html
 * @returns {string}
 */
function newlinesToBr(html) {
  return html.replace(/\r\n|\r|\n/g, '<br>')
}

/**
 * Index of the matching `}` for `{` at openIdx, or -1. Ignores `{` / `}` inside JSON strings.
 * @param {string} s
 * @param {number} openIdx
 * @returns {number}
 */
export function findMatchingBraceEnd(s, openIdx) {
  if (openIdx < 0 || openIdx >= s.length || s[openIdx] !== '{') return -1
  let depth = 1
  let inString = false
  let i = openIdx + 1
  while (i < s.length) {
    const c = s[i]
    if (inString) {
      if (c === '\\') {
        i++
        if (i >= s.length) return -1
        if (s[i] === 'u') {
          i++
          for (let k = 0; k < 4; k++) {
            if (i >= s.length) return -1
            if (!/[0-9a-fA-F]/.test(s[i])) return -1
            i++
          }
          continue
        }
        i++
        continue
      }
      if (c === '"') {
        inString = false
      }
      i++
      continue
    }
    if (c === '"') {
      inString = true
      i++
      continue
    }
    if (c === '{') depth++
    else if (c === '}') {
      depth--
      if (depth === 0) return i
    }
    i++
  }
  return -1
}

/**
 * Non-overlapping `{ ... }` spans that parse as JSON.
 * @param {string} line
 * @returns {Array<{start: number, end: number, json: string}>}
 */
export function findValidJsonObjectSpans(line) {
  const spans = []
  let i = 0
  while (i < line.length) {
    if (line[i] === '{') {
      const end = findMatchingBraceEnd(line, i)
      if (end === -1) {
        i++
        continue
      }
      const json = line.slice(i, end + 1)
      try {
        JSON.parse(json)
        spans.push({ start: i, end: end, json })
      } catch {
        /* not valid JSON */
      }
      i = end + 1
    } else {
      i++
    }
  }
  return spans
}

const TOGGLE_ICON_SVG =
  '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="status-alert__json-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 0 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 0 1 0-3 1.5 1.5 0 0 1 0 3z"/></svg>'

/**
 * HTML for one status line with long JSON segments collapsed to `{ [icon] }`.
 * @param {string} line
 * @param {object} options
 * @param {Record<string, boolean>} options.expanded
 * @param {string} options.keyPrefix e.g. 'w' or 'e'
 * @param {number} options.lineIdx
 * @param {{ expand: string }} options.labels
 * @param {number} [options.threshold]
 * @returns {string}
 */
export function formatStatusLineHtml(line, options) {
  const {
    expanded,
    keyPrefix,
    lineIdx,
    labels,
    threshold = JSON_INNER_COLLAPSE_THRESHOLD
  } = options

  const spans = findValidJsonObjectSpans(line)
  if (spans.length === 0) {
    return newlinesToBr(escapeHtml(line))
  }

  let out = ''
  let last = 0
  for (let spanIdx = 0; spanIdx < spans.length; spanIdx++) {
    const span = spans[spanIdx]
    out += escapeHtml(line.slice(last, span.start))

    const inner = line.slice(span.start + 1, span.end)
    const key = `${keyPrefix}-${lineIdx}-${spanIdx}`
    const collapsed = inner.length > threshold

    if (!collapsed) {
      out += escapeHtml(span.json)
    } else if (expanded[key]) {
      out += `<span class="status-alert__json-expanded">${escapeHtml(span.json)}</span>`
    } else {
      const tip = escapeHtml(labels.expand)
      out += `{<button type="button" class="status-alert__json-toggle" data-json-key="${escapeHtml(key)}" title="${tip}" aria-label="${tip}" aria-expanded="false">${TOGGLE_ICON_SVG}</button>}`
    }

    last = span.end + 1
  }
  out += escapeHtml(line.slice(last))
  return newlinesToBr(out)
}