lib/robot.ex

defmodule Robot do
  @moduledoc """
    Given a robot which can only move in four directions, UP(U), DOWN(D), LEFT(L), RIGHT(R).

    Given a string consisting of instructions to move. Output the coordinates of a robot
    after executing the instructions. Initial position of robot is at origin(0, 0).

    If the robot exceeds the established limits it will travel to the negative limit of
    the maximum position.

    The maximum position by default is set to 5, but it can be changed.
    For more details, see `Robot.setBounds/2`.
  """

  @robot_position_changed_payload :robot_position_changed

  alias Robot.Models.Point

  ## API

  @doc """
  Initialize a Robot process.

  Returns `{:ok, #PID<0.162.0>}`.

  ## Examples

      iex> {:ok, _pid} = Robot.start()

  """
  @spec start(pid()) :: {:ok, pid()}
  def start(caller), do: Agent.start_link(fn -> %Point{subscribers: [caller]} end)
  def start(), do: Agent.start_link(fn -> %Point{} end)

  @doc """
  Stop a Robot process.

  Returns `:ok`.

  ## Examples

      iex> {:ok, pid} = Robot.start()
      iex> :ok = Robot.stop(pid)
      :ok

  """
  @spec stop(pid()) :: :ok
  def stop(pid), do: Agent.stop(pid)

  @doc """
  Change the Robot bounds by a given tuple `{x, y}`.

  Returns `:ok`.

  ## Examples

      iex> {:ok, pid} = Robot.start()
      iex> :ok = Robot.setBounds(pid, {10, 10})
      :ok

  """
  @spec setBounds(pid(), tuple()) :: :ok
  def setBounds(pid, {x, y}), do: Agent.update(pid, &(Map.put(&1, :bounds, %{x: x, y: y})))

  @doc """
  Move the Robot to the desired location according to the given commands.

  Returns `:ok`.

  ## Examples

      iex> {:ok, pid} = Robot.start()
      iex> Robot.move(pid, "UUUDR")
      :ok

  """
  @spec move(pid(), String.t()) :: :ok
  def move(pid, command) when is_binary(command) do
    :ok =
      command
      |> split_command()
      |> update_movement(pid)
  end
  def move(_pid, _command), do: :ok

  @doc """
  Get the current position of the Robot.

  Returns `%Robot.Models.Point{}`.

  ## Examples

      iex> {:ok, pid} = Robot.start()
      iex> Robot.get(pid)
      %Robot.Models.Point{x: 0, y: 0}

  """
  @spec get(pid()) :: %Point{}
  def get(pid), do: Agent.get(pid, &(&1))


  ## private helpers

  defp update_movement(["U" | tail], pid) do
    Agent.update(pid, &(Map.put(&1, :y, (&1.y + 1) |> adjust_point(&1.bounds.y))))

    pid |> get() |> send_message_robot_moved()

    update_movement(tail, pid)
  end
  defp update_movement(["D" | tail], pid) do
    Agent.update(pid, &(Map.put(&1, :y, (&1.y - 1) |> adjust_point(&1.bounds.y))))

    pid |> get() |> send_message_robot_moved()

    update_movement(tail, pid)
  end
  defp update_movement(["L" | tail], pid) do
    Agent.update(pid, &(Map.put(&1, :x, (&1.x - 1) |> adjust_point(&1.bounds.x))))

    pid |> get() |> send_message_robot_moved()

    update_movement(tail, pid)
  end
  defp update_movement(["R" | tail], pid) do
    Agent.update(pid, &(Map.put(&1, :x, (&1.x + 1) |> adjust_point(&1.bounds.x))))

    pid |> get() |> send_message_robot_moved()

    update_movement(tail, pid)
  end
  defp update_movement(_, _pid), do: :ok

  defp adjust_point(value, bounds) when (value >= (bounds * -1) and value <= bounds), do: value
  defp adjust_point(_value, bounds), do: (bounds * -1)

  defp split_command(command), do: String.split(command, "", trim: true)

  defp send_message_robot_moved(%Point{subscribers: []}), do: :ok
  defp send_message_robot_moved(%Point{subscribers: subscribers, x: x, y: y}) do
    for pid <- subscribers, do: send(pid, {@robot_position_changed_payload, %{x: x, y: y}})
    :ok
  end
end