Check tokey_expiry_field is not nil

Check tokey_expiry_field is not nil

defmodule EspiDni.TokenSupervisor do
  use GenServer
  use Supervisor
  use Timex
  require Logger
  alias EspiDni.Team
  alias EspiDni.Repo
  import Ecto.Query, only: [from: 1, from: 2]

  @expiring_soon_in_seconds 60
  @refresh_wait_in_seconds 2_700
  @milliseconds_in_seconds 1_000

  def start_link do
    GenServer.start_link(__MODULE__, :ok, [name: __MODULE__])
  end

  def init(:ok) do
    for team <- teams_with_refresh_tokens do
      queue_for_refresh(team)
    end
    {:ok, %{}}
  end

  def queue_for_refresh(team) do
    diff = expires_in_unix(team) - :os.system_time(:seconds)

    if diff < @expiring_soon_in_seconds do
      refresh_now(team)
    else
      refresh_later(team, queue_delay_ms(diff))
    end
  end

  defp refresh_now(team) do
    Task.async fn ->
      __MODULE__.refresh_token!(team)
    end
  end

  defp refresh_later(team, delay) do
    Logger.info "Queueing token refresh for team: #{team.id} in #{delay}ms"
    :timer.apply_after(delay, __MODULE__, :refresh_token!, [team])
  end

  def refresh_token!(team) do
    Logger.info "Refreshing token for team: #{team.id}"
    new_token =  EspiDni.GoogleAnalyticsClient.get_new_token(team)

    if new_token do
      team
      |> Team.changeset(%{google_token: new_token, google_token_expires_at: new_expires_at(team)})
      |> Repo.update()
      |> elem(1)
      |> queue_for_refresh()
    else
      Logger.error "Could no refresh token for team: #{team.id}"
    end
  end

  defp expires_in_unix(team) do
    Timex.to_unix(Ecto.DateTime.to_erl(team.google_token_expires_at))
  end

  defp new_expires_at(team) do
    Timex.from_unix(expires_in_unix(team) + @refresh_wait_in_seconds)
  end

  defp queue_delay_ms(time) do
    (time - @expiring_soon_in_seconds) * @milliseconds_in_seconds
  end

  defp teams_with_refresh_tokens do
    Repo.all(
      from team in Team,
      where: not is_nil(team.google_refresh_token),
      where: not is_nil(team.google_token_expires_at)
    )
  end

end

defmodule EspiDni.TokenSupervisor do
  use GenServer
  use Supervisor
  use Timex
  require Logger
  alias EspiDni.Team
  alias EspiDni.Repo
  import Ecto.Query, only: [from: 1, from: 2]

  @expiring_soon_in_seconds 60
  @refresh_wait_in_seconds 2_700
  @milliseconds_in_seconds 1_000

  def start_link do
    GenServer.start_link(__MODULE__, :ok, [name: __MODULE__])
  end

  def init(:ok) do
    for team <- teams_with_refresh_tokens do
      queue_for_refresh(team)
    end
    {:ok, %{}}
  end

  def queue_for_refresh(team) do
    diff = expires_in_unix(team) - :os.system_time(:seconds)

    if diff < @expiring_soon_in_seconds do
      refresh_now(team)
    else
      refresh_later(team, queue_delay_ms(diff))
    end
  end

  defp refresh_now(team) do
    Task.async fn ->
      __MODULE__.refresh_token!(team)
    end
  end

  defp refresh_later(team, delay) do
    Logger.info "Queueing token refresh for team: #{team.id} in #{delay}ms"
    :timer.apply_after(delay, __MODULE__, :refresh_token!, [team])
  end

  def refresh_token!(team) do
    Logger.info "Refreshing token for team: #{team.id}"
    new_token =  EspiDni.GoogleAnalyticsClient.get_new_token(team)

    if new_token do
      team
      |> Team.changeset(%{google_token: new_token, google_token_expires_at: new_expires_at(team)})
      |> Repo.update()
      |> elem(1)
      |> queue_for_refresh()
    else
      Logger.error "Could no refresh token for team: #{team.id}"
    end
  end

  defp expires_in_unix(team) do
    Timex.to_unix(Ecto.DateTime.to_erl(team.google_token_expires_at))
  end

  defp new_expires_at(team) do
    Timex.from_unix(expires_in_unix(team) + @refresh_wait_in_seconds)
  end

  defp queue_delay_ms(time) do
    (time - @expiring_soon_in_seconds) * @milliseconds_in_seconds
  end

  defp teams_with_refresh_tokens do
    Repo.all(from team in Team, where: not is_nil(team.google_refresh_token))
  end

end


Pull complex boolean test into unparseable? predicate

require Logger

defmodule OpenAperture.Fleet.ServiceFileParser do

  @moduledoc """
  This module contains logic to parse Fleet service files
  """

  @doc """
  Method to parse a Fleet service file into a FleetApi.Unit

  ## Options

  The `unit_name` option is the requested name of the unit

  The `filepath` option is an absolute file location containing the service file.

  ## Return values

  FleetApi.Unit
  """
  @spec parse_unit(String.t, String.t) :: FleetApi.Unit.t
  def parse_unit(unit_name, filepath) do
    raw_options = parse(filepath)
    unit_options = if raw_options == nil || length(raw_options) == 0 do
      []
    else
      Enum.reduce raw_options, [], fn raw_unit, unit_options ->
        unit_options ++ [FleetApi.UnitOption.from_map(raw_unit)]
      end
    end

    %FleetApi.Unit{
      name: unit_name,
      options: unit_options
    }
  end

  @doc """
  Method to parse a Fleet service file into a List of unit Maps

  ## Options

  The `filepath` option is an absolute file location containing the service file.

  ## Return values

  list containing the UnitOption maps
  """
  @spec parse(String.t) :: list
  def parse(filepath) do
    Logger.info("Parsing service file #{filepath}...")
    if File.exists?(filepath) do
      input_file = File.open!(filepath, [:read, :utf8])
      process_file(input_file, "", [])
    else
      Logger.error("Unable to parse service file - file #{filepath} doesn't exist!")
      nil
    end
  end

  @doc false
  # Method to loop through the file and parse each line into a UnitOption
  #
  ## Options
  #
  # The `input_file` option is the contains of the File, line by line.
  #
  # The `current_section` option is the string representing the current section of the options.
  #
  # The `unit_options` option contains the List of currently known UnitOptions.
  #
  ## Return Values
  #
  # The list of all known UnitOptions
  #
  @spec process_file(term, String.t, list) :: list
  defp process_file(input_file, current_section, unit_options) do
    line = IO.read(input_file, :line)
    if (line != :eof) do
      current_line = String.strip(line)
      if (String.starts_with? current_line, "[") do
        current_section = String.slice(current_line, 1, 100)
        current_section = String.slice(current_section, 0, String.length(current_section)-1)
        process_file(input_file, current_section, process_line(line, current_section, unit_options))
      else
        process_file(input_file, current_section, process_line(line, current_section, unit_options))
      end
    else
      unit_options
    end
  end

  @doc false
  # Method to parse an individual line in the service file.
  #
  ## Options
  #
  # The `line` option is the String of the current line.
  #
  # The `current_section` option is the string representing the current section of the options.
  #
  # The `unit_options` option contains the List of currently known UnitOptions.
  #
  ## Return Values
  #
  # The list of all known UnitOptions
  #
  @spec process_line(String.t, String.t, list) :: list
  defp process_line(line, current_section, unit_options) do
    current_line = String.strip(line)
    if (String.starts_with? current_line, "#") do
      unit_options
    else
      if (String.starts_with? current_line, "[") do
        unit_options
      else
        new_option = parse_unit_option(current_section, current_line)
        if (new_option != nil) do
          unit_options ++ [new_option]
        else
          unit_options
        end
      end
    end
  end

  @doc false
  # Method to parse a line into a UnitOption
  #
  ## Options
  #
  # The `current_section` option is the string representing the current section of the options.
  #
  # The `current_line` option is the String of the current line.
  #
  ## Return Values
  #
  # The parsed UnitOption or nil (if the line isn't an option)
  #
  @spec parse_unit_option(String.t, String.t) :: term
  defp parse_unit_option(current_section, current_line) do
    {_, name_index} = Enum.reduce( String.codepoints(current_line), {0, -1}, fn(str, { i, first_occurrence })->
      if str == "=" && first_occurrence < 0 do
        first_occurrence = i
      end
      { i + 1, first_occurrence }
    end)

    name  = String.slice(current_line, 0..name_index-1)
    value = String.slice(current_line, name_index+1..-1)
    case unparseable?(name, value) do
      true -> nil
      _    -> %{"section" => current_section, "name"=>name, "value"=>value}
    end
  end

  @spec unparseable?(String.t, String.t) :: boolean
  defp unparseable?(name, value) do
    (name == nil || String.length(name) == 0 || value == nil || String.length(value) == 0)
  end
end

require Logger

defmodule OpenAperture.Fleet.ServiceFileParser do

  @moduledoc """
  This module contains logic to parse Fleet service files
  """

  @doc """
  Method to parse a Fleet service file into a FleetApi.Unit

  ## Options

  The `unit_name` option is the requested name of the unit

  The `filepath` option is an absolute file location containing the service file.

  ## Return values

  FleetApi.Unit
  """
  @spec parse_unit(String.t, String.t) :: FleetApi.Unit.t
  def parse_unit(unit_name, filepath) do
    raw_options = parse(filepath)
    unit_options = if raw_options == nil || length(raw_options) == 0 do
      []
    else
      Enum.reduce raw_options, [], fn raw_unit, unit_options ->
        unit_options ++ [FleetApi.UnitOption.from_map(raw_unit)]
      end
    end

    %FleetApi.Unit{
      name: unit_name,
      options: unit_options
    }
  end

  @doc """
  Method to parse a Fleet service file into a List of unit Maps

  ## Options

  The `filepath` option is an absolute file location containing the service file.

  ## Return values

  list containing the UnitOption maps
  """
  @spec parse(String.t) :: list
  def parse(filepath) do
    Logger.info("Parsing service file #{filepath}...")
    if File.exists?(filepath) do
      input_file = File.open!(filepath, [:read, :utf8])
      process_file(input_file, "", [])
    else
      Logger.error("Unable to parse service file - file #{filepath} doesn't exist!")
      nil
    end
  end

  @doc false
  # Method to loop through the file and parse each line into a UnitOption
  #
  ## Options
  #
  # The `input_file` option is the contains of the File, line by line.
  #
  # The `current_section` option is the string representing the current section of the options.
  #
  # The `unit_options` option contains the List of currently known UnitOptions.
  #
  ## Return Values
  #
  # The list of all known UnitOptions
  #
  @spec process_file(term, String.t, list) :: list
  defp process_file(input_file, current_section, unit_options) do
    line = IO.read(input_file, :line)
    if (line != :eof) do
      current_line = String.strip(line)
      if (String.starts_with? current_line, "[") do
        current_section = String.slice(current_line, 1, 100)
        current_section = String.slice(current_section, 0, String.length(current_section)-1)
        process_file(input_file, current_section, process_line(line, current_section, unit_options))
      else
        process_file(input_file, current_section, process_line(line, current_section, unit_options))
      end
    else
      unit_options
    end
  end

  @doc false
  # Method to parse an individual line in the service file.
  #
  ## Options
  #
  # The `line` option is the String of the current line.
  #
  # The `current_section` option is the string representing the current section of the options.
  #
  # The `unit_options` option contains the List of currently known UnitOptions.
  #
  ## Return Values
  #
  # The list of all known UnitOptions
  #
  @spec process_line(String.t, String.t, list) :: list
  defp process_line(line, current_section, unit_options) do
    current_line = String.strip(line)
    if (String.starts_with? current_line, "#") do
      unit_options
    else
      if (String.starts_with? current_line, "[") do
        unit_options
      else
        new_option = parse_unit_option(current_section, current_line)
        if (new_option != nil) do
          unit_options ++ [new_option]
        else
          unit_options
        end
      end
    end
  end

  @doc false
  # Method to parse a line into a UnitOption
  #
  ## Options
  #
  # The `current_section` option is the string representing the current section of the options.
  #
  # The `current_line` option is the String of the current line.
  #
  ## Return Values
  #
  # The parsed UnitOption or nil (if the line isn't an option)
  #
  @spec parse_unit_option(String.t, String.t) :: term
  defp parse_unit_option(current_section, current_line) do
    {_, name_index} = Enum.reduce( String.codepoints(current_line), {0, -1}, fn(str, { i, first_occurrence })->
      if str == "=" && first_occurrence < 0do
        first_occurrence = i
      end
      { i + 1, first_occurrence }
    end)

    name = String.slice(current_line, 0..name_index-1)
    value = String.slice(current_line, name_index+1..-1)
    if (name == nil || String.length(name) == 0 || value == nil || String.length(value) == 0) do
      nil
    else
      %{"section" => current_section, "name"=>name, "value"=>value}
    end
  end
end


Copy/paste error in Acronym test module.

if System.get_env("EXERCISM_TEST_EXAMPLES") do
  Code.load_file("example.exs")
