package Bitcoin::Crypto::Transaction::Digest;
$Bitcoin::Crypto::Transaction::Digest::VERSION = '4.001';
use v5.10;
use strict;
use warnings;

use Moo;
use Mooish::AttributeBuilder -standard;
use Types::Common -types;

use Crypt::Digest::SHA256 qw(sha256);
use Bitcoin::Crypto::Helpers qw(ensure_length);
use Bitcoin::Crypto::Util qw(hash256 pack_compactsize);
use Bitcoin::Crypto::Types -types;
use Bitcoin::Crypto::Exception;
use Bitcoin::Crypto::Constants;
use Bitcoin::Crypto::Transaction::Digest::Config;
use Bitcoin::Crypto::Transaction::Digest::Result;

use namespace::clean;

has param 'transaction' => (
	isa => InstanceOf ['Bitcoin::Crypto::Transaction'],
	weak_ref => 1,
);

has field 'config' => (
	coerce => (InstanceOf ['Bitcoin::Crypto::Transaction::Digest::Config'])
		->plus_coercions(HashRef, q{Bitcoin::Crypto::Transaction::Digest::Config->new($_)}),
	writer => 1,
	handles => [
		qw(
			signing_index
			signing_subscript
			sighash
			_default_sighash
			taproot_ext_flag
			taproot_ext
			taproot_annex
		)
	],
);

has field '_cache' => (
	isa => HashRef,
	default => sub { {} },
);

sub get_digest
{
	my ($self) = @_;
	my $sign_no = $self->signing_index;
	my $input = $self->transaction->inputs->[$sign_no];

	Bitcoin::Crypto::Exception::Transaction->raise(
		"can't find input with index $sign_no"
	) if !$input;

	my $procedure = '_get_digest_default';
	if ($input->is_taproot) {
		$procedure = '_get_digest_taproot';
	}
	elsif ($input->is_segwit) {
		$procedure = '_get_digest_segwit';
	}

	return $self->$procedure();
}

sub _get_digest_default
{
	my ($self) = @_;

	$self->_default_sighash(Bitcoin::Crypto::Constants::sighash_all);

	my $sighash_type = $self->sighash & 31;
	my $anyonecanpay = $self->sighash & Bitcoin::Crypto::Constants::sighash_anyonecanpay;

	my $transaction = $self->transaction;
	my $tx_copy = $transaction->clone;

	@{$tx_copy->inputs} = ();
	foreach my $input (@{$transaction->inputs}) {
		my $input_copy = $input->clone;

		$input_copy->set_signature_script('');
		$tx_copy->add_input($input_copy);
	}

	my $this_input = $tx_copy->inputs->[$self->signing_index];
	if ($self->signing_subscript) {
		$this_input->set_signature_script($self->signing_subscript);
	}
	else {
		Bitcoin::Crypto::Exception::Transaction->raise(
			"can't guess the subscript from a non-standard transaction"
		) unless $this_input->utxo->output->is_standard;

		$this_input->set_signature_script($this_input->script_base->to_serialized);
	}

	# Handle sighashes
	if ($sighash_type == Bitcoin::Crypto::Constants::sighash_none) {
		@{$tx_copy->outputs} = ();
		foreach my $input (@{$tx_copy->inputs}) {
			$input->set_sequence_no(0)
				unless $input == $this_input;
		}
	}
	elsif ($sighash_type == Bitcoin::Crypto::Constants::sighash_single) {
		if ($self->signing_index >= @{$transaction->outputs}) {

			# this should verify with constant digest (without hashing)
			return Bitcoin::Crypto::Transaction::Digest::Result->new(
				hash => scalar reverse ensure_length("\x01", 32),
			);
		}

		@{$tx_copy->outputs} = ();
		my @wanted_outputs = @{$transaction->outputs}[0 .. $self->signing_index - 1];
		foreach my $output (@wanted_outputs) {
			my $output_copy = $output->clone;
			$output_copy->set_locking_script('');
			$output_copy->set_max_value;
			$tx_copy->add_output($output_copy);
		}

		$tx_copy->add_output($transaction->outputs->[$self->signing_index]);

		foreach my $input (@{$tx_copy->inputs}) {
			$input->set_sequence_no(0)
				unless $input == $this_input;
		}
	}

	if ($anyonecanpay) {
		@{$tx_copy->inputs} = ($this_input);
	}

	my $serialized = $tx_copy->to_serialized(witness => 0);
	$serialized .= pack 'V', $self->sighash;

	return Bitcoin::Crypto::Transaction::Digest::Result->new(preimage => $serialized);
}

sub _get_digest_segwit
{
	my ($self) = @_;

	$self->_default_sighash(Bitcoin::Crypto::Constants::sighash_all);

	my $sighash_type = $self->sighash & 31;
	my $anyonecanpay = $self->sighash & Bitcoin::Crypto::Constants::sighash_anyonecanpay;

	my $transaction = $self->transaction->clone;
	my $this_input = $transaction->inputs->[$self->signing_index]->clone;
	$transaction->inputs->[$self->signing_index] = $this_input;

	my $empty_hash = "\x00" x 32;
	my $single = $sighash_type == Bitcoin::Crypto::Constants::sighash_single;
	my $none = $sighash_type == Bitcoin::Crypto::Constants::sighash_none;

	if ($self->signing_subscript) {

		# NOTE: sets witness for proper behavior of _script_code in
		# Bitcoin::Crypto::Transaction::Input for P2WSH
		$this_input->set_witness([$self->signing_subscript]);
	}

	# According to https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
	# Double SHA256 of the serialization of:
	# 1. nVersion of the transaction (4-byte little endian)
	# 2. hashPrevouts (32-byte hash)
	# 3. hashSequence (32-byte hash)
	# 4. outpoint (32-byte hash + 4-byte little endian)
	# 5. scriptCode of the input (serialized as scripts inside CTxOuts)
	# 6. value of the output spent by this input (8-byte little endian)
	# 7. nSequence of the input (4-byte little endian)
	# 8. hashOutputs (32-byte hash)
	# 9. nLocktime of the transaction (4-byte little endian)
	# 10. sighash type of the signature (4-byte little endian)

	my $serialized = '';
	$serialized .= pack 'V', $transaction->version;

	my @prevouts;
	my @sequences;
	foreach my $input (@{$transaction->inputs}) {
		push @prevouts, $input->prevout;
		push @sequences, pack 'V', $input->sequence_no;
	}

	my @outputs;
	foreach my $output (@{$transaction->outputs}) {
		push @outputs, $output->to_serialized;
	}

	# handle prevouts
	$serialized .= $anyonecanpay
		? $empty_hash
		: hash256(join '', @prevouts)
		;

	# handle sequences
	$serialized .= $anyonecanpay || $single || $none
		? $empty_hash
		: hash256(join '', @sequences)
		;

	$serialized .= $this_input->prevout;

	my $script_base = $this_input->script_base->to_serialized;
	$serialized .= pack_compactsize(length $script_base);
	$serialized .= $script_base;

	$serialized .= $this_input->utxo->output->value_serialized;
	$serialized .= pack 'V', $this_input->sequence_no;

	# handle outputs
	if (!$single && !$none) {
		$serialized .= hash256(join '', @outputs);
	}
	elsif ($single && $self->signing_index < @outputs) {
		$serialized .= hash256($outputs[$self->signing_index]);
	}
	else {
		$serialized .= $empty_hash;
	}

	$serialized .= pack 'V', $transaction->locktime;
	$serialized .= pack 'V', $self->sighash;

	return Bitcoin::Crypto::Transaction::Digest::Result->new(preimage => $serialized);
}

sub _get_digest_taproot
{
	my ($self) = @_;

	$self->_default_sighash(Bitcoin::Crypto::Constants::sighash_default);

	my $sighash_type = $self->sighash & 3;
	my $anyonecanpay = $self->sighash & Bitcoin::Crypto::Constants::sighash_anyonecanpay;

	my $transaction = $self->transaction->clone;
	my $this_input = $transaction->inputs->[$self->signing_index]->clone;
	$transaction->inputs->[$self->signing_index] = $this_input;
	my $annex = $self->taproot_annex;
	my $ext_flag = $self->taproot_ext_flag;

	my $all = $sighash_type == Bitcoin::Crypto::Constants::sighash_all
		|| $sighash_type == Bitcoin::Crypto::Constants::sighash_default;
	my $single = $sighash_type == Bitcoin::Crypto::Constants::sighash_single;
	my $none = $sighash_type == Bitcoin::Crypto::Constants::sighash_none;

	Bitcoin::Crypto::Exception::Transaction->raise(
		"can't digest taproot transaction with unknown SIGHASH"
	) unless $all || $single || $none;

	# According to https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
	# SHA256 of the serialization of:
	# hash_type (1).
	# nVersion (4): the nVersion of the transaction.
	# nLockTime (4): the nLockTime of the transaction.
	# If the hash_type & 0x80 does not equal SIGHASH_ANYONECANPAY:
	# - sha_prevouts (32): the SHA256 of the serialization of all input outpoints.
	# - sha_amounts (32): the SHA256 of the serialization of all input amounts.
	# - sha_scriptpubkeys (32): the SHA256 of all spent outputs' scriptPubKeys, serialized as script inside CTxOut.
	# - sha_sequences (32): the SHA256 of the serialization of all input nSequence.
	# If hash_type & 3 does not equal SIGHASH_NONE or SIGHASH_SINGLE:
	# - sha_outputs (32): the SHA256 of the serialization of all outputs in CTxOut format.
	# spend_type (1): equal to (ext_flag * 2) + annex_present, where annex_present is 0 if no annex is present, or 1 otherwise (the original witness stack has two or more witness elements, and the first byte of the last element is 0x50)
	# If hash_type & 0x80 equals SIGHASH_ANYONECANPAY:
	# - outpoint (36): the COutPoint of this input (32-byte hash + 4-byte little-endian).
	# - amount (8): value of the previous output spent by this input.
	# - scriptPubKey (35): scriptPubKey of the previous output spent by this input, serialized as script inside CTxOut. Its size is always 35 bytes.
	# - nSequence (4): nSequence of this input.
	# If hash_type & 0x80 does not equal SIGHASH_ANYONECANPAY:
	# - input_index (4): index of this input in the transaction input vector. Index of the first input is 0.
	# If an annex is present (the lowest bit of spend_type is set):
	# - sha_annex (32): the SHA256 of (compact_size(size of annex) || annex), where annex includes the mandatory 0x50 prefix.
	# If hash_type & 3 equals SIGHASH_SINGLE:
	# - sha_single_output (32): the SHA256 of the corresponding output in CTxOut format.

	my $serialized = '';
	$serialized .= "\x00";    # sighash epoch
	$serialized .= pack 'C', $self->sighash;
	$serialized .= pack 'V', $transaction->version;
	$serialized .= pack 'V', $transaction->locktime;

	if (!$anyonecanpay) {
		$serialized .= $self->_cache->{taproot_common_tx_data} //= do {
			my @prevouts;
			my @amounts;
			my @pubkeys;
			my @sequences;
			foreach my $input (@{$transaction->inputs}) {
				push @prevouts, $input->prevout;
				push @amounts, $input->utxo->output->value_serialized;
				push @sequences, pack 'V', $input->sequence_no;

				my $pubkey = $input->utxo->output->locking_script->to_serialized;
				push @pubkeys, pack_compactsize(length $pubkey) . $pubkey;
			}

			sha256(join '', @prevouts)
				. sha256(join '', @amounts)
				. sha256(join '', @pubkeys)
				. sha256(join '', @sequences);
		};
	}

	my $outputs = $self->_cache->{taproot_outputs} //= do {
		my @outputs;
		foreach my $output (@{$transaction->outputs}) {
			my $tmp = $output->locking_script->to_serialized;
			push @outputs, $output->to_serialized;
		}

		\@outputs;
	};

	if (!$none && !$single) {
		$serialized .= sha256(join '', @$outputs);
	}

	$serialized .= pack 'C', $ext_flag * 2 + defined $annex;

	if ($anyonecanpay) {
		$serialized .= $this_input->prevout;
		$serialized .= $this_input->utxo->output->value_serialized;

		my $pubkey = $this_input->utxo->output->locking_script->to_serialized;
		$serialized .= pack_compactsize(length $pubkey) . $pubkey;

		$serialized .= pack 'V', $this_input->sequence_no;
	}
	else {
		$serialized .= pack 'V', $self->signing_index;
	}

	if (defined $annex) {
		$serialized .= sha256(pack_compactsize(length $annex) . $annex);
	}

	if ($single && $self->signing_index < @$outputs) {
		$serialized .= sha256($outputs->[$self->signing_index]);
	}
	elsif ($single) {
		Bitcoin::Crypto::Exception::Transaction->raise(
			"can't digest taproot transaction with SIGHASH_SINGLE without corresponding output"
		);
	}

	# BIP342 extension
	if ($ext_flag == 1) {
		my $ext = $self->taproot_ext;

		Bitcoin::Crypto::Exception::Transaction->raise(
			"missing taproot extension for ext_flag=1"
		) unless defined $ext;

		$serialized .= $ext;
	}

	return Bitcoin::Crypto::Transaction::Digest::Result->new(
		taproot => !!1,
		preimage => $serialized,
	);
}

1;

