///|
fn markdown_editor_set_code_language(
  source : String,
  selection : MarkdownEditorSelection,
  language : String,
) -> MarkdownEditorEditResult {
  match markdown_editor_code_block_at_selection(source, selection) {
    Some(block) =>
      markdown_editor_replace_code_language(source, selection, block, language)
    None => markdown_editor_append_snippet(source, SetCodeLanguage(language))
  }
}

///|
fn markdown_editor_set_html_block_text(
  source : String,
  selection : MarkdownEditorSelection,
  text : String,
) -> MarkdownEditorEditResult {
  match markdown_editor_html_block_at_selection(source, selection) {
    Some(block) =>
      markdown_editor_replace_html_block_text(source, selection, block, text)
    None => markdown_editor_append_snippet(source, SetHtmlBlockText(text))
  }
}

///|
fn markdown_editor_replace_html_block_text(
  source : String,
  selection : MarkdownEditorSelection,
  block : MarkdownEditorBlock,
  text : String,
) -> MarkdownEditorEditResult {
  let chars = source.to_array()
  let next_source = String::from_array(chars[:block.source_range.start]) +
    text +
    String::from_array(chars[block.source_range.end:])
  let caret = block.source_range.start + markdown_editor_text_length(text)
  { source: next_source, caret, selection: selection.selection }
}

///|
fn markdown_editor_replace_code_language(
  source : String,
  selection : MarkdownEditorSelection,
  block : MarkdownEditorBlock,
  language : String,
) -> MarkdownEditorEditResult {
  let chars = source.to_array()
  let marker_end = markdown_editor_code_fence_marker_end(source, block)
  let language = language.trim().to_owned()
  let text = String::from_array(chars[:marker_end]) +
    language +
    String::from_array(chars[block.content_range.start - 1:])
  let delta = markdown_editor_text_length(language) -
    (block.content_range.start - 1 - marker_end)
  let caret = if selection.caret >= block.content_range.start {
    selection.caret + delta
  } else {
    selection.caret
  }
  { source: text, caret, selection: selection.selection }
}

///|
fn markdown_editor_code_fence_marker_end(
  source : String,
  block : MarkdownEditorBlock,
) -> Int {
  let chars = source.to_array()
  let mut index = block.source_range.start
  while index < chars.length() && chars[index] != '`' {
    index = index + 1
  }
  while index < chars.length() && chars[index] == '`' {
    index = index + 1
  }
  index
}

///|
fn markdown_editor_toggle_code_block(
  source : String,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorEditResult {
  if source == "" {
    return markdown_editor_append_snippet(source, CodeBlock)
  }
  match markdown_editor_containing_quoted_code_block(source, selection) {
    Some(block) =>
      return markdown_editor_unwrap_quoted_code_block(source, block, selection)
    None => ()
  }
  match markdown_editor_containing_code_block(source, selection) {
    Some(block) => markdown_editor_unwrap_code_block(source, block, selection)
    None => markdown_editor_wrap_lines_in_code_block(source, selection)
  }
}

///|
pub(all) struct MarkdownEditorQuotedCodeBlock {
  source_range : MarkdownSourceRange
  content_range : MarkdownSourceRange
} derive(Eq, Debug)

///|
pub(all) struct MarkdownEditorQuotedCodeFence {
  quote_prefix : String
  fence : String
} derive(Eq, Debug)

///|
fn markdown_editor_containing_quoted_code_block(
  source : String,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorQuotedCodeBlock? {
  let lines = markdown_editor_lines(source)
  if lines.length() < 2 {
    return None
  }
  let anchor = match markdown_editor_active_selection(selection.selection) {
    Some(range) => range.start
    None => selection.caret
  }
  let anchor = markdown_editor_clamp_int(
    anchor,
    0,
    markdown_editor_text_length(source),
  )
  let anchor_line = markdown_editor_line_index_at_offset(lines, anchor)
  let mut open_index = anchor_line
  while open_index >= 0 {
    match markdown_editor_quoted_code_fence_opening(lines[open_index].text) {
      Some(opening) =>
        for close_index in (open_index + 1)..= lines[open_index].start &&
              anchor <= lines[close_index].end {
              let content_start = if close_index > open_index + 1 {
                lines[open_index + 1].start
              } else {
                lines[close_index].start
              }
              let content_end = if close_index > open_index + 1 {
                lines[close_index - 1].end
              } else {
                lines[close_index].start
              }
              return Some({
                source_range: {
                  start: lines[open_index].start,
                  end: lines[close_index].end,
                },
                content_range: { start: content_start, end: content_end },
              })
            }
            break
          }
        }
      None => ()
    }
    if open_index == 0 {
      break
    }
    open_index = open_index - 1
  }
  None
}