else
  Code.load_file("acronym.exs")
end

ExUnit.start
ExUnit.configure exclude: :pending, trace: true

defmodule AcronymTest do
  use ExUnit.Case

  test "it produces acronyms from title case" do
    assert Acronym.abbreviate("Portable Networks Graphic") === "PNG"
  end

  @tag :pending
  test "it produces acronyms from lower case" do
    assert Acronym.abbreviate("Ruby on Rails") === "ROR"
  end

  @tag :pending
  test "it produces acronyms from inconsistent case" do
    assert Acronym.abbreviate("HyperText Markup Language") === "HTML"
  end

  @tag :pending
  test "it ignores punctuation" do
    assert Acronym.abbreviate("First in, First out") === "FIFO"
  end

  @tag :pending
  test "produces acronyms ignoring punctuation and casing" do
    assert Acronym.abbreviate("Complementary Metal-Oxide semiconductor") === "CMOS"
  end
end

if System.get_env("EXERCISM_TEST_EXAMPLES") do
  Code.load_file("example.exs")
else
  Code.load_file("acronym.exs")
end

ExUnit.start
ExUnit.configure exclude: :pending, trace: true

defmodule AnagramTest do
  use ExUnit.Case

  test "it produces acronyms from title case" do
    assert Acronym.abbreviate("Portable Networks Graphic") === "PNG"
  end

  @tag :pending
  test "it produces acronyms from lower case" do
    assert Acronym.abbreviate("Ruby on Rails") === "ROR"
  end

  @tag :pending
  test "it produces acronyms from inconsistent case" do
    assert Acronym.abbreviate("HyperText Markup Language") === "HTML"
  end

  @tag :pending
  test "it ignores punctuation" do
    assert Acronym.abbreviate("First in, First out") === "FIFO"
  end

  @tag :pending
  test "produces acronyms ignoring punctuation and casing" do
    assert Acronym.abbreviate("Complementary Metal-Oxide semiconductor") === "CMOS"
  end
end


Remove unused vars

Remove unused vars

defmodule FarmbotCore.Asset do
  @moduledoc """
  Top level module, with some helpers. Persists application resources to disk.
  Submodules of this module usually (but not always) correspond to a
  resource in the REST API. See official REST API docs for details.
  """

  alias FarmbotCore.Asset.{
    CriteriaRetriever,
    Device,
    DeviceCert,
    FarmEvent,
    FarmwareEnv,
    FarmwareInstallation,
    FbosConfig,
    FirmwareConfig,
    FirstPartyFarmware,
    Peripheral,
    PinBinding,
    Point,
    PointGroup,
    PublicKey,
    Regimen,
    RegimenInstance,
    Repo,
    Sensor,
    SensorReading,
    Sequence,
    Tool
  }

  alias FarmbotCore.AssetSupervisor

  import Ecto.Query
  require Logger

  ## Begin Device

  def device() do
    Repo.one(Device) || %Device{}
  end

  def device(field) do
    Map.fetch!(device(), field)
  end

  def update_device!(params) do
    device()
    |> Device.changeset(params)
    |> Repo.insert_or_update!()
  end

  def delete_device!(id) do
    if device = Repo.get_by(Device, id: id) do
      Repo.delete!(device)
    end

    :ok
  end

  ## End Device

  ## Begin FarmEvent

  @doc "Returns all FarmEvents"
  def list_farm_events do
    Repo.all(FarmEvent)
  end

  def new_farm_event!(params) do
    %FarmEvent{}
    |> FarmEvent.changeset(params)
    |> Repo.insert!()
  end

  @doc "Returns a FarmEvent by its API id."
  def get_farm_event(id) do
    Repo.get_by(FarmEvent, id: id)
  end

  def update_farm_event!(farm_event, params) do
    farm_event =
      farm_event
      |> FarmEvent.changeset(params)
      |> Repo.update!()

    if farm_event.executable_type == "Regimen" do
      regimen_instance = get_regimen_instance(farm_event)

      if regimen_instance do
        regimen_instance
        |> Repo.preload([:farm_event, :regimen])
        |> RegimenInstance.changeset(%{updated_at: DateTime.utc_now()})
        |> Repo.update!()
      end
    end

    farm_event
  end

  def delete_farm_event!(farm_event) do
    ri = get_regimen_instance(farm_event)
    ri && Repo.delete!(ri)
    Repo.delete!(farm_event)
  end

  def add_execution_to_farm_event!(%FarmEvent{} = farm_event, params \\ %{}) do
    %FarmEvent.Execution{}
    |> FarmEvent.Execution.changeset(params)
    |> Ecto.Changeset.put_assoc(:farm_event, farm_event)
    |> Repo.insert!()
  end

  def get_farm_event_execution(%FarmEvent{} = farm_event, scheduled_at) do
    Repo.one(
      from(e in FarmEvent.Execution,
        where:
          e.farm_event_local_id == ^farm_event.local_id and
            e.scheduled_at == ^scheduled_at
      )
    )
  end

  ## End FarmEvent

  ## Begin FbosConfig

  @doc "Gets the local config"
  def fbos_config() do
    Repo.one(FbosConfig) || %FbosConfig{}
  end

  @doc "Gets a field on the local config."
  def fbos_config(field) do
    Map.fetch!(fbos_config(), field)
  end

  @doc """
  This function updates Farmbot OS's local database. It will **NOT** send any
  HTTP requests to the API. To do this, `FarmbotCore.Asset.Private.mark_dirty!/2`
  is almost certainly what you want.
  """
  def update_fbos_config!(fbos_config \\ nil, params) do
    new_data =
      FbosConfig.changeset(fbos_config || fbos_config(), params)
      |> Repo.insert_or_update!()

    AssetSupervisor.cast_child(new_data, {:new_data, new_data})
    new_data
  end

  def delete_fbos_config!(id) do
    if fbos_config = Repo.get_by(FbosConfig, id: id) do
      Repo.delete!(fbos_config)
    end

    :ok
  end

  ## End FbosConfig

  ## Begin FirmwareConfig

  def firmware_config() do
    Repo.one(FirmwareConfig) || %FirmwareConfig{}
  end

  def firmware_config(field) do
    Map.fetch!(firmware_config(), field)
  end

  def update_firmware_config!(firmware_config \\ nil, params) do
    new_data =
      FirmwareConfig.changeset(firmware_config || firmware_config(), params)
      |> Repo.insert_or_update!()

    AssetSupervisor.cast_child(new_data, {:new_data, new_data})
    new_data
  end

  def delete_firmware_config!(id) do
    if firmware_config = Repo.get_by(FirmwareConfig, id: id) do
      Repo.delete!(firmware_config)
    end

    :ok
  end

  ## End FirmwareConfig

  ## Begin RegimenInstance

  @doc "returns every regimen instance"
  def list_regimen_instances() do
    RegimenInstance
    |> Repo.all()
    |> Repo.preload([:regimen, :farm_event])
  end

  def get_regimen_instance(%FarmEvent{} = farm_event) do
    regimen = Repo.one(from(r in Regimen, where: r.id == ^farm_event.executable_id))

    regimen &&
      Repo.one(
        from(ri in RegimenInstance,
          where: ri.regimen_id == ^regimen.local_id and ri.farm_event_id == ^farm_event.local_id
        )
      )
  end

  def new_regimen_instance!(%FarmEvent{} = farm_event, params \\ %{}) do
    regimen = Repo.one!(from(r in Regimen, where: r.id == ^farm_event.executable_id))

    RegimenInstance.changeset(%RegimenInstance{}, params)
    |> Ecto.Changeset.put_assoc(:regimen, regimen)
    |> Ecto.Changeset.put_assoc(:farm_event, farm_event)
    |> Repo.insert!()
  end

  def delete_regimen_instance!(%RegimenInstance{} = ri) do
    Repo.delete!(ri)
  end

  def add_execution_to_regimen_instance!(%RegimenInstance{} = ri, params \\ %{}) do
    %RegimenInstance.Execution{}
    |> RegimenInstance.Execution.changeset(params)
    |> Ecto.Changeset.put_assoc(:regimen_instance, ri)
    |> Repo.insert!()
  end

  def get_regimen_instance_execution(%RegimenInstance{} = ri, scheduled_at) do
    Repo.one(
      from(e in RegimenInstance.Execution,
        where:
          e.regimen_instance_local_id == ^ri.local_id and
            e.scheduled_at == ^scheduled_at
      )
    )
  end

  ## End RegimenInstance

  ## Begin PinBinding

  @doc "Lists all available pin bindings"
  def list_pin_bindings do
    Repo.all(PinBinding)
  end

  ## End PinBinding

  ## Begin Point

  def get_point(params) do
    Repo.get_by(Point, params)
  end

  def update_point(point, params) do
    point
    |> Point.changeset(params)
    |> Repo.update()
  end

  @doc "Returns all points matching Point.pointer_type"
  def get_all_points_by_type(type, order_by \\ "random") do
    from(p in Point, where: p.pointer_type == ^type and is_nil(p.discarded_at))
    |> Repo.all()
    |> sort_points(order_by)
  end

  def sort_points(points, order_by) do
    points
    |> Enum.group_by(&group_points_by(&1, order_by))
    |> Enum.sort(&group_sort(&1, &2, order_by))
    |> Enum.map(fn {_group_index, group} -> Enum.sort(group, &sort_points(&1, &2, order_by)) end)
    |> List.flatten()
  end

  def group_points_by(%{x: x}, algo) when algo in ~w(xy_ascending xy_descending), do: x
  def group_points_by(%{y: y}, algo) when algo in ~w(yx_ascending yx_descending), do: y
  def group_points_by(%{x: x, y: y}, "random"), do: Enum.random([x, y])

  def group_sort({lgroup, _}, {rgroup, _}, "xy_ascending"), do: lgroup <= rgroup
  def group_sort({lgroup, _}, {rgroup, _}, "yx_ascending"), do: lgroup <= rgroup

  def group_sort({lgroup, _}, {rgroup, _}, "xy_descending"), do: lgroup >= rgroup
  def group_sort({lgroup, _}, {rgroup, _}, "yx_descending"), do: lgroup >= rgroup
  def group_sort(_, _, "random"), do: Enum.random([true, false])

  def sort_points(%{y: ly}, %{y: ry}, "xy_ascending"), do: ly <= ry
  def sort_points(%{y: ly}, %{y: ry}, "xy_descending"), do: ly >= ry
  def sort_points(%{x: lx}, %{x: rx}, "yx_ascending"), do: lx <= rx
  def sort_points(%{x: lx}, %{x: rx}, "yx_descending"), do: lx >= rx
  def sort_points(_, _, "random"), do: Enum.random([true, false])

  ## End Point

  ## Begin PointGroup

  def get_point_group(params) do
    case Repo.get_by(PointGroup, params) do
      nil ->
        nil

      %{sort_type: nil} = group ->
        group

      %{point_ids: _unsorted, sort_type: sort_by} = point_group ->
        sorted_and_augmented = CriteriaRetriever.run(point_group)
          |> sort_points(sort_by)
          |> Enum.map(&Map.fetch!(&1, :id))

        %{point_group | point_ids: sorted_and_augmented}
    end
  end

  def new_point_group!(params) do
    %PointGroup{}
    |> PointGroup.changeset(params)
    |> Repo.insert!()
  end

  def update_point_group!(point_group, params) do
    updated =
      point_group
      |> PointGroup.changeset(params)
      |> Repo.update!()

    regimen_instances = list_regimen_instances()
    farm_events = list_farm_events()

    # check for any matching asset using this point group.
    # This is pretty recursive and probably isn't super great
    # for performance, but SQL can't check this stuff unfortunately.
    for asset <- farm_events ++ regimen_instances do
      # TODO(Connor) this might be worth creating a behaviour for
      if uses_point_group?(asset, point_group) do
        Logger.debug("#{inspect(asset)} uses PointGroup: #{inspect(point_group)}. Reindexing it.")
        FarmbotCore.AssetSupervisor.update_child(asset)
      end
    end

    updated
  end

  def delete_point_group!(%PointGroup{} = point_group) do
    Repo.delete!(point_group)
  end

  def uses_point_group?(%FarmEvent{body: body}, %PointGroup{id: point_group_id}) do
    any_body_node_uses_point_group?(body, point_group_id)
  end

  def uses_point_group?(%Regimen{body: body, regimen_items: regimen_items}, %PointGroup{
        id: point_group_id
      }) do
    any_body_node_uses_point_group?(body, point_group_id) ||
      Enum.find(regimen_items, fn %{sequence_id: sequence_id} ->
        any_body_node_uses_point_group?(get_sequence(sequence_id).body, point_group_id)
      end)
  end

  def uses_point_group?(%RegimenInstance{farm_event: farm_event, regimen: regimen}, point_group) do
    uses_point_group?(farm_event, point_group) || uses_point_group?(regimen, point_group)
  end

  def any_body_node_uses_point_group?(body, point_group_id) do
    Enum.find(body, fn
      %{
        kind: "execute",
        body: execute_body
      } ->
        any_body_node_uses_point_group?(execute_body, point_group_id)

      %{
        args: %{
          "data_value" => %{
            "args" => %{"resource_id" => ^point_group_id},
            "kind" => "point_group"
          },
          "label" => "parent"
        },
        kind: "parameter_application"
      } ->
        true

      %{
        args: %{
          "data_value" => %{
            "args" => %{"point_group_id" => ^point_group_id},
            "kind" => "point_group"
          },
          "label" => "parent"
        },
        kind: "parameter_application"
      } ->
        true

      _ ->
        false
    end)
  end

  ## End PointGroup

  ## Begin PublicKey

  def get_public_key(id) do
    Repo.get_by(PublicKey, id: id)
  end

  def new_public_key!(params) do
    %PublicKey{}
    |> PublicKey.changeset(params)
    |> Repo.insert!()
  end

  def update_public_key!(public_key, params) do
    public_key
    |> PublicKey.changeset(params)
    |> Repo.update!()
  end

  def delete_public_key!(public_key) do
    Repo.delete!(public_key)
  end

  def new_public_key_from_home!() do
    public_key_path = Path.join([System.get_env("HOME"), ".ssh", "id_rsa.pub"])
    public_key = File.read!(public_key_path)

    %PublicKey{}
    |> PublicKey.changeset(%{public_key: public_key})
    |> Repo.insert()
  end

  def new_public_key_from_string!(public_key) do
    %PublicKey{}
    |> PublicKey.changeset(%{public_key: public_key})
    |> Repo.insert()
  end

  ## End PublicKey

  ## Begin Regimen

  @doc "Get a regimen by it's API id"
  def get_regimen(id) do
    Repo.get_by(Regimen, id: id)
  end

  @doc "Enter a new regimen into the DB"
  def new_regimen!(params) do
    %Regimen{}
    |> Regimen.changeset(params)
    |> Repo.insert!()
  end

  def delete_regimen!(regimen) do
    regimen_instances =
      Repo.all(from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id))

    for ri <- regimen_instances do
      IO.puts("deleting regimen instance: #{inspect(ri)}")
      delete_regimen_instance!(ri)
    end

    Repo.delete!(regimen)
  end

  @doc "Update an existing regimen"
  def update_regimen!(regimen, params) do
    regimen_instances =
      Repo.all(from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id))
      |> Repo.preload([:farm_event, :regimen])

    for ri <- regimen_instances do
      ri
      |> RegimenInstance.changeset(%{updated_at: DateTime.utc_now()})
      |> Repo.update!()
    end

    regimen
    |> Regimen.changeset(params)
    |> Repo.update!()
  end

  ## End Regimen

  ## Begin Sequence

  @doc "Get a sequence by it's API id"
  def get_sequence(id) do
    Repo.get_by(Sequence, id: id)
  end

  def update_sequence!(%Sequence{} = sequence, params \\ %{}) do
    sequence_id = sequence.id

    farm_events =
      Repo.all(
        from(f in FarmEvent,
          where:
            f.executable_type == "Sequence" and
              f.executable_id == ^sequence_id
        )
      )

    regimen_instances =
      RegimenInstance
      |> Repo.all()
      |> Repo.preload([:regimen, :farm_event])
      |> Enum.filter(fn
        %{regimen: %{regimen_items: items}} ->
          Enum.find(items, fn
            %{sequence_id: ^sequence_id} -> true
            %{sequence_id: _} -> true
          end)

        %{regimen: nil} ->
          false
      end)

    for asset <- farm_events ++ regimen_instances do
      FarmbotCore.AssetSupervisor.update_child(asset)
    end

    Sequence.changeset(sequence, params)
    |> Repo.update!()
  end

  def new_sequence!(params \\ %{}) do
    Sequence.changeset(%Sequence{}, params)
    |> Repo.insert!()
  end

  ## End Sequence

  ## Begin FarmwareInstallation

  @doc "Get a FarmwareManifest by it's name."
  def get_farmware_manifest(package) do
    first_party_farmwares = Repo.all(from(fwi in FirstPartyFarmware, select: fwi.manifest))
    regular_farmwares = Repo.all(from(fwi in FarmwareInstallation, select: fwi.manifest))

    Enum.find(
      first_party_farmwares ++ regular_farmwares,
      fn
        %{package: pkg} -> pkg == package
        _ -> false
      end
    )
  end

  def get_farmware_installation(package) do
    first_party_farmwares = Repo.all(from(fwi in FirstPartyFarmware))
    regular_farmwares = Repo.all(from(fwi in FarmwareInstallation))

    Enum.find(
      first_party_farmwares ++ regular_farmwares,
      fn
        %{manifest: %{package: pkg}} -> pkg == package
        _ -> false
      end
    )
  end

  def upsert_farmware_manifest_by_id(id, params) do
    fwi = Repo.get_by(FarmwareInstallation, id: id) || %FarmwareInstallation{}

    FarmwareInstallation.changeset(fwi, params)
    |> Repo.insert_or_update()
  end

  def upsert_first_party_farmware_manifest_by_id(id, params) do
    fwi = Repo.get_by(FirstPartyFarmware, id: id) || %FirstPartyFarmware{}

    FirstPartyFarmware.changeset(fwi, params)
    |> Repo.insert_or_update()
  end

  ## End FarmwareInstallation

  ## Begin FarmwareEnv

  def list_farmware_env() do
    Repo.all(FarmwareEnv)
  end

  def upsert_farmware_env_by_id(id, params) do
    fwe = Repo.get_by(FarmwareEnv, id: id) || %FarmwareEnv{}

    FarmwareEnv.changeset(fwe, params)
    |> Repo.insert_or_update()
  end

  def new_farmware_env(params) do
    key = params["key"] || params[:key]

    fwe =
      with key when is_binary(key) <- key,
           [fwe | _] <- Repo.all(from(fwe in FarmwareEnv, where: fwe.key == ^key)) do
        fwe
      else
        _ -> %FarmwareEnv{}
      end

    FarmwareEnv.changeset(fwe, params)
    |> Repo.insert_or_update()
  end

  ## End FarmwareEnv

  ## Begin Peripheral

  def get_peripheral(args) do
    Repo.get_by(Peripheral, args)
  end

  def get_peripheral_by_pin(pin) do
    Repo.get_by(Peripheral, pin: pin)
  end

  ## End Peripheral

  ## Begin Sensor

  def get_sensor(id) do
    Repo.get_by(Sensor, id: id)
  end

  def get_sensor_by_pin(pin) do
    Repo.get_by(Sensor, pin: pin)
  end

  def new_sensor!(params) do
    Sensor.changeset(%Sensor{}, params)
    |> Repo.insert!()
  end

  def update_sensor!(sensor, params) do
    sensor
    |> Sensor.changeset(params)
    |> Repo.update!()
  end

  ## End Sensor

  ## Begin SensorReading

  def get_sensor_reading(id) do
    Repo.get_by(SensorReading, id: id)
  end

  def new_sensor_reading!(params) do
    SensorReading.changeset(%SensorReading{}, params)
    |> Repo.insert!()
  end

  def update_sensor_reading!(sensor_reading, params) do
    sensor_reading
    |> SensorReading.changeset(params)
    |> Repo.update!()
  end

  ## End SensorReading

  ## Begin DeviceCert

  def new_device_cert(params) do
    DeviceCert.changeset(%DeviceCert{}, params)
    |> Repo.insert()
  end

  def get_device_cert(args) do
    Repo.get_by(DeviceCert, args)
  end

  def update_device_cert(cert, params) do
    cert
    |> DeviceCert.changeset(params)
    |> Repo.update()
  end

  ## End DeviceCert

  ## Begin Tool

  def get_tool(args) do
    Repo.get_by(Tool, args)
  end

  ## End Tool
