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'
import {
  findPrimitiveCurveReferenceSpans
} from '../../core/primitive/diagnosticReference.js'

/** 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] }`
 * and primitive-curve references rendered as interactive controls.
 * @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 {object} options.labels
 * @param {string} options.labels.expand
 * @param {function(number): string} [options.labels.primitiveCurve]
 * @param {number} [options.threshold]
 * @returns {string}
 */
export function formatStatusLineHtml(line, options) {
  const {
    expanded,
    keyPrefix,
    lineIdx,
    labels,
    threshold = JSON_INNER_COLLAPSE_THRESHOLD
  } = options

  const jsonSpans = findValidJsonObjectSpans(line).map(span => ({
    ...span,
    kind: 'json'
  }))
  const primitiveCurveSpans = findPrimitiveCurveReferenceSpans(line)
    .filter(span => !jsonSpans.some(
      jsonSpan => span.start >= jsonSpan.start && span.end <= jsonSpan.end
    ))
    .map(span => ({ ...span, kind: 'primitiveCurve' }))
  const spans = [...jsonSpans, ...primitiveCurveSpans]
    .sort((a, b) => a.start - b.start)
  if (spans.length === 0) {
    return newlinesToBr(escapeHtml(line))
  }

  let out = ''
  let last = 0
  let jsonSpanIdx = 0
  for (const span of spans) {
    out += escapeHtml(line.slice(last, span.start))

    if (span.kind === 'primitiveCurve') {
      const tip = escapeHtml(
        labels.primitiveCurve?.(span.curveId) ??
        `Primitive curve ${span.curveId}`
      )
      out += `<button type="button" class="status-alert__primitive-curve-ref" data-primitive-curve-id="${span.curveId}" title="${tip}" aria-label="${tip}">${span.curveId}</button>`
    } else {
      const inner = line.slice(span.start + 1, span.end)
      const key = `${keyPrefix}-${lineIdx}-${jsonSpanIdx}`
      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>}`
      }
      jsonSpanIdx++
    }

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