defmodule EarmarkParser.Ast.Inline do
  @moduledoc false

  alias EarmarkParser.{Context, Message, Parser}
  alias EarmarkParser.Helpers.PureLinkHelpers
  alias Parser.LinkParser

  import EarmarkParser.Ast.Emitter
  import EarmarkParser.Ast.Renderer.AstWalker
  import EarmarkParser.Helpers
  import EarmarkParser.Helpers.AttrParser
  import EarmarkParser.Helpers.StringHelpers, only: [behead: 2]
  import EarmarkParser.Helpers.AstHelpers
  import Context, only: [set_value: 2]

  @typep conversion_data :: {String.t(), non_neg_integer(), EarmarkParser.Context.t(), boolean()}
  def convert(src, lnb, context)

  def convert(list, lnb, context) when is_list(list) do
    # IO.inspect(context.rules)
    _convert(Enum.join(list, "\n"), lnb, context, true)
  end

  def convert(src, lnb, context) do
    # IO.inspect(context.rules)
    _convert(src, lnb, context, true)
  end

  defp _convert(src, current_lnb, context, use_linky?)

  defp _convert(src, _, %{options: %{parse_inline: false}} = context, _) do
    prepend(context, src)
  end

  defp _convert("", _, context, _), do: context

  defp _convert(src, current_lnb, context, use_linky?) do
    {src1, lnb1, context1, use_linky1?} = _convert_next(src, current_lnb, context, use_linky?)
    _convert(src1, lnb1, context1, use_linky1?)
  end

  defp all_converters do
    [
      converter_for_escape: &converter_for_escape/1,
      converter_for_autolink: &converter_for_autolink/1,
      # only if use_linky?
      converter_for_link_and_image: &converter_for_link_and_image/1,
      converter_for_reflink: &converter_for_reflink/1,
      converter_for_footnote: &converter_for_footnote/1,
      converter_for_nolink: &converter_for_nolink/1,
      #
      converter_for_strikethrough_gfm: &converter_for_strikethrough_gfm/1,
      converter_for_strong: &converter_for_strong/1,
      converter_for_em: &converter_for_em/1,
      # only for option sub_sup
      converter_for_sub: &converter_for_sub/1,
      converter_for_sup: &converter_for_sup/1,
      #
      converter_for_math_display: &converter_for_math_display/1,
      converter_for_math_inline: &converter_for_math_inline/1,
      #
      converter_for_code: &converter_for_code/1,
      converter_for_br: &converter_for_br/1,
      converter_for_inline_ial: &converter_for_inline_ial/1,
      converter_for_pure_link: &converter_for_pure_link/1,
      converter_for_text: &converter_for_text/1
    ]
  end

  defp _convert_next(src, lnb, context, use_linky?) do
    _find_and_execute_converter({src, lnb, context, use_linky?})
  end

  defp _find_and_execute_converter({src, lnb, context, use_linky?}) do
    all_converters()
    |> Enum.find_value(fn {_converter_name, converter} ->
      converter.({src, lnb, context, use_linky?})
    end)
  end

  ######################
  #
  #  Converters
  #
  ######################
  @escape_rule ~r{^\\([\\`*\{\}\[\]()\#+\-.!_>$])}
  def converter_for_escape({src, lnb, context, use_linky?}) do
    if match = Regex.run(@escape_rule, src) do
      [match, escaped] = match
      {behead(src, match), lnb, prepend(context, escaped), use_linky?}
    end
  end

  @autolink_rgx ~r{^<([^ >]+(@|:\/)[^ >]+)>}
  def converter_for_autolink({src, lnb, context, use_linky?}) do
    if match = Regex.run(@autolink_rgx, src) do
      [match, link, protocol] = match
      {href, text} = convert_autolink(link, protocol)
      out = render_link(href, text)
      {behead(src, match), lnb, prepend(context, out), use_linky?}
    end
  end

  def converter_for_pure_link({src, lnb, context, use_linky?}) do
    if context.options.pure_links do
      case PureLinkHelpers.convert_pure_link(src) do
        {ast, length} -> {behead(src, length), lnb, prepend(context, ast), use_linky?}
        _ -> nil
      end
    end
  end

  def converter_for_link_and_image({src, lnb, context, use_linky?}) do
    if use_linky? do
      match = LinkParser.parse_link(src, lnb)

      if match do
        {match1, text, href, title, link_or_img} = match

        out =
          case link_or_img do
            :link -> output_link(context, text, href, title, lnb)
            :wikilink -> maybe_output_wikilink(context, text, href, title, lnb)
            :image -> render_image(text, href, title)
          end

        if out do
          {behead(src, match1), lnb, prepend(context, out), use_linky?}
        end
      end
    end
  end

  @link_text ~S{(?:\[[^]]*\]|[^][]|\])*}
  @reflink ~r{^!?\[(#{@link_text})\]\s*\[([^]]*)\]}x
  def converter_for_reflink({src, lnb, context, use_linky?}) do
    if use_linky? do
      if match = Regex.run(@reflink, src) do
        {match_, alt_text, id} =
          case match do
            [match__, id, ""] -> {match__, id, id}
            [match__, alt_text, id] -> {match__, alt_text, id}
          end

        case reference_link(context, match_, alt_text, id, lnb) do
          {:ok, out} -> {behead(src, match_), lnb, prepend(context, out), use_linky?}
          _ -> nil
        end
      end
    end
  end

  @link_text ~S{(?:\[[^]]*\]|[^][]|\])*}
  def converter_for_footnote({src, lnb, context, use_linky?}) do
    if use_linky? && context.options.footnotes do
      case Regex.run(~r{^\[\^(#{@link_text})\]}, src) do
        [match, id] -> convert_footnote(match, id, src, lnb, context, use_linky?)
        _ -> nil
      end
    end
  end

  @nolink ~r{^!?\[((?:\[[^]]*\]|[^][])*)\]}
  def converter_for_nolink({src, lnb, context, use_linky?}) do
    if use_linky? do
      case Regex.run(@nolink, src) do
        [match, id] ->
          case reference_link(context, match, id, id, lnb) do
            {:ok, out} -> {behead(src, match), lnb, prepend(context, out), use_linky?}
            _ -> nil
          end

        _ ->
          nil
      end
    end
  end

  ################################
  # Simple Tags: em, strong, del #
  ################################
  @strikethrough_rgx ~r{\A~~(?=\S)([\s\S]*?\S)~~}
  def converter_for_strikethrough_gfm({src, _, _, _} = conv_tuple) do
    if match = Regex.run(@strikethrough_rgx, src) do
      _converter_for_simple_tag(conv_tuple, match, "del")
    end
  end

  @strong_rgx ~r{\A__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)}
  def converter_for_strong({src, _, _, _} = conv_tuple) do
    if match = Regex.run(@strong_rgx, src) do
      _converter_for_simple_tag(conv_tuple, match, "strong")
    end
  end

  @emphasis_rgx ~r{\A\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)}
  def converter_for_em({src, _, _, _} = conv_tuple) do
    if match = Regex.run(@emphasis_rgx, src) do
      _converter_for_simple_tag(conv_tuple, match, "em")
    end
  end

  @sub_rgx ~r{\A~(?=\S)(.*?\S)~}
  def converter_for_sub({src, _, %{options: %{sub_sup: true}}, _} = conv_tuple) do
    if match = Regex.run(@sub_rgx, src) do
      _converter_for_simple_tag(conv_tuple, match, "sub")
    end
  end

  def converter_for_sub(_), do: nil

  @sup_rgx ~r{\A\^(?=\S)(.*?\S)\^}
  def converter_for_sup({src, _, %{options: %{sub_sup: true}}, _} = conv_tuple) do
    if match = Regex.run(@sup_rgx, src) do
      _converter_for_simple_tag(conv_tuple, match, "sup")
    end
  end

  def converter_for_sup(_), do: nil

  @math_inline_rgx ~r{\A\$(?=[^\s$])([\s\S]*?[^\s\\])\$}
  def converter_for_math_inline({src, lnb, %{options: %{math: true}} = context, use_linky?}) do
    if match = Regex.run(@math_inline_rgx, src) do
      [match, content] = match
      content = String.trim(content)
      out = math_inline(content, lnb)
      {behead(src, match), lnb, prepend(context, out), use_linky?}
    end
  end

  def converter_for_math_inline(_), do: nil

  @math_display_rgx ~r{\A\$\$([\s\S]+?)\$\$}
  def converter_for_math_display({src, lnb, %{options: %{math: true}} = context, use_linky?}) do
    if match = Regex.run(@math_display_rgx, src) do
      [match, content] = match
      content = String.trim(content)
      out = math_display(content, lnb)
      {behead(src, match), lnb, prepend(context, out), use_linky?}
    end
  end

  def converter_for_math_display(_), do: nil

  @squash_ws ~r{\s+}
  @code ~r{^
  (`+)		# $1 = Opening run of `
  (.+?)		# $2 = The code block
  (?<!`)
  \1			# Matching closer
  (?!`)
}xs
  def converter_for_code({src, lnb, context, use_linky?}) do
    if match = Regex.run(@code, src) do
      [match, _, content] = match
      # Commonmark
      content1 =
        content
        |> String.trim()
        |> String.replace(@squash_ws, " ")

      out = codespan(content1, lnb)
      {behead(src, match), lnb, prepend(context, out), use_linky?}
    end
  end

  @inline_ial ~r<^\s*\{:\s*(.*?)\s*}>

  def converter_for_inline_ial({src, lnb, context, use_linky?}) do
    if match = Regex.run(@inline_ial, src) do
      [match, ial] = match
      {context1, ial_attrs} = parse_attrs(context, ial, lnb)
      new_tags = augment_tag_with_ial(context.value, ial_attrs, match)
      {behead(src, match), lnb, set_value(context1, new_tags), use_linky?}
    end
  end

  def converter_for_br({src, lnb, context, use_linky?}) do
    if match = Regex.run(context.rules.br, src, return: :index) do
      [{0, match_len}] = match
      {behead(src, match_len), lnb, prepend(context, emit("br")), use_linky?}
    end
  end

  @line_ending ~r{\r\n?|\n}
  @spec converter_for_text(conversion_data()) :: conversion_data()
  def converter_for_text({src, lnb, context, _}) do
    matched =
      case Regex.run(context.rules.text, src) do
        [match] -> match
      end

    line_count = matched |> String.split(@line_ending) |> Enum.count()

    ast = hard_line_breaks(matched, context.options.gfm)
    ast = walk_ast(ast, &gruber_line_breaks/1)
    {behead(src, matched), lnb + line_count - 1, prepend(context, ast), true}
  end

  ######################
  #
  #  Helpers
  #
  ######################
  defp _converter_for_simple_tag({src, lnb, context, use_linky?}, match, for_tag) do
    {match1, content} =
      case match do
        [m, _, c] -> {m, c}
        [m, c] -> {m, c}
      end

    context1 = _convert(content, lnb, set_value(context, []), use_linky?)

    {behead(src, match1), lnb, prepend(context, emit(for_tag, context1.value |> Enum.reverse())),
     use_linky?}
  end

  defp _prepend_footnote(context, out, id) do
    context
    |> Map.update!(:referenced_footnote_ids, &MapSet.put(&1, id))
    |> prepend(out)
  end

  defp convert_autolink(link, separator)

  defp convert_autolink(link, _separator = "@") do
    link = if String.at(link, 6) == ":", do: behead(link, 7), else: link
    text = link
    href = "mailto:" <> text
    {href, text}
  end

  defp convert_autolink(link, _separator) do
    {link, link}
  end

  @gruber_line_break Regex.compile!(" {2,}(?>\n)", "m")
  defp gruber_line_breaks(text) do
    text
    |> String.split(@gruber_line_break)
    |> Enum.intersperse(emit("br"))
    |> _remove_leading_empty()
  end

  @gfm_hard_line_break ~r{\\\n}
  defp hard_line_breaks(text, gfm)
  defp hard_line_breaks(text, false), do: text
  defp hard_line_breaks(text, nil), do: text

  defp hard_line_breaks(text, _) do
    text
    |> String.split(@gfm_hard_line_break)
    |> Enum.intersperse(emit("br"))
    |> _remove_leading_empty()
  end

  defp output_image_or_link(context, link_or_image, text, href, title, lnb)

  defp output_image_or_link(_context, "!" <> _, text, href, title, _lnb) do
    render_image(text, href, title)
  end

  defp output_image_or_link(context, _, text, href, title, lnb) do
    output_link(context, text, href, title, lnb)
  end

  defp output_link(context, text, href, title, lnb) do
    context1 = %{context | options: %{context.options | pure_links: false}}

    context2 = _convert(text, lnb, set_value(context1, []), String.starts_with?(text, "!"))

    if title do
      emit("a", Enum.reverse(context2.value), href: href, title: title)
    else
      emit("a", Enum.reverse(context2.value), href: href)
    end
  end

  defp maybe_output_wikilink(context, text, href, title, lnb) do
    if context.options.wikilinks do
      {tag, attrs, content, meta} = output_link(context, text, href, title, lnb)
      {tag, attrs, content, Map.put(meta, :wikilink, true)}
    end
  end

  defp reference_link(context, match, alt_text, id, lnb) do
    id = id |> replace(~r{\s+}, " ") |> String.downcase()
    case Map.fetch(context.links, id) do
      {:ok, link} ->
        {:ok, output_image_or_link(context, match, alt_text, link.url, link.title, lnb)}

      _ ->
        nil
    end
  end

  defp convert_footnote(match, id, src, lnb, context, use_linky?) do
    case footnote_link(context, match, id) do
      {:ok, out} ->
        {behead(src, match), lnb, _prepend_footnote(context, out, id), use_linky?}
      _ ->
        converter_for_text(
          {src, lnb,
           Message.add_message(
             context,
             {:error, lnb, "footnote #{id} undefined, reference to it ignored"}
           ), use_linky?}
        )
    end
  end

  defp footnote_link(context, _match, id) do
    case Map.fetch(context.footnotes, id) do
      {:ok, _} ->
        {:ok, render_footnote_link("fn:#{id}", "fnref:#{id}", id)}

      _ ->
        nil
    end
  end

  defp prepend(%Context{} = context, prep) do
    _prepend(context, prep)
  end

  defp _prepend(context, value)

  defp _prepend(context, [bin | rest]) when is_binary(bin) do
    _prepend(_prepend(context, bin), rest)
  end

  defp _prepend(%Context{value: [str | rest]} = context, prep)
       when is_binary(str) and is_binary(prep) do
    %{context | value: [str <> prep | rest]}
  end

  defp _prepend(%Context{value: value} = context, prep) when is_list(prep) do
    %{context | value: Enum.reverse(prep) ++ value}
  end

  defp _prepend(%Context{value: value} = context, prep) do
    %{context | value: [prep | value]}
  end

  defp _remove_leading_empty(list)
  defp _remove_leading_empty(["" | rest]), do: rest
  defp _remove_leading_empty(list), do: list
end

# SPDX-License-Identifier: Apache-2.0
