lib/table.ex

defmodule ExDbmigrate.Table.Server do
  use GenServer
  require Logger

  @registry_name :tables
  @assoc_types [:belongs_to, :many_to_many, :has_one, :has_many]

  defstruct table: nil,
            links: [],
            incoming_links: [],
            assoc_type: nil,
            schema: %{},
            timestamps: false

  def child_spec(args) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [args]},
      type: :worker
    }
  end

  def schema(arg, data) do
    name = via_tuple(arg)
    GenServer.call(name, {:schema, data})
  end

  def links(arg, data) do
    name = via_tuple(arg)
    GenServer.call(name, {:links, data})
  end

  def send_incoming_links(arg) do
    name = via_tuple(arg)
    GenServer.call(name, :send_incoming_links)
  end

  def assoc_type(arg) do
    name = via_tuple(arg)
    GenServer.call(name, :assoc_type)
  end

  def init(init_arg) do
    data = ExDbmigrate.list_foreign_keys(init_arg)

    ref_type = ExDbmigrate.Config.key_type()

    timestamp_fields =
      Application.get_env(:ex_dbmigrate, :timestamp_fields, [:updated_at, :inserted_at])

    link_data =
      Enum.map(data, fn data ->
        %{
          column_name: data.column_name,
          references: %{
            ref_table: data.foreign_table_name,
            ref_column: data.foreign_column_name,
            type: ref_type
          }
        }
      end)

    assoc_type =
      case Enum.count(link_data) do
        x when x > 1 -> :many_to_many
        1 -> :belongs_to
        0 -> nil
      end

    data = ExDbmigrate.fetch_table_data(init_arg)

    column_names =
      Enum.map(link_data, fn data ->
        data.column_name
      end)

    schema_data =
      Enum.map(data.rows, fn [id, _is_null, type, _position, _max_length] ->
        unless id == "id" || Enum.member?(column_names, id) do
          type = String.to_atom(ExDbmigrate.type_select(type))
          {String.to_atom(id), type}
        end
      end)
      |> Enum.reject(fn x -> is_nil(x) end)

    column_names =
      Enum.map(schema_data, fn data ->
        {id, _d} = data

        id
      end)

    timestamps =
      Enum.map(column_names, fn x ->
        Enum.member?(timestamp_fields, x)
      end)
      |> Enum.reject(fn x -> x != true end)
      |> Enum.count() > 0

    schema_data = Keyword.drop(schema_data, timestamp_fields)

    {:ok,
     %__MODULE__{
       table: init_arg,
       links: link_data,
       schema: schema_data,
       timestamps: timestamps,
       assoc_type: assoc_type
     }}
  end

  def start_link([arg]) do
    name = via_tuple(arg)
    GenServer.start_link(__MODULE__, arg, name: name)
  end

  def shutdown() do
    GenServer.call(__MODULE__, :shutdown)
  end

  def show(name) do
    name = via_tuple(name)
    GenServer.call(name, :show)
  end

  @impl true
  def handle_call(
        :send_incoming_links,
        _from,
        state
      ) do
    Enum.map(state.links, fn x ->
      name = via_tuple(x.references.ref_table)
      references = %{x.references | ref_table: state.table}
      x = %{x | references: references}
      GenServer.call(name, {:incoming_link, x})
    end)

    {:reply, state, state}
  end

  @impl true
  def handle_call(
        :assoc_type,
        _from,
        state
      ) do
    ref_type = ExDbmigrate.Config.key_type()

    assoc_type =
      case Enum.count(state.links) do
        x when x > 1 -> :many_to_many
        1 -> :belongs_to
        0 -> nil
      end

    state = %{state | assoc_type: assoc_type}
    {:reply, state, state}
  end

  @impl true
  def handle_call(
        {:links, data},
        _from,
        state
      ) do
    link_data =
      Enum.map(data, fn x ->
        ref_type = ExDbmigrate.Config.key_type()

        %{
          column_name: x.column_name,
          references: %{ref_table: x.foreign_table_name, type: ref_type}
        }
      end)

    state = %{state | links: link_data}

    {:reply, state, state}
  end

  def incoming_link(name, data) do
    name = via_tuple(name)
    GenServer.call(name, {:incoming_link, data})
  end

  @impl true
  def handle_call(
        {:incoming_link, data},
        _from,
        state
      ) do
    link_data = state.incoming_links ++ [data]

    state = %{state | incoming_links: link_data}

    {:reply, state, state}
  end

  def handle_call(
        {:schema, data},
        _from,
        state
      ) do
    state = %{state | schema: data}
    {:reply, state, state}
  end

  def handle_call(
        :shutdown,
        _from,
        state
      ) do
    {:stop, {:ok, "Normal Shutdown"}, state}
  end

  def handle_call(
        :show,
        _from,
        state
      ) do
    {:reply, state, state}
  end

  def handle_cast(
        :shutdown,
        state
      ) do
    {:stop, :normal, state}
  end

  @doc false
  def via_tuple(data, registry \\ @registry_name) do
    {:via, Registry, {registry, data}}
  end

  @impl true
  def handle_info({:DOWN, ref, :process, _pid, _reason}, {names, refs}) do
    {:noreply, {names, refs}}
  end

  @impl true
  def handle_info(_msg, state) do
    {:noreply, state}
  end
end