defmodule TypeCheck.Builtin.SizedBitstring do
  defstruct [:prefix_size, :unit_size]

  use TypeCheck

  @type! t :: %__MODULE__{prefix_size: non_neg_integer(), unit_size: nil | 1..256}
  @type! problem_tuple ::
           {t(), :no_match, %{}, any()}
           | {t(), :wrong_size, %{}, any()}

  defimpl TypeCheck.Protocols.ToCheck do
    def to_check(s, param) do
      if s.unit_size == nil do
        quote generated: true, location: :keep do
          case unquote(param) do
            x when not is_bitstring(x) ->
              {:error, {unquote(Macro.escape(s)), :no_match, %{}, unquote(param)}}

            x when bit_size(x) != unquote(s.prefix_size) ->
              {:error, {unquote(Macro.escape(s)), :wrong_size, %{}, unquote(param)}}

            _ ->
              {:ok, [], unquote(param)}
          end
        end
      else
        quote generated: true, location: :keep do
          case unquote(param) do
            x when not is_bitstring(x) ->
              {:error, {unquote(Macro.escape(s)), :no_match, %{}, x}}

            x
            when bit_size(x) < unquote(s.prefix_size) or
                   rem(bit_size(x) - unquote(s.prefix_size), unquote(s.unit_size)) != 0 ->
              {:error, {unquote(Macro.escape(s)), :wrong_size, %{}, x}}

            correct_value ->
              {:ok, [], correct_value}
          end
        end
      end
    end
  end

  defimpl TypeCheck.Protocols.Inspect do
    def inspect(s, opts) do
      prefix_size = s.prefix_size |> to_string() |> Inspect.Algebra.color(:number, opts)
      unit_size = s.unit_size |> to_string() |> Inspect.Algebra.color(:number, opts)

      cond do
        s.unit_size == nil ->
          "<<_::"
          |> Inspect.Algebra.concat(prefix_size)
          |> Inspect.Algebra.concat(Inspect.Algebra.color(">>", :binary, opts))

        s.prefix_size == 0 ->
          "<<_::_*"
          |> Inspect.Algebra.concat(unit_size)
          |> Inspect.Algebra.concat(Inspect.Algebra.color(">>", :binary, opts))

        true ->
          "<<_::"
          |> Inspect.Algebra.concat(prefix_size)
          |> Inspect.Algebra.concat(Inspect.Algebra.color(", _::_*", :binary, opts))
          |> Inspect.Algebra.concat(unit_size)
          |> Inspect.Algebra.concat(Inspect.Algebra.color(">>", :binary, opts))
      end
      |> Inspect.Algebra.color(:binary, opts)
    end
  end

  if Code.ensure_loaded?(StreamData) do
    defimpl TypeCheck.Protocols.ToStreamData do
      def to_gen(s) do
        if s.unit_size == nil do
          StreamData.bitstring(length: s.prefix_size)
        else
          StreamData.positive_integer()
          |> StreamData.bind(fn int ->
            StreamData.bitstring(length: s.prefix_size + int * s.unit_size)
          end)
        end
      end
    end
  end
end
