lib/archeometer/reports/fragment.ex

defmodule Archeometer.Reports.Fragment do
  @moduledoc """
  Represents a fragment, which is part of a section in a page.
  """

  use Archeometer.Repo
  alias Archeometer.Reports.Config

  def make_custom_env do
    import Archeometer.Query, warn: false
    alias Archeometer.Schema.{Function, Module, XRef, Application, Behaviour}, warn: false
    alias Archeometer.Repo, warn: false
    alias Archeometer.Analysis.DSM, warn: false
    alias Archeometer.Analysis.DSM.ConsoleRender, warn: false
    Code.put_compiler_option(:tracers, [])
    __ENV__
  end

  defstruct [:query_type, :result_type, :desc, :code, :result, :uuid, :alt_code, :alt_code_lang]

  defmodule Definition do
    @moduledoc """
    Represents the definition of a Fragment.
    """

    defstruct [
      :query_type,
      :result_type,
      :desc,
      :code,
      :result_name,
      :table_headers,
      :alt_code,
      :alt_code_lang,
      :receives
    ]
  end

  def process(%__MODULE__.Definition{} = fdef, bindings, db_name \\ default_db_name()) do
    _process(fdef, bindings, db_name)
  end

  def alt_process(
        %__MODULE__.Definition{query_type: :cql, result_type: :table} = fdef,
        bindings,
        db_name \\ default_db_name()
      ) do
    {:ok, conn} = DB.open(db_name)

    query = render(fdef.alt_code, bindings)

    query_res = execute_query(conn, query, [])

    DB.close(conn)

    res = struct(__MODULE__, Map.from_struct(fdef))

    %{
      res
      | code: query,
        result: %{headers: fdef.table_headers, values: query_res},
        uuid: UUID.uuid4()
    }
  end

  defp _process(
         %__MODULE__.Definition{query_type: :cql, result_type: :table} = fdef,
         bindings,
         _db_name
       ) do
    description = render(fdef.desc, bindings)
    query = render(fdef.code, bindings)
    alt_query = render(fdef.alt_code, bindings)
    {query_res, _} = Code.eval_string(query, [], make_custom_env())
    res = struct(__MODULE__, Map.from_struct(fdef)) |> validate_fragment_result(query_res.rows)

    %{
      res
      | code: query,
        result: %{headers: fdef.table_headers, values: query_res.rows},
        uuid: UUID.uuid4(),
        alt_code: alt_query,
        desc: description
    }
  end

  defp _process(
         %__MODULE__.Definition{query_type: :elixir, result_type: :table} = fdef,
         bindings,
         db_name
       ) do
    description = render(fdef.desc, bindings)
    code_str = render(fdef.code, bindings ++ [db_name: db_name])
    {code_res, _} = Code.eval_string(code_str, bindings ++ [db_name: db_name], make_custom_env())
    res = struct(__MODULE__, Map.from_struct(fdef)) |> validate_fragment_result(code_res)

    %{
      res
      | code: code_str,
        result: %{headers: fdef.table_headers, values: code_res},
        uuid: UUID.uuid4(),
        desc: description
    }
  end

  defp _process(
         %__MODULE__.Definition{query_type: :mix_task, result_type: :table} = fdef,
         bindings,
         db_name
       ) do
    ["mix" | [task | _args]] = String.split(fdef.code)

    base_fname = task <> "." <> "csv"

    fname =
      Path.expand(
        UUID.uuid4() <> base_fname,
        Config.static_report_img_path()
      )

    path = String.split(db_name, "/") |> List.delete_at(-1) |> Enum.join("/")

    code_str = render(fdef.code, bindings ++ [fname: base_fname, db: db_name, path: path])
    command = render(fdef.code, bindings ++ [fname: fname, db: db_name, path: path])

    ["mix" | [^task | task_args]] = String.split(command)

    :ok = Mix.Task.rerun(task, task_args)

    table_values = read_table(fname)

    res = struct(__MODULE__, Map.from_struct(fdef))

    %{
      res
      | code: code_str,
        result: %{headers: fdef.table_headers, values: table_values, cell_align: "text-top"},
        uuid: UUID.uuid4()
    }
  end

  defp _process(
         %__MODULE__.Definition{query_type: :mix_task} = fdef,
         bindings,
         db_name
       ) do
    ["mix" | [task | args]] = String.split(fdef.code)
    {switches, _params, []} = OptionParser.parse(args, switches: [])

    format =
      if Enum.member?([:treemap_svg, :svg], fdef.result_type),
        do: "svg",
        else: Keyword.get(switches, :format)

    base_fname = task <> "." <> format

    fname =
      Path.expand(
        UUID.uuid4() <> "-" <> base_fname,
        Config.static_report_img_path()
      )

    code_str = render(fdef.code, bindings ++ [fname: base_fname])
    execute_code_str = render(fdef.code, bindings ++ [fname: fname])

    ["mix" | [^task | task_args]] = String.split(execute_code_str)

    task_args = task_args ++ ["--db", db_name]

    :ok = Mix.Task.rerun(task, task_args)

    res = struct(__MODULE__, Map.from_struct(fdef))
    %{res | code: code_str, result: fname, uuid: UUID.uuid4()}
  end

  defp _process(
         %__MODULE__.Definition{query_type: :cli_command} = fdef,
         bindings,
         db_name
       ) do
    [cmd | args] = fdef.code |> String.split()

    {switches, _params, []} =
      OptionParser.parse(
        args,
        switches: [],
        aliases: [f: :format]
      )

    format =
      if Enum.member?([:treemap_svg, :svg], fdef.result_type),
        do: "svg",
        else: Keyword.get(switches, :format, "png")

    base_fname = cmd <> "." <> format

    fname =
      Path.expand(
        UUID.uuid4() <> base_fname,
        Config.static_report_img_path()
      )

    code_str = render(fdef.code, bindings ++ [fname: base_fname, db: db_name])
    command = render(fdef.code, bindings ++ [fname: fname, db: db_name])

    Mix.Shell.cmd(command, [], &IO.puts/1)

    res = struct(__MODULE__, Map.from_struct(fdef))
    %{res | code: code_str, result: fname, uuid: UUID.uuid4()}
  end

  defp _process(
         %__MODULE__.Definition{query_type: :empty_input} = fdef,
         _bindings,
         _db_name
       ) do
    res = struct(__MODULE__, Map.from_struct(fdef))

    %{
      res
      | result_type: :empty_input,
        uuid: UUID.uuid4()
    }
  end

  defp render(code_str, bindings) do
    EEx.eval_string(code_str, assigns: bindings)
  end

  defp validate_fragment_result(res, []) do
    %{
      res
      | result_type: :empty_result
    }
  end

  defp validate_fragment_result(res, _query_res), do: res

  defp read_table(fname) do
    fname
    |> File.read!()
    |> String.split("\n", trim: true)
    |> Enum.map(&String.split(&1, ","))
    |> Enum.map(fn row_values -> extract_multi_values(row_values) end)
  end

  defp extract_multi_values(row_values) do
    Enum.map(row_values, fn value ->
      if String.contains?(value, " ") do
        String.split(value, " ", trim: true)
      else
        value
      end
    end)
  end
end