defmodule Search.LiteralParser do
  import NimbleParsec

  defp to_number(input), do: Search.Helpers.to_number(input)

  float =
    ascii_string([?0..?9], min: 1)
    |> optional(ascii_char('.') |> ascii_string([?0..?9], min: 1))
    |> reduce({List, :to_string, []})
    |> reduce(:to_number)

  edit_distance =
    ignore(string("~"))
    |> concat(float)
    |> unwrap_and_tag(:fuzz)
    |> eos()

  stopwords =
    choice([
      string("*"),
      string("?"),
      edit_distance
    ])

  normal =
    lookahead_not(stopwords)
    |> choice([
      ignore(string("\\")) |> utf8_char([]),
      utf8_char([])
    ])
    |> repeat()
    |> reduce({List, :to_string, []})
    |> unwrap_and_tag(:literal)
    |> optional(edit_distance)
    |> eos()

  # Runs of Kleene stars are coalesced.
  # Fuzzy search has no meaning in wildcard mode, so we ignore it.
  wildcard =
    lookahead_not(edit_distance)
    |> choice([
      ignore(string("\\")) |> utf8_char([]),
      string("*") |> ignore(repeat(string("*"))),
      utf8_char([])
    ])
    |> repeat()
    |> reduce({List, :to_string, []})
    |> unwrap_and_tag(:wildcard)
    |> ignore(optional(edit_distance))
    |> eos()

  literal =
    choice([
      normal,
      wildcard
    ])

  defparsec(:parse, literal)
end