///|
fn markdown_editor_line_index_at_offset(
  lines : Array[MarkdownLine],
  offset : Int,
) -> Int {
  let mut result = 0
  for index in 0..= lines[index].start && offset <= lines[index].end {
      return index
    }
    if offset > lines[index].end {
      result = index
    }
  }
  result
}

///|
fn markdown_editor_quoted_code_fence_opening(
  line : String,
) -> MarkdownEditorQuotedCodeFence? {
  let quote_prefix = markdown_editor_quote_context_prefix(line)
  if quote_prefix == "" {
    return None
  }
  let nested = markdown_editor_substring(
    line,
    markdown_editor_text_length(quote_prefix),
    markdown_editor_text_length(line),
  )
  match markdown_editor_opening_code_fence_marker(nested) {
    Some(fence) => Some({ quote_prefix, fence: fence.marker })
    None => None
  }
}

///|
fn markdown_editor_quoted_code_fence_closing(
  line : String,
  opening : MarkdownEditorQuotedCodeFence,
) -> Bool {
  let chars = line.to_array()
  if !markdown_editor_source_has_marker(chars, 0, opening.quote_prefix) {
    return false
  }
  let nested = markdown_editor_substring(
    line,
    markdown_editor_text_length(opening.quote_prefix),
    markdown_editor_text_length(line),
  )
  markdown_editor_closing_code_fence(nested, opening.fence)
}

