<commit_before>defmodule Blockytalky.UserState do
  use GenServer
  require Logger
  require Integer
  alias Blockytalky.BrickPi, as: BP
  alias Blockytalky.GrovePi, as: GP
  alias Blockytalky.GrovePiState, as: GPS
  alias Blockytalky.MockHW, as: MockHW
  @moduledoc """
  Keeps track of the stateful-ness of the client's BT program.
  e.g. hardware change over time, message queue to be handled
  Called by IR as needed to stash data between iterations
  state looks like:

  """
  @max_history_size 1_000
  #the state that gets passed around by this gen server e.g. %Blockytalky.UserState{}
  defstruct message_queue: [], port_values: %{},
  var_map: %{}, user_code: "", upid: nil,
  l: 0, init_funs: [], loop_funs: []
  ####
  # External API
  ## UserCode state State
  def update_state() do
    #update mock state
    if :mock in Blockytalky.RuntimeUtils.supported_hardware, do: update_mock_state()
    #update brickpi state
    if :btbrickpi in Blockytalky.RuntimeUtils.supported_hardware, do: update_bp_state()
    #update grove state
    if :btgrovepi in Blockytalky.RuntimeUtils.supported_hardware, do: update_gp_state()
  end
  def upload_user_code(code_map) do
    GenServer.cast(__MODULE__, {:upload_user_code, code_map})
  end

  def execute_user_code() do
    stop_user_code(false) #stop currently running code before rerunning
    code_map = GenServer.call(__MODULE__, :get_user_code)
    GenServer.call(__MODULE__, :clear_funs)
    GenServer.call(__MODULE__, :clear_state)
    try do
      code_string = code_map |> Map.get("code")
      Code.compile_string(code_string)
      if(code_string != "") do
        loop_stream = Stream.repeatedly (fn ->
          Blockytalky.UserCode.loop() # do one iteration of the loop
          receive do #wait for values to be updated to run again (FRP-like)
            _ -> :ok #only :updated for now
          end
        end)
        uc_pid = spawn (fn ->
          Blockytalky.Endpoint.broadcast("uc:command", "progress", %{body: "Program Running!"})
          Blockytalky.UserCode.init(:ok) #call init once, this is code such as "when I start"
          for _ <- loop_stream, do: :ok
        end)
        Logger.debug "running program: #{inspect uc_pid}"
        GenServer.cast(__MODULE__, {:set_upid, uc_pid})
      else
        Blockytalky.Endpoint.broadcast("uc:command", "error", %{body: "No program to run!"})
      end
    rescue
      e ->
        Logger.debug "Error compiling user code: #{inspect e}"
        Blockytalky.Endpoint.broadcast("uc:command", "error", %{body: "Failed to compile program"}) #user should never see this, but for now it is a nicity
    end
  end
  def stop_user_code(verbose \\ true) do
    upid = GenServer.call(__MODULE__, :get_upid)
    if upid do
      Process.exit(upid, :kill)
      if verbose, do: Blockytalky.Endpoint.broadcast("uc:command", "progress", %{body: "Program Stopped!"})
    else
      if verbose, do: Blockytalky.Endpoint.broadcast("uc:command", "progress", %{body: "No code running."})
    end
    GenServer.call(__MODULE__, :clear_state)
  end
  def queue_message(msg) do
    GenServer.cast(__MODULE__, {:queue_message, msg})
  end
  @doc """
  returns {:ok, msg} if there is one,
  returns {:nosender,:nomsg} if the queue is empty
  """
  def dequeue_message do
    GenServer.call(__MODULE__, :dequeue_message)
  end
  @doc """
  updates the state with the new value for that port,
  setting the old value to the previous new value if there was one
  or both to the new value if there wasn't a previous value
  """
  def put_value(value, port_id) do
    GenServer.cast(__MODULE__,{:put_value, value, port_id})
  end
  def get_value(port_id) do
    case GenServer.call(__MODULE__, {:get_port_value, port_id}) do
      [{_, value} | _ ] -> value
      _ -> nil
    end
  end
  @doc """
  applies the passed function with airity 3 to new value, and old value of
  the port with the passed id.
  ((x,y,z) -> any), port_id, value_for_z) -> any
  where x is the newest value of that port and y is the previous value
  useful for seeing if a user defined value is within/outside of a range
  ## Example
      iex> Blockytalky.UserScript.apply(fn(x,y,z) -> x > z and x < y end, 1, 10)
  """
  def apply(fun, port_id, value) do
    {new_value, old_value } = case GenServer.call(__MODULE__,{:get_port_value,port_id}) do
      [{_current, nv}, {_prev, ov} | _tail] -> {nv, ov}
      [{_current, nv} | _] -> {nv, nil}
      _ -> {nil, nil}
    end
    fun.(new_value, old_value, value)
  end

  def set_var(var_name, var_value), do: GenServer.cast(__MODULE__,{:set_var,var_name, var_value})
  def get_var(var_name) do
     case GenServer.call(__MODULE__, {:get_var,var_name}) do
       [{_, value} | _ ] -> value
       _ -> nil
     end
  end
  def update_var(var_name, fun) do
    GenServer.cast(__MODULE__, {:update_var, var_name, fun})
  end

  def get_var_history(var_name) do
    GenServer.call(__MODULE__, {:get_var,var_name})
  end
  ####
  # Internal API
  defp update_bp_state do
    sensor_ports = ~w/PORT_1 PORT_2 PORT_3 PORT_4/
    motor_ports = ~w/PORT_A PORT_B PORT_C PORT_D/
    _ = for x <- sensor_ports do
      case BP.get_sensor_value(x) do
        :undefined -> :ok # if no value, drop it for now
        v -> put_value(v,x) #value, port
      end
    end
    _ = for x <- motor_ports do
      case BP.get_encoder_value(x) do
        :undefined -> :ok # if no value returned from python, drop it for now<commit_msg>Update user_state.ex<commit_after>defmodule Blockytalky.UserState do
  use GenServer
  require Logger
  require Integer
  alias Blockytalky.BrickPi, as: BP
  alias Blockytalky.GrovePi, as: GP
  alias Blockytalky.GrovePiState, as: GPS
  alias Blockytalky.MockHW, as: MockHW
  @moduledoc """
  Keeps track of the stateful-ness of the client's BT program.
  e.g. hardware change over time, message queue to be handled
  Called by IR as needed to stash data between iterations
  state looks like:

  """
  @max_history_size 1_000
  #the state that gets passed around by this gen server e.g. %Blockytalky.UserState{}
  defstruct message_queue: [], port_values: %{},
  var_map: %{}, user_code: "", upid: nil,
  l: 0, init_funs: [], loop_funs: []
  ####
  # External API
  ## UserCode state State
  def update_state() do
    #update mock state
    if :mock in Blockytalky.RuntimeUtils.supported_hardware, do: update_mock_state()
    #update brickpi state
    if :btbrickpi in Blockytalky.RuntimeUtils.supported_hardware, do: update_bp_state()
    #update grove state
    if :btgrovepi in Blockytalky.RuntimeUtils.supported_hardware, do: update_gp_state()
  end
  def upload_user_code(code_map) do
    GenServer.cast(__MODULE__, {:upload_user_code, code_map})
  end

  def execute_user_code() do
    stop_user_code(false) #stop currently running code before rerunning
    code_map = GenServer.call(__MODULE__, :get_user_code)
    GenServer.call(__MODULE__, :clear_funs)
    GenServer.call(__MODULE__, :clear_state)
    try do
      code_string = code_map |> Map.get("code")
      Code.compile_string(code_string)
      if(code_string != "") do
        loop_stream = Stream.repeatedly (fn ->
          Blockytalky.UserCode.loop() # do one iteration of the loop
          receive do #wait for values to be updated to run again (FRP-like)
            _ -> :ok #only :updated for now
          end
        end)
        uc_pid = spawn (fn ->
          Blockytalky.Endpoint.broadcast("uc:command", "progress", %{body: "Program Running!"})
          Blockytalky.UserCode.init(:ok) #call init once, this is code such as "when I start"
          for _ <- loop_stream, do: :ok
        end)
        Logger.debug "running program: #{inspect uc_pid}"
        GenServer.cast(__MODULE__, {:set_upid, uc_pid})
      else
        Blockytalky.Endpoint.broadcast("uc:command", "error", %{body: "No program to run!"})
      end
    rescue
      e ->
        Logger.debug "Error compiling user code: #{inspect e}"
        Blockytalky.Endpoint.broadcast("uc:command", "error", %{body: "Failed to compile program"}) #user should never see this, but for now it is a nicity
    end
  end
  def stop_user_code(verbose \\ true) do
    upid = GenServer.call(__MODULE__, :get_upid)
    if upid do
      Process.exit(upid, :kill)
      if verbose, do: Blockytalky.Endpoint.broadcast("uc:command", "progress", %{body: "Program Stopped!"})
    else
      if verbose, do: Blockytalky.Endpoint.broadcast("uc:command", "progress", %{body: "No code running."})
    end
    GenServer.call(__MODULE__, :clear_state)
  end
  def queue_message(msg) do
    GenServer.cast(__MODULE__, {:queue_message, msg})
  end
  @doc """
  returns {:ok, msg} if there is one,
  returns {:nosender,:nomsg} if the queue is empty
  """
  def dequeue_message do
    GenServer.call(__MODULE__, :dequeue_message)
  end
  @doc """
  updates the state with the new value for that port,
  setting the old value to the previous new value if there was one
  or both to the new value if there wasn't a previous value
  """
  def put_value(value, port_id) do
    GenServer.cast(__MODULE__,{:put_value, value, port_id})
  end
  def get_value(port_id) do
    case GenServer.call(__MODULE__, {:get_port_value, port_id}) do
      [{_, value} | _ ] -> value
      _ -> nil
    end
  end
  @doc """
  applies the passed function with airity 3 to new value, and old value of
  the port with the passed id.
  ((x,y,z) -> any), port_id, value_for_z) -> any
  where x is the newest value of that port and y is the previous value
  useful for seeing if a user defined value is within/outside of a range
  ## Example
      iex> Blockytalky.UserScript.apply(fn(x,y,z) -> x > z and x < y end, 1, 10)
  """
  def apply(fun, port_id, value) do
    {new_value, old_value } = case GenServer.call(__MODULE__,{:get_port_value,port_id}) do
      [{_current, nv}, {_prev, ov} | _tail] -> {nv, ov}
      [{_current, nv} | _] -> {nv, nil}
      _ -> {nil, nil}
    end
    fun.(new_value, old_value, value)
  end

  def set_var(var_name, var_value), do: GenServer.cast(__MODULE__,{:set_var,var_name, var_value})
  def get_var(var_name) do
     case GenServer.call(__MODULE__, {:get_var,var_name}) do
       [{_, value} | _ ] -> value
       _ -> nil
     end
  end
  def update_var(var_name, fun) do
    #GenServer.cast(__MODULE__, {:update_var, var_name, fun})
    case get_var(var_name) do
      var -> set_var(var_name,fun.(var))
      _   -> :ok
    end
  end

  def get_var_history(var_name) do
    GenServer.call(__MODULE__, {:get_var,var_name})
  end
  ####
  # Internal API
  defp update_bp_state do
    sensor_ports = ~w/PORT_1 PORT_2 PORT_3 PORT_4/
    motor_ports = ~w/PORT_A PORT_B PORT_C PORT_D/
    _ = for x <- sensor_ports do
      case BP.get_sensor_value(x) do
        :undefined -> :ok # if no value, drop it for now
        v -> put_value(v,x) #value, port
      end
    end
    _ = for x <- motor_ports do
      case BP.get_encoder_value(x) do
        :undefined -> :ok # if no value returned from python, drop it for now
{'fraction_non_alphanumeric': 0.10901639344262296, 'fraction_numerical': 0.0023679417122040073, 'mean_word_length': 3.1880244088482073, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7379132', 'n_tokens_mistral': 3717, 'n_tokens_neox': 3628, 'n_words': 1115}