end

defmodule FarmbotCore.Asset do
  @moduledoc """
  Top level module, with some helpers. Persists application resources to disk.
  Submodules of this module usually (but not always) correspond to a
  resource in the REST API. See official REST API docs for details.
  """

  alias FarmbotCore.Asset.{
    CriteriaRetriever,
    Device,
    DeviceCert,
    FarmEvent,
    FarmwareEnv,
    FarmwareInstallation,
    FbosConfig,
    FirmwareConfig,
    FirstPartyFarmware,
    Peripheral,
    PinBinding,
    Point,
    PointGroup,
    PublicKey,
    Regimen,
    RegimenInstance,
    Repo,
    Sensor,
    SensorReading,
    Sequence,
    Tool
  }

  alias FarmbotCore.AssetSupervisor

  import Ecto.Query
  require Logger

  ## Begin Device

  def device() do
    Repo.one(Device) || %Device{}
  end

  def device(field) do
    Map.fetch!(device(), field)
  end

  def update_device!(params) do
    device()
    |> Device.changeset(params)
    |> Repo.insert_or_update!()
  end

  def delete_device!(id) do
    if device = Repo.get_by(Device, id: id) do
      Repo.delete!(device)
    end

    :ok
  end

  ## End Device

  ## Begin FarmEvent

  @doc "Returns all FarmEvents"
  def list_farm_events do
    Repo.all(FarmEvent)
  end

  def new_farm_event!(params) do
    %FarmEvent{}
    |> FarmEvent.changeset(params)
    |> Repo.insert!()
  end

  @doc "Returns a FarmEvent by its API id."
  def get_farm_event(id) do
    Repo.get_by(FarmEvent, id: id)
  end

  def update_farm_event!(farm_event, params) do
    farm_event =
      farm_event
      |> FarmEvent.changeset(params)
      |> Repo.update!()

    if farm_event.executable_type == "Regimen" do
      regimen_instance = get_regimen_instance(farm_event)

      if regimen_instance do
        regimen_instance
        |> Repo.preload([:farm_event, :regimen])
        |> RegimenInstance.changeset(%{updated_at: DateTime.utc_now()})
        |> Repo.update!()
      end
    end

    farm_event
  end

  def delete_farm_event!(farm_event) do
    ri = get_regimen_instance(farm_event)
    ri && Repo.delete!(ri)
    Repo.delete!(farm_event)
  end

  def add_execution_to_farm_event!(%FarmEvent{} = farm_event, params \\ %{}) do
    %FarmEvent.Execution{}
    |> FarmEvent.Execution.changeset(params)
    |> Ecto.Changeset.put_assoc(:farm_event, farm_event)
    |> Repo.insert!()
  end

  def get_farm_event_execution(%FarmEvent{} = farm_event, scheduled_at) do
    Repo.one(
      from(e in FarmEvent.Execution,
        where:
          e.farm_event_local_id == ^farm_event.local_id and
            e.scheduled_at == ^scheduled_at
      )
    )
  end

  ## End FarmEvent

  ## Begin FbosConfig

  @doc "Gets the local config"
  def fbos_config() do
    Repo.one(FbosConfig) || %FbosConfig{}
  end

  @doc "Gets a field on the local config."
  def fbos_config(field) do
    Map.fetch!(fbos_config(), field)
  end

  @doc """
  This function updates Farmbot OS's local database. It will **NOT** send any
  HTTP requests to the API. To do this, `FarmbotCore.Asset.Private.mark_dirty!/2`
  is almost certainly what you want.
  """
  def update_fbos_config!(fbos_config \\ nil, params) do
    new_data =
      FbosConfig.changeset(fbos_config || fbos_config(), params)
      |> Repo.insert_or_update!()

    AssetSupervisor.cast_child(new_data, {:new_data, new_data})
    new_data
  end

  def delete_fbos_config!(id) do
    if fbos_config = Repo.get_by(FbosConfig, id: id) do
      Repo.delete!(fbos_config)
    end

    :ok
  end

  ## End FbosConfig

  ## Begin FirmwareConfig

  def firmware_config() do
    Repo.one(FirmwareConfig) || %FirmwareConfig{}
  end

  def firmware_config(field) do
    Map.fetch!(firmware_config(), field)
  end

  def update_firmware_config!(firmware_config \\ nil, params) do
    new_data =
      FirmwareConfig.changeset(firmware_config || firmware_config(), params)
      |> Repo.insert_or_update!()

    AssetSupervisor.cast_child(new_data, {:new_data, new_data})
    new_data
  end

  def delete_firmware_config!(id) do
    if firmware_config = Repo.get_by(FirmwareConfig, id: id) do
      Repo.delete!(firmware_config)
    end

    :ok
  end

  ## End FirmwareConfig

  ## Begin RegimenInstance

  @doc "returns every regimen instance"
  def list_regimen_instances() do
    RegimenInstance
    |> Repo.all()
    |> Repo.preload([:regimen, :farm_event])
  end

  def get_regimen_instance(%FarmEvent{} = farm_event) do
    regimen = Repo.one(from(r in Regimen, where: r.id == ^farm_event.executable_id))

    regimen &&
      Repo.one(
        from(ri in RegimenInstance,
          where: ri.regimen_id == ^regimen.local_id and ri.farm_event_id == ^farm_event.local_id
        )
      )
  end

  def new_regimen_instance!(%FarmEvent{} = farm_event, params \\ %{}) do
    regimen = Repo.one!(from(r in Regimen, where: r.id == ^farm_event.executable_id))

    RegimenInstance.changeset(%RegimenInstance{}, params)
    |> Ecto.Changeset.put_assoc(:regimen, regimen)
    |> Ecto.Changeset.put_assoc(:farm_event, farm_event)
    |> Repo.insert!()
  end

  def delete_regimen_instance!(%RegimenInstance{} = ri) do
    Repo.delete!(ri)
  end

  def add_execution_to_regimen_instance!(%RegimenInstance{} = ri, params \\ %{}) do
    %RegimenInstance.Execution{}
    |> RegimenInstance.Execution.changeset(params)
    |> Ecto.Changeset.put_assoc(:regimen_instance, ri)
    |> Repo.insert!()
  end

  def get_regimen_instance_execution(%RegimenInstance{} = ri, scheduled_at) do
    Repo.one(
      from(e in RegimenInstance.Execution,
        where:
          e.regimen_instance_local_id == ^ri.local_id and
            e.scheduled_at == ^scheduled_at
      )
    )
  end

  ## End RegimenInstance

  ## Begin PinBinding

  @doc "Lists all available pin bindings"
  def list_pin_bindings do
    Repo.all(PinBinding)
  end

  ## End PinBinding

  ## Begin Point

  def get_point(params) do
    Repo.get_by(Point, params)
  end

  def update_point(point, params) do
    point
    |> Point.changeset(params)
    |> Repo.update()
  end

  @doc "Returns all points matching Point.pointer_type"
  def get_all_points_by_type(type, order_by \\ "random") do
    from(p in Point, where: p.pointer_type == ^type and is_nil(p.discarded_at))
    |> Repo.all()
    |> sort_points(order_by)
  end

  def sort_points(points, order_by) do
    points
    |> Enum.group_by(&group_points_by(&1, order_by))
    |> Enum.sort(&group_sort(&1, &2, order_by))
    |> Enum.map(fn {_group_index, group} -> Enum.sort(group, &sort_points(&1, &2, order_by)) end)
    |> List.flatten()
  end

  def group_points_by(%{x: x}, algo) when algo in ~w(xy_ascending xy_descending), do: x
  def group_points_by(%{y: y}, algo) when algo in ~w(yx_ascending yx_descending), do: y
  def group_points_by(%{x: x, y: y}, "random"), do: Enum.random([x, y])

  def group_sort({lgroup, _}, {rgroup, _}, "xy_ascending"), do: lgroup <= rgroup
  def group_sort({lgroup, _}, {rgroup, _}, "yx_ascending"), do: lgroup <= rgroup

  def group_sort({lgroup, _}, {rgroup, _}, "xy_descending"), do: lgroup >= rgroup
  def group_sort({lgroup, _}, {rgroup, _}, "yx_descending"), do: lgroup >= rgroup
  def group_sort(_, _, "random"), do: Enum.random([true, false])

  def sort_points(%{y: ly}, %{y: ry}, "xy_ascending"), do: ly <= ry
  def sort_points(%{y: ly}, %{y: ry}, "xy_descending"), do: ly >= ry
  def sort_points(%{x: lx}, %{x: rx}, "yx_ascending"), do: lx <= rx
  def sort_points(%{x: lx}, %{x: rx}, "yx_descending"), do: lx >= rx
  def sort_points(_, _, "random"), do: Enum.random([true, false])

  ## End Point

  ## Begin PointGroup

  def get_point_group(params) do
    case Repo.get_by(PointGroup, params) do
      nil ->
        nil

      %{sort_type: nil} = group ->
        group

      %{point_ids: unsorted, sort_type: sort_by} = point_group ->
        sorted_and_augmented = CriteriaRetriever.run(point_group)
          |> sort_points(sort_by)
          |> Enum.map(&Map.fetch!(&1, :id))

        %{point_group | point_ids: sorted_and_augmented}
    end
  end

  def new_point_group!(params) do
    %PointGroup{}
    |> PointGroup.changeset(params)
    |> Repo.insert!()
  end

  def update_point_group!(point_group, params) do
    updated =
      point_group
      |> PointGroup.changeset(params)
      |> Repo.update!()

    regimen_instances = list_regimen_instances()
    farm_events = list_farm_events()

    # check for any matching asset using this point group.
    # This is pretty recursive and probably isn't super great
    # for performance, but SQL can't check this stuff unfortunately.
    for asset <- farm_events ++ regimen_instances do
      # TODO(Connor) this might be worth creating a behaviour for
      if uses_point_group?(asset, point_group) do
        Logger.debug("#{inspect(asset)} uses PointGroup: #{inspect(point_group)}. Reindexing it.")
        FarmbotCore.AssetSupervisor.update_child(asset)
      end
    end

    updated
  end

  def delete_point_group!(%PointGroup{} = point_group) do
    Repo.delete!(point_group)
  end

  def uses_point_group?(%FarmEvent{body: body}, %PointGroup{id: point_group_id}) do
    any_body_node_uses_point_group?(body, point_group_id)
  end

  def uses_point_group?(%Regimen{body: body, regimen_items: regimen_items}, %PointGroup{
        id: point_group_id
      }) do
    any_body_node_uses_point_group?(body, point_group_id) ||
      Enum.find(regimen_items, fn %{sequence_id: sequence_id} ->
        any_body_node_uses_point_group?(get_sequence(sequence_id).body, point_group_id)
      end)
  end

  def uses_point_group?(%RegimenInstance{farm_event: farm_event, regimen: regimen}, point_group) do
    uses_point_group?(farm_event, point_group) || uses_point_group?(regimen, point_group)
  end

  def any_body_node_uses_point_group?(body, point_group_id) do
    Enum.find(body, fn
      %{
        kind: "execute",
        body: execute_body
      } ->
        any_body_node_uses_point_group?(execute_body, point_group_id)

      %{
        args: %{
          "data_value" => %{
            "args" => %{"resource_id" => ^point_group_id},
            "kind" => "point_group"
          },
          "label" => "parent"
        },
        kind: "parameter_application"
      } ->
        true

      %{
        args: %{
          "data_value" => %{
            "args" => %{"point_group_id" => ^point_group_id},
            "kind" => "point_group"
          },
          "label" => "parent"
        },
        kind: "parameter_application"
      } ->
        true

      _ ->
        false
    end)
  end

  ## End PointGroup

  ## Begin PublicKey

  def get_public_key(id) do
    Repo.get_by(PublicKey, id: id)
  end

  def new_public_key!(params) do
    %PublicKey{}
    |> PublicKey.changeset(params)
    |> Repo.insert!()
  end

  def update_public_key!(public_key, params) do
    public_key
    |> PublicKey.changeset(params)
    |> Repo.update!()
  end

  def delete_public_key!(public_key) do
    Repo.delete!(public_key)
  end

  def new_public_key_from_home!() do
    public_key_path = Path.join([System.get_env("HOME"), ".ssh", "id_rsa.pub"])
    public_key = File.read!(public_key_path)

    %PublicKey{}
    |> PublicKey.changeset(%{public_key: public_key})
    |> Repo.insert()
  end

  def new_public_key_from_string!(public_key) do
    %PublicKey{}
    |> PublicKey.changeset(%{public_key: public_key})
    |> Repo.insert()
  end

  ## End PublicKey

  ## Begin Regimen

  @doc "Get a regimen by it's API id"
  def get_regimen(id) do
    Repo.get_by(Regimen, id: id)
  end

  @doc "Enter a new regimen into the DB"
  def new_regimen!(params) do
    %Regimen{}
    |> Regimen.changeset(params)
    |> Repo.insert!()
  end

  def delete_regimen!(regimen) do
    regimen_instances =
      Repo.all(from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id))

    for ri <- regimen_instances do
      IO.puts("deleting regimen instance: #{inspect(ri)}")
      delete_regimen_instance!(ri)
    end

    Repo.delete!(regimen)
  end

  @doc "Update an existing regimen"
  def update_regimen!(regimen, params) do
    regimen_instances =
      Repo.all(from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id))
      |> Repo.preload([:farm_event, :regimen])

    for ri <- regimen_instances do
      ri
      |> RegimenInstance.changeset(%{updated_at: DateTime.utc_now()})
      |> Repo.update!()
    end

    regimen
    |> Regimen.changeset(params)
    |> Repo.update!()
  end

  ## End Regimen

  ## Begin Sequence

  @doc "Get a sequence by it's API id"
  def get_sequence(id) do
    Repo.get_by(Sequence, id: id)
  end

  def update_sequence!(%Sequence{} = sequence, params \\ %{}) do
    sequence_id = sequence.id

    farm_events =
      Repo.all(
        from(f in FarmEvent,
          where:
            f.executable_type == "Sequence" and
              f.executable_id == ^sequence_id
        )
      )

    regimen_instances =
      RegimenInstance
      |> Repo.all()
      |> Repo.preload([:regimen, :farm_event])
      |> Enum.filter(fn
        %{regimen: %{regimen_items: items}} ->
          Enum.find(items, fn
            %{sequence_id: ^sequence_id} -> true
            %{sequence_id: _} -> true
          end)

        %{regimen: nil} ->
          false
      end)

    for asset <- farm_events ++ regimen_instances do
      FarmbotCore.AssetSupervisor.update_child(asset)
    end

    Sequence.changeset(sequence, params)
    |> Repo.update!()
  end

  def new_sequence!(params \\ %{}) do
    Sequence.changeset(%Sequence{}, params)
    |> Repo.insert!()
  end

  ## End Sequence

  ## Begin FarmwareInstallation

  @doc "Get a FarmwareManifest by it's name."
  def get_farmware_manifest(package) do
    first_party_farmwares = Repo.all(from(fwi in FirstPartyFarmware, select: fwi.manifest))
    regular_farmwares = Repo.all(from(fwi in FarmwareInstallation, select: fwi.manifest))

    Enum.find(
      first_party_farmwares ++ regular_farmwares,
      fn
        %{package: pkg} -> pkg == package
        _ -> false
      end
    )
  end

  def get_farmware_installation(package) do
    first_party_farmwares = Repo.all(from(fwi in FirstPartyFarmware))
    regular_farmwares = Repo.all(from(fwi in FarmwareInstallation))

    Enum.find(
      first_party_farmwares ++ regular_farmwares,
      fn
        %{manifest: %{package: pkg}} -> pkg == package
        _ -> false
      end
    )
  end

  def upsert_farmware_manifest_by_id(id, params) do
    fwi = Repo.get_by(FarmwareInstallation, id: id) || %FarmwareInstallation{}

    FarmwareInstallation.changeset(fwi, params)
    |> Repo.insert_or_update()
  end

  def upsert_first_party_farmware_manifest_by_id(id, params) do
    fwi = Repo.get_by(FirstPartyFarmware, id: id) || %FirstPartyFarmware{}

    FirstPartyFarmware.changeset(fwi, params)
    |> Repo.insert_or_update()
  end

  ## End FarmwareInstallation

  ## Begin FarmwareEnv

  def list_farmware_env() do
    Repo.all(FarmwareEnv)
  end

  def upsert_farmware_env_by_id(id, params) do
    fwe = Repo.get_by(FarmwareEnv, id: id) || %FarmwareEnv{}

    FarmwareEnv.changeset(fwe, params)
    |> Repo.insert_or_update()
  end

  def new_farmware_env(params) do
    key = params["key"] || params[:key]

    fwe =
      with key when is_binary(key) <- key,
           [fwe | _] <- Repo.all(from(fwe in FarmwareEnv, where: fwe.key == ^key)) do
        fwe
      else
        _ -> %FarmwareEnv{}
      end

    FarmwareEnv.changeset(fwe, params)
    |> Repo.insert_or_update()
  end

  ## End FarmwareEnv

  ## Begin Peripheral

  def get_peripheral(args) do
    Repo.get_by(Peripheral, args)
  end

  def get_peripheral_by_pin(pin) do
    Repo.get_by(Peripheral, pin: pin)
  end

  ## End Peripheral

  ## Begin Sensor

  def get_sensor(id) do
    Repo.get_by(Sensor, id: id)
  end

  def get_sensor_by_pin(pin) do
    Repo.get_by(Sensor, pin: pin)
  end

  def new_sensor!(params) do
    Sensor.changeset(%Sensor{}, params)
    |> Repo.insert!()
  end

  def update_sensor!(sensor, params) do
    sensor
    |> Sensor.changeset(params)
    |> Repo.update!()
  end

  ## End Sensor

  ## Begin SensorReading

  def get_sensor_reading(id) do
    Repo.get_by(SensorReading, id: id)
  end

  def new_sensor_reading!(params) do
    SensorReading.changeset(%SensorReading{}, params)
    |> Repo.insert!()
  end

  def update_sensor_reading!(sensor_reading, params) do
    sensor_reading
    |> SensorReading.changeset(params)
    |> Repo.update!()
  end

  ## End SensorReading

  ## Begin DeviceCert

  def new_device_cert(params) do
    DeviceCert.changeset(%DeviceCert{}, params)
    |> Repo.insert()
  end

  def get_device_cert(args) do
    Repo.get_by(DeviceCert, args)
  end

  def update_device_cert(cert, params) do
    cert
    |> DeviceCert.changeset(params)
    |> Repo.update()
  end

  ## End DeviceCert

  ## Begin Tool

  def get_tool(args) do
    Repo.get_by(Tool, args)
  end

  ## End Tool
end


bloop

bloop

defmodule Farmbot.Serial.Handler do
  @moduledoc """
    Handles communication between farmbot and uart devices
  """

  alias Farmbot.BotState
  alias Farmbot.Context
  alias Farmbot.Lib.Maths
  alias Farmbot.Serial.Gcode.Parser
  alias Nerves.UART
  require Logger
  use GenServer

  # use Farmbot.DebugLog, enable: false
  use Farmbot.DebugLog

  @typedoc """
    Handler pid or name
  """
  @type handler :: pid | atom

  @typedoc """
    Nerves.UART pid or name
  """
  @type nerves :: handler

  @typedoc """
    Status of the arduino
  """
  @type status :: :busy | :done

  @typedoc """
    State for this GenServer
  """
  @type state :: %{
    context: Context.t,
    nerves: nerves,
    tty: binary,
    current: current,
    timeouts: integer,
    status: status,
    initialized: boolean
  }

  @typedoc """
    The current message being handled
  """
  @type current :: %{
    timer: reference,
    reply: tuple,
    status: status,
    from: {pid, reference},
    q: binary
  } | nil

  @default_timeout_ms 15_000
  @max_timeouts 10

  @doc """
    Starts a UART GenServer
  """
  def start_link(%Context{} = ctx, nerves, tty, opts)
  when is_pid(nerves) and is_binary(tty) do
    GenServer.start_link(__MODULE__, {ctx, nerves, tty}, opts)
  end

  def start_link(%Context{} = ctx, tty, opts) when is_binary(tty) do
    GenServer.start_link(__MODULE__, {ctx, tty}, opts)
  end

  @doc """
  Blocks the calling process until serial is ready.
  """
  @spec wait_for_available(Context.t) :: :ok | {:error, term}
  def wait_for_available(%Context{serial: handler} = context)
    when is_atom(handler)
  do
    pid = Process.whereis(handler)
    if is_pid(pid) and Process.alive?(pid) do
      new = %{context | serial: pid}
      wait_for_available(new)
    else
      {:error, :noproc}
    end
  end

  def wait_for_available(%Context{serial: handler}) do
    GenServer.call(handler, :wait_for_available, 12_000)
  end

  @doc """
  Checks if we have a handler available
  """
  @spec available?(Context.t) :: boolean
  def available?(context)

  # If handler is a pid
  def available?(%Context{serial: handler}) when is_pid(handler) do
    if Process.alive?(handler) do
      GenServer.call(handler, :available?)
    else
      false
    end
  end

  # if its a name, look it up
  def available?(%Context{serial: handler} = ctx) when is_atom(handler) do
    uh = Process.whereis(ctx.serial)
    if uh do
      available?(%{ctx | serial: uh})
    else
      false
    end
  end

  @doc """
    Writes a string to the uart line
  """
  @spec write(Context.t, binary, integer) :: binary | {:error, atom}
  def write(context, string, timeout \\ @default_timeout_ms)
  def write(%Context{} = ctx, str, timeout)
  when is_binary(str) and is_number(timeout) do
    if available?(ctx) do
      # try do
      #   fn() -> debug_log("write begin from: #{inspect Process.info(self())}") end.()
      # rescue
      #   _ -> :ok
      # end
      GenServer.call(ctx.serial, {:write, str, timeout}, :infinity)
    else
      {:error, :unavailable}
    end
  end

  @doc """
    Send the E stop command to the arduino.
  """
  @spec emergency_lock(Context.t) :: :ok | no_return
  def emergency_lock(%Context{} = ctx) do
    if available?(ctx) do
      GenServer.call(ctx.serial, :emergency_lock)
    else
      {:error, :unavailable}
    end
  end

  @doc """
    Tell the arduino its fine now.
  """
  @spec emergency_unlock(Context.t) :: :ok | no_return
  def emergency_unlock(%Context{} = ctx)  do
    # We check for aliveness here, not availableness.
    if is_alive?(ctx) do
      GenServer.call(ctx.serial, :emergency_unlock)
    else
      {:error, :unavailable}
    end
  end

  defp is_alive?(%Context{serial: serial}) when is_pid(serial) do
    Process.alive?(serial)
  end

  defp is_alive?(%Context{serial: serial} = ctx) when is_atom(serial) do
    pid = Process.whereis(serial)
    if pid do
      is_alive?(%{ctx | serial: pid})
    else
      false
    end
  end

  ## Private

  def init({ctx, nerves, tty}) when is_pid(nerves) and is_binary(tty) do
    Process.link(nerves)
    case open_tty(nerves, tty) do
      :ok ->
        state = %{
          context:     ctx,
          nerves:      nerves,
          tty:         tty,
          current:     nil,
          timeouts:    0,
          status:      :busy,
          waiting:     [],
          initialized: false
        }
        {:ok, state}
      err   ->
        debug_log "could not open tty: #{inspect err}"
        {:stop, :normal, :no_state}
    end
  end

  def init({ctx, tty}) when is_binary(tty) do
    {:ok, nerves} = UART.start_link()
    init({ctx, nerves, tty})
  end

  @spec open_tty(nerves, binary) :: :ok
  defp open_tty(nerves, tty) do
    # Open the tty
    case UART.open(nerves, tty, speed: 115_200, active: true) do
      :ok ->
        :ok = configure_uart(nerves, true)
        # Flush the buffers so we start fresh
        :ok = UART.flush(nerves)
        :ok
      err -> err
    end
  end

  defp configure_uart(nerves, active) do
    debug_log "reconfigureing uart: #{active}"
    UART.configure(nerves,
      framing: {UART.Framing.Line, separator: "\r\n"},
      active: active,
      rx_framing_timeout: 500)
  end

  def handle_call(:wait_for_available, from, state) do
    case state.status do
      :idle   -> {:reply, :ok,               state}
      :locked -> {:reply, {:error, :locked}, state}
      _       ->
        Process.send_after(self(), {:waiting_timeout, from}, 10_000)
        {:noreply, %{state | waiting: [from | state.waiting]}}
    end
  end

  def handle_call(:emergency_lock, _, state) do
    UART.write(state.nerves, "E")
    status = handle_locked(state.current, state.context)
    next = %{state |
      current: nil,
      timeouts: 0,
      status: status,
      initialized: false
    }
    {:reply, :ok, next}
  end

  def handle_call(:emergency_unlock, _, state) do
    UART.write(state.nerves, "F09")
    next = %{state | status: :idle, initialized: false}
    {:reply, :ok, next}
  end

  def handle_call(:available?, _, state), do: {:reply, state.initialized, state}

  def handle_call({:write, str, timeout}, from, %{status: :idle} = state)
  when is_binary(str) do
    handshake = generate_handshake()
    writeme =  "#{str} #{handshake}"

    # :ok = configure_uart(state.nerves, false)

    debug_log "writing: #{writeme}"
    :ok = UART.write(state.nerves, writeme)

    echo_ok = recieve_echo(state.nerves, writeme, "")

    # :ok = configure_uart(state.nerves, true)
    case echo_ok do
      :ok ->
        debug_log "timing this out in #{timeout} ms."
        timer = Process.send_after(self(), :timeout, timeout)
        current = %{
          status:   nil,
          reply:    nil,
          from:     from,
          q:        handshake,
          timer:    timer,
          callback: nil
        }
        {:noreply, %{state | current: current}}
      {:error, reason} ->
        {:reply, {:error, reason}, %{state | current: nil}}
    end

  end

  def handle_call({:write, _str, _timeout}, _from, %{status: status} = state) do
    debug_log "Serial Handler status: #{status}"
    {:reply, {:error, :bad_status}, state}
  end

  def handle_cast({:update_fw, file, pid}, state) do
    UART.close(state.nerves)
    Process.sleep(1000)
    if String.contains?(state.tty, "tnt") do
      Logger.warn "Not a real arduino!"
      send(pid, :done)
      {:noreply, state}
    else
      flash_firmware(state.context, state.tty, file, pid)
      {:stop, :update, state}
    end
  end

  def handle_info(:timeout, state) do
    current = state.current
    if current do
      if current.reply do
        debug_log "ignoring timeout. This transaction seems fine."
        {:noreply, %{state | current: %{current | timer: nil}}}
      else
        debug_log "timing out current: #{inspect current}"
        GenServer.reply(current.from, {:error, :timeout})
        check_timeouts(state)
      end
    else
      debug_log "got stray timeout."
      {:noreply, %{state | current: nil}}
    end
  end

  def handle_info({:waiting_timeout, from}, state) do
    if from in state.waiting do
      debug_log("Arduino is taking too long. Timing out waiting process: #{inspect from}")
      GenServer.reply(from, {:error, :timeout})
      {:noreply, %{state | waiting: List.delete(state.waiting, from)}}
    else
      {:noreply, state}
    end
  end

  def handle_info({:nerves_uart, tty, {:error, error}}, state) do
    Logger.error "#{tty} handler exiting!: #{error}"
    {:stop, error, state}
  end

  def handle_info({:nerves_uart, _tty, {:partial, _}}, s), do: {:noreply, s}

  def handle_info({:nerves_uart, _, str}, %{initialized: false} = state) do
    if String.contains?(str, "R00") do
      Logger.info "Initializing Firmware!"
      fn ->
        Process.sleep(2000)
        GenServer.cast(state.context.hardware, {:serial_ready, state.context})
      end.()
      {:noreply, %{state | initialized: true, status: :idle}}
    else
      debug_log "Serial not initialized yet: #{str}"
      {:noreply, state}
    end
  end

  def handle_info({:nerves_uart, _, str}, state) when is_binary(str) do
    debug_log "Reading: #{str}"
    case str |> Parser.parse_code |> do_handle(state.current, state.context) do
      :locked -> {:noreply, %{state | current: nil, status: :locked}}
      {:callback, str, current} ->
        debug_log "setting callback: #{str}"
        :ok = UART.write(state.nerves, str)
        {:noreply, %{state | current: current}}
      current ->
        if state.current.timer do
          true = Process.cancel_timer(state.current.timer)
        end
        next = %{state |
          current: current, status: current[:status] || :idle, timeouts: 0
        }
        if next.status == :idle do
          waiting = do_resolve_waiting(state.waiting)
          {:noreply, %{next | waiting: waiting}}
        else
          {:noreply, next}
        end
    end
  end

  def terminate(_, :no_state), do: :ok
  def terminate(_reason, state) do
    UART.close(state.nerves)
    UART.stop(state.nerves)
  end

  # This function should be called after every write and makes a couple assumptions.
  defp recieve_echo(_nerves, writeme, acc) do
    debug_log "Waiting for echo: sent: #{writeme} have: #{acc}"
    # this could return {:error, reason}
    receive do
      {:nerves_uart, _, bin} when is_binary(bin) -> parse_echo(acc <> bin, writeme)
      {:nerves_uart, _, {:error, reason}} -> {:error, reason}
    end
  end

  defp parse_echo(echo, writeme) do
    debug_log "Parsing echo: #{echo}"
    case echo do
      # R08 means valid command + whatever you wrote.
      "R08 " <> echo ->
        if echo |> String.trim() |> String.contains?(writeme), do: :ok, else: {:error, :bad_echo}
      # R09  means invalid command.
      << "R09", _ :: binary >> -> {:error, :invalid}
      # R87 is E stop
      << "R87", _ :: binary >> -> {:error, :emergency_lock}
      other                    -> {:error, "unhandled echo: #{other}"}
    end
  end

  defp do_resolve_waiting(list) do
    from = List.last(list)
    if from do
      GenServer.reply(from, :ok)
    end
    List.delete(list, from)
  end

  @spec do_handle({binary, any}, current | nil, Context.t)
    :: current | nil | :locked
  defp do_handle({_qcode, parsed}, current, %Context{} = ctx)
  when is_map(current) do
    if current.timer do
      true = Process.cancel_timer(current.timer)
    end
    results = handle_gcode(parsed, ctx)
    debug_log "Handling results: #{inspect results}"
    case results do
      {:status, :done}   -> handle_done(current)
      {:status, :busy}   -> handle_busy(current)
      {:status, :locked} -> handle_locked(current, ctx)
      {:status, status}  -> %{current | status: status}
      {:callback, str}   -> %{current | callback: str}
      {:reply,  reply}   -> %{current | reply: reply}
      thing              -> handle_other(thing, current)
    end
  end

  defp do_handle({_qcode, parsed}, nil, %Context{} = ctx) do
    handle_gcode(parsed, ctx)
    nil
  end

  @spec handle_locked(current, Context.t) :: :locked
  defp handle_locked(current, ctx) do
    if current do
      true = Process.cancel_timer(current.timer)
    end
    # Side effects.
    Farmbot.BotState.lock_bot(ctx)
    :locked
  end

  @spec handle_busy(current) :: current
  defp handle_busy(current) do
    debug_log "refreshing timer."
    true = Process.cancel_timer(current.timer)
    timer = Process.send_after(self(), :timeout, @default_timeout_ms)
    %{current | status: :busy, timer: timer}
  end

  defp handle_other(thing, current) do
    unless is_nil(thing) do
      debug_log "Unexpected thing: #{inspect thing}"
    end
    current
  end

  defp handle_done(current) do
    debug_log "replying to #{inspect current.from} with: #{inspect current.reply}"
    if current.callback do
      Process.cancel_timer(current.timer)
      handshake = generate_handshake()
      writeme   =  "#{current.callback} #{handshake}"
      timer     = Process.send_after(self(), :timeout, @default_timeout_ms)
      current   = %{
        status:   nil,
        reply:    nil,
        from:     current.from,
        q:        handshake,
        timer:    timer,
        callback: nil
      }
      {:callback, writeme, current}
    else
      GenServer.reply(current.from, current.reply || "R02")
      nil
    end
  end

  @spec generate_handshake :: binary
  defp generate_handshake, do: "Q#{:rand.uniform(99)}"

  @spec handle_gcode(any, Context.t) :: {:status, any} | {:reply, any} | nil

  defp handle_gcode(:report_emergency_lock, _),    do: {:status, :locked}
  defp handle_gcode(:idle, %Context{} = _ctx),     do: {:status, :idle}
  defp handle_gcode(:busy, %Context{} = _ctx),     do: {:status, :busy}
  defp handle_gcode(:done, %Context{} = _ctx),     do: {:status, :done}
  defp handle_gcode(:received, %Context{} = _ctx), do: {:status, :received}
  defp handle_gcode(:error, %Context{} = _ctx),    do: {:reply, :error}
  defp handle_gcode(:report_params_complete, _),   do: {:reply, :report_params_complete}
  defp handle_gcode(:noop, %Context{} = _ctx),     do: nil

  defp handle_gcode({:debug_message, message}, %Context{} = _ctx) do
    debug_log "R99 #{message}"
    nil
  end

  defp handle_gcode({:report_pin_value, pin, value} = reply, %Context{} = ctx)
  when is_integer(pin) and is_integer(value) do
    BotState.set_pin_value(ctx, pin, value)
    {:reply, reply}
  end

  defp handle_gcode({:report_current_position, x_steps, y_steps, z_steps} = reply, %Context{} = ctx) do
    thing_x = spm(:x, ctx)
    thing_y = spm(:y, ctx)
    thing_z = spm(:z, ctx)
    r = BotState.set_pos(ctx,
      Maths.steps_to_mm(x_steps, thing_x),
      Maths.steps_to_mm(y_steps, thing_y),
      Maths.steps_to_mm(z_steps, thing_z))
    debug_log "Position report reply: #{inspect r}"
    {:reply, reply}
  end

  defp handle_gcode({:report_parameter_value, param, value} = reply, %Context{} = ctx)
  when is_atom(param) and is_integer(value) do
    unless value == -1 do
      BotState.set_param(ctx, param, value)
    end
    {:reply, reply}
  end

  defp handle_gcode({:report_axis_calibration, param, value}, ctx) do
    p = Parser.parse_param(param)
    BotState.set_param(ctx, param, value)
    {:callback, "F22 P#{p} V#{value}"}
  end

  defp handle_gcode({:report_calibration, axis, status} = reply, _ctx) do
    Logger.info ">> Calibration message: #{axis}: #{status}"
    {:reply, reply}
  end

  defp handle_gcode(
    {:report_end_stops, x1, x2, y1, y2, z1, z2} = reply, %Context{} = ctx)
  do
    BotState.set_end_stops(ctx, {x1, x2, y1, y2, z1, z2})
    {:reply, reply}
  end

  defp handle_gcode({:report_encoder_position_scaled, x, y, z} = reply, %Context{} = ctx) do
    BotState.set_scaled_encoders(ctx, x, y, z)
    {:reply, reply}
  end

  defp handle_gcode({:report_encoder_position_raw, x, y, z} = reply, %Context{} = ctx) do
    BotState.set_raw_encoders(ctx, x, y, z)
    {:reply, reply}
  end

  defp handle_gcode({:report_software_version, version} = reply, %Context{} = ctx) do
    BotState.set_fw_version(ctx, version)
    {:reply, reply}
  end

  defp handle_gcode({:unhandled_gcode, code}, %Context{} = _ctx) do
    Logger.warn ">> got an misc gcode #{code}"
    {:reply, code}
  end

  defp handle_gcode(parsed, %Context{} = _ctx) do
    Logger.warn "Unhandled message: #{inspect parsed}"
    {:reply, parsed}
  end

  defp check_timeouts(state) do
    if state.timeouts > @max_timeouts do
      debug_log "reached max timeouts!"
      :ok = UART.close(state.nerves)
      Process.sleep(5000)
      {:stop, :max_timeouts, state}
    else
      {:noreply, %{state | current: nil, timeouts: state.timeouts + 1}}
    end
  end

  def flash_firmware(%Context{} = _ctx, tty, hex_file, pid) do
    Logger.info ">> Starting arduino firmware flash", type: :busy
    args =
      ["-patmega2560",
       "-cwiring",
       "-P/dev/#{tty}",
       "-b115200",
       "-D", "-q", "-V",
       "-Uflash:w:#{hex_file}:i"]

     avrdude = System.find_executable("avrdude")
     port_args = [
       :stream,
       :binary,
       :exit_status,
       :hide,
       :use_stdio,
       :stderr_to_stdout,
       args: args
     ]
     port = Port.open({:spawn_executable, avrdude}, port_args)
     timer = Process.send_after(self(), :flash_timeout, 20_000)
     r = handle_port(port, timer)
     send(pid, r)
  end

  defp handle_port(port, timer) do
    receive do
      {^port, {:data, contents}} ->
        debug_log(contents)
        handle_port(port, timer)
      {^port, {:exit_status, 0}} ->
        Logger.info(">> Flashed new arduino firmware", type: :success)
        Process.cancel_timer(timer)
        :done
      {^port, {:exit_status, error_code}} ->
        Logger.error ">> Could not flash firmware (#{error_code})"
        Process.cancel_timer(timer)
        {:error, error_code}
      :flash_timeout ->
        Logger.error ">> Timed out flashing firmware!"
        # info = Port.info(port)
        # if info do
        #   send port, {self(), :close}
        #   "kill" |> System.cmd(["15", "#{info.os_pid}"])
        # end
        {:error, :flash_timeout}
      after 21_000 ->
        Logger.error ">> Timed out flashing firmware!"
        {:error, :flash_timeout}
    end
  end

  @spec spm(atom, Context.t) :: integer
  defp spm(xyz, %Context{} = ctx) do
    spm = "steps_per_mm_#{xyz}" |> String.to_atom
    Farmbot.BotState.get_config(ctx, spm)
  end
end

defmodule Farmbot.Serial.Handler do
  @moduledoc """
    Handles communication between farmbot and uart devices
  """

  alias Farmbot.BotState
  alias Farmbot.Context
  alias Farmbot.Lib.Maths
  alias Farmbot.Serial.Gcode.Parser
  alias Nerves.UART
  require Logger
  use GenServer

  # use Farmbot.DebugLog, enable: false
  use Farmbot.DebugLog

  @typedoc """
    Handler pid or name
  """
  @type handler :: pid | atom

  @typedoc """
    Nerves.UART pid or name
  """
  @type nerves :: handler

  @typedoc """
    Status of the arduino
  """
  @type status :: :busy | :done

  @typedoc """
    State for this GenServer
  """
  @type state :: %{
    context: Context.t,
    nerves: nerves,
    tty: binary,
    current: current,
    timeouts: integer,
    status: status,
    initialized: boolean
  }

  @typedoc """
    The current message being handled
  """
  @type current :: %{
    timer: reference,
    reply: tuple,
    status: status,
    from: {pid, reference},
    q: binary
  } | nil

  @default_timeout_ms 15_000
  @max_timeouts 10

  @doc """
    Starts a UART GenServer
  """
  def start_link(%Context{} = ctx, nerves, tty, opts)
  when is_pid(nerves) and is_binary(tty) do
    GenServer.start_link(__MODULE__, {ctx, nerves, tty}, opts)
  end

  def start_link(%Context{} = ctx, tty, opts) when is_binary(tty) do
    GenServer.start_link(__MODULE__, {ctx, tty}, opts)
  end

  @doc """
  Blocks the calling process until serial is ready.
  """
  @spec wait_for_available(Context.t) :: :ok | {:error, term}
  def wait_for_available(%Context{serial: handler} = context)
    when is_atom(handler)
  do
    pid = Process.whereis(handler)
    if is_pid(pid) and Process.alive?(pid) do
      new = %{context | serial: pid}
      wait_for_available(new)
    else
      {:error, :noproc}
    end
  end

  def wait_for_available(%Context{serial: handler}) do
    GenServer.call(handler, :wait_for_available, 12_000)
  end

  @doc """
  Checks if we have a handler available
  """
  @spec available?(Context.t) :: boolean
  def available?(context)

  # If handler is a pid
  def available?(%Context{serial: handler}) when is_pid(handler) do
    if Process.alive?(handler) do
      GenServer.call(handler, :available?)
    else
      false
    end
  end

  # if its a name, look it up
  def available?(%Context{serial: handler} = ctx) when is_atom(handler) do
    uh = Process.whereis(ctx.serial)
    if uh do
      available?(%{ctx | serial: uh})
    else
      false
    end
  end

  @doc """
    Writes a string to the uart line
  """
  @spec write(Context.t, binary, integer) :: binary | {:error, atom}
  def write(context, string, timeout \\ @default_timeout_ms)
  def write(%Context{} = ctx, str, timeout)
  when is_binary(str) and is_number(timeout) do
    if available?(ctx) do
      # try do
      #   fn() -> debug_log("write begin from: #{inspect Process.info(self())}") end.()
      # rescue
      #   _ -> :ok
      # end
      GenServer.call(ctx.serial, {:write, str, timeout}, :infinity)
    else
      {:error, :unavailable}
    end
  end

  @doc """
    Send the E stop command to the arduino.
  """
  @spec emergency_lock(Context.t) :: :ok | no_return
  def emergency_lock(%Context{} = ctx) do
    if available?(ctx) do
      GenServer.call(ctx.serial, :emergency_lock)
    else
      {:error, :unavailable}
    end
  end

  @doc """
    Tell the arduino its fine now.
  """
  @spec emergency_unlock(Context.t) :: :ok | no_return
  def emergency_unlock(%Context{} = ctx)  do
    # We check for aliveness here, not availableness.
    if is_alive?(ctx) do
      GenServer.call(ctx.serial, :emergency_unlock)
    else
      {:error, :unavailable}
    end
  end

  defp is_alive?(%Context{serial: serial}) when is_pid(serial) do
    Process.alive?(serial)
  end

  defp is_alive?(%Context{serial: serial} = ctx) when is_atom(serial) do
    pid = Process.whereis(serial)
    if pid do
      is_alive?(%{ctx | serial: pid})
    else
      false
    end
  end

  ## Private

  def init({ctx, nerves, tty}) when is_pid(nerves) and is_binary(tty) do
    Process.link(nerves)
    case open_tty(nerves, tty) do
      :ok ->
        state = %{
          context:     ctx,
          nerves:      nerves,
          tty:         tty,
          current:     nil,
          timeouts:    0,
          status:      :busy,
          waiting:     [],
          initialized: false
        }
        {:ok, state}
      err   ->
        debug_log "could not open tty: #{inspect err}"
        {:stop, :normal, :no_state}
    end
  end

  def init({ctx, tty}) when is_binary(tty) do
    {:ok, nerves} = UART.start_link()
    init({ctx, nerves, tty})
  end

  @spec open_tty(nerves, binary) :: :ok
  defp open_tty(nerves, tty) do
    # Open the tty
    case UART.open(nerves, tty, speed: 115_200, active: true) do
      :ok ->
        :ok = configure_uart(nerves, true)
        # Flush the buffers so we start fresh
        :ok = UART.flush(nerves)
        :ok
      err -> err
    end
  end

  defp configure_uart(nerves, active) do
    debug_log "reconfigureing uart: #{active}"
    UART.configure(nerves,
      framing: {UART.Framing.Line, separator: "\r\n"},
      active: active,
      rx_framing_timeout: 500)
  end

  def handle_call(:wait_for_available, from, state) do
    case state.status do
      :idle   -> {:reply, :ok,               state}
      :locked -> {:reply, {:error, :locked}, state}
      _       ->
        Process.send_after(self(), {:waiting_timeout, from}, 10_000)
        {:noreply, %{state | waiting: [from | state.waiting]}}
    end
  end

  def handle_call(:emergency_lock, _, state) do
    UART.write(state.nerves, "E")
    status = handle_locked(state.current, state.context)
    next = %{state |
      current: nil,
      timeouts: 0,
      status: status,
      initialized: false
    }
    {:reply, :ok, next}
  end

  def handle_call(:emergency_unlock, _, state) do
    UART.write(state.nerves, "F09")
    next = %{state | status: :idle, initialized: false}
    {:reply, :ok, next}
  end

  def handle_call(:available?, _, state), do: {:reply, state.initialized, state}

  def handle_call({:write, str, timeout}, from, %{status: :idle} = state)
  when is_binary(str) do
    handshake = generate_handshake()
    writeme =  "#{str} #{handshake}"

    # :ok = configure_uart(state.nerves, false)

    debug_log "writing: #{writeme}"
    :ok = UART.write(state.nerves, writeme)

    echo_ok = recieve_echo(state.nerves, writeme, "")

    # :ok = configure_uart(state.nerves, true)
    case echo_ok do
      :ok ->
        debug_log "timing this out in #{timeout} ms."
        timer = Process.send_after(self(), :timeout, timeout)
        current = %{
          status:   nil,
          reply:    nil,
          from:     from,
          q:        handshake,
          timer:    timer,
          callback: nil
        }
        {:noreply, %{state | current: current}}
      {:error, reason} ->
        {:reply, {:error, reason}, %{state | current: nil}}
    end

  end

  def handle_call({:write, _str, _timeout}, _from, %{status: status} = state) do
    debug_log "Serial Handler status: #{status}"
    {:reply, {:error, :bad_status}, state}
  end

  def handle_cast({:update_fw, file, pid}, state) do
    UART.close(state.nerves)
    Process.sleep(1000)
    if String.contains?(state.tty, "tnt") do
      Logger.warn "Not a real arduino!"
      send(pid, :done)
      {:noreply, state}
    else
      flash_firmware(state.context, state.tty, file, pid)
      {:stop, :update, state}
    end
  end

  def handle_info(:timeout, state) do
    current = state.current
    if current do
      debug_log "Timing out current: #{inspect current}"
      GenServer.reply(current.from, {:error, :timeout})
      check_timeouts(state)
    else
      debug_log "Got stray timeout."
      {:noreply, %{state | current: nil}}
    end
  end

  def handle_info({:waiting_timeout, from}, state) do
    if from in state.waiting do
      debug_log("Arduino is taking too long. Timing out waiting process: #{inspect from}")
      GenServer.reply(from, {:error, :timeout})
      {:noreply, %{state | waiting: List.delete(state.waiting, from)}}
    else
      {:noreply, state}
    end
  end

  def handle_info({:nerves_uart, tty, {:error, error}}, state) do
    Logger.error "#{tty} handler exiting!: #{error}"
    {:stop, error, state}
  end

  def handle_info({:nerves_uart, _tty, {:partial, _}}, s), do: {:noreply, s}

  def handle_info({:nerves_uart, _, str}, %{initialized: false} = state) do
    if String.contains?(str, "R00") do
      Logger.info "Initializing Firmware!"
      fn ->
        Process.sleep(2000)
        GenServer.cast(state.context.hardware, {:serial_ready, state.context})
      end.()
      {:noreply, %{state | initialized: true, status: :idle}}
    else
      debug_log "Serial not initialized yet: #{str}"
      {:noreply, state}
    end
  end

  def handle_info({:nerves_uart, _, str}, state) when is_binary(str) do
    debug_log "Reading: #{str}"
    case str |> Parser.parse_code |> do_handle(state.current, state.context) do
      :locked -> {:noreply, %{state | current: nil, status: :locked}}
      {:callback, str, current} ->
        debug_log "setting callback: #{str}"
        :ok = UART.write(state.nerves, str)
        {:noreply, %{state | current: current}}
      current ->
        if state.current.timer do
          true = Process.cancel_timer(state.current.timer)
        end
        next = %{state |
          current: current, status: current[:status] || :idle, timeouts: 0
        }
        if next.status == :idle do
          waiting = do_resolve_waiting(state.waiting)
          {:noreply, %{next | waiting: waiting}}
        else
          {:noreply, next}
        end
    end
  end

  def terminate(_, :no_state), do: :ok
  def terminate(_reason, state) do
    UART.close(state.nerves)
    UART.stop(state.nerves)
  end

  # This function should be called after every write and makes a couple assumptions.
  defp recieve_echo(_nerves, writeme, acc) do
    debug_log "Waiting for echo: sent: #{writeme} have: #{acc}"
    # this could return {:error, reason}
    receive do
      {:nerves_uart, _, bin} when is_binary(bin) -> parse_echo(acc <> bin, writeme)
      {:nerves_uart, _, {:error, reason}} -> {:error, reason}
    end
  end

  defp parse_echo(echo, writeme) do
    debug_log "Parsing echo: #{echo}"
    case echo do
      # R08 means valid command + whatever you wrote.
      "R08 " <> echo ->
        if echo |> String.trim() |> String.contains?(writeme), do: :ok, else: {:error, :bad_echo}
      # R09  means invalid command.
      << "R09", _ :: binary >> -> {:error, :invalid}
      # R87 is E stop
      << "R87", _ :: binary >> -> {:error, :emergency_lock}
      other                    -> {:error, "unhandled echo: #{other}"}
    end
  end

  defp do_resolve_waiting(list) do
    from = List.last(list)
    if from do
      GenServer.reply(from, :ok)
    end
    List.delete(list, from)
  end

  @spec do_handle({binary, any}, current | nil, Context.t)
    :: current | nil | :locked
  defp do_handle({_qcode, parsed}, current, %Context{} = ctx)
  when is_map(current) do
    if current.timer do
      true = Process.cancel_timer(current.timer)
    end
    results = handle_gcode(parsed, ctx)
    debug_log "Handling results: #{inspect results}"
    case results do
      {:status, :done}   -> handle_done(current)
      {:status, :busy}   -> handle_busy(current)
      {:status, :locked} -> handle_locked(current, ctx)
      {:status, status}  -> %{current | status: status}
      {:callback, str}   -> %{current | callback: str}
      {:reply,  reply}   -> %{current | reply: reply}
      thing              -> handle_other(thing, current)
    end
  end

  defp do_handle({_qcode, parsed}, nil, %Context{} = ctx) do
    handle_gcode(parsed, ctx)
    nil
  end

  @spec handle_locked(current, Context.t) :: :locked
  defp handle_locked(current, ctx) do
    if current do
      true = Process.cancel_timer(current.timer)
    end
    # Side effects.
    Farmbot.BotState.lock_bot(ctx)
    :locked
  end

  @spec handle_busy(current) :: current
  defp handle_busy(current) do
    debug_log "refreshing timer."
    true = Process.cancel_timer(current.timer)
    timer = Process.send_after(self(), :timeout, @default_timeout_ms)
    %{current | status: :busy, timer: timer}
  end

  defp handle_other(thing, current) do
    unless is_nil(thing) do
      debug_log "Unexpected thing: #{inspect thing}"
    end
    current
  end

  defp handle_done(current) do
    debug_log "replying to #{inspect current.from} with: #{inspect current.reply}"
    if current.callback do
      Process.cancel_timer(current.timer)
      handshake = generate_handshake()
      writeme   =  "#{current.callback} #{handshake}"
      timer     = Process.send_after(self(), :timeout, @default_timeout_ms)
      current   = %{
        status:   nil,
        reply:    nil,
        from:     current.from,
        q:        handshake,
        timer:    timer,
        callback: nil
      }
      {:callback, writeme, current}
    else
      GenServer.reply(current.from, current.reply || "R02")
      nil
    end
  end

  @spec generate_handshake :: binary
  defp generate_handshake, do: "Q#{:rand.uniform(99)}"

  @spec handle_gcode(any, Context.t) :: {:status, any} | {:reply, any} | nil

  defp handle_gcode(:report_emergency_lock, _),    do: {:status, :locked}
  defp handle_gcode(:idle, %Context{} = _ctx),     do: {:status, :idle}
  defp handle_gcode(:busy, %Context{} = _ctx),     do: {:status, :busy}
  defp handle_gcode(:done, %Context{} = _ctx),     do: {:status, :done}
  defp handle_gcode(:received, %Context{} = _ctx), do: {:status, :received}
  defp handle_gcode(:error, %Context{} = _ctx),    do: {:reply, :error}
  defp handle_gcode(:report_params_complete, _),   do: {:reply, :report_params_complete}
  defp handle_gcode(:noop, %Context{} = _ctx),     do: nil

  defp handle_gcode({:debug_message, message}, %Context{} = _ctx) do
    debug_log "R99 #{message}"
    nil
  end

  defp handle_gcode({:report_pin_value, pin, value} = reply, %Context{} = ctx)
  when is_integer(pin) and is_integer(value) do
    BotState.set_pin_value(ctx, pin, value)
    {:reply, reply}
  end

  defp handle_gcode({:report_current_position, x_steps, y_steps, z_steps} = reply, %Context{} = ctx) do
    thing_x = spm(:x, ctx)
    thing_y = spm(:y, ctx)
    thing_z = spm(:z, ctx)
    r = BotState.set_pos(ctx,
      Maths.steps_to_mm(x_steps, thing_x),
      Maths.steps_to_mm(y_steps, thing_y),
      Maths.steps_to_mm(z_steps, thing_z))
    debug_log "Position report reply: #{inspect r}"
    {:reply, reply}
  end

  defp handle_gcode({:report_parameter_value, param, value} = reply, %Context{} = ctx)
  when is_atom(param) and is_integer(value) do
    unless value == -1 do
      BotState.set_param(ctx, param, value)
    end
    {:reply, reply}
  end

  defp handle_gcode({:report_axis_calibration, param, value}, ctx) do
    p = Parser.parse_param(param)
    BotState.set_param(ctx, param, value)
    {:callback, "F22 P#{p} V#{value}"}
  end

  defp handle_gcode({:report_calibration, axis, status} = reply, _ctx) do
    Logger.info ">> Calibration message: #{axis}: #{status}"
    {:reply, reply}
  end

  defp handle_gcode(
    {:report_end_stops, x1, x2, y1, y2, z1, z2} = reply, %Context{} = ctx)
  do
    BotState.set_end_stops(ctx, {x1, x2, y1, y2, z1, z2})
    {:reply, reply}
  end

  defp handle_gcode({:report_encoder_position_scaled, x, y, z} = reply, %Context{} = ctx) do
    BotState.set_scaled_encoders(ctx, x, y, z)
    {:reply, reply}
  end

  defp handle_gcode({:report_encoder_position_raw, x, y, z} = reply, %Context{} = ctx) do
    BotState.set_raw_encoders(ctx, x, y, z)
    {:reply, reply}
  end

  defp handle_gcode({:report_software_version, version} = reply, %Context{} = ctx) do
    BotState.set_fw_version(ctx, version)
    {:reply, reply}
  end

  defp handle_gcode({:unhandled_gcode, code}, %Context{} = _ctx) do
    Logger.warn ">> got an misc gcode #{code}"
    {:reply, code}
  end

  defp handle_gcode(parsed, %Context{} = _ctx) do
    Logger.warn "Unhandled message: #{inspect parsed}"
    {:reply, parsed}
  end

  defp check_timeouts(state) do
    if state.timeouts > @max_timeouts do
      debug_log "reached max timeouts!"
      :ok = UART.close(state.nerves)
      Process.sleep(5000)
      {:stop, :max_timeouts, state}
    else
      {:noreply, %{state | current: nil, timeouts: state.timeouts + 1}}
    end
  end

  def flash_firmware(%Context{} = _ctx, tty, hex_file, pid) do
    Logger.info ">> Starting arduino firmware flash", type: :busy
    args =
      ["-patmega2560",
       "-cwiring",
       "-P/dev/#{tty}",
       "-b115200",
       "-D", "-q", "-V",
       "-Uflash:w:#{hex_file}:i"]

     avrdude = System.find_executable("avrdude")
     port_args = [
       :stream,
       :binary,
       :exit_status,
       :hide,
       :use_stdio,
       :stderr_to_stdout,
       args: args
     ]
     port = Port.open({:spawn_executable, avrdude}, port_args)
     timer = Process.send_after(self(), :flash_timeout, 20_000)
     r = handle_port(port, timer)
     send(pid, r)
  end

  defp handle_port(port, timer) do
    receive do
      {^port, {:data, contents}} ->
        debug_log(contents)
        handle_port(port, timer)
      {^port, {:exit_status, 0}} ->
        Logger.info(">> Flashed new arduino firmware", type: :success)
        Process.cancel_timer(timer)
        :done
      {^port, {:exit_status, error_code}} ->
        Logger.error ">> Could not flash firmware (#{error_code})"
        Process.cancel_timer(timer)
        {:error, error_code}
      :flash_timeout ->
        Logger.error ">> Timed out flashing firmware!"
        # info = Port.info(port)
        # if info do
        #   send port, {self(), :close}
        #   "kill" |> System.cmd(["15", "#{info.os_pid}"])
        # end
        {:error, :flash_timeout}
      after 21_000 ->
        Logger.error ">> Timed out flashing firmware!"
        {:error, :flash_timeout}
    end
  end

  @spec spm(atom, Context.t) :: integer
  defp spm(xyz, %Context{} = ctx) do
    spm = "steps_per_mm_#{xyz}" |> String.to_atom
    Farmbot.BotState.get_config(ctx, spm)
  end
