defmodule EEx.Tokenizer do
  @moduledoc false

  @doc """
  Tokenizes the given char list or binary.
  It returns 4 different types of tokens as result:

    * `{:text, contents}`
    * `{:expr, line, marker, contents}`
    * `{:start_expr, line, marker, contents}`
    * `{:middle_expr, line, marker, contents}`
    * `{:end_expr, line, marker, contents}`

  """
  def tokenize(bin, line) when is_binary(bin) do
    tokenize(String.to_char_list(bin), line)
  end

  def tokenize(list, line) do
    Enum.reverse(tokenize(list, line, [], []))
  end

  defp tokenize('<%%' ++ t, line, buffer, acc) do
    {buffer, new_line, rest} = tokenize_expr t, line, [?%, ?<|buffer]
    tokenize rest, new_line, [?>, ?%|buffer], acc
  end

  defp tokenize('<%#' ++ t, line, buffer, acc) do
    {_, new_line, rest} = tokenize_expr t, line, []
    tokenize rest, new_line, buffer, acc
  end

  defp tokenize('<%' ++ t, line, buffer, acc) do
    {marker, t} = retrieve_marker(t)
    {expr, new_line, rest} = tokenize_expr t, line, []

    token = token_name(expr)
    acc   = tokenize_text(buffer, acc)
    final = {token, line, marker, Enum.reverse(expr)}
    tokenize rest, new_line, [], [final | acc]
  end

  defp tokenize('\n' ++ t, line, buffer, acc) do
    tokenize t, line + 1, [?\n|buffer], acc
  end

  defp tokenize([h|t], line, buffer, acc) do
    tokenize t, line, [h|buffer], acc
  end

  defp tokenize([], _line, buffer, acc) do
    tokenize_text(buffer, acc)
  end

  # Retrieve marker for <%

  defp retrieve_marker('=' ++ t) do
    {"=", t}
  end

  defp retrieve_marker(t) do
    {"", t}
  end

  # Tokenize an expression until we find %>

  defp tokenize_expr([?%, ?>|t], line, buffer) do
    {buffer, line, t}
  end

  defp tokenize_expr('\n' ++ t, line, buffer) do
    tokenize_expr t, line + 1, [?\n|buffer]
  end

  defp tokenize_expr([h|t], line, buffer) do
    tokenize_expr t, line, [h|buffer]
  end

  defp tokenize_expr([], _line, _buffer) do
    raise EEx.SyntaxError, message: "missing token: %>"
  end

  # Receive an expression content and check
  # if it is a start, middle or an end token.
  #
  # Start tokens finish with `do` and `fn ->`
  # Middle tokens are marked with `->` or keywords
  # End tokens contain only the end word

  defp token_name([h|t]) when h in [?\s, ?\t] do
    token_name(t)
  end

  defp token_name('od' ++ [h|_]) when h in [?\s, ?\t, ?)] do
    :start_expr
  end

  defp token_name('>-' ++ rest) do
    rest = Enum.reverse(rest)

    # Tokenize the remaining passing check_terminators as
    # false, which relax the tokenizer to not error on
    # unmatched pairs. Then, we check if there is a "fn"
    # token and, if so, it is not followed by an "end"
    # token. If this is the case, we are on a start expr.
    case :elixir_tokenizer.tokenize(rest, 1, file: "eex", check_terminators: false) do
      {:ok, _line, tokens} ->
        tokens   = Enum.reverse(tokens)
        fn_index = fn_index(tokens)

        if fn_index && end_index(tokens) > fn_index do
          :start_expr
        else
          :middle_expr
        end
      _error ->
        :middle_expr
    end
  end

  defp token_name('esle' ++ t),   do: check_spaces(t, :middle_expr)
  defp token_name('retfa' ++ t),  do: check_spaces(t, :middle_expr)
  defp token_name('hctac' ++ t),  do: check_spaces(t, :middle_expr)
  defp token_name('eucser' ++ t), do: check_spaces(t, :middle_expr)
  defp token_name('dne' ++ t),    do: check_spaces(t, :end_expr)

  defp token_name(_) do
    :expr
  end

  defp fn_index(tokens) do
    Enum.find_index tokens, fn
      {:fn_paren, _} -> true
      {:fn, _}       -> true
      _                -> false
    end
  end

  defp end_index(tokens) do
    Enum.find_index(tokens, &match?({:end, _}, &1)) || :infinity
  end

  defp check_spaces(string, token) do
    if Enum.all?(string, &(&1 in [?\s, ?\t])) do
      token
    else
      :expr
    end
  end

  # Tokenize the buffered text by appending
  # it to the given accumulator.

  defp tokenize_text([], acc) do
    acc
  end

  defp tokenize_text(buffer, acc) do
    [{:text, Enum.reverse(buffer)} | acc]
  end
end
