defmodule Bitcoinex.Script do
  @moduledoc """
  	a module for manipulating Bitcoin Scripts
  """

  import Bitcoinex.Opcode

  alias Bitcoinex.Secp256k1.Point

  alias Bitcoinex.{Utils, Address, Segwit, Base58, Network}

  @wsh_length 32
  @tapkey_length 32
  @h160_length 20
  @pubkey_lengths [33, 65]

  @type script_type :: :p2pk | :p2pkh | :p2sh | :p2wpkh | :p2wsh | :p2tr | :multi | :non_standard

  @type t :: %__MODULE__{
          items: list
        }

  @enforce_keys [
    :items
  ]
  defstruct [:items]

  defguard is_valid_multi(m, pubkeys)
           when is_integer(m) and m > 0 and length(pubkeys) > 0 and length(pubkeys) >= m

  defp invalid_opcode_error(msg), do: {:error, "invalid opcode: #{msg}"}

  def is_valid_opcode(i) when is_integer(i), do: i >= 0x00 && i < 0xFF

  @doc """
  	new returns an empty script object.
  """
  @spec new() :: t()
  def new, do: %__MODULE__{items: []}

  @doc """
  	to_list returns the script as a list of items
  """
  @spec to_list(t()) :: list
  def to_list(%__MODULE__{items: script}), do: script

  @doc """
  	empty? returns true for empty scripts, false otherwise.
  """
  @spec empty?(t()) :: bool
  def empty?(%__MODULE__{items: []}), do: true
  def empty?(_), do: false

  @doc """
  	script_length returns the number of items in the script.
  """
  @spec script_length(t()) :: non_neg_integer()
  def script_length(%__MODULE__{items: items}), do: length(items)

  @doc """
  	byte_length returns the byte length of the serialized script.
  """
  @spec byte_length(t()) :: non_neg_integer()
  def byte_length(script) do
    script
    |> serialize_script()
    |> byte_size()
  end

  @doc """
    hash160 is a helper function which returns the hash160
    digest of the serialized script, as used in P2SH scripts.
  """
  @spec hash160(t()) :: binary
  def hash160(script = %__MODULE__{}) do
    script
    |> serialize_script()
    |> Utils.hash160()
  end

  @doc """
    hash256 is a helper function which returns the hash256
    digest of the serialized script, as used in P2WSH scripts.
  """
  @spec sha256(t()) :: binary
  def sha256(script = %__MODULE__{}) do
    script
    |> serialize_script()
    |> Utils.sha256()
  end

  @doc """
  	get_op_num returns the integer associated with the passed opcode atom.
  """
  @spec get_op_num(atom) :: {:ok, non_neg_integer()} | :error
  def get_op_num(op), do: Map.fetch(opcode_atoms(), op)

  @doc """
  	get_op_atom returns the atom associated with the passed opcode integer.
  """
  @spec get_op_atom(non_neg_integer()) :: non_neg_integer() | {:ok, atom}
  def get_op_atom(i), do: if(i > 0 and i < 0x4C, do: i, else: Map.fetch(opcode_nums(), i))

  @doc """
  	pop returns the first element of the script and the remaining script.
  	Returns nil if script is empty
  """
  @spec pop(t()) :: nil | {:ok, non_neg_integer() | binary, t()}
  def pop(%__MODULE__{items: []}), do: nil
  def pop(%__MODULE__{items: [item | stack]}), do: {:ok, item, %__MODULE__{items: stack}}

  @doc """
  	push_op pushes a single opcode to the script as an integer and returns the script.
  """
  @spec push_op(t(), atom | non_neg_integer()) :: {:ok, t()} | {:error, String.t()}
  def push_op(%__MODULE__{items: stack}, item) do
    # item is opcode num
    if is_integer(item) and item >= 0 and item < 0xFF do
      {:ok, %__MODULE__{items: [item | stack]}}
    else
      # item is atom
      case get_op_num(item) do
        :error -> invalid_opcode_error(item)
        {:ok, op} -> {:ok, %__MODULE__{items: [op | stack]}}
      end
    end
  end

  # used to push data lengths and raw binary
  defp push_raw_data(%__MODULE__{items: stack}, data) do
    %__MODULE__{items: [data | stack]}
  end

  @doc """
  	push_data returns a script with the binary data and any
  	accompanying pushdata or pushbytes opcodes added to the front of the script.
  """
  @spec push_data(t(), binary) :: {:ok, t()} | {:error, String.t()}
  def push_data(script = %__MODULE__{}, data) do
    datalen = byte_size(data)
    script = push_raw_data(script, data)

    cond do
      datalen < 0x4C ->
        push_op(script, datalen)

      datalen <= 0xFF ->
        push_op(script, :op_pushdata1)

      datalen <= 0xFFFF ->
        push_op(script, :op_pushdata2)

      datalen <= 0xFFFFFFFF ->
        push_op(script, :op_pushdata4)

      true ->
        {:error, "invalid data length, must be 0..0xffffffff, got #{datalen}"}
    end
  end

  # SERIALIZE & PARSE
  defp serializer(%__MODULE__{items: []}, acc), do: acc

  defp serializer(%__MODULE__{items: [item | script]}, acc) when is_integer(item) do
    # prevents UTF-8 ints from becoming strings
    serializer(%__MODULE__{items: script}, acc <> Utils.int_to_little(item, 1))
  end

  # For data pushes
  defp serializer(%__MODULE__{items: [item | script]}, acc) when is_binary(item) do
    len = byte_size(item)

    cond do
      # CHECK IF PUSHBYTES75 is valid
      len < 0x4C ->
        serializer(%__MODULE__{items: script}, acc <> item)

      len <= 0xFF ->
        len = len |> Utils.int_to_little(1)
        serializer(%__MODULE__{items: script}, acc <> len <> item)

      # PUSHDATA limited to 520 bytes, so no PUSHDATA2 > 520 is a valid script.
      # Should we allow this?
      len <= 0xFFFF ->
        len = Utils.int_to_little(len, 2)
        serializer(%__MODULE__{items: script}, acc <> len <> item)

      # no PUSHDATA4 is a valid script.
      # Should we allow this?
      len <= 0xFFFFFFFF ->
        len = Utils.int_to_little(len, 4)
        serializer(%__MODULE__{items: script}, acc <> len <> item)
    end
  end

  @doc """
  	serialize_script serializes the script into binary
  	according to Bitcoin's standard.
  """
  @spec serialize_script(t()) :: binary
  def serialize_script(script = %__MODULE__{}) do
    # serialize_script(%Script{items: [0x51]}) will still display "Q" but
    # it functions as binary 0x51. Use to_hex for displaying scripts.
    serializer(script, <<>>)
  end

  @doc """
  	to_hex returns the hex of a serialized script.
  """
  @spec to_hex(t()) :: String.t()
  def to_hex(script) do
    script
    |> serialize_script()
    |> Base.encode16(case: :lower)
  end

  @doc """
  	parse_script parses a binary or hex string into a script.
  """
  @spec parse_script(binary) :: {:ok, t()} | {:error, String.t()}
  def parse_script(script_str) when is_binary(script_str) do
    try do
      case Utils.hex_to_bin(script_str) do
        {:error, _msg} ->
          # necessary to allow parse_script to accept raw binary script
          parser(new(), script_str)

        bin ->
          parser(new(), bin)
      end
    rescue
      _ -> {:error, "invalid script. parse_script accepts hex or binary."}
    end
  end

  defp parser(script, <<>>), do: {:ok, script}

  defp parser(script, <<next::binary-size(1), bin::binary>>) do
    op = :binary.decode_unsigned(next)

    cond do
      # PUSHBYTES
      op > 0x00 and op < 0x4C ->
        {:ok, rest} = parser(script, :binary.part(bin, op, byte_size(bin) - op))

        rest
        |> push_raw_data(:binary.part(bin, 0, op))
        |> push_op(op)

      # PUSHDATA1
      op == 0x4C ->
        len = bin |> :binary.part(0, 1) |> Utils.little_to_int()
        {:ok, rest} = parser(script, :binary.part(bin, len + 1, byte_size(bin) - len - 1))

        rest
        |> push_raw_data(:binary.part(bin, 1, len))
        |> push_op(op)

      # PUSHDATA2
      op == 0x4D ->
        len = bin |> :binary.part(0, 2) |> Utils.little_to_int()
        {:ok, rest} = parser(script, :binary.part(bin, len + 2, byte_size(bin) - len - 2))

        rest
        |> push_raw_data(:binary.part(bin, 2, len))
        |> push_op(op)

      # PUSHDATA4
      op == 0x4E ->
        len = bin |> :binary.part(0, 4) |> Utils.little_to_int()
        {:ok, rest} = parser(script, :binary.part(bin, len + 4, byte_size(bin) - len - 4))

        rest
        |> push_raw_data(:binary.part(bin, 4, len))
        |> push_op(op)

      # OPCODE
      is_valid_opcode(op) ->
        {:ok, rest} = parser(script, :binary.part(bin, 0, byte_size(bin)))
        push_op(rest, op)

      true ->
        invalid_opcode_error(op)
    end
  end

  @doc """
  	raw_combine directly concatenates two scripts with no checks.
  """
  @spec raw_combine(t(), t()) :: t()
  def raw_combine(%__MODULE__{items: s1}, %__MODULE__{items: s2}),
    do: %__MODULE__{items: s1 ++ s2}

  @doc """
  	display_script returns a human readable string of the script, with
  	op_codes shown by name rather than number.
  """
  @spec display_script(t()) :: String.t()
  def display_script(script) do
    " " <> scriptxt = display_script(script, "")
    scriptxt
  end

  defp display_script(%__MODULE__{items: []}, acc), do: acc

  defp display_script(%__MODULE__{items: [item | stack]}, acc) when is_integer(item) do
    if item > 0 and item < 0x4C do
      display_script(%__MODULE__{items: stack}, acc <> " OP_PUSHBYTES_#{item}")
    else
      {:ok, op_atom} = get_op_atom(item)
      upper_op = op_atom |> to_string() |> String.upcase()
      display_script(%__MODULE__{items: stack}, acc <> " " <> upper_op)
    end
  end

  defp display_script(%__MODULE__{items: [item | stack]}, acc) when is_binary(item) do
    display_script(%__MODULE__{items: stack}, acc <> " " <> Base.encode16(item, case: :lower))
  end

  # SCRIPT TYPE DETERMINERS

  @doc """
  	is_p2pk? returns whether a given script is of the p2pk format:
  	<33-byte or 65-byte pubkey> OP_CHECKSIG
  """
  @spec is_p2pk?(t()) :: boolean
  def is_p2pk?(%__MODULE__{
        items: [len, pubkey, 0xAC]
      })
      when len in @pubkey_lengths and len == byte_size(pubkey) do
    true
  end

  def is_p2pk?(%__MODULE__{}), do: false

  @doc """
  	is_p2pkh? returns whether a given script is of the p2pkh format:
  	OP_DUP OP_HASH160 OP_PUSHBYTES_20 <20-byte hash> OP_EQUALVERIFY OP_CHECKSIG
  """
  @spec is_p2pkh?(t()) :: boolean
  def is_p2pkh?(%__MODULE__{
        items: [0x76, 0xA9, @h160_length, <<_::binary-size(@h160_length)>>, 0x88, 0xAC]
      }),
      do: true

  def is_p2pkh?(%__MODULE__{}), do: false

  @doc """
  	is_p2sh? returns whether a given script is of the p2sh format:
  	OP_HASH160 OP_PUSHBYTES_20 <20-byte hash> OP_EQUAL
  """
  @spec is_p2sh?(t()) :: boolean
  def is_p2sh?(%__MODULE__{items: [0xA9, @h160_length, <<_::binary-size(@h160_length)>>, 0x87]}),
    do: true

  def is_p2sh?(%__MODULE__{}), do: false

  @doc """
  	is_p2wpkh? returns whether a given script is of the p2wpkh format:
  	OP_0 OP_PUSHBYTES_20 <20-byte hash>
  """
  @spec is_p2wpkh?(t()) :: boolean
  def is_p2wpkh?(%__MODULE__{items: [0x00, @h160_length, <<_::binary-size(@h160_length)>>]}),
    do: true

  def is_p2wpkh?(%__MODULE__{}), do: false

  @doc """
  	is_p2wsh? returns whether a given script is of the p2wsh format:
  	OP_0 OP_PUSHBYTES_32 <32-byte hash>
  """
  @spec is_p2wsh?(t()) :: boolean
  def is_p2wsh?(%__MODULE__{items: [0x00, @wsh_length, <<_::binary-size(@wsh_length)>>]}),
    do: true

  def is_p2wsh?(%__MODULE__{}), do: false

  @doc """
  	is_p2tr? returns whether a given script is of the p2tr format:
  	OP_1 OP_PUSHBYTES_32 <32-byte hash>
  """
  @spec is_p2tr?(t()) :: boolean
  def is_p2tr?(%__MODULE__{items: [0x51, @tapkey_length, <<_::binary-size(@tapkey_length)>>]}),
    do: true

  def is_p2tr?(%__MODULE__{}), do: false

  @doc """
  	is_multi? returns whether a given script is of the raw multisig format:
  	OP_(INT) [Public Keys] OP_(INT) OP_CHECKMULTISIG
  """
  @spec is_multi?(t()) :: boolean
  def is_multi?(%__MODULE__{items: [op_m | rest]})
      when op_m > 0x50 and op_m <= 0x60 and length(rest) > 3 do
    test_multi(rest, 0, op_m)
  end

  def is_multi?(_), do: false

  defp test_multi([op_n, 0xAE], n, m) when op_n == 0x50 + n and m <= op_n, do: true

  defp test_multi([op_push | [pk | rest]], n, m) when op_push in @pubkey_lengths do
    case Point.parse_public_key(pk) do
      {:ok, _pk} -> test_multi(rest, n + 1, m)
      {:error, _msg} -> false
    end
  end

  defp test_multi(_, _, _), do: false

  @doc """
    extract_multi_policy takes in a raw multisig script and returns the m, the
    number of signatures required and the n authorized public keys.
  """
  @spec extract_multi_policy(t()) ::
          {:ok, non_neg_integer(), list(Point.t())} | {:error, String.t()}
  def extract_multi_policy(script = %__MODULE__{items: [op_m | items]}) do
    if is_multi?(script) do
      {:ok, op_m - 0x50, extractor(items, [])}
    else
      {:error, "invalid raw multisig script"}
    end
  end

  defp extractor([_op_n, 0xAE], keys), do: keys

  defp extractor([_op_push | [key | items]], keys) do
    case Point.parse_public_key(key) do
      {:ok, pk} -> [pk | extractor(items, keys)]
      {:error, msg} -> {:error, "invalid public key: #{msg}"}
    end
  end

  @doc """
  	get_script_type determines the type of a script based on its elements
  	returns :non_standard if no type matches
  """
  @spec get_script_type(t()) :: script_type
  def get_script_type(script = %__MODULE__{}) do
    cond do
      # sorted by most prevalent
      is_p2pkh?(script) -> :p2pkh
      is_p2sh?(script) -> :p2sh
      is_p2wpkh?(script) -> :p2wpkh
      is_p2wsh?(script) -> :p2wsh
      is_p2pk?(script) -> :p2pk
      is_p2tr?(script) -> :p2tr
      is_multi?(script) -> :multi
      true -> :non_standard
    end
  end

  # CREATE COMMON SCRIPTS

  @doc """
  	create_p2pk creates a p2pk script using the passed public key
  """
  @spec create_p2pk(binary) :: {:ok, t()} | {:error, String.t()}
  def create_p2pk(pk) when is_binary(pk) and byte_size(pk) in [33, 65] do
    {:ok, s} = push_op(new(), 0xAC)
    push_data(s, pk)
  end

  def create_p2pk(_), do: {:error, "pubkey must be 33 or 65 bytes compressed or uncompressed SEC"}

  @doc """
  	create_p2pkh creates a p2pkh script using the passed 20-byte public key hash
  """
  @spec create_p2pkh(binary) :: {:ok, t()} | {:error, String.t()}
  def create_p2pkh(<<pkh::binary-size(@h160_length)>>) do
    {:ok, s} = push_op(new(), 0xAC)
    {:ok, s} = push_op(s, 0x88)
    {:ok, s} = push_data(s, pkh)
    {:ok, s} = push_op(s, 0xA9)
    push_op(s, 0x76)
  end

  def create_p2pkh(_), do: {:error, "pubkey hash must be a #{@h160_length}-byte hash"}

  @doc """
  	create_p2sh creates a p2sh script using the passed 20-byte public key hash
  """
  @spec create_p2sh(binary) :: {:ok, t()} | {:error, String.t()}
  def create_p2sh(<<sh::binary-size(@h160_length)>>) do
    {:ok, s} = push_op(new(), 0x87)
    {:ok, s} = push_data(s, sh)
    push_op(s, 0xA9)
  end

  def create_p2sh(_), do: {:error, "script hash must be a #{@h160_length}-byte hash"}

  @doc """
    to_p2sh wraps any script in a p2sh by first hashing it (hash160)
    and then wrapping then script hash in a p2sh script.
  """
  @spec to_p2sh(t()) :: {:ok, t()} | {:error, String.t()}
  def to_p2sh(script = %__MODULE__{}) do
    script
    |> hash160()
    |> create_p2sh()
  end

  @doc """
    create_multi creates a raw multisig script using m and the list of public keys.
  """
  @spec create_multi(non_neg_integer(), list(Point.t())) :: {:ok, t()} | {:error, String.t()}
  def create_multi(m, pubkeys) when is_valid_multi(m, pubkeys) do
    try do
      # checkmultisig
      {:ok, s} = push_op(new(), 0xAE)
      {:ok, s} = push_op(s, 0x50 + length(pubkeys))
      s = fill_multi_keys(s, pubkeys)
      push_op(s, 0x50 + m)
    rescue
      _ -> {:error, "invalid public key."}
    end
  end

  def create_multi(_, _), do: {:error, "invalid multisig: must be of form: (int, list(%Point)"}

  defp fill_multi_keys(s, []), do: s

  defp fill_multi_keys(s, [pk = %Point{} | pubkeys]) do
    {:ok, s} = push_data(fill_multi_keys(s, pubkeys), Point.sec(pk))
    s
  end

  defp fill_multi_keys(_, _), do: raise(ArgumentError)

  @doc """
    create_p2sh_multi returns both a P2SH-wrapped multisig script
    and the underlying raw multisig script using m and the list of public keys.
  """
  @spec create_p2sh_multi(non_neg_integer(), list(Point.t())) ::
          {:ok, t(), t()} | {:error, String.t()}
  def create_p2sh_multi(m, pubkeys) do
    case create_multi(m, pubkeys) do
      {:ok, multi} ->
        h160 = hash160(multi)
        {:ok, p2sh} = create_p2sh(h160)
        {:ok, p2sh, multi}

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

  @doc """
    create_p2wsh_multi returns both a P2WSH-wrapped multisig script
    and the underlying raw multisig script using m and the list of public keys.
  """
  @spec create_p2wsh_multi(non_neg_integer(), list(Point.t())) ::
          {:ok, t(), t()} | {:error, String.t()}
  def create_p2wsh_multi(m, pubkeys) do
    case create_multi(m, pubkeys) do
      {:ok, multi} ->
        h256 = sha256(multi)
        {:ok, p2wsh} = create_p2wsh(h256)
        {:ok, p2wsh, multi}

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

  @doc """
  	create_witness_scriptpubkey creates any witness script from a witness version
  	and witness program. It performs no validity checks.
  """
  @spec create_witness_scriptpubkey(non_neg_integer(), binary) :: {:ok, t()}
  def create_witness_scriptpubkey(version, witness_program) do
    wit_version_adjusted = if(version == 0, do: 0, else: version + 0x50)
    {:ok, s} = push_data(new(), witness_program)
    push_op(s, wit_version_adjusted)
  end

  @doc """
  	create_p2wpkh creates a p2wpkh script using the passed 20-byte public key hash
  """
  @spec create_p2wpkh(binary) :: {:ok, t()}
  def create_p2wpkh(<<pkh::binary-size(@h160_length)>>),
    do: create_witness_scriptpubkey(0, pkh)

  def create_p2wpkh(_), do: {:error, "pubkey hash must be a #{@h160_length}-byte hash"}

  @doc """
  	create_p2wsh creates a p2wsh script using the passed 32-byte script hash
  """
  @spec create_p2wsh(binary) :: {:ok, t()}
  def create_p2wsh(<<sh::binary-size(@wsh_length)>>), do: create_witness_scriptpubkey(0, sh)
  def create_p2wsh(_), do: {:error, "script hash must be a #{@wsh_length}-byte hash"}

  @doc """
    to_p2wsh converts any script into a p2wsh script by hashing it (SHA256)
    then wrapping the script hash as a p2wsh script.
  """
  @spec to_p2wsh(t()) :: {:ok, t()}
  def to_p2wsh(script = %__MODULE__{}) do
    script
    |> sha256()
    |> create_p2wsh()
  end

  @doc """
  	create_p2tr creates a p2tr script using the passed 32-byte public key
    or Point. If a point is passed, it's interpreted as q, the full witness
    program or taproot output key per BIP 341 rather than the keyspend pubkey.
  """
  @spec create_p2tr(binary | Point.t()) :: {:ok, t()}
  def create_p2tr(<<pk::binary-size(@tapkey_length)>>), do: create_witness_scriptpubkey(1, pk)
  def create_p2tr(q = %Point{}), do: create_witness_scriptpubkey(1, Point.x_bytes(q))
  def create_p2tr(_), do: {:error, "public key must be #{@tapkey_length}-bytes"}

  @doc """
  	create_p2sh_p2wpkh creates a p2wsh script using the passed 20-byte public key hash
  """
  @spec create_p2sh_p2wpkh(binary) :: {:ok, t(), t()}
  def create_p2sh_p2wpkh(<<pkh::binary-size(@h160_length)>>) do
    {:ok, p2wpkh} = create_p2wpkh(pkh)

    {:ok, p2sh} =
      p2wpkh
      |> serialize_script()
      |> Utils.hash160()
      |> create_p2sh()

    # return both p2sh script and redeem script (p2wpkh script)
    {:ok, p2sh, p2wpkh}
  end

  def create_p2sh_p2wpkh(_), do: {:error, "public key hash must be #{@h160_length}-bytes"}

  # CREATE SCRIPTS FROM PUBKEYS

  @doc """
  	public_key_hash takes the hash160 of the public key's compressed sec encoding.
  	Can be used to create a pkh script.
  """
  @spec public_key_hash(Point.t()) :: binary
  def public_key_hash(p = %Point{}) do
    p
    |> Point.sec()
    |> Utils.hash160()
  end

  @doc """
  	public_key_to_p2pkh creates a p2pkh script from a public key.
  	All public keys are compressed.
  """
  @spec public_key_to_p2pkh(Point.t()) :: {:ok, t()}
  def public_key_to_p2pkh(p = %Point{}) do
    p
    |> public_key_hash()
    |> create_p2pkh()
  end

  def public_key_to_p2pkh(_), do: {:error, "invalid public key"}

  @doc """
  	public_key_to_p2wpkh creates a p2wpkh script from a public key.
  	All public keys are compressed.
  """
  @spec public_key_to_p2wpkh(Point.t()) :: {:ok, t()}
  def public_key_to_p2wpkh(p = %Point{}) do
    p
    |> public_key_hash()
    |> create_p2wpkh()
  end

  def public_key_to_p2wpkh(_), do: {:error, "invalid public key"}

  @doc """
  	public_key_to_p2sh_p2wpkh creates a p2sh-p2wpkh script from a public key.
  	All public keys are compressed.
  """
  @spec public_key_to_p2sh_p2wpkh(Point.t()) :: {:ok, t(), t()}
  def public_key_to_p2sh_p2wpkh(p = %Point{}) do
    p
    |> public_key_hash()
    |> create_p2sh_p2wpkh()
  end

  def public_key_to_p2sh_p2wpkh(_), do: {:error, "invalid public key"}

  # ADDRESS CREATION & DECODING

  @doc """
  	from_address produces the scriptpubkey from an address.
  """
  @spec from_address(String.t()) ::
          {:error, String.t()} | {:ok, t(), Bitcoinex.Network.network_name()}
  def from_address(addr) do
    case String.slice(addr, 0, 2) do
      # segwit addresses
      p when p in ["bc", "tb"] ->
        case Segwit.decode_address(addr) do
          {:ok, {network, version, program}} ->
            {:ok, script} = create_witness_scriptpubkey(version, :binary.list_to_bin(program))
            {:ok, script, network}

          {:error, msg} ->
            {:error, "invalid segwit address: #{msg}"}
        end

      # legacy addresses
      _ ->
        try do
          {:ok, <<pfx::little-size(8), body::binary>>} = Base58.decode(addr)
          tpkh = Network.testnet().p2pkh_version_decimal_prefix
          mpkh = Network.mainnet().p2pkh_version_decimal_prefix
          tsh = Network.testnet().p2sh_version_decimal_prefix
          msh = Network.mainnet().p2sh_version_decimal_prefix

          case pfx do
            # p2pkh testnet
            ^tpkh ->
              {:ok, s} = create_p2pkh(body)
              {:ok, s, :testnet}

            # p2pkh mainnet
            ^mpkh ->
              {:ok, s} = create_p2pkh(body)
              {:ok, s, :mainnet}

            # p2sh testnet
            ^tsh ->
              {:ok, s} = create_p2sh(body)
              {:ok, s, :testnet}

            # p2sh mainnet
            ^msh ->
              {:ok, s} = create_p2sh(body)
              {:ok, s, :mainnet}
          end
        rescue
          _ -> {:error, "invalid address"}
        end
    end
  end

  @doc """
  	to_address converts a script object into the proper address type
  """
  @spec to_address(t(), Network.network_name()) ::
          {:ok, String.t()} | {:error, String.t()}
  def to_address(script = %__MODULE__{}, network) do
    {:ok, head, script} = pop(script)

    case head do
      # segwit 0
      0x00 ->
        {:ok, len, script} = pop(script)
        {:ok, <<res::binary-size(len)>>, _script} = pop(script)

        if len in [@h160_length, @wsh_length] do
          Segwit.encode_address(network, 0, :binary.bin_to_list(res))
        else
          {:error, "invalid witness program length. Must be in [#{@h160_length}, #{@wsh_length}]"}
        end

      # segwit 1 (taproot)
      0x51 ->
        {:ok, @tapkey_length, script} = pop(script)
        {:ok, <<res::binary-size(@tapkey_length)>>, _script} = pop(script)
        Segwit.encode_address(network, 1, :binary.bin_to_list(res))

      # p2sh
      0xA9 ->
        {:ok, @h160_length, script} = pop(script)
        {:ok, <<res::binary-size(@h160_length)>>, _script} = pop(script)
        {:ok, Address.encode(res, network, :p2sh)}

      # p2pkh
      0x76 ->
        {:ok, 0xA9, script} = pop(script)
        {:ok, @h160_length, script} = pop(script)
        {:ok, <<res::binary-size(@h160_length)>>, _script} = pop(script)
        {:ok, Address.encode(res, network, :p2pkh)}

      _ ->
        {:error, "non standard script type"}
    end
  end
end

defimpl String.Chars, for: Bitcoinex.Script do
  def to_string(script) do
    Bitcoinex.Script.display_script(script)
  end
end
