lib/tablex/code_generate.ex

defmodule Tablex.CodeGenerate do
  @moduledoc """
  This module is responsible for generating Elixir code from a table.
  """

  alias Tablex.Parser
  alias Tablex.Table
  alias Tablex.Util.DeepMap

  @flatten_path """
    put_recursively = fn
      _, [], value, _ ->
        value

      %{} = acc, [head | rest], value, f ->
        v = f.(%{}, rest, value, f)

        Map.update(acc, head, v, fn
          old_v ->
            Map.merge(old_v, v, fn
              _, %{} = v1, %{} = v2 ->
                Map.merge(v1, v2)

              _, _, v2 ->
                v2
            end)
        end)
    end

    flatten_path = fn outputs ->
      Enum.reduce(outputs, %{}, fn {path, v}, acc ->
        acc |> put_recursively.(path, v, put_recursively)
      end)
    end
  """

  defguardp(
    is_literal(exp)
    when is_nil(exp) or is_atom(exp) or is_number(exp) or is_binary(exp) or is_map(exp)
  )

  @doc """
  Transform a table into Elixir code.


  ## Examples

      iex> table = \"""
      ...> F CreditScore EmploymentStatus Debt-to-Income-Ratio || Action
      ...> 1 700         employed         <0.43                || Approved
      ...> 2 700         unemployed       -                    || "Further Review"
      ...> 3 <=700       -                -                    || Denied
      ...> \"""
      ...>
      ...> code = generate(table)
      ...> is_binary(code)
      true
      


  The code generated in the above example is:

      case {credit_score, employment_status, debt_to_income_ratio} do
        {700, "employed", debt_to_income_ratio}
        when is_number(debt_to_income_ratio) and debt_to_income_ratio < 0.43 ->
          %{action: "Approved"}

        {700, "unemployed", _} ->
          %{action: "Further Review"}

        {credit_score, _, _} when is_number(credit_score) and credit_score <= 700 ->
          %{action: "Denied"}
      end

  """
  @spec generate(String.t() | Table.t()) :: String.t()
  def generate(table) when is_binary(table) do
    table |> Parser.parse!([]) |> generate()
  end

  def generate(%Table{hit_policy: :first_hit} = table) do
    [
      "binding =\n",
      ["  case {", top_input_tuple_expr(table), "} do\n"],
      rule_clauses(table) |> Enum.intersperse("\n"),
      "\nend"
    ]
    |> IO.iodata_to_binary()
  end

  def generate(%Table{hit_policy: :collect, rules: rules, inputs: [], outputs: out_def}) do
    outputs =
      rules
      |> Stream.map(fn [_, _input, {:output, outputs}] ->
        to_output(outputs, out_def)
      end)
      |> Enum.intersperse(", ")

    code = [?[ | outputs] ++ [?]]
    code |> IO.iodata_to_binary()
  end

  def generate(%Table{hit_policy: :collect, inputs: in_def} = table) do
    rule_functions =
      table.rules
      |> Enum.map_intersperse(",\n", fn [_, {:input, inputs}, {:output, outputs}] ->
        [
          "if(match?(",
          rule_cond(inputs, in_def),
          ", {",
          top_input_tuple_expr(table),
          "}), do: ",
          to_output(outputs, table.outputs),
          ?)
        ]
      end)

    ["for ret when not is_nil(ret) <- [", rule_functions, "], do: ret"]
    |> IO.iodata_to_binary()
  end

  def generate(%Table{hit_policy: :merge} = table) do
    empty = table.outputs |> Enum.map(fn _ -> :any end)

    clauses =
      table.rules
      |> Enum.map_intersperse(", ", fn
        [_, {:input, rule_inputs}, {:output, rule_outputs} | _] ->
          [
            "fn\n",
            [
              "  ",
              rule_cond(rule_inputs, table.inputs),
              " -> ",
              rule_output_values(rule_outputs),
              "\n"
            ],
            "  _ -> nil\n",
            "end"
          ]
      end)

    [
      "binding = {",
      top_input_tuple_expr(table),
      ?},
      ?\n,
      "out_values = [",
      clauses,
      "]\n",
      "|> Enum.reduce_while(",
      i(empty),
      """
      , fn rule_fn, acc ->
        case rule_fn.(binding) do
          output when is_list(output) ->
            {acc, []} =
              output
              |> Enum.reduce({[], acc}, fn
                :any, {acc, [h | t]} ->
                  {[h | acc], t}

                other, {acc, [:any | t]} ->
                  {[other | acc], t}

                _other, {acc, [other | t]} ->
                  {[other | acc], t}
              end)

            acc = Enum.reverse(acc)

            if Enum.member?(acc, :any),
              do: {:cont, acc},
              else: {:halt, acc}

          nil ->
            {:cont, acc}
        end
      end)
      """,
      @flatten_path,
      "\n",
      "Stream.zip([",
      output_pathes(table.outputs) |> Enum.intersperse(", "),
      "], out_values)\n",
      "|> flatten_path.()"
    ]
    |> IO.iodata_to_binary()
  end

  def generate(%Table{hit_policy: :reverse_merge} = table) do
    table =
      Map.update!(table, :rules, fn rules ->
        Enum.reverse(rules)
        |> Stream.with_index(1)
        |> Enum.map(fn {[_ | rest], n} -> [n | rest] end)
      end)

    generate(%{table | hit_policy: :merge})
  end

  defp top_input_tuple_expr(%{inputs: inputs}) do
    Enum.map_intersperse(inputs, ", ", fn %{name: var, path: path} ->
      (path ++ [var]) |> hd() |> to_string()
    end)
  end

  defp rule_clauses(%{rules: rules, inputs: in_def, outputs: out_def}) do
    rules
    |> Stream.map(&rule_clause(&1, in_def, out_def))
  end

  defp rule_clause([_n, input: inputs, output: outputs], in_def, out_def) do
    [rule_cond(inputs, in_def), " ->\n  ", to_output(outputs, out_def)]
  end

  defp rule_cond(inputs, in_def) do
    {patterns, guards} =
      inputs
      |> Stream.zip(in_def)
      |> Enum.reduce({[], []}, fn {value, df}, {p, g} ->
        case pattern_guard(value, df) do
          {pattern, guard} ->
            {[to_nested_pattern(pattern, df) | p], [guard | g]}

          pattern ->
            {[to_nested_pattern(pattern, df) | p], g}
        end
      end)

    case {patterns, guards} do
      {_, []} ->
        ["{", patterns |> Enum.reverse() |> Enum.intersperse(", "), "}"]

      _ ->
        p = ["{", patterns |> Enum.reverse() |> Enum.intersperse(", "), "}"]
        w = guards |> Enum.intersperse(" and ")

        [p, " when ", w]
    end
  end

  defp pattern_guard(:any, _), do: "_"

  defp pattern_guard({comp, number}, %{name: name, path: path}) when comp in ~w[!= < <= >= >]a do
    var_name = Enum.join(path ++ [name], "_")

    {var_name,
     ["is_number(", var_name, ") and ", var_name, " ", to_string(comp), " ", to_string(number)]}
  end

  defp pattern_guard(%Range{first: first, last: last}, %{name: name, path: path}) do
    var_name = Enum.join(path ++ [name], "_")

    {var_name, [var_name, " in ", "#{first}..#{last}"]}
  end

  defp pattern_guard(list, %{name: name, path: path} = var) when is_list(list) do
    var_name = Enum.join(path ++ [name], "_")

    case Enum.split_with(list, &is_literal/1) do
      {[], complex_values} ->
        join_pattern_guard(var_name, complex_values, var)

      {literal_values, []} ->
        {var_name, join_literal_pattern_guard(var_name, literal_values)}

      {literal_values, complex_values} ->
        {^var_name, complex_pattern} = join_pattern_guard(var_name, complex_values, var)

        {
          var_name,
          [join_literal_pattern_guard(var_name, literal_values), " or ", complex_pattern]
        }
    end
  end

  defp pattern_guard(literal, _) when is_literal(literal) do
    i(literal)
  end

  defp join_literal_pattern_guard(var_name, [v]) do
    [var_name, " == ", i(v)]
  end

  defp join_literal_pattern_guard(var_name, list) do
    [var_name, " in ", i(list)]
  end

  defp join_pattern_guard(var_name, list, %{name: name, path: path}) when is_list(list) do
    guard =
      list
      |> Stream.map(&pattern_guard(&1, %{name: name, path: path}))
      |> Enum.map_intersperse(" or ", fn
        {_var, guard} ->
          guard

        value when is_binary(value) ->
          [var_name, " == ", value]
      end)

    {var_name, ["(", guard, ")"]}
  end

  defp to_output(outputs, out_def) do
    map =
      Stream.zip(out_def, outputs)
      |> Map.new(fn
        {%{name: name, path: path}, value} ->
          {path ++ [name], value}
      end)

    map |> DeepMap.flatten() |> to_code()
  end

  defp to_code(%{} = map) do
    kvs =
      map
      |> Enum.map_join(", ", fn {k, v} ->
        [to_string(k), ": ", to_code(v)]
      end)

    ["%{", kvs, "}"]
  end

  defp to_code({:code, code}) when is_binary(code) do
    code
  end

  defp to_code(v) do
    i(v)
  end

  defp rule_output_values(outputs) do
    [
      "[",
      outputs
      |> Stream.map(&to_code/1)
      |> Enum.intersperse(", "),
      "]"
    ]
  end

  defp output_pathes(outputs) do
    for %{name: name, path: path} <- outputs, do: i(path ++ [name])
  end

  defp to_nested_pattern("_", _) do
    "_"
  end

  defp to_nested_pattern(flat_pattern, %{name: name, path: path}) do
    %{tl(path ++ [name]) => {:code, flat_pattern}}
    |> DeepMap.flatten()
    |> to_code()
  end

  defp i(v) do
    inspect(v, limit: :infinity, charlists: :as_lists)
  end
end