defmodule HL7.MLLPStream do
  @moduledoc false
  require Logger

  # ^K - VT (Vertical Tab) - 0x0B
  @sb "\v"
  # ^\ - FS (File Separator)
  @eb <<0x1C>>
  # ^M - CR (Carriage Return) - 0x0D
  @cr "\r"
  @ending @eb <> @cr

  @spec get_prefix() :: String.t()
  def get_prefix() do
    @sb
  end

  @spec get_suffix() :: String.t()
  def get_suffix() do
    @ending
  end

  @spec raw_to_messages(Enumerable.t()) :: Enumerable.t()
  def raw_to_messages(input_stream) do
    Stream.chunk_while(input_stream, "", &chunker/2, &after_chunking/1) |> Stream.concat()
  end

  @spec after_sb(String.t()) :: nil | String.t()
  defp after_sb(text) do
    chunks = text |> String.split(@sb, parts: 2)

    case chunks do
      [_chunk] -> nil
      [_sb, msg] -> msg
    end
  end

  @spec to_list_and_remnant(list()) :: {:cont, list(), String.t()}
  defp to_list_and_remnant(potential_messages) do
    [remnant | reverse_msgs] = potential_messages |> Enum.reverse()

    msgs =
      reverse_msgs
      |> Enum.map(&after_sb(&1))
      |> Enum.filter(fn m -> is_binary(m) and m != "" end)
      |> Enum.reverse()

    {:cont, msgs, remnant}
  end

  @spec chunker(String.t(), String.t()) :: {:cont, String.t()} | {:cont, list(), String.t()}
  defp chunker(element, acc) when is_binary(element) do
    # {:cont, chunk, acc} | {:cont, acc} | {:halt, acc})

    text = acc <> element
    potential_msg_list = String.split(text, @ending)

    case potential_msg_list do
      [not_found] -> {:cont, not_found}
      _ -> potential_msg_list |> to_list_and_remnant
    end
  end

  @spec after_chunking(any()) :: {:cont, []}
  defp after_chunking(_acc) do
    {:cont, []}
  end
end
