#let styled = text(red)[lorem].func()

#let _gridded(dir, fixed, to-wrap, ..kwargs) = {
  let dir-kwargs = (:)
  if dir not in (ltr, rtl) {
    panic("Specify either `rtl` or `ltr` as the wrap direction")
  }
  let args = if dir == rtl {
    (to-wrap, fixed)
  } else {
    (fixed, to-wrap)
  }
  grid(..args, columns: 2, rows: 2, column-gutter: 1em, ..kwargs)
}

#let _grid-height(content, container-size, styles) = {
  measure(box(width: container-size.width, content), styles).height
}

#let _get-chunk(words, end, reverse, start: 0) = {
  if end < 0 {
    return words.join(" ")
  }
  if reverse { words = words.rev() }
  let subset = words.slice(start, end)
  if reverse {
    subset = subset.rev()
  }
  subset.join(" ")
}

#let _get-wrap-index(height-func, words, goal-height, reverse) = {
  for index in range(1, words.len(), step: 1) {
    let cur-height = height-func(_get-chunk(words, index, reverse))
    if cur-height > goal-height {
      return index - 1
    }
  }
  return -1
}

#let _rewrap(element, new-content) = {
  let fields = element.fields()
  for key in ("body", "text", "children", "child") {
    if key in fields {
      let _ = fields.remove(key)
    }
  }
  let positional = (new-content, )
  if "styles" in fields {
    positional.push(fields.remove("styles"))
  }
  element.func()(..fields, ..positional)
}

#let split-other(body, height-func, goal-height, align, splitter-func) = {
  (wrapped: none, rest: body)
}

#let split-has-text(body, height-func, goal-height, align, splitter-func) = {
  let words = body.text.split(" ")
  let reverse = align.y == bottom
  let wrap-index = _get-wrap-index(height-func, words, goal-height, reverse)
  let _rewrap = _rewrap.with(body)
  if wrap-index > 0 {
    let chunk = _rewrap(_get-chunk(words, wrap-index, reverse))
    let end-chunk = _rewrap(_get-chunk(words, words.len(), reverse, start: wrap-index))
    (wrapped: chunk, rest: end-chunk)
  } else {
    (wrapped: none, rest: body)
  }
}

#let split-has-children(body, height-func, goal-height, align, splitter-func) = {
  let reverse = align.y == bottom
  let children = if reverse { body.children.rev() } else { body.children }
  for (ii, child) in children.enumerate() {
    let prev-children = children.slice(0, ii).join()
    let new-height-func(child) = {
      height-func((prev-children, child).join())
    }
    let height = new-height-func(child)
    if height <= goal-height {
      continue
    }
    // height func calculator should now account for prior children
    let split = splitter-func(child, new-height-func, goal-height, align)
    let new-children = (..children.slice(0, ii), split.wrapped)
    let new-rest = children.slice(ii+1)
    if split.rest != none {
      new-rest.insert(0, split.rest)
    }
    if reverse {
      new-children = new-children.rev()
      new-rest = new-rest.rev()
    }
    return (
      wrapped: _rewrap(body, new-children),
      rest: _rewrap(body, new-rest)
    )
  }
  panic(
    "This function should only be called if the seq child should be split"
  )
}

#let split-has-body(body, height-func, goal-height, align, splitter-func) = {
  // Elements that can be split and have a 'body' field.
  let splittable = (strong, emph, underline, stroke, overline, highlight, list.item, styled)

  let new-height-func(content) = {
    height-func(_rewrap(body, content))
  }
  let args = (new-height-func, goal-height, align, splitter-func)
  let body-text = body.at("body", default: body.at("child", default: none))
  if body.func() in splittable {
    let result = splitter-func(body-text, new-height-func, goal-height, align)
    if result.wrapped != none {
      return (wrapped: _rewrap(body, result.wrapped), rest: _rewrap(body, result.rest))
    } else {
      return split-other(body, ..args)
    }
  }
  // Shape doesn't split nicely, so treat it as unwrappable
  return split-other(body, ..args)
}

#let splitter(body, height-func, goal-height, align) = {
  let self-height = height-func(body)
  if self-height <= goal-height {
    return (wrapped: body, rest: none)
  }
  if type(body) == str {
    body = text(body)
  }
  let body-splitter = if body.has("text") {
    split-has-text
  } else if body.has("body") or body.has("child") {
    split-has-body
  } else if body.has("children") {
    split-has-children
  } else {
    split-other
  }
  return body-splitter(body, height-func, goal-height, align, splitter)
}

#let _inner-wrap-content(to-wrap, y-align, grid-func, styles, container-size, ..grid-kwargs) = {
  let height-func(txt) = _grid-height(grid-func(txt), container-size, styles)
  let goal-height = height-func([])
  if y-align == top {
     goal-height += measure(v(1em), styles).height
  }
  let result = splitter(to-wrap, height-func, goal-height, y-align)
  if y-align == top {
    grid-func(result.wrapped)
    result.rest
  } else {
    result.rest
    grid-func(result.wrapped)
  }
}

/// Places `to-wrap` next to `fixed`, wrapping `to-wrap` as its height overflows `fixed`.
///
/// *Basic Use:*
/// ```typ
/// #let body = lorem(40)
/// #wrap-content(rect(fill: teal), body)
/// ```
///
/// *Something More Fun:*
/// ```typ
/// #set par(justify: true)
/// // Helpers; not required
/// #let grad(map) = {
///   gradient.linear(
///     ..eval("color.map." + map)
///   )
/// }
/// #let make-fig(fill) = {
///   set figure.caption(separator: "")
///   fill = grad(fill)
///   figure(
///     rect(fill: fill, radius: 0.5em),
///     caption: [],
///   )
/// }
/// #let (fig1, fig2) = {
///   ("viridis", "plasma").map(make-fig)
/// }
/// #wrap-content(fig1, body, align: right)
/// #wrap-content(fig2, [#body #body], align: bottom)
/// ```
///
/// Note that you can increase the distance between a figure's bottom and the wrapped
/// text by boxing it with an inset:
/// ```typ
/// #let spaced = box(
///   make-fig("rocket"),
///   inset: (bottom: 0.3em)
/// )
/// #wrap-content(spaced, body)
/// ```
///
/// - fixed (content): Content that will not be wrapped, (i.e., a figure).
/// - to-wrap (content): Content that will be wrapped, (i.e., text). Currently, logic 
///   works best with pure-text content, but hypothetically will work with any `content`.
/// - align (alignment): Alignment of `fixed` relative to `to-wrap`. `top` will align
///   the top of `fixed` with the top of `to-wrap`, and `bottom` will align the bottom of
///   `fixed` with the bottom of `to-wrap`. `left` and `right` alignments determine
///   horizontal alignment of `fixed` relative to `to-wrap`. Alignments can be combined,
///   i.e., `bottom + right` will align the bottom-right corner of `fixed` with the
///   bottom-right corner of `to-wrap`.
///
///  ```typ
///  #wrap-content(
///    make-fig("turbo"),
///    body,
///    align: bottom + right
///  )
///  ```
/// - ..grid-kwargs (any): Keyword arguments to pass to the underlying `grid` function.
///   Of note:
///     - `column-gutter` controls horizontal margin between `fixed` and `to-wrap`. Or,
///       you can surround the fixed content in a box with `(inset: ...)` for more
///       fine-grained control.
///     - `columns` can be set to force sizing of `fixed` and `to-wrap`. For instance,
///       `columns: (50%, 50%)` will force `fixed` and `to-wrap` to each take up half
///       of the available space. If content isn't this big, the fill will be blank
///       margin.
///  ```typ
///  #let spaced = box(
///    make-fig("mako"), inset: 0.5em
///  )
///  #wrap-content(spaced, body)
///  ```
///  ```typ
///  #wrap-content(
///    make-fig("spectral"),
///    body,
///    align: bottom,
///    columns: (50%, 50%),
///  )
///  ```
///
#let wrap-content(fixed, to-wrap, align: top + left, styles: auto, size: auto, ..grid-kwargs) = {
  if center in (align.x, align.y) {
    panic("Center alignment is not supported")
  }

  // "none" x alignment defaults to left
  let dir = if align.x == right { rtl } else { ltr }
  let gridded(..args) = box(_gridded(dir, fixed, ..grid-kwargs, ..args))
  // "none" y alignment defaults to top
  let y-align = if align.y == bottom { bottom } else { top }

  if styles != auto and size != auto {
    _inner-wrap-content(to-wrap, y-align, gridded, styles, size, ..grid-kwargs)
  } else {
    style(styles => layout(container-size => {
      _inner-wrap-content(to-wrap, y-align, gridded, styles, container-size, ..grid-kwargs)
    }))
  }
}

/// Wrap a body of text around two pieces of content. The logic only works if enough text
/// exists to overflow both the top and bottom content. Use this instead of 2 separate
/// `wrap-content` calls if you want to avoid a paragraph break between the top and bottom
/// content.
///
/// *Example:*
/// ```typ
/// #let fig1 = make-fig("inferno")
/// #let fig2 = make-fig("rainbow")
/// #wrap-top-bottom(fig1, fig2, lorem(60))
/// ```
/// - top-fixed (content): Content that will not be wrapped, (i.e., a figure).
/// - bottom-fixed (content): Content that will not be wrapped, (i.e., a figure).
/// - body (content): Content that will be wrapped, (i.e., text)
/// - top-kwargs (any): Keyword arguments to pass to the underlying `wrap-content` function
///   for the top content. `x` alignment is kept (left/right), but `y` alignment is
///   overridden to `top`.
/// - bottom-kwargs (any): Keyword arguments to pass to the underlying `wrap-content` function
///   for the bottom content. `x` alignment is kept (left/right), but `y` alignment is
///   overridden to `bottom`.
///
#let wrap-top-bottom(
  top-fixed,
  bottom-fixed,
  body,
  top-kwargs: (:),
  bottom-kwargs: (:),
) = {
  top-kwargs = top-kwargs + (
    align: top-kwargs.at("align", default: top + left).x + top
  )
  bottom-kwargs = bottom-kwargs + (
    align: bottom-kwargs.at("align", default: bottom + right).x + bottom
  )
  style(styles => {
    layout(size => {
      let wrapfig(..args) = wrap-content(styles: styles, size: size, ..args)
      wrapfig(top-fixed, ..top-kwargs)[
        #wrapfig(bottom-fixed, ..bottom-kwargs)[
          #body
        ]
      ]
    })
  })
}
