defmodule Slime.Compiler do
  @moduledoc """
  Compile a tree of parsed Slime into EEx.
  """

  alias Slime.Doctype

  alias Slime.Parser.Nodes.{DoctypeNode, EExNode, HTMLCommentNode, HTMLNode, InlineHTMLNode, VerbatimTextNode}

  @void_elements ~w(
    area br col doctype embed hr img input link meta base param
    keygen source menuitem track wbr
  )

  def compile([]), do: ""

  def compile(tags) when is_list(tags) do
    tags
    |> Enum.map_join(&compile(&1))
    |> String.replace("\r", "")
  end

  def compile(%DoctypeNode{name: name}), do: Doctype.for(name)
  def compile(%VerbatimTextNode{content: content}), do: compile(content)

  def compile(%HTMLNode{name: name, spaces: spaces} = tag) do
    attrs = Enum.map(tag.attributes, &render_attribute/1)
    tag_head = Enum.join([name | attrs])

    body =
      cond do
        tag.closed ->
          "<" <> tag_head <> "/>"

        name in @void_elements ->
          "<" <> tag_head <> ">"

        :otherwise ->
          "<" <> tag_head <> ">" <> compile(tag.children) <> "</" <> name <> ">"
      end

    leading_space(spaces) <> body <> trailing_space(spaces)
  end

  def compile(%EExNode{content: code, spaces: spaces, output: output} = eex) do
    code = if eex.safe?, do: "{:safe, " <> code <> "}", else: code
    opening = if(output, do: "<%= ", else: "<% ") <> code <> " %>"

    closing =
      if Regex.match?(~r/(fn.*->| do)\s*$/, code) do
        "<% end %>"
      else
        ""
      end

    body = opening <> compile(eex.children) <> closing

    leading_space(spaces) <> body <> trailing_space(spaces)
  end

  def compile(%InlineHTMLNode{content: content, children: children}) do
    compile(content) <> compile(children)
  end

  def compile(%HTMLCommentNode{content: content}) do
    "<!--" <> compile(content) <> "-->"
  end

  def compile({:eex, eex}), do: "<%= " <> eex <> "%>"
  def compile({:safe_eex, eex}), do: "<%= {:safe, " <> eex <> "} %>"
  def compile(raw), do: raw

  @spec hide_dialyzer_spec(any) :: any
  def hide_dialyzer_spec(input), do: input

  defp render_attribute({_, []}), do: ""
  defp render_attribute({_, ""}), do: ""

  defp render_attribute({name, {safe_eex, content}}) do
    case content do
      "true" ->
        " #{name}"

      "false" ->
        ""

      "nil" ->
        ""

      _ ->
        {:ok, quoted_content} = Code.string_to_quoted(content)
        render_attribute_code(name, content, quoted_content, safe_eex)
    end
  end

  defp render_attribute({name, value}) do
    if value == true do
      " #{name}"
    else
      value =
        cond do
          is_binary(value) -> value
          is_list(value) -> Enum.join(value, " ")
          true -> value
        end

      ~s( #{name}="#{value}")
    end
  end

  defp render_attribute_code(name, _content, quoted, _safe)
       when is_number(quoted) or is_atom(quoted) do
    ~s[ #{name}="#{quoted}"]
  end

  defp render_attribute_code(name, _content, quoted, _) when is_list(quoted) do
    quoted |> Enum.map_join(" ", &Kernel.to_string/1) |> (&~s[ #{name}="#{&1}"]).()
  end

  defp render_attribute_code(name, _content, quoted, :eex) when is_binary(quoted), do: ~s[ #{name}="#{quoted}"]

  defp render_attribute_code(name, _content, quoted, _) when is_binary(quoted),
    do: ~s[ #{name}="<%= {:safe, "#{quoted}"} %>"]

  # NOTE: string with interpolation or strings concatination
  defp render_attribute_code(name, content, {op, _, _}, safe) when op in [:<<>>, :<>] do
    value = if safe == :eex, do: content, else: "{:safe, #{content}}"
    ~s[ #{name}="<%= #{value} %>"]
  end

  defp render_attribute_code(name, content, _, safe) do
    value = if safe == :eex, do: "slim__v", else: "{:safe, slim__v}"

    """
    <% slim__k = "#{name}"; slim__v = Slime.Compiler.hide_dialyzer_spec(#{content}) %>\
    <%= if slim__v do %> <%= slim__k %><%= unless slim__v == true do %>\
    ="<%= #{value} %>"<% end %><% end %>\
    """
  end

  defp leading_space(%{leading: true}), do: " "
  defp leading_space(_), do: ""

  defp trailing_space(%{trailing: true}), do: " "
  defp trailing_space(_), do: ""
end