///|
fn markdown_editor_unwrap_quoted_code_block(
  source : String,
  block : MarkdownEditorQuotedCodeBlock,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorEditResult {
  let chars = source.to_array()
  let text = String::from_array(chars[:block.source_range.start]) +
    String::from_array(chars[block.content_range.start:block.content_range.end]) +
    String::from_array(chars[block.source_range.end:])
  let removed_before_content = block.content_range.start -
    block.source_range.start
  let removed_total = removed_before_content +
    block.source_range.end -
    block.content_range.end
  let caret = markdown_editor_unwrapped_quoted_code_offset(
    selection.caret,
    block,
    removed_before_content,
    removed_total,
  )
  let next_selection = match
    markdown_editor_active_selection(selection.selection) {
    Some(range) =>
      Some(
        @core.TextRange::new(
          start=markdown_editor_unwrapped_quoted_code_offset(
            range.start,
            block,
            removed_before_content,
            removed_total,
          ),
          end=markdown_editor_unwrapped_quoted_code_offset(
            range.end,
            block,
            removed_before_content,
            removed_total,
          ),
        ),
      )
    None => None
  }
  { source: text, caret, selection: next_selection }
}

///|
fn markdown_editor_unwrapped_quoted_code_offset(
  offset : Int,
  block : MarkdownEditorQuotedCodeBlock,
  removed_before_content : Int,
  removed_total : Int,
) -> Int {
  if offset <= block.source_range.start {
    offset
  } else if offset <= block.source_range.end {
    markdown_editor_clamp_int(
      offset - removed_before_content,
      block.source_range.start,
      block.source_range.end - removed_total,
    )
  } else {
    offset - removed_total
  }
}

///|
fn markdown_editor_containing_code_block(
  source : String,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorBlock? {
  let anchor = match markdown_editor_active_selection(selection.selection) {
    Some(range) => range.start
    None => selection.caret
  }
  let anchor = markdown_editor_clamp_int(
    anchor,
    0,
    markdown_editor_text_length(source),
  )
  for block in parse_markdown_editor_blocks(source) {
    if block.block.kind is MarkdownBlockKind::CodeBlock &&
      anchor >= block.source_range.start &&
      anchor <= block.source_range.end {
      return Some(block)
    }
  }
  None
}

///|
fn markdown_editor_unwrap_code_block(
  source : String,
  block : MarkdownEditorBlock,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorEditResult {
  let chars = source.to_array()
  let open_end = markdown_editor_min_int(
    block.content_range.start,
    chars.length(),
  )
  let close_start = markdown_editor_max_int(
    block.content_range.end - 1,
    open_end,
  )
  let text = String::from_array(chars[:block.source_range.start]) +
    String::from_array(chars[open_end:close_start]) +
    String::from_array(chars[block.source_range.end:])
  let removed_before_content = open_end - block.source_range.start
  let removed_total = removed_before_content +
    block.source_range.end -
    close_start
  let caret = if selection.caret <= block.source_range.start {
    selection.caret
  } else if selection.caret <= block.source_range.end {
    markdown_editor_clamp_int(
      selection.caret - removed_before_content,
      block.source_range.start,
      block.source_range.end - removed_total,
    )
  } else {
    selection.caret - removed_total
  }
  let next_selection = match
    markdown_editor_active_selection(selection.selection) {
    Some(range) =>
      Some(
        @core.TextRange::new(
          start=markdown_editor_unwrapped_code_offset(
            range.start,
            block,
            removed_before_content,
            removed_total,
          ),
          end=markdown_editor_unwrapped_code_offset(
            range.end,
            block,
            removed_before_content,
            removed_total,
          ),
        ),
      )
    None => None
  }
  { source: text, caret, selection: next_selection }
}

///|
fn markdown_editor_unwrapped_code_offset(
  offset : Int,
  block : MarkdownEditorBlock,
  removed_before_content : Int,
  removed_total : Int,
) -> Int {
  if offset <= block.source_range.start {
    offset
  } else if offset <= block.source_range.end {
    markdown_editor_clamp_int(
      offset - removed_before_content,
      block.source_range.start,
      block.source_range.end - removed_total,
    )
  } else {
    offset - removed_total
  }
}

///|
fn markdown_editor_wrap_lines_in_code_block(
  source : String,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorEditResult {
  let chars = source.to_array()
  let range = markdown_editor_selected_line_range(source, selection)
  let selected = String::from_array(chars[range.start:range.end])
  let quote_prefix = markdown_editor_code_block_quote_prefix(selected)
  let prefix = quote_prefix + "```\n"
  let suffix = "\n" + quote_prefix + "```"
  let text = String::from_array(chars[:range.start]) +
    prefix +
    selected +
    suffix +
    String::from_array(chars[range.end:])
  let prefix_length = markdown_editor_text_length(prefix)
  let caret = if selection.caret < range.start {
    selection.caret
  } else if selection.caret <= range.end {
    selection.caret + prefix_length
  } else {
    selection.caret + prefix_length + markdown_editor_text_length(suffix)
  }
  let next_selection = match
    markdown_editor_active_selection(selection.selection) {
    Some(active) =>
      Some(
        @core.TextRange::new(
          start=active.start + prefix_length,
          end=active.end + prefix_length,
        ),
      )
    None => None
  }
  { source: text, caret, selection: next_selection }
}

///|
fn markdown_editor_code_block_quote_prefix(selected : String) -> String {
  let lines = markdown_editor_lines(selected)
  if lines.length() == 0 {
    return ""
  }
  let prefix = markdown_editor_quote_context_prefix(lines[0].text)
  if prefix == "" {
    return ""
  }
  for line in lines {
    let text = line.text
    if text.trim().to_owned() == "" {
      continue
    }
    let line_chars = text.to_array()
    if !markdown_editor_source_has_marker(line_chars, 0, prefix) {
      return ""
    }
  }
  prefix
}

///|