defmodule Bitcoinex.Utils do
  @moduledoc """
  Contains useful utility functions used in Bitcoinex.
  """

  @spec sha256(iodata()) :: binary
  def sha256(str) do
    :crypto.hash(:sha256, str)
  end

  def tagged_hash(tag, str) do
    tag_hash = sha256(tag)
    sha256(tag_hash <> tag_hash <> str)
  end

  @spec replicate(term(), integer()) :: list(term())
  def replicate(_num, 0) do
    []
  end

  def replicate(x, num) when x > 0 do
    for _ <- 1..num, do: x
  end

  @spec double_sha256(iodata()) :: binary
  def double_sha256(preimage) do
    :crypto.hash(
      :sha256,
      :crypto.hash(:sha256, preimage)
    )
  end

  @spec hash160(iodata()) :: binary
  def hash160(preimage) do
    :crypto.hash(
      :ripemd160,
      :crypto.hash(:sha256, preimage)
    )
  end

  @typedoc """
    The pad_type describes the padding to use.
  """
  @type pad_type :: :leading | :trailing

  @doc """
  pads binary according to the byte length and the padding type. A binary can be padded with leading or trailing zeros.
  """
  @spec pad(bin :: binary, byte_len :: integer, pad_type :: pad_type) :: binary
  def pad(bin, byte_len, _pad_type) when is_binary(bin) and byte_size(bin) == byte_len do
    bin
  end

  def pad(bin, byte_len, pad_type) when is_binary(bin) and pad_type == :leading do
    pad_len = 8 * byte_len - byte_size(bin) * 8
    <<0::size(pad_len)>> <> bin
  end

  def pad(bin, byte_len, pad_type) when is_binary(bin) and pad_type == :trailing do
    pad_len = 8 * byte_len - byte_size(bin) * 8
    bin <> <<0::size(pad_len)>>
  end

  @spec int_to_big(non_neg_integer(), non_neg_integer()) :: binary
  def int_to_big(i, p) do
    i
    |> :binary.encode_unsigned()
    |> pad(p, :leading)
  end

  def int_to_little(i, p) do
    i
    |> :binary.encode_unsigned(:little)
    |> pad(p, :trailing)
  end

  def little_to_int(i), do: :binary.decode_unsigned(i, :little)

  def encode_int(i) when i > 0 do
    cond do
      i < 0xFD -> :binary.encode_unsigned(i)
      i <= 0xFFFF -> <<0xFD>> <> int_to_little(i, 2)
      i <= 0xFFFFFFFF -> <<0xFE>> <> int_to_little(i, 4)
      i <= 0xFFFFFFFFFFFFFFFF -> <<0xFF>> <> int_to_little(i, 8)
      true -> {:error, "invalid integer size"}
    end
  end

  def hex_to_bin(str) do
    str
    |> String.downcase()
    |> Base.decode16(case: :lower)
    |> case do
      # In case of error, its already binary or its invalid
      :error -> {:error, "invalid string"}
      # valid binary
      {:ok, bin} -> bin
    end
  end

  # todo: better to just convert to ints and XOR them?
  @spec xor_bytes(binary, binary) :: binary
  def xor_bytes(bin0, bin1) do
    {bl0, bl1} = {:binary.bin_to_list(bin0), :binary.bin_to_list(bin1)}

    Enum.zip(bl0, bl1)
    |> Enum.map(fn {b0, b1} -> Bitwise.bxor(b0, b1) end)
    |> :binary.list_to_bin()
  end
end
