export function getCharFormat(segment, index) {
  const base = {
    fontFamily: segment.fontFamily || 'Inter',
    fontWeight: segment.fontWeight || '400',
    fontStyle: segment.fontStyle || 'normal',
    fontSizeScale: 1,
  };
  for (let i = (segment.formatRanges?.length || 0) - 1; i >= 0; i -= 1) {
    const range = segment.formatRanges[i];
    if (index >= range.start && index < range.end) {
      return {
        fontFamily: range.fontFamily || base.fontFamily,
        fontWeight: range.fontWeight || base.fontWeight,
        fontStyle: range.fontStyle || base.fontStyle,
        fontSizeScale: range.fontSizeScale ?? base.fontSizeScale,
      };
    }
  }
  return base;
}

export function textFormatsEqual(first, second) {
  if (!first && !second) return true;
  if (!first || !second) return false;
  return first.fontFamily === second.fontFamily
    && first.fontWeight === second.fontWeight
    && first.fontStyle === second.fontStyle
    && (first.fontSizeScale ?? 1) === (second.fontSizeScale ?? 1);
}

export function applyFormatToSelection(segment, start, end, properties) {
  if (start >= end) return;
  const formats = Array.from({ length: end - start }, (_, offset) => getCharFormat(segment, start + offset));
  const ranges = [];
  for (const range of segment.formatRanges || []) {
    if (range.end <= start || range.start >= end) ranges.push(range);
    else {
      if (range.start < start) ranges.push({ ...range, end: start });
      if (range.end > end) ranges.push({ ...range, start: end });
    }
  }
  let active = null;
  let rangeStart = start;
  for (let index = start; index <= end; index += 1) {
    const format = index < end ? { ...formats[index - start], ...properties } : null;
    if (index === end || (active && !textFormatsEqual(format, active))) {
      if (active && rangeStart < index) {
        const range = { start: rangeStart, end: index };
        for (const key of ['fontFamily', 'fontWeight', 'fontStyle']) {
          if (active[key]) range[key] = active[key];
        }
        if (active.fontSizeScale !== undefined && active.fontSizeScale !== 1) {
          range.fontSizeScale = active.fontSizeScale;
        }
        ranges.push(range);
      }
      rangeStart = index;
    }
    active = format;
  }
  ranges.sort((a, b) => a.start - b.start);
  const merged = [];
  for (const range of ranges) {
    const previous = merged.at(-1);
    const same = previous && previous.end === range.start
      && previous.fontFamily === range.fontFamily
      && previous.fontWeight === range.fontWeight
      && previous.fontStyle === range.fontStyle
      && (previous.fontSizeScale ?? 1) === (range.fontSizeScale ?? 1);
    if (same) previous.end = range.end;
    else merged.push(range);
  }
  const defaults = getCharFormat({ ...segment, formatRanges: [] }, 0);
  segment.formatRanges = merged.filter(range => !textFormatsEqual({
    fontFamily: range.fontFamily || defaults.fontFamily,
    fontWeight: range.fontWeight || defaults.fontWeight,
    fontStyle: range.fontStyle || defaults.fontStyle,
    fontSizeScale: range.fontSizeScale ?? 1,
  }, defaults));
}

export function adjustRangesForEdit(segment, editIndex, insertedCount, deletedCount) {
  if (!segment.formatRanges?.length) return;
  const deleteEnd = editIndex + deletedCount;
  const adjusted = [];
  for (const range of segment.formatRanges) {
    let { start, end } = range;
    if (deletedCount > 0) {
      if (end <= editIndex) {}
      else if (start >= deleteEnd) { start -= deletedCount; end -= deletedCount; }
      else if (start >= editIndex && end <= deleteEnd) continue;
      else if (start < editIndex && end > deleteEnd) end -= deletedCount;
      else if (start < editIndex) end = editIndex;
      else { start = editIndex; end -= deletedCount; }
    }
    if (insertedCount > 0 && start >= editIndex) { start += insertedCount; end += insertedCount; }
    else if (insertedCount > 0 && end > editIndex) end += insertedCount;
    if (end > start) adjusted.push({ ...range, start, end });
  }
  segment.formatRanges = adjusted;
}

export function getSelectionFormat(segment, start, end) {
  if (start >= end) return null;
  const first = getCharFormat(segment, start);
  for (let index = start + 1; index < end; index += 1) {
    if (!textFormatsEqual(getCharFormat(segment, index), first)) return { mixed: true };
  }
  return first;
}

export function toggleBoldOnSelection(segment, start, end) {
  const format = getSelectionFormat(segment, start, end);
  if (!format) return;
  applyFormatToSelection(segment, start, end, {
    fontWeight: format.mixed || format.fontWeight === '400' || !format.fontWeight ? '700' : '400',
  });
}

export function toggleItalicOnSelection(segment, start, end) {
  const format = getSelectionFormat(segment, start, end);
  if (!format) return;
  applyFormatToSelection(segment, start, end, {
    fontStyle: format.mixed || format.fontStyle === 'normal' || !format.fontStyle ? 'italic' : 'normal',
  });
}

export function adjustFontSizeScaleOnSelection(segment, start, end, delta) {
  const format = getSelectionFormat(segment, start, end);
  const current = format && !format.mixed && format.fontSizeScale ? format.fontSizeScale : 1;
  applyFormatToSelection(segment, start, end, {
    fontSizeScale: Math.max(0.25, Math.min(4, current + delta)),
  });
}

export function findWordBoundary(text, position, direction) {
  if (direction < 0) {
    let index = position - 1;
    while (index > 0 && /\s/.test(text[index])) index -= 1;
    while (index > 0 && /\w/.test(text[index - 1])) index -= 1;
    return Math.max(0, index);
  }
  let index = position;
  while (index < text.length && /\s/.test(text[index])) index += 1;
  while (index < text.length && /\w/.test(text[index])) index += 1;
  return Math.min(text.length, index);
}

export function wordRangeAt(text, position) {
  let start = position;
  let end = position;
  while (start > 0 && /\w/.test(text[start - 1])) start -= 1;
  while (end < text.length && /\w/.test(text[end])) end += 1;
  if (start === end) {
    while (start > 0 && /\s/.test(text[start - 1])) start -= 1;
    while (end < text.length && /\s/.test(text[end])) end += 1;
  }
  return start === end ? null : { start, end };
}
