import socket
import struct
import random
import decimal
from binascii import hexlify, unhexlify
import time
from codecs import encode
from io import BytesIO
import copy
from test_framework.schnorr import sign
from test_framework import util
from test_framework.script import find_and_delete, CScript, OP_CODESEPARATOR
from test_framework.constants import (
    SIGHASH_ALL_IN,
    SIGHASH_FIRSTN_IN,
    SIGHASH_THIS_IN,
    BTCBCH_SIGHASH_ANYONECANPAY,
    BTCBCH_SIGHASH_SINGLE,
    BTCBCH_SIGHASH_NONE,
)
from ..constants import COIN
from ..serialize import (
    ser_string,
    deser_uint256,
    ser_uint256,
    uint256_from_bigendian,
    uint256_from_str,
    deser_string,
    deser_vector,
    ser_vector,
    deser_uint256_vector,
    ser_uint256_vector,
    deser_varint,
    ser_varint,
    uint256_from_compact,
    from_hex,
)
from ..constants import TEST_PEER_SUBVERSION, TEST_PEER_VERSION
from ..util import sha256, hash256

# Serialization Types
SER_DEFAULT = 0
SER_ID = 0
SER_IDEM = 1

INVALID_OPCODE = b"\xff"

# Objects that map to bitcoind objects, which can be serialized/deserialized


# because the nVersion field has not been passed before the VERSION message the protocol uses an old format for the CAddress (missing nTime)
# This class handles that old format
class CAddressInVersion:
    def __init__(self, ip="0.0.0.0", port=0):
        self.n_services = 1
        self.pch_reserved = (
            b"\x00" * 10 + b"\xff" * 2
        )  # ip is 16 bytes on wire to handle v6
        self.ip = ip
        self.port = port

    def deserialize(self, f):
        self.n_services = struct.unpack("<Q", f.read(8))[0]
        self.pch_reserved = f.read(12)
        self.ip = socket.inet_ntoa(f.read(4))
        self.port = struct.unpack(">H", f.read(2))[0]

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<Q", self.n_services)
        r += self.pch_reserved
        r += socket.inet_aton(self.ip)
        r += struct.pack(">H", self.port)
        return r

    def __repr__(self):
        return f"CAddressInVersion(nServices={int(self.n_services)} ip={self.ip} port={int(self.port)})"


# Handle new-style CAddress objects (with nTime)
class CAddress:
    def __init__(self, ip="0.0.0.0", port=0):
        self.n_services = 1
        self.n_time = int(time.time())
        self.pch_reserved = (
            b"\x00" * 10 + b"\xff" * 2
        )  # ip is 16 bytes on wire to handle v6
        self.ip = ip
        self.port = port

    def deserialize(self, f):
        self.n_time = struct.unpack("<L", f.read(4))[0]
        self.n_services = struct.unpack("<Q", f.read(8))[0]
        self.pch_reserved = f.read(12)
        self.ip = socket.inet_ntoa(f.read(4))
        self.port = struct.unpack(">H", f.read(2))[0]

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<L", self.n_time)
        r += struct.pack("<Q", self.n_services)
        r += self.pch_reserved
        r += socket.inet_aton(self.ip)
        r += struct.pack(">H", self.port)
        return r

    def __repr__(self):
        return f"CAddress(nServices={int(self.n_services)} ip={self.ip} port={int(self.port)} time={int(self.n_time)})"


class CInv:
    MSG_TX = 1
    MSG_BLOCK = 2
    MSG_FILTERED_BLOCK = 3
    MSG_CMPCT_BLOCK = 4
    MSG_XTHINBLOCK = 5
    MSG_THINBLOCK = MSG_CMPCT_BLOCK
    typemap = {
        0: "Error",
        1: "TX",
        2: "Block",
        3: "FilteredBlock",
        4: "CompactBlock",
        5: "XThinBlock",
    }

    def __init__(self, t=0, h=0):
        assert isinstance(t, int)
        if isinstance(h, bytes):
            h = deser_uint256(h)
        assert isinstance(h, int)
        self.type = t
        self.hash = h

    def deserialize(self, f):
        self.type = struct.unpack("<i", f.read(4))[0]
        self.hash = deser_uint256(f)

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<i", self.type)
        r += ser_uint256(self.hash)
        return r

    def __repr__(self):
        return f"CInv(type={self.typemap[self.type]} hash={self.hash:064x})"


class CBlockLocator:
    def __init__(self):
        self.n_version = TEST_PEER_VERSION
        self.v_have = []

    def deserialize(self, f):
        self.n_version = struct.unpack("<i", f.read(4))[0]
        self.v_have = deser_uint256_vector(f)

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<i", self.n_version)
        r += ser_uint256_vector(self.v_have)
        return r

    def __repr__(self):
        return (
            f"CBlockLocator(nVersion={int(self.n_version)} vHave={repr(self.v_have)})"
        )


class COutPoint:
    def __init__(self, outpoint_hash=0):
        if isinstance(outpoint_hash, COutPoint):
            outpoint_hash = outpoint_hash.hash
        if isinstance(outpoint_hash, str):
            outpoint_hash = uint256_from_bigendian(outpoint_hash)
        if isinstance(outpoint_hash, bytes):
            outpoint_hash = uint256_from_str(outpoint_hash)
        self.hash = outpoint_hash

    def from_idem_and_idx(self, txidem, n):
        if isinstance(txidem, str):
            txidem = uint256_from_bigendian(txidem)
        if isinstance(txidem, int):
            txidem = ser_uint256(txidem)
        r = txidem
        r += struct.pack("<I", n)
        self.hash = uint256_from_str(sha256(r))
        return self

    def deserialize(self, f):
        self.hash = deser_uint256(f)

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += ser_uint256(self.hash)
        return r

    def rpc_hex(self):
        return util.uint256_to_rpc_hex(self.hash)

    def __repr__(self):
        return f"COutPoint(hash={self.hash:064x})"


class CTxIn:
    def __init__(self, outpoint=None, amount=0, script_sig=b"", n_sequence=0):
        # ctor determination and input arg conversion
        if isinstance(amount, decimal.Decimal):
            amount = int(amount * COIN)
        assert isinstance(amount, int)
        if isinstance(outpoint, dict):
            self.from_rpc_utxo(outpoint)
            return
        if script_sig is None:
            script_sig = b""
        assert isinstance(script_sig, bytes)

        # Initialization
        if outpoint is None:
            self.prevout = COutPoint()
        else:
            self.prevout = outpoint
        self.t = 0
        self.script_sig = script_sig
        self.n_sequence = n_sequence
        self.amount = amount

    def from_rpc_utxo(self, d):
        """Initialize this CTxIn from a dictionary received by the listunspent RPC call"""
        self.t = 0
        self.prevout = COutPoint(d["outpoint"])
        self.script_sig = b""
        self.amount = int(d["amount"] * COIN)
        self.n_sequence = 0

    def deserialize(self, f):
        self.t = struct.unpack("<B", f.read(1))[0]
        self.prevout = COutPoint()
        self.prevout.deserialize(f)
        self.script_sig = deser_string(f)
        self.n_sequence = struct.unpack("<I", f.read(4))[0]
        self.amount = struct.unpack("<q", f.read(8))[0]

    def serialize(self, stype):
        r = b""
        r += struct.pack("<B", self.t)
        r += self.prevout.serialize()
        if stype != SER_IDEM:
            r += ser_string(self.script_sig)
        r += struct.pack("<I", self.n_sequence)
        r += struct.pack("<q", int(self.amount))
        return r

    def __repr__(self):
        return f"CTxIn(prevout={repr(self.prevout)} amount={int(self.amount)} scriptSig={hexlify(self.script_sig)} nSequence={int(self.n_sequence)})"


# General transaction output
class TxOut:
    TYPE_SATOSCRIPT = 0
    TYPE_TEMPLATE = 1

    def __init__(self, typ=TYPE_SATOSCRIPT, n_value=0, script_pub_key=b""):
        assert isinstance(script_pub_key, bytes)
        assert isinstance(typ, int)
        self.t = typ
        # if its a decimal, its assumed to be in COINs
        assert isinstance(n_value, (int, decimal.Decimal))
        if isinstance(n_value, decimal.Decimal):
            self.n_value = int(n_value * COIN)
        else:
            self.n_value = n_value

        self.script_pub_key = script_pub_key

    def deserialize(self, f):
        self.t = struct.unpack("<B", f.read(1))[0]
        self.n_value = struct.unpack("<q", f.read(8))[0]
        self.script_pub_key = deser_string(f)

    def serialize(self, _ser_type=SER_DEFAULT):
        r = struct.pack("<B", self.t)
        r += struct.pack("<q", int(self.n_value))
        r += ser_string(self.script_pub_key)
        return r

    def __repr__(self):
        return f"TxOut(type={self.t:x}, nValue={int(self.n_value // COIN)}.{int(self.n_value % COIN):08} scriptPubKey={hexlify(self.script_pub_key)})"


# Legacy script mode tx out
class CTxOut(TxOut):
    def __init__(self, n_value=0, script_pub_key=b"", out_type=TxOut.TYPE_TEMPLATE):
        TxOut.__init__(self, out_type, n_value, script_pub_key)


# pylint: disable=too-many-public-methods
class CTransaction:
    def __init__(self, tx=None):
        self.id = None
        self.idem = None
        if isinstance(tx, dict):  # handle result from RPC call
            self.from_hex(tx["hex"])
            assert (
                util.uint256_to_rpc_hex(self.get_id()) == tx["txid"]
            ), "Deserialized id does not match what was received from RPC"
        elif isinstance(tx, str):
            self.from_hex(tx)
        elif tx is None:
            self.n_version = 0
            self.vin = []
            self.vout = []
            self.n_lock_time = 0
        else:
            self.n_version = tx.n_version
            self.vin = copy.deepcopy(tx.vin)
            self.vout = copy.deepcopy(tx.vout)
            self.n_lock_time = tx.n_lock_time

    def deserialize(self, f):
        if isinstance(f, str):
            # str - assumed to be hex string
            f = BytesIO(unhexlify(f))
        elif isinstance(f, bytes):
            f = BytesIO(f)
        self.n_version = struct.unpack("<B", f.read(1))[0]
        self.vin = deser_vector(f, CTxIn)
        self.vout = deser_vector(f, CTxOut)
        self.n_lock_time = struct.unpack("<I", f.read(4))[0]
        self.id = None
        self.idem = None
        return self

    def serialize(self, stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<B", self.n_version)
        r += ser_vector(self.vin, stype)
        r += ser_vector(self.vout, stype)
        r += struct.pack("<I", self.n_lock_time)
        return r

    def from_hex(self, hexdata):
        self.deserialize(hexdata)
        return self

    def to_hex(self):
        """Return the hex string serialization of this object"""
        return hexlify(self.serialize()).decode("utf-8")

    def rehash(self):
        self.id = None
        self.idem = None
        self.calc_idem()
        self.calc_id()
        return self.id

    def get_id(self):
        """Returns the Id as bytes"""
        if self.id is None:
            self.calc_id()
        return self.id

    def get_id_as_int(self):
        """Returns the Id as a number"""
        return deser_uint256(self.get_id())

    def get_rpc_hex_id(self):
        """Returns the Id in the same format it would be returned via a RPC call"""
        return util.uint256_to_rpc_hex(self.get_id())

    def get_idem(self):
        """Returns the Idem as bytes"""
        if self.idem is None:
            self.calc_idem()
        return self.idem

    def get_rpc_hex_idem(self):
        """Returns the Idem in the same format it would be returned via a RPC call"""
        return util.uint256_to_rpc_hex(self.get_idem())

    def calc_idem(self):
        self.idem = hash256(self.serialize(SER_IDEM))
        return self.idem

    def calc_id(self):
        idem_hash = self.calc_idem()
        sigs = []
        sigs.append(struct.pack("<I", len(self.vin)))
        for tin in self.vin:
            sigs.append(bytes(tin.script_sig))
            sigs.append(INVALID_OPCODE)  # Separator
        sigs = b"".join(sigs)
        sigs_hash = hash256(sigs)
        # print("sigs_hash: " + util.uint256_to_rpc_hex(sigs_hash) + "  len: " + str(len(sigs)) + " bytes: " + sigs.hex())
        self.id = hash256(idem_hash + sigs_hash)
        return self.id

    def outpoint_at(self, idx):
        assert idx < len(self.vout)
        return COutPoint().from_idem_and_idx(self.get_idem(), idx)

    def spend_output(self, idx, satisfier_script=None):
        assert idx < len(self.vout)
        return CTxIn(
            COutPoint().from_idem_and_idx(self.get_idem(), idx),
            self.vout[idx].nValue,
            satisfier_script,
        )

    def __repr__(self):
        return f"CTransaction(nVersion={int(self.n_version)} vin={repr(self.vin)} vout={repr(self.vout)} nLockTime={int(self.n_lock_time)})"

    def hash_prevouts_nexa(self, hashcode, in_number):
        if len(hashcode) == 0:
            hashcode = bytes([0])
        out_code = hashcode[0] & 0xF0
        op_ser = b""
        if out_code in (SIGHASH_ALL_IN, SIGHASH_FIRSTN_IN):
            loop_end = len(self.vin) if out_code == SIGHASH_ALL_IN else hashcode[1]
            for i in range(0, loop_end):
                inp = self.vin[i]
                op_ser += struct.pack("<B", inp.t)
                op_ser += inp.prevout.serialize(SER_IDEM)
        elif out_code == SIGHASH_THIS_IN:
            inp = self.vin[in_number]
            op_ser += struct.pack("<B", inp.t)
            op_ser += inp.prevout.serialize(SER_IDEM)
        else:
            assert False, "unknown hashcode"
        return hash256(op_ser)

    def hash_input_amounts_nexa(self, hashcode, in_number):
        if len(hashcode) == 0:
            hashcode = bytes([0])
        out_code = hashcode[0] & 0xF0
        op_ser = b""
        if out_code in (SIGHASH_ALL_IN, SIGHASH_FIRSTN_IN):
            loop_end = len(self.vin) if out_code == SIGHASH_ALL_IN else hashcode[1]
            for i in range(0, loop_end):
                inp = self.vin[i]
                op_ser += struct.pack("<q", int(inp.amount))
        elif out_code == SIGHASH_THIS_IN:
            inp = self.vin[in_number]
            op_ser += struct.pack("<q", int(inp.amount))
        else:
            assert False, "unknown hashcode"
        return hash256(op_ser)

    def hash_sequence_nexa(self, hashcode, in_number):
        if len(hashcode) == 0:
            hashcode = bytes([0])
        out_code = hashcode[0] & 0xF0
        op_ser = b""
        if out_code in (SIGHASH_ALL_IN, SIGHASH_FIRSTN_IN):
            loop_end = len(self.vin) if out_code == SIGHASH_ALL_IN else hashcode[1]
            for i in range(0, loop_end):
                inp = self.vin[i]
                op_ser += struct.pack("<I", inp.n_sequence)
        elif out_code == SIGHASH_THIS_IN:
            inp = self.vin[in_number]
            op_ser += struct.pack("<I", inp.n_sequence)
        else:
            assert False, "unknown hashcode"
        return hash256(op_ser)

    def hash_prevouts(self, hashcode):
        if hashcode & BTCBCH_SIGHASH_ANYONECANPAY:
            return 32 * b"\x00"

        op_ser = b""
        for inp in self.vin:
            op_ser += struct.pack("<B", inp.t)
            op_ser += inp.prevout.serialize(SER_IDEM)
        return hash256(op_ser)

    def hash_input_amounts(self, hashcode):
        if hashcode & BTCBCH_SIGHASH_ANYONECANPAY:
            return 32 * b"\x00"

        op_ser = b""
        for inp in self.vin:
            op_ser += struct.pack("<q", int(inp.amount))
        return hash256(op_ser)

    def hash_sequence(self, hashcode):
        if (
            hashcode & BTCBCH_SIGHASH_ANYONECANPAY
            or hashcode & 0x1F == BTCBCH_SIGHASH_SINGLE
            or hashcode & 0x1F == BTCBCH_SIGHASH_NONE
        ):
            return 32 * b"\x00"

        seq_ser = b""
        for inp in self.vin:
            seq_ser += struct.pack("<I", inp.n_sequence)
        return hash256(seq_ser)

    def hash_outputs(self, hashcode, in_number):
        if hashcode & 0x1F == BTCBCH_SIGHASH_SINGLE and in_number < len(self.vout):
            return hash256(self.vout[in_number].serialize())

        if (not hashcode & 0x1F == BTCBCH_SIGHASH_SINGLE) and (
            not hashcode & 0x1F == BTCBCH_SIGHASH_NONE
        ):
            out_ser = b""
            for out in self.vout:
                out_ser += out.serialize()
            return hash256(out_ser)

        return 32 * b"\x00"

    def signature_hash_legacy(self, script, in_idx, hashtype):
        """Consensus-correct SignatureHash (legacy variant)

        Returns (hash, err) to precisely match the consensus-critical behavior of
        the BTCBCH_SIGHASH_SINGLE bug. (in_idx is *not* checked for validity)
        """

        hash_one = b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

        if in_idx >= len(self.vin):
            return (hash_one, f"in_idx {int(in_idx)} out of range ({len(self.vin)})")

        # create copy as it is going to be modified with find_and_delete(..)
        txtmp = CTransaction(self)

        for txin in txtmp.vin:
            txin.script_sig = b""
        txtmp.vin[in_idx].script_sig = find_and_delete(
            script, CScript([OP_CODESEPARATOR])
        )

        if (hashtype & 0x1F) == BTCBCH_SIGHASH_NONE:
            txtmp.vout = []

            for i, txin in enumerate(txtmp.vin):
                if i != in_idx:
                    txin.n_sequence = 0

        elif (hashtype & 0x1F) == BTCBCH_SIGHASH_SINGLE:
            out_idx = in_idx
            if out_idx >= len(txtmp.vout):
                return (
                    hash_one,
                    f"out_idx {int(out_idx)} out of range ({len(txtmp.vout)})",
                )

            tmp = txtmp.vout[out_idx]
            txtmp.vout = []
            for i in range(out_idx):
                txtmp.vout.append(CTxOut())
            txtmp.vout.append(tmp)

            for i, txin in enumerate(txtmp.vin):
                if i != in_idx:
                    txin.n_sequence = 0

        if hashtype & BTCBCH_SIGHASH_ANYONECANPAY:
            tmp = txtmp.vin[in_idx]
            txtmp.vin = []
            txtmp.vin.append(tmp)

        s = txtmp.serialize()
        s += struct.pack(b"<I", hashtype)

        h = hash256(s)

        return (h, None)


# pylint: disable=too-many-instance-attributes
class CBlockHeader:
    def __init__(self, header=None):
        if header is None:
            self.set_null()
        else:
            self.set(header)

    def set(self, header):
        self.hash_prev_block = header.hash_prev_block
        self.n_bits = header.n_bits
        self.hash_ancestor = header.hash_ancestor
        self.hash_merkle_root = header.hash_merkle_root
        self.hash_tx_filter = header.hash_tx_filter
        self.n_time = header.n_time
        self.height = header.height
        self.chain_work = header.chain_work
        self.size = header.size
        self.tx_count = header.tx_count
        self.fee_pool_amt = header.fee_pool_amt
        self.utxo_commitment = header.utxo_commitment
        self.miner_data = header.miner_data
        self.nonce = header.nonce
        self.hash_num = None
        self.calc_hash()

    def set_null(self):
        self.hash_prev_block = 0
        self.n_bits = 0
        self.hash_ancestor = 0
        self.hash_merkle_root = 0
        self.hash_tx_filter = 0
        self.n_time = 0
        self.height = 0
        self.chain_work = 0
        self.size = 0
        self.tx_count = 0
        self.fee_pool_amt = 0
        self.utxo_commitment = b""
        self.miner_data = b""
        self.nonce = None
        self.hash_num = None
        self.hash = None

    # pylint: disable=attribute-defined-outside-init
    def deserialize(self, f):
        self.hash_prev_block = deser_uint256(f)
        self.n_bits = struct.unpack("<I", f.read(4))[0]
        self.hash_ancestor = deser_uint256(f)
        self.hash_merkle_root = deser_uint256(f)
        self.hash_tx_filter = deser_uint256(f)
        self.n_time = struct.unpack("<I", f.read(4))[0]
        self.height = deser_varint(f)
        self.chain_work = deser_uint256(f)
        self.size = struct.unpack("<Q", f.read(8))[0]
        self.tx_count = deser_varint(f)
        self.fee_pool_amt = deser_varint(f)
        self.utxo_commitment = deser_string(f)
        self.miner_data = deser_string(f)
        self.nonce = deser_string(f)
        self.hash_num = None
        self.hash = None

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += ser_uint256(self.hash_prev_block)
        r += struct.pack("<I", self.n_bits)
        r += ser_uint256(self.hash_ancestor)
        r += ser_uint256(self.hash_merkle_root)
        r += ser_uint256(self.hash_tx_filter)
        r += struct.pack("<I", self.n_time)
        r += ser_varint(self.height)
        r += ser_uint256(self.chain_work)
        r += struct.pack("<Q", self.size)
        r += ser_varint(self.tx_count)
        r += ser_varint(self.fee_pool_amt)
        r += ser_string(self.utxo_commitment)
        r += ser_string(self.miner_data)
        r += ser_string(self.nonce)
        return r

    def hex_str(self):
        """Match the C++ equivalent"""
        return self.serialize().hex()

    def calc_mining_commitment(self):
        hpv = ser_uint256(self.hash_prev_block)
        mh = b""
        mh += hpv
        mh += struct.pack("<I", self.n_bits)
        # print("mh bytes: " + mh.hex())
        sha_mh = sha256(mh)
        # print("miniheader: " + sha_mh[::-1].hex())  # note bitcoind prints
        # hash backwards from how it uses it so that's why [::-1]

        eh = b""
        eh += ser_uint256(self.hash_ancestor)
        eh += ser_uint256(self.hash_tx_filter)
        eh += ser_uint256(self.hash_merkle_root)
        eh += struct.pack("<I", self.n_time)
        eh += struct.pack("<Q", self.height)
        eh += ser_uint256(self.chain_work)
        eh += struct.pack("<Q", self.size)
        eh += struct.pack("<Q", self.tx_count)
        eh += struct.pack("<Q", self.fee_pool_amt)
        eh += ser_string(self.utxo_commitment)
        eh += ser_string(self.miner_data)
        sha_eh = sha256(eh)
        # print("extended bytes: " + eh.hex())
        # print("ext: " + sha_eh[::-1].hex())

        hash_bytes = sha256(sha_mh + sha_eh)
        # print("full mining commitment: " + hash_bytes.hex())
        return hash_bytes

    def calc_hash(self):
        if self.hash_num is None:
            mh = b""
            mh += ser_uint256(self.hash_prev_block)
            mh += struct.pack("<I", self.n_bits)
            sha_mh = sha256(mh)

            eh = b""
            eh += ser_uint256(self.hash_ancestor)
            eh += ser_uint256(self.hash_tx_filter)
            eh += ser_uint256(self.hash_merkle_root)
            eh += struct.pack("<I", self.n_time)
            eh += struct.pack("<Q", self.height)
            eh += ser_uint256(self.chain_work)
            eh += struct.pack("<Q", self.size)
            eh += struct.pack("<Q", self.tx_count)
            eh += struct.pack("<Q", self.fee_pool_amt)
            eh += ser_string(self.utxo_commitment)
            eh += ser_string(self.miner_data)
            eh += ser_string(self.nonce)

            sha_eh = sha256(eh)
            hash_bytes = sha256(sha_mh + sha_eh)
            self.hash_num = uint256_from_str(hash_bytes)
            self.hash = encode(hash_bytes[::-1], "hex_codec").decode("ascii")
        return self.hash

    def gethashprevblock(self, encoding="int"):
        assert encoding in ("hex", "int")
        if encoding == "int":
            return self.hash_prev_block
        return hex(self.hash_prev_block)

    def gethash(self, encoding="int"):
        assert encoding in ("hex", "int")
        self.calc_hash()
        if encoding == "int":
            return self.hash_num
        # return as a 32 bytes hex (64 / 2)
        return f"{self.hash_num:064x}"

    def gethashhex(self):
        self.calc_hash()
        return self.hash

    def rehash(self):
        self.hash_num = None
        self.calc_hash()
        return self.hash_num

    def summary(self):
        s = []
        s.append(
            f"Block:  {self.gethash():064x}  Time:{time.ctime(self.n_time)}  Bits:0x{self.n_bits:08x}\n"
        )
        s.append(
            f"Parent: {self.hash_prev_block:064x}  Merkle: {self.hash_merkle_root:064x}"
        )
        return "".join(s)

    def __str__(self):
        return f"CBlockHeader(hash={self.gethash():064x} hashPrevBlock={self.hash_prev_block:064x} hashMerkleRoot={self.hash_merkle_root:064x} nTime={time.ctime(self.n_time)} nBits={self.n_bits:08x} nonce={self.nonce.hex()})"

    def __repr__(self):
        return f"CBlockHeader(hashPrevBlock={self.hash_prev_block:064x} hashMerkleRoot={self.hash_merkle_root:064x} nTime={time.ctime(self.n_time)} nBits={self.n_bits:08x} nonce={self.nonce.hex()})"


# pylint: disable=too-many-instance-attributes
class CBlock(CBlockHeader):
    def __init__(self, header=None):
        if isinstance(header, str):
            from_hex(self, header)
        else:
            super().__init__(header)
            self.vtx = []

    def deserialize(self, f):
        super().deserialize(f)
        self.vtx = deser_vector(f, CTransaction)

    def serialize(self, stype=SER_DEFAULT):
        r = b""
        r += super().serialize(stype)
        r += ser_vector(self.vtx)
        return r

    # pylint: disable=attribute-defined-outside-init
    def update_fields(self):
        self.hash_merkle_root = self.calc_merkle_root()
        self.calc_size()
        self.tx_count = len(self.vtx)
        self.hash_num = None  # force recalculation of hash since block changed
        self.hash = None

    def calc_merkle_root(self, debug=False):
        hashes = []
        for tx in self.vtx:
            hashes.append(tx.get_id())

        if debug:
            print("merkle root hashes:")
            for x in hashes:
                print(util.uint256_to_rpc_hex(x))
        while len(hashes) > 1:
            newhashes = []
            for i in range(0, len(hashes), 2):
                i2 = min(i + 1, len(hashes) - 1)
                newhashes.append(hash256(hashes[i] + hashes[i2]))
            hashes = newhashes
        if hashes:
            return uint256_from_str(hashes[0])
        return 0

    def calc_size(self):
        self.size = len(self.serialize()) - (len(self.nonce) + 1)

    def calc_mining_hash(self):
        raise NotImplementedError()

    def is_valid(self):
        mining_hash = self.calc_mining_hash()
        target = uint256_from_compact(self.n_bits)
        if mining_hash > target:
            return False
        for tx in self.vtx:
            if not tx.is_valid():
                return False
        if self.calc_merkle_root() != self.hash_merkle_root:
            return False
        return True

    def solve(self):
        assert self.tx_count == len(self.vtx)
        assert self.size == len(self.serialize()) - (len(self.nonce) + 1)

        target = uint256_from_compact(self.n_bits)
        mining_commitment = uint256_from_str(self.calc_mining_commitment())
        while True:
            r = b""
            r += ser_uint256(mining_commitment)
            r += ser_string(self.nonce)
            mining_hash = hash256(r)
            sha256of_mh = sha256(mining_hash)

            # create a private key from the blockhash
            private_key = mining_hash

            # create a schorr sig by signing with the sha256(blockhash) and,
            # private key from above
            sig = sign(private_key, sha256of_mh)

            # get the sha256 of the schnorr sig
            schnorr_sha256 = uint256_from_str(sha256(sig))
            if schnorr_sha256 < target:
                break

            # Roll 3 bytes TODO: verify that nonce is big enough
            if self.nonce[0] == 255:
                if len(self.nonce) == 1:
                    self.nonce.append(0)
                if self.nonce[1] == 255:
                    if len(self.nonce) == 2:
                        self.nonce.append(0)
                    self.nonce[2] += 1
                self.nonce[1] += 1
            self.nonce[0] += 1
        self.hash_num = None  # force recalculation of hash since block changed
        self.hash = None

    def __str__(self):
        return f"CBlock(hashPrevBlock={self.hash_prev_block:064x} hashMerkleRoot={self.hash_merkle_root:064x} nTime={time.ctime(self.n_time)} nBits={self.n_bits:08x} nonce={self.nonce.hex()} vtx_len={len(self.vtx)})"

    def __repr__(self):
        return f"CBlock(hashPrevBlock={self.hash_prev_block:064x} hashMerkleRoot={self.hash_merkle_root:064x} nTime={time.ctime(self.n_time)} nBits={self.n_bits:08x} nonce={self.nonce.hex()} vtx={repr(self.vtx)})"


# Objects that correspond to messages on the wire
# pylint: disable=too-many-instance-attributes
class MsgVersion:
    command = b"version"

    def __init__(self):
        self.n_version = TEST_PEER_VERSION
        self.n_services = 1
        self.n_time = int(time.time())
        self.addr_to = CAddressInVersion()
        self.addr_from = CAddressInVersion()
        self.n_nonce = random.getrandbits(64)
        self.str_sub_ver = TEST_PEER_SUBVERSION
        self.n_starting_height = -1

    def deserialize(self, f):
        self.n_version = struct.unpack("<i", f.read(4))[0]
        self.n_services = struct.unpack("<Q", f.read(8))[0]
        self.n_time = struct.unpack("<q", f.read(8))[0]
        self.addr_to = CAddressInVersion()
        self.addr_to.deserialize(f)
        if self.n_version >= 106:
            self.addr_from = CAddressInVersion()
            self.addr_from.deserialize(f)
            self.n_nonce = struct.unpack("<Q", f.read(8))[0]
            self.str_sub_ver = deser_string(f)
            if self.n_version >= 209:
                self.n_starting_height = struct.unpack("<i", f.read(4))[0]
            else:
                self.n_starting_height = None
        else:
            self.addr_from = None
            self.n_nonce = None
            self.str_sub_ver = None
            self.n_starting_height = None

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<i", self.n_version)
        r += struct.pack("<Q", self.n_services)
        r += struct.pack("<q", self.n_time)
        r += self.addr_to.serialize()
        r += self.addr_from.serialize()
        r += struct.pack("<Q", self.n_nonce)
        r += ser_string(self.str_sub_ver)
        r += struct.pack("<i", self.n_starting_height)
        return r

    def __repr__(self):
        return f"MsgVersion(nVersion={int(self.n_version)} nServices={int(self.n_services)} nTime={time.ctime(self.n_time)} addrTo={repr(self.addr_to)} addrFrom={repr(self.addr_from)} nNonce=0x{self.n_nonce:016X} strSubVer={self.str_sub_ver} nStartingHeight={int(self.n_starting_height)})"


class MsgVerack:
    command = b"verack"

    def __init__(self):
        pass

    def deserialize(self, f):
        pass

    def serialize(self):
        return b""

    def __repr__(self):
        return "MsgVerack()"


class MsgAddr:
    command = b"addr"

    def __init__(self):
        self.addrs = []

    def deserialize(self, f):
        self.addrs = deser_vector(f, CAddress)

    def serialize(self, _stype=SER_DEFAULT):
        return ser_vector(self.addrs)

    def __repr__(self):
        return f"MsgAddr(addrs={repr(self.addrs)})"


class MsgInv:
    command = b"inv"

    def __init__(self, inv=None):
        if inv is None:
            self.inv = []
        else:
            self.inv = inv

    def deserialize(self, f):
        self.inv = deser_vector(f, CInv)

    def serialize(self, _stype=SER_DEFAULT):
        return ser_vector(self.inv)

    def __repr__(self):
        return f"MsgInv(inv={repr(self.inv)})"


class MsgGetdata:
    command = b"getdata"

    def __init__(self, inv=None):
        if inv is None:
            self.inv = []
        elif isinstance(inv, list):
            self.inv = inv
        else:
            self.inv = [inv]

    def deserialize(self, f):
        self.inv = deser_vector(f, CInv)

    def serialize(self, _stype=SER_DEFAULT):
        return ser_vector(self.inv)

    def __repr__(self):
        return f"MsgGetdata(inv={repr(self.inv)})"


class MsgGetblocks:
    command = b"getblocks"

    def __init__(self):
        self.locator = CBlockLocator()
        self.hashstop = 0

    def deserialize(self, f):
        self.locator = CBlockLocator()
        self.locator.deserialize(f)
        self.hashstop = deser_uint256(f)

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += self.locator.serialize()
        r += ser_uint256(self.hashstop)
        return r

    def __repr__(self):
        return (
            f"MsgGetblocks(locator={repr(self.locator)} hashstop={self.hashstop:064x})"
        )


class MsgTx:
    command = b"tx"

    def __init__(self, tx=CTransaction()):
        self.tx = tx

    def deserialize(self, f):
        self.tx.deserialize(f)

    def serialize(self, _stype=SER_DEFAULT):
        return self.tx.serialize()

    def __repr__(self):
        return f"MsgTx(tx={repr(self.tx)})"


class MsgBlock:
    command = b"block"

    def __init__(self, block=None):
        if block is None:
            self.block = CBlock()
        else:
            self.block = block

    def deserialize(self, f):
        self.block.deserialize(f)

    def serialize(self, _stype=SER_DEFAULT):
        return self.block.serialize()

    def __str__(self):
        return f"MsgBlock(block={str(self.block)})"

    def __repr__(self):
        return f"MsgBlock(block={repr(self.block)})"


class MsgGetaddr:
    command = b"getaddr"

    def __init__(self):
        pass

    def deserialize(self, f):
        pass  # This message has no addtl data

    def serialize(self, _stype=SER_DEFAULT):
        return b""  # This message has no addtl data

    def __repr__(self):
        return "MsgGetaddr()"


class MsgPing:
    command = b"ping"

    def __init__(self, nonce=0):
        self.nonce = nonce

    def deserialize(self, f):
        self.nonce = struct.unpack("<Q", f.read(8))[0]

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<Q", self.nonce)
        return r

    def __repr__(self):
        return f"MsgPing(nonce={self.nonce:08x})"


class MsgPong:
    command = b"pong"

    def __init__(self, nonce=0):
        self.nonce = nonce

    def deserialize(self, f):
        self.nonce = struct.unpack("<Q", f.read(8))[0]

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += struct.pack("<Q", self.nonce)
        return r

    def __repr__(self):
        return f"MsgPong(nonce={self.nonce:08x})"


class MsgMempool:
    command = b"mempool"

    def __init__(self):
        pass

    def deserialize(self, f):
        pass

    def serialize(self, _stype=SER_DEFAULT):
        return b""

    def __repr__(self):
        return "MsgMempool()"


class MsgSendheaders:
    command = b"sendheaders"

    def __init__(self):
        pass

    def deserialize(self, f):
        pass

    def serialize(self, _stype=SER_DEFAULT):
        return b""

    def __repr__(self):
        return "MsgSendheaders()"


class MsgGetheaders:
    """
    getheaders message has
    locator: CBlockLocator object that identifies what block to start with
    hash_stop: hash of last desired block header, 0 to get as many as possible
    """

    command = b"getheaders"

    def __init__(self):
        self.locator = CBlockLocator()
        self.hashstop = 0

    def deserialize(self, f):
        self.locator = CBlockLocator()
        self.locator.deserialize(f)
        self.hashstop = deser_uint256(f)

    def serialize(self, _stype=SER_DEFAULT):
        r = b""
        r += self.locator.serialize()
        r += ser_uint256(self.hashstop)
        return r

    def __repr__(self):
        return f"MsgGetheaders(locator={repr(self.locator)}, stop={self.hashstop:064x})"


class MsgHeaders:
    """
    headers message has
    <count> <vector of block headers>
    """

    command = b"headers"

    def __init__(self, headers=None):
        self.headers = [] if headers is None else headers

    def deserialize(self, f):
        # In BCH/BTC these are serialized/deserialized as blocks with 0 transactions (regardless of the actual # tx)
        # In Nexa these are serialized/deserialized as block headers
        self.headers = deser_vector(f, CBlockHeader)

    def serialize(self, stype=SER_DEFAULT):
        for x in self.headers:
            assert isinstance(x, CBlockHeader)
        return ser_vector(self.headers, stype)

    def __repr__(self):
        return f"MsgHeaders(headers={repr(self.headers)})"


class MsgReject:
    command = b"reject"
    REJECT_MALFORMED = 1

    def __init__(self):
        self.message = b""
        self.code = 0
        self.reason = b""
        self.data = 0

    def deserialize(self, f):
        self.message = deser_string(f)
        self.code = struct.unpack("<B", f.read(1))[0]
        self.reason = deser_string(f)
        if self.code != self.REJECT_MALFORMED and (self.message in (b"block", b"tx")):
            self.data = deser_uint256(f)

    def serialize(self, _stype=SER_DEFAULT):
        r = ser_string(self.message)
        r += struct.pack("<B", self.code)
        r += ser_string(self.reason)
        if self.code != self.REJECT_MALFORMED and (self.message in (b"block", b"tx")):
            r += ser_uint256(self.data)
        return r

    def __repr__(self):
        return f"MsgReject: message={self.message} code={int(self.code)} reason={self.reason} data=[{self.data:064x}]"
