defmodule Bitcoinex.Transaction do
  @moduledoc """
  Bitcoin on-chain transaction structure.
  Supports serialization of transactions.
  """
  alias Bitcoinex.Transaction
  alias Bitcoinex.Transaction.In
  alias Bitcoinex.Transaction.Out
  alias Bitcoinex.Transaction.Witness
  alias Bitcoinex.Utils
  alias Bitcoinex.Transaction.Utils, as: TxUtils

  @type t() :: %__MODULE__{
          version: non_neg_integer(),
          inputs: list(In.t()),
          outputs: list(Out.t()),
          witnesses: list(Witness.t()),
          lock_time: non_neg_integer()
        }

  defstruct [
    :version,
    :inputs,
    :outputs,
    :witnesses,
    :lock_time
  ]

  @doc """
    Returns the TxID of the given tranasction.

    TxID is sha256(sha256(nVersion | txins | txouts | nLockTime))
  """
  def transaction_id(txn) do
    legacy_txn = TxUtils.serialize(%{txn | witnesses: []})

    Base.encode16(
      <<:binary.decode_unsigned(
          Utils.double_sha256(legacy_txn),
          :big
        )::little-size(256)>>,
      case: :lower
    )
  end

  @doc """
    Decodes a transaction in a hex encoded string into binary.
  """
  def decode(tx_hex) when is_binary(tx_hex) do
    case Base.decode16(tx_hex, case: :lower) do
      {:ok, tx_bytes} ->
        case parse(tx_bytes) do
          {:ok, txn} ->
            {:ok, txn}

          :error ->
            {:error, :parse_error}
        end

      :error ->
        {:error, :decode_error}
    end
  end

  # returns transaction
  defp parse(<<version::little-size(32), remaining::binary>>) do
    {is_segwit, remaining} =
      case remaining do
        <<1::size(16), segwit_remaining::binary>> ->
          {:segwit, segwit_remaining}

        _ ->
          {:not_segwit, remaining}
      end

    # Inputs.
    {in_counter, remaining} = TxUtils.get_counter(remaining)
    {inputs, remaining} = In.parse_inputs(in_counter, remaining)

    # Outputs.
    {out_counter, remaining} = TxUtils.get_counter(remaining)
    {outputs, remaining} = Out.parse_outputs(out_counter, remaining)

    # If flag 0001 is present, this indicates an attached segregated witness structure.
    {witnesses, remaining} =
      if is_segwit == :segwit do
        Witness.parse_witness(in_counter, remaining)
      else
        {nil, remaining}
      end

    <<lock_time::little-size(32), remaining::binary>> = remaining

    if byte_size(remaining) != 0 do
      :error
    else
      {:ok,
       %Transaction{
         version: version,
         inputs: inputs,
         outputs: outputs,
         witnesses: witnesses,
         lock_time: lock_time
       }}
    end
  end
end

defmodule Bitcoinex.Transaction.Utils do
  @moduledoc """
  Utilities for when dealing with transaction objects.
  """
  alias Bitcoinex.Transaction
  alias Bitcoinex.Transaction.In
  alias Bitcoinex.Transaction.Out
  alias Bitcoinex.Transaction.Witness

  @doc """
    Returns the Variable Length Integer used in serialization.

    Reference: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
  """
  @spec get_counter(binary) :: {non_neg_integer(), binary()}
  def get_counter(<<counter::little-size(8), vec::binary>>) do
    case counter do
      # 0xFD followed by the length as uint16_t
      0xFD ->
        <<len::little-size(16), vec::binary>> = vec
        {len, vec}

      # 0xFE followed by the length as uint32_t
      0xFE ->
        <<len::little-size(32), vec::binary>> = vec
        {len, vec}

      # 0xFF followed by the length as uint64_t
      0xFF ->
        <<len::little-size(64), vec::binary>> = vec
        {len, vec}

      _ ->
        {counter, vec}
    end
  end

  @spec serialize(Transaction.t()) :: binary()
  def serialize(%Transaction{witnesses: witness} = txn)
      when is_list(witness) and length(witness) > 0 do
    version = <<txn.version::little-size(32)>>
    marker = <<0x00::big-size(8)>>
    flag = <<0x01::big-size(8)>>
    tx_in_count = serialize_compact_size_unsigned_int(length(txn.inputs))
    inputs = In.serialize_inputs(txn.inputs) |> :erlang.list_to_binary()
    tx_out_count = serialize_compact_size_unsigned_int(length(txn.outputs))
    outputs = Out.serialize_outputs(txn.outputs) |> :erlang.list_to_binary()
    witness = Witness.serialize_witness(txn.witnesses)
    lock_time = <<txn.lock_time::little-size(32)>>

    version <>
      marker <> flag <> tx_in_count <> inputs <> tx_out_count <> outputs <> witness <> lock_time
  end

  def serialize(txn) do
    version = <<txn.version::little-size(32)>>
    tx_in_count = serialize_compact_size_unsigned_int(length(txn.inputs))
    inputs = In.serialize_inputs(txn.inputs) |> :erlang.list_to_binary()
    tx_out_count = serialize_compact_size_unsigned_int(length(txn.outputs))
    outputs = Out.serialize_outputs(txn.outputs) |> :erlang.list_to_binary()
    lock_time = <<txn.lock_time::little-size(32)>>

    version <> tx_in_count <> inputs <> tx_out_count <> outputs <> lock_time
  end

  @doc """
    Returns the serialized variable length integer.
  """
  def serialize_compact_size_unsigned_int(compact_size) do
    cond do
      compact_size >= 0 and compact_size <= 0xFC ->
        <<compact_size::little-size(8)>>

      compact_size <= 0xFFFF ->
        <<0xFD>> <> <<compact_size::little-size(16)>>

      compact_size <= 0xFFFFFFFF ->
        <<0xFE>> <> <<compact_size::little-size(32)>>

      compact_size <= 0xFF ->
        <<0xFF>> <> <<compact_size::little-size(64)>>
    end
  end
end

defmodule Bitcoinex.Transaction.Witness do
  @moduledoc """
  Witness structure part of an on-chain transaction.
  """
  alias Bitcoinex.Transaction.Witness
  alias Bitcoinex.Transaction.Utils, as: TxUtils

  @type t :: %__MODULE__{
          txinwitness: list(binary())
        }
  defstruct [
    :txinwitness
  ]

  @doc """
    Wtiness accepts a binary and deserializes it.
  """
  @spec witness(binary) :: t()
  def witness(witness_bytes) do
    {stack_size, witness_bytes} = TxUtils.get_counter(witness_bytes)

    {witness, _} =
      if stack_size == 0 do
        {%Witness{txinwitness: []}, witness_bytes}
      else
        {stack_items, witness_bytes} = parse_stack(witness_bytes, [], stack_size)
        {%Witness{txinwitness: stack_items}, witness_bytes}
      end

    witness
  end

  @spec serialize_witness(list(Witness.t())) :: binary
  def serialize_witness(witnesses) do
    serialize_witness(witnesses, <<>>)
  end

  defp serialize_witness([], serialized_witnesses), do: serialized_witnesses

  defp serialize_witness(witnesses, serialized_witnesses) do
    [witness | witnesses] = witnesses

    serialized_witness =
      if Enum.empty?(witness.txinwitness) do
        <<0x0::big-size(8)>>
      else
        stack_len = TxUtils.serialize_compact_size_unsigned_int(length(witness.txinwitness))

        field =
          Enum.reduce(witness.txinwitness, <<>>, fn v, acc ->
            {:ok, item} = Base.decode16(v, case: :lower)
            item_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(item))
            acc <> item_len <> item
          end)

        stack_len <> field
      end

    serialize_witness(witnesses, serialized_witnesses <> serialized_witness)
  end

  def parse_witness(0, remaining), do: {nil, remaining}

  def parse_witness(counter, witnesses) do
    parse(witnesses, [], counter)
  end

  defp parse(remaining, witnesses, 0), do: {Enum.reverse(witnesses), remaining}

  defp parse(remaining, witnesses, count) do
    {stack_size, remaining} = TxUtils.get_counter(remaining)

    {witness, remaining} =
      if stack_size == 0 do
        {%Witness{txinwitness: 0}, remaining}
      else
        {stack_items, remaining} = parse_stack(remaining, [], stack_size)
        {%Witness{txinwitness: stack_items}, remaining}
      end

    parse(remaining, [witness | witnesses], count - 1)
  end

  defp parse_stack(remaining, stack_items, 0), do: {Enum.reverse(stack_items), remaining}

  defp parse_stack(remaining, stack_items, stack_size) do
    {item_size, remaining} = TxUtils.get_counter(remaining)

    <<stack_item::binary-size(item_size), remaining::binary>> = remaining

    parse_stack(
      remaining,
      [Base.encode16(stack_item, case: :lower) | stack_items],
      stack_size - 1
    )
  end
end

defmodule Bitcoinex.Transaction.In do
  @moduledoc """
  Transaction Input part of an on-chain transaction.
  """
  alias Bitcoinex.Transaction.In
  alias Bitcoinex.Transaction.Utils, as: TxUtils

  @type t :: %__MODULE__{
          prev_txid: binary(),
          prev_vout: non_neg_integer(),
          script_sig: binary(),
          sequence_no: non_neg_integer()
        }

  defstruct [
    :prev_txid,
    :prev_vout,
    :script_sig,
    :sequence_no
  ]

  @spec serialize_inputs(list(In.t())) :: iolist()
  def serialize_inputs(inputs) do
    serialize_input(inputs, [])
  end

  defp serialize_input([], serialized_inputs), do: serialized_inputs

  defp serialize_input(inputs, serialized_inputs) do
    [input | inputs] = inputs

    {:ok, prev_txid} = Base.decode16(input.prev_txid, case: :lower)

    prev_txid =
      prev_txid
      |> :binary.decode_unsigned(:big)
      |> :binary.encode_unsigned(:little)
      |> Bitcoinex.Utils.pad(32, :trailing)

    {:ok, script_sig} = Base.decode16(input.script_sig, case: :lower)

    script_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(script_sig))

    serialized_input = [
      prev_txid,
      <<input.prev_vout::little-size(32)>>,
      script_len,
      script_sig,
      <<input.sequence_no::little-size(32)>>
    ]

    serialize_input(inputs, [serialized_inputs, serialized_input])
  end

  def parse_inputs(counter, inputs) do
    parse(inputs, [], counter)
  end

  defp parse(remaining, inputs, 0), do: {Enum.reverse(inputs), remaining}

  defp parse(
         <<prev_txid::binary-size(32), prev_vout::little-size(32), remaining::binary>>,
         inputs,
         count
       ) do
    {script_len, remaining} = TxUtils.get_counter(remaining)

    <<script_sig::binary-size(script_len), sequence_no::little-size(32), remaining::binary>> =
      remaining

    input = %In{
      prev_txid:
        Base.encode16(<<:binary.decode_unsigned(prev_txid, :big)::little-size(256)>>,
          case: :lower
        ),
      prev_vout: prev_vout,
      script_sig: Base.encode16(script_sig, case: :lower),
      sequence_no: sequence_no
    }

    parse(remaining, [input | inputs], count - 1)
  end
end

defmodule Bitcoinex.Transaction.Out do
  @moduledoc """
  Transaction Output part of an on-chain transaction.
  """
  alias Bitcoinex.Transaction.Out
  alias Bitcoinex.Transaction.Utils, as: TxUtils

  @type t :: %__MODULE__{
          value: non_neg_integer(),
          script_pub_key: binary()
        }

  defstruct [
    :value,
    :script_pub_key
  ]

  @spec serialize_outputs(list(Out.t())) :: iolist()
  def serialize_outputs(outputs) do
    serialize_output(outputs, [])
  end

  defp serialize_output([], serialized_outputs), do: serialized_outputs

  defp serialize_output(outputs, serialized_outputs) do
    [output | outputs] = outputs

    {:ok, script_pub_key} = Base.decode16(output.script_pub_key, case: :lower)

    script_len = TxUtils.serialize_compact_size_unsigned_int(byte_size(script_pub_key))

    serialized_output = [<<output.value::little-size(64)>>, script_len, script_pub_key]
    serialize_output(outputs, [serialized_outputs, serialized_output])
  end

  def output(out_bytes) do
    <<value::little-size(64), out_bytes::binary>> = out_bytes
    {script_len, out_bytes} = TxUtils.get_counter(out_bytes)
    <<script_pub_key::binary-size(script_len), _::binary>> = out_bytes
    %Out{value: value, script_pub_key: Base.encode16(script_pub_key, case: :lower)}
  end

  def parse_outputs(counter, outputs) do
    parse(outputs, [], counter)
  end

  defp parse(remaining, outputs, 0), do: {Enum.reverse(outputs), remaining}

  defp parse(<<value::little-size(64), remaining::binary>>, outputs, count) do
    {script_len, remaining} = TxUtils.get_counter(remaining)

    <<script_pub_key::binary-size(script_len), remaining::binary>> = remaining

    output = %Out{
      value: value,
      script_pub_key: Base.encode16(script_pub_key, case: :lower)
    }

    parse(remaining, [output | outputs], count - 1)
  end
end
