defmodule EQRCode.Encode do
  @moduledoc """
  Data encoding in Byte Mode.
  """

  alias EQRCode.SpecTable
  import Bitwise

  @error_correction_level SpecTable.error_correction_level()

  @pad <<236, 17>>
  @mask0 <<0x99999999999999666666666666669966666666659999999996699533333333332CCD332CCCCCCCCCCCCCCD333333333333332CCD332CCCCCCCCCCCCCCD333333333333332CCD332CCCCCCCCCCCCCCD333333333333332CCD332CCCCCCCCCCCCCCD333333333333332CCD332CCCCCCCCCCCCCCD33333333333333333332CCCCCCCCCD33333333::1072>>

  @doc """
  Encode the binary.

  ## Examples

      iex> EQRCode.Encode.encode("hello world!", :l)
      {1, :l, [0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1,
       0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1,
       0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1,
       1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0,
       0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1,
       1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0]}

  """
  @spec encode(binary, SpecTable.error_correction_level()) ::
          {SpecTable.version(), SpecTable.error_correction_level(), [0 | 1]}
  def encode(bin, error_correction_level)
      when error_correction_level in @error_correction_level do
    {:ok, version} = version(bin, error_correction_level)
    cci_len = SpecTable.character_count_indicator_bits(version, error_correction_level)
    mode = SpecTable.mode_indicator()

    encoded =
      [<<mode::4>>, <<byte_size(bin)::size(cci_len)>>, bin, <<0::4>>]
      |> Enum.flat_map(&bits/1)
      |> pad_bytes(version, error_correction_level)

    {version, error_correction_level, encoded}
  end

  # Encode the binary with custom pattern bits.
  @spec encode(binary, SpecTable.error_correction_level(), bitstring) ::
          {SpecTable.version(), SpecTable.error_correction_level(), [0 | 1]}
  def encode(bin, error_correction_level, bits)
      when error_correction_level in @error_correction_level do
    version = 5
    n = byte_size(bin)
    n1 = n + 2
    n2 = SpecTable.code_words_len(version, error_correction_level) - n1
    cci_len = SpecTable.character_count_indicator_bits(version, error_correction_level)
    mode = SpecTable.mode_indicator()
    <<_::binary-size(n1), mask::binary-size(n2), _::binary>> = @mask0

    encoded =
      <<mode::4, n::size(cci_len), bin::binary-size(n), 0::4, xor(bits, mask)::bits>>
      |> bits()
      |> pad_bytes(version, error_correction_level)

    {version, error_correction_level, encoded}
  end

  defp xor(<<>>, _), do: <<>>
  defp xor(_, <<>>), do: <<>>

  defp xor(<<a::1, t1::bits>>, <<b::1, t2::bits>>) do
    <<bxor(a, b)::1, xor(t1, t2)::bits>>
  end

  @doc """
  Returns the lowest version for the given binary.

  ## Examples

      iex> EQRCode.Encode.version("hello world!", :l)
      {:ok, 1}

  """
  @spec version(binary, SpecTable.error_correction_level()) ::
          {:error, :no_version_found} | {:ok, SpecTable.version()}
  def version(bin, error_correction_level) do
    byte_size(bin)
    |> SpecTable.find_version(error_correction_level)
  end

  @doc """
  Returns bits for any binary data.

  ## Examples

      iex> EQRCode.Encode.bits(<<123, 4>>)
      [0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0]

  """
  @spec bits(bitstring) :: [0 | 1]
  def bits(bin) do
    for <<b::1 <- bin>>, do: b
  end

  defp pad_bytes(list, version, error_correction_level) do
    n = SpecTable.code_words_len(version, error_correction_level) * 8 - length(list)

    Enum.concat(
      list,
      Stream.cycle(bits(@pad))
      |> Stream.take(n)
    )
  end
end
