defmodule ExpressionParser do
  import NimbleParsec

  whitespace = ignore(optional(ascii_string([?\s, ?\t, ?\n, ?\r], min: 1)))

  identifier =
    ascii_string([?a..?z, ?A..?Z, ?_, ?0..?9], min: 1)
    |> label("identifier")
    |> unwrap_and_tag(:field)

  quoted_string =
    ignore(string("\""))
    |> repeat(utf8_char(not: ?"))
    |> reduce({List, :to_string, []})
    |> ignore(string("\""))

  number =
    ascii_string([?0..?9], min: 1)
    |> map({String, :to_integer, []})

  atom_value =
    ignore(string("'"))
    |> ascii_string([?a..?z, ?A..?Z], min: 1)
    |> ignore(string("'"))
    |> map({__MODULE__, :identity, []})

  list_item =
    choice([
      number,
      atom_value,
      quoted_string |> unwrap_and_tag(:string)
    ])

  atom_list =
    ignore(string("["))
    |> optional(whitespace)
    |> repeat(
      list_item
      |> optional(ignore(string(",")))
      |> optional(whitespace)
    )
    |> ignore(string("]"))
    |> tag(:list)



  operator =
    choice([
      string("ilike") |> replace(:ilike),
      string(">=") |> replace(:>=),
      string("<=") |> replace(:<=),
      string("!=") |> replace(:!=),
      string(">") |> replace(:>),
      string("<") |> replace(:<),
      string("=") |> replace(:==),
      string("in") |> replace(:in)
    ])

  value =
    choice([
      quoted_string |> unwrap_and_tag(:string),
      number |> unwrap_and_tag(:number),
      atom_list
    ])

  comparison =
    identifier
    |> ignore(whitespace)
    |> concat(operator)
    |> ignore(whitespace)
    |> concat(value)
    |> tag(:comparison)

  defcombinatorp :expr, parsec(:logic_expr)

  paren_expr =
    ignore(string("("))
    |> ignore(whitespace)
    |> parsec(:expr)
    |> ignore(whitespace)
    |> ignore(string(")"))

  logic_term =
    choice([
      paren_expr,
      comparison
    ])

  logical_op =
    ignore(whitespace)
    |> choice([
      string("and") |> replace(:and),
      string("or") |> replace(:or)
    ])
    |> ignore(whitespace)

  logic_expr =
    logic_term
    |> repeat(
      logical_op
      |> concat(logic_term)
    )
    |> reduce({__MODULE__, :reduce_logic, []})

  defparsec :logic_expr, logic_expr

  def parse(input) do
    case logic_expr(input) do
      {:ok, result, "", _, _, _} -> {:ok, result}
      {:ok, _, rest, _, _, _} -> {:error, "Unexpected remaining input: #{inspect(rest)}"}
      {:error, reason, _, _, _, _} -> {:error, reason}
    end
  end

  def reduce_logic([head | tail]) do
    Enum.chunk_every(tail, 2)
    |> Enum.reduce(head, fn [op, right], acc ->
      {op, acc, right}
    end)
  end

  def identity(value), do: value

end