end


:hammer: extracts some methods

:hammer: extracts some methods

defmodule GameWorkbench.Guesser do
  alias TextClient.State

  def guess_move(game = %State{ tally: tally }) do
    Map.put(game, :guess, guess_letter(tally))
  end

  ######

  defp guess_letter(tally = %{ letters_used: letters_used }) do
    possible_letters(tally) -- Enum.uniq(letters_used)
    |> List.first
  end

  # Different strategies for finding the next letter:
  # - Sorted alphabet
  # - Random alphabet
  # - Frequency-sorted alphabet
  # - Possible words
  # - Frequency-sorted from possible words
  defp possible_letters(tally) do
    # alphabet()
    # alphabet_random()
    # alphabet_by_frequency_in_spanish()
    # alphabet_by_existing_words(tally)
    most_probable_alphabet_by_existing_words(tally)
  end

  def most_probable_alphabet_by_existing_words(tally) do
    letters_in_existing_words = alphabet_by_existing_words(tally)
    alphabet_by_frequency_in_spanish()
    |> Enum.filter(&(&1 in letters_in_existing_words))
  end

  def alphabet_by_existing_words(%{ letters: letters, letters_used: letters_used }) do
    possible_words(Enum.join(letters), letters_used)
    |> List.flatten
    |> Enum.join
    |> String.downcase
    |> String.codepoints
    |> Enum.uniq
  end

  def possible_words(word_so_far, letters_used) do
    words_by_matches_count = Dictionary.word_list(String.length(word_so_far))
    |> Enum.reduce(%{}, fn(word, acc) -> check_matches_count_and_accumulate(word, word_so_far, letters_used, acc) end)

    max_matches_count = Enum.max(Map.keys(words_by_matches_count))
    words_by_matches_count[max_matches_count]
  end

  defp check_matches_count_and_accumulate(word, word_so_far, letters_used, acc) do
    matches_count = compute_matches_count(word, word_so_far)
    cond do
      # all letters already tried, discard
      Enum.uniq(String.codepoints(word)) -- letters_used == [] ->
        acc
      # there is a word with more matches already in the list, discard
      Map.has_key?(acc, matches_count + 1) ->
        acc
      true ->
        Map.put(acc, matches_count, Enum.into(Map.get(acc, matches_count) || [], [word]))
    end
  end

  defp compute_matches_count(word, word_so_far) do
    String.myers_difference(word_so_far, word)
    |> Enum.reduce(0, fn({ key, substring }, matches) -> sum_myers_difference_match({key, substring}, matches) end)
  end

  defp sum_myers_difference_match({ :eq, substring }, matches_so_far) do
    matches_so_far + String.length(substring)
  end

  defp sum_myers_difference_match({ _key, _substring }, matches_so_far) do
    matches_so_far
  end

  # 26.9 - 27.6% in 1_000 iterations
  def alphabet_by_frequency_in_spanish do
    String.codepoints "eaosrnidlctumpbgvyqhfzjxkw"
  end

  # 0.1 - 0.3 - 0.5 - 1.1% in 1_000 iterations
  defp alphabet_random do
    for n <- ?a..?z, do: << n :: utf8 >>
    |> Enum.shuffle
  end

  defp alphabet do
    for n <- ?a..?z, do: << n :: utf8 >>
  end
end

defmodule GameWorkbench.Guesser do
  alias TextClient.State

  def guess_move(game = %State{ tally: tally }) do
    Map.put(game, :guess, guess_letter(tally))
  end

  ######

  defp guess_letter(tally = %{ letters_used: letters_used }) do
    possible_letters(tally) -- Enum.uniq(letters_used)
    |> List.first
  end

  # Different strategies for finding the next letter:
  # - Sorted alphabet
  # - Random alphabet
  # - Frequency-sorted alphabet
  # - Possible words
  # - Frequency-sorted from possible words
  defp possible_letters(tally) do
    # alphabet()
    # alphabet_random()
    # alphabet_by_frequency_in_spanish()
    # alphabet_by_existing_words(tally)
    most_probable_alphabet_by_existing_words(tally)
  end

  def most_probable_alphabet_by_existing_words(tally) do
    letters_in_existing_words = alphabet_by_existing_words(tally)
    alphabet_by_frequency_in_spanish()
    |> Enum.filter(&(&1 in letters_in_existing_words))
  end

  def alphabet_by_existing_words(%{ letters: letters, letters_used: letters_used }) do
    possible_words(Enum.join(letters), letters_used)
    |> List.flatten
    |> Enum.join
    |> String.downcase
    |> String.codepoints
    |> Enum.uniq
  end

  def possible_words(word_so_far, letters_used) do
    words_by_matches_count = Dictionary.start(String.length(word_so_far))
    |> Enum.reduce(%{}, fn(word, acc) ->
                         matches_count = String.myers_difference(word_so_far, word)
                                         |> Enum.reduce(0, fn({key, substring}, matches) ->
                                              cond do
                                                key == :eq ->
                                                  matches + String.length(substring)
                                                true ->
                                                  matches
                                              end
                                            end)
                         cond do
                           # all letters already tried, discard
                           Enum.uniq(String.codepoints(word)) -- letters_used == [] ->
                             acc
                           # there is a word with more matches already in the list, discard
                           Map.has_key?(acc, matches_count + 1) ->
                             acc
                           true ->
                             Map.put(acc, matches_count, Enum.into(Map.get(acc, matches_count) || [], [word]))
                         end
                       end)

    max_matches_count = Enum.max(Map.keys(words_by_matches_count))
    words_by_matches_count[max_matches_count]
  end

  # 26.9 - 27.6% in 1_000 iterations
  def alphabet_by_frequency_in_spanish do
    String.codepoints "eaosrnidlctumpbgvyqhfzjxkw"
  end

  # 0.1 - 0.3 - 0.5 - 1.1% in 1_000 iterations
  defp alphabet_random do
    for n <- ?a..?z, do: << n :: utf8 >>
    |> Enum.shuffle
  end

  defp alphabet do
    for n <- ?a..?z, do: << n :: utf8 >>
  end
end


add compile option as an example

add compile option as an example

defmodule HttpProxy.Utils do
  @moduledoc false

  if Mix.env == :test do
    @compile :export_all
    @compile :nowarn_export_all
  end

  @str_list "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"

  @doc ~S"""

  Get paths defined on `config/"#{Mix.env}.exs"`

  ## Exmaple

      iex> HttpProxy.Utils.rand_st(0) |> String.length
      0

      iex> HttpProxy.Utils.rand_st(5) |> String.length
      5
  """
  def rand_st(n) do
    rand_s("", n)
  end

  defp rand_s(string, n) when n == 0 and is_bitstring(string), do: string

  defp rand_s(string, n) when is_bitstring(string) do
    additional =
      @str_list
      |> String.codepoints()
      |> Enum.random()

    string = string <> additional
    rand_s(string, n - 1)
  end
end

defmodule HttpProxy.Utils do
  @moduledoc false

  @str_list "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"

  @doc ~S"""

  Get paths defined on `config/"#{Mix.env}.exs"`

  ## Exmaple

      iex> HttpProxy.Utils.rand_st(0) |> String.length
      0

      iex> HttpProxy.Utils.rand_st(5) |> String.length
      5
  """
  def rand_st(n) do
    rand_s("", n)
  end

  defp rand_s(string, n) when n == 0 and is_bitstring(string), do: string

  defp rand_s(string, n) when is_bitstring(string) do
    additional =
      @str_list
      |> String.codepoints()
      |> Enum.random()

    string = string <> additional
    rand_s(string, n - 1)
  end
end


Use v0.14.0 in eval hook as the default

Use v0.14.0 in eval hook as the default

defmodule EvalHook do
  def run(_sender, msg) do
    result = case msg do
      "eval~ " <> expr ->
        {expr, version: "v0.14.0"}

      "eval~" <> rest ->
        case Regex.run(~r/([\d.]+)(.+)$/, rest) do
          [_, version, expr] ->
            {expr, version: "v"<>version}
          _ -> nil
        end

      "erleval~ " <> expr ->
        {expr, lang: "erlang", version: "17.0"}

      "erleval~r16 " <> expr ->
        {expr, lang: "erlang", version: "R16B03-1"}

      _ -> nil
    end

    if result do
      {expr, opts} = result

      expr = String.strip(expr)
      output = Evaluator.eval(expr, opts)

      if output do
        lines = String.split(output, "\n")
        lines_to_msg(lines, {expr, output})
      end
    end
  end

  @maxlines 3

  defp lines_to_msg(lines, data), do:
    lines_to_msg(lines, @maxlines, [], data)

  defp lines_to_msg([], _, acc, _), do:
    Enum.reverse(acc)

  defp lines_to_msg(_, 0, acc, data) do
    link = gist_text(data)
    Enum.reverse([{:msg, "Output truncated: #{link}"}|acc])
  end

  defp lines_to_msg([line|rest], n, acc, data), do:
    lines_to_msg(rest, n-1, [{:msg, line}|acc], data)

  defp gist_text({expr, output}) do
    output = "# #{escape_newlines(expr)}\n" <> output
    json = """
    {
      "description": "",
      "public": true,
      "files": {
        "output": {
          "content": #{inspect output}
        }
      }
    }
    """
    result = :httpc.request(:post, {'https://api.github.com/gists', [{'User-Agent', 'beamie bot'}], 'application/json', json}, [], [sync: true])
    case result do
      {:error, _reason} ->
        IO.inspect _reason
        "*failed to gist the output*"
      {:ok, {status, _headers, data} } ->
        IO.inspect status
        case status do
          {_, 201, _} ->
            json = List.to_string(data)
            case Regex.run(~r/"html_url":\s*"([^"]+)",/, json) do
              [_, url] -> url
              _        -> "*failed to gist the output*"
            end

          _ ->
            "*failed to gist the output*"
        end
    end
  end

  defp escape_newlines("") do
    ""
  end

  defp escape_newlines(<<?\n::utf8>> <> rest) do
    <<?\\, ?n>> <> escape_newlines(rest)
  end

  defp escape_newlines(<<?\r::utf8>> <> rest) do
    <<?\\, ?r>> <> escape_newlines(rest)
  end

  defp escape_newlines(<<?\t::utf8>> <> rest) do
    <<?\\, ?t>> <> escape_newlines(rest)
  end

  defp escape_newlines(<<c::utf8>> <> rest) do
    <<c::utf8>> <> escape_newlines(rest)
  end
end

defmodule EvalHook do
  def run(_sender, msg) do
    result = case msg do
      "eval~ " <> expr ->
        {expr, version: "latest"}

      "eval~" <> rest ->
        case Regex.run(~r/([^ ]+)(.+)$/, rest) do
          [_, version, expr] ->
            {expr, version: "v"<>version}
          _ -> nil
        end

      "erleval~ " <> expr ->
        {expr, lang: "erlang", version: "17.0"}

      "erleval~r16 " <> expr ->
        {expr, lang: "erlang", version: "R16B03-1"}

      _ -> nil
    end

    if result do
      {expr, opts} = result

      expr = String.strip(expr)
      output = Evaluator.eval(expr, opts)

      if output do
        lines = String.split(output, "\n")
        lines_to_msg(lines, {expr, output})
      end
    end
  end

  @maxlines 3

  defp lines_to_msg(lines, data), do:
    lines_to_msg(lines, @maxlines, [], data)

  defp lines_to_msg([], _, acc, _), do:
    Enum.reverse(acc)

  defp lines_to_msg(_, 0, acc, data) do
    link = gist_text(data)
    Enum.reverse([{:msg, "Output truncated: #{link}"}|acc])
  end

  defp lines_to_msg([line|rest], n, acc, data), do:
    lines_to_msg(rest, n-1, [{:msg, line}|acc], data)

  defp gist_text({expr, output}) do
    output = "# #{escape_newlines(expr)}\n" <> output
    json = """
    {
      "description": "",
      "public": true,
      "files": {
        "output": {
          "content": #{inspect output}
        }
      }
    }
    """
    result = :httpc.request(:post, {'https://api.github.com/gists', [{'User-Agent', 'beamie bot'}], 'application/json', json}, [], [sync: true])
    case result do
      {:error, _reason} ->
        IO.inspect _reason
        "*failed to gist the output*"
      {:ok, {status, _headers, data} } ->
        IO.inspect status
        case status do
          {_, 201, _} ->
            json = List.to_string(data)
            case Regex.run(~r/"html_url":\s*"([^"]+)",/, json) do
              [_, url] -> url
              _        -> "*failed to gist the output*"
            end

          _ ->
            "*failed to gist the output*"
        end
    end
  end

  defp escape_newlines("") do
    ""
  end

  defp escape_newlines(<<?\n::utf8>> <> rest) do
    <<?\\, ?n>> <> escape_newlines(rest)
  end

  defp escape_newlines(<<?\r::utf8>> <> rest) do
    <<?\\, ?r>> <> escape_newlines(rest)
  end

  defp escape_newlines(<<?\t::utf8>> <> rest) do
    <<?\\, ?t>> <> escape_newlines(rest)
  end

  defp escape_newlines(<<c::utf8>> <> rest) do
    <<c::utf8>> <> escape_newlines(rest)
  end
end