from btc.utils import hmac_sha512, sha256, ripemd160, base58_encode, \
    base58_decode, int2bytes, bytes2int
from btc.ecpoint import ECPoint
from btc.keys import KeysBTC


MAINNET_PUBLIC = b"\<KEY>"
MAINNET_PRIVATE = b"\<KEY>"
TESTNET_PUBLIC = b"\<KEY>"
TESTNET_PRIVATE = b"\<KEY>"


class ExtendedKey:
    """Represents an extended private (k,c) or public key (K,c) (BIP32)"""

    def __init__(self, key, chain_code: bytes, level: int = 0,
                 index: int = 0, fingerprint: bytes = b"\x00\x00\x00\x00"):
        """Construct an object.

        Parameters:
            key -- bytes if an extended private key, an ECPoint if an extended
                   public key (left 32 bytes of HMAC-SHA512),
            chain_code -- a chain code (right 32 bytes of HMAC-SHA512),
            level -- a depth of the key,
            index -- an index of the key,
            fingerprint -- checksum for the parent public key.
        """
        self.key = key
        self.chain_code = chain_code
        self.level = level
        self.index = index
        self.fingerprint = fingerprint


    def __repr__(self):
        return \
            str({
                "key": self.key.hex()
                           if isinstance(self.key, bytes) else self.key,
                "chain_code": self.chain_code.hex(),
                "level": self.level,
                "index": self.index,
                "fingerprint": self.fingerprint.hex()
            })


    def is_public(self):
        """Return True if this is an extended public key"""
        return isinstance(self.key, ECPoint)


    def is_private(self):
        """Return True if this is an extended private key"""
        return isinstance(self.key, bytes)


    def serialize(self):
        """Return serialized format (str) of this key (xprv / xpub)"""
        if self.is_private():
            ser_key = (
                MAINNET_PRIVATE +
                int2bytes(self.level, 1) +
                self.fingerprint +
                int2bytes(self.index, 4) +
                self.chain_code +
                b"\x00" +
                self.key
            )
        elif self.is_public():
            ser_key = (
                MAINNET_PUBLIC +
                int2bytes(self.level, 1) +
                self.fingerprint +
                int2bytes(self.index, 4) +
                self.chain_code +
                KeysBTC.point_to_publickey(self.key)
            )
        else:
            raise ValueError("Invalid extended key")

        checksum = sha256(sha256(ser_key))[:4]

        return base58_encode(ser_key + checksum)


    def deserialize(self, ser_key: str):
        """Deserialize a serialized key into self"""
        ser_key = base58_decode(ser_key, 82)

        # Checksum
        if sha256(sha256(ser_key[:78]))[:4] != ser_key[78:]:
            raise ValueError(
                "Wrong checksum of the extended key: {:s}".format(ser_key.hex())
            )

        self.level = ser_key[4]
        self.fingerprint = ser_key[5:9]
        self.index = bytes2int(ser_key[9:13])
        self.chain_code = ser_key[13:45]

        if ser_key[:4] == MAINNET_PRIVATE:
            # Miss 00 and get the private key
            self.key = ser_key[46:78]
        elif ser_key[:4] == MAINNET_PUBLIC:
            # Get x coordinate of the public point
            x = bytes2int(ser_key[46:78])
            # Calculate y coordinate of the public point
            y = ECPoint.get_secp256k1_y(x)
            # Choice even y if prefix = 02, else choice odd y
            if ser_key[45] == 2:
                y = ECPoint.get_secp256k1_p() - y if y % 2 != 0 else y
            else:
                y = ECPoint.get_secp256k1_p() - y if y % 2 == 0 else y

            self.key = ECPoint(x, y)
        else:
            raise ValueError(
                "Invalid serialized extended key: {:s}".format(ser_key.hex())
            )


    @staticmethod
    def get_fingerprint(public_key):
        """Return first 4 bytes of HASH160 of the public_key"""
        return ripemd160(sha256(public_key))[:4]


    @staticmethod
    def seed_to_master_key(seed):
        """Return an extended master (root) private key (BIP32) for a seed"""
        hash = hmac_sha512(b"Bitcoin seed", seed)
        key = hash[:32]
        chain_code = hash[32:]

        # Check key
        key_int = bytes2int(key)
        if key_int == 0 or key_int >= ECPoint.get_secp256k1_order():
            raise ValueError("Wrong master key")

        return ExtendedKey(key, chain_code)


class BIP32:
    """Represents hierarchy deterministic keys BIP32"""

    def __init__(self, master: ExtendedKey, level_indexes=None):
        """Construct an object.

        Parameters:
        master -- an extended master (root) key (ExtendedKey)
        level_indexes -- a path to the last key level (list),
                         ex. for m/0/1/0/<list> - [0, 1, 0]
        """
        self.level_indexes = [] if level_indexes is None else level_indexes

        if master.is_private():
            self.master_prv = master
            self.master_pub = \
                ExtendedKey(
                    KeysBTC(self.master_prv.key).get_public_point(),
                    self.master_prv.chain_code
                )
        elif master.is_public():
            self.master_prv = None
            self.master_pub = master
        else:
            raise ValueError("Invalid master key: {:s}", master)


    @staticmethod
    def hardened_index(index):
        """Return a hardened key index for a key index"""
        return index + 2 ** 31


    @staticmethod
    def prv_to_child(parent_prv: ExtendedKey, index: int):
        """Return an extended child private key.

        Parameters:
            parent_prv -- a parent private key,
            index -- an index of a parent private key
        """
        cur_key = KeysBTC(parent_prv.key)
        ser32_index = int2bytes(index, 4)

        # If a hardened index the take the private key,
        # otherwise, take the public key.
        if index >= 2 ** 31:
            data = b"\x00" + cur_key.get_private_key() + ser32_index
        else:
            data = cur_key.get_public_key() + ser32_index

        child_hash = hmac_sha512(parent_prv.chain_code, data)

        child_hash_left = bytes2int(child_hash[:32])
        k_i = (child_hash_left + cur_key.get_private_key_int()) % \
              ECPoint.get_secp256k1_order()
        # Check the left part
        if child_hash_left >= ECPoint.get_secp256k1_order() or k_i == 0:
            raise ValueError("The resulting key is invalid")

        # The right part is child_hash[32:]
        return ExtendedKey(
            int2bytes(k_i),
            child_hash[32:],
            parent_prv.level + 1,    # increase level
            index,
            ExtendedKey.get_fingerprint(cur_key.get_public_key())
        )


    @staticmethod
    def pub_to_child(parent_pub: ExtendedKey, index: int):
        """Return an extended child public key.

        Parameters:
            parent_pub -- a parent public key,
            index -- an index of a parent public key
        """
        # Check if index is not a hardened key
        if index >= 2 ** 31:
            raise ValueError(
                "Cannot generate a child public key because "
                "it is a hardened key"
            )

        ser32_index = int2bytes(index, 4)
        public_key = KeysBTC.point_to_publickey(parent_pub.key)

        data = public_key + ser32_index

        child_hash = hmac_sha512(parent_pub.chain_code, data)

        child_hash_left = bytes2int(child_hash[:32])
        K_i = KeysBTC.get_generator_point() * child_hash_left + parent_pub.key
        # Check the left part
        if child_hash_left >= ECPoint.get_secp256k1_order() or \
                K_i == ECPoint.infinity():
            raise ValueError(
                "The resulting key is invalid for index {:d}".format(index)
            )

        # The right part is child_hash[32:]
        return ExtendedKey(
            K_i,
            child_hash[32:],
            parent_pub.level + 1,    # increase level
            index,
            ExtendedKey.get_fingerprint(public_key)
        )


    def build_keys_path(self, level_indexes = None):
        """Build a key chain to the last key level.

        Parameters:
            level_indexes -- a path to the last key level (list),
                             ex. for m/0/1/0/<list> - [0, 1, 0].
        """
        self.level_indexes = level_indexes \
            if level_indexes else self.level_indexes

        parent_prv = self.master_prv
        parent_pub = self.master_pub
        # keys_path -- the list with the couples of extended keys
        # {"private", "public"} for each level in level_indexes.
        #
        # The root is the couple of the master keys
        self.keys_path = [
            {"private": parent_prv,
             "public": parent_pub}
        ]

        # For each level calculate an apropriate key
        for level in range(0, len(self.level_indexes)):
            index = self.level_indexes[level]
            if not isinstance(index, int):
                raise ValueError("Wrong index in the list")

            # Calculate a child private key if the parent private key exists
            try:
                if parent_prv is not None:
                    parent_prv = self.prv_to_child(parent_prv, index)
            except ValueError as err:
                parent_prv = str(err)

            # Calculate a child public key if the parent public key exists
            try:
                if parent_pub is not None:
                    parent_pub = self.pub_to_child(parent_pub, index)
            except ValueError as err:
                parent_pub = str(err)

            self.keys_path.append(
                {"private": parent_prv,
                 "public": parent_pub}
            )
        return


    def ckd_priv(self, index_list):
        """Children key derivation (CKDpriv) from the master (root) private.

        Parameters:
            index_list -- a list with indexes for derivating child keys.
        Return -- a list with the extended private child keys (the last level).
        """
        self.build_keys_path()

        # Derivate child keys from the last key in the key_path
        return [
            self.prv_to_child(self.keys_path[-1]["private"], index)
                for index in index_list
        ]


    def ckd_pub(self, index_list):
        """Children key derivation (CKDpub) from the master (root) public.

        Parameters:
            index_list -- a list with indexes for derivating child keys.
        Return -- a list with the extended public child keys (the last level).
        """
        self.build_keys_path()

        # Derivate child keys from the last key in the key_path
        return [
            self.pub_to_child(self.keys_path[-1]["public"], index)
                for index in index_list
        ]


from btc.utils import hmac_sha512, sha256, ripemd160, base58_encode, \
    base58_decode, int2bytes, bytes2int
from btc.ecpoint import ECPoint
from btc.keys import KeysBTC


MAINNET_PUBLIC = b"\<KEY>"
MAINNET_PRIVATE = b"\<KEY>"
TESTNET_PUBLIC = b"\<KEY>"
TESTNET_PRIVATE = b"\<KEY>"


class ExtendedKey:
    """Represents an extended private (k,c) or public key (K,c) (BIP32)"""

    def __init__(self, key, chain_code: bytes, level: int = 0,
                 index: int = 0, fingerprint: bytes = b"\x00\x00\x00\x00"):
        """Construct an object.

        Parameters:
            key -- bytes if an extended private key, an ECPoint if an extended
                   public key (left 32 bytes of HMAC-SHA512),
            chain_code -- a chain code (right 32 bytes of HMAC-SHA512),
            level -- a depth of the key,
            index -- an index of the key,
            fingerprint -- checksum for the parent public key.
        """
        self.key = key
        self.chain_code = chain_code
        self.level = level
        self.index = index
        self.fingerprint = fingerprint


    def __repr__(self):
        return \
            str({
                "key": self.key.hex()
                           if isinstance(self.key, bytes) else self.key,
                "chain_code": self.chain_code.hex(),
                "level": self.level,
                "index": self.index,
                "fingerprint": self.fingerprint.hex()
            })


    def is_public(self):
        """Return True if this is an extended public key"""
        return isinstance(self.key, ECPoint)


    def is_private(self):
        """Return True if this is an extended private key"""
        return isinstance(self.key, bytes)


    def serialize(self):
        """Return serialized format (str) of this key (xprv / xpub)"""
        if self.is_private():
            ser_key = (
                MAINNET_PRIVATE +
                int2bytes(self.level, 1) +
                self.fingerprint +
                int2bytes(self.index, 4) +
                self.chain_code +
                b"\x00" +
                self.key
            )
        elif self.is_public():
            ser_key = (
                MAINNET_PUBLIC +
                int2bytes(self.level, 1) +
                self.fingerprint +
                int2bytes(self.index, 4) +
                self.chain_code +
                KeysBTC.point_to_publickey(self.key)
            )
        else:
            raise ValueError("Invalid extended key")

        checksum = sha256(sha256(ser_key))[:4]

        return base58_encode(ser_key + checksum)


    def deserialize(self, ser_key: str):
        """Deserialize a serialized key into self"""
        ser_key = base58_decode(ser_key, 82)

        # Checksum
        if sha256(sha256(ser_key[:78]))[:4] != ser_key[78:]:
            raise ValueError(
                "Wrong checksum of the extended key: {:s}".format(ser_key.hex())
            )

        self.level = ser_key[4]
        self.fingerprint = ser_key[5:9]
        self.index = bytes2int(ser_key[9:13])
        self.chain_code = ser_key[13:45]

        if ser_key[:4] == MAINNET_PRIVATE:
            # Miss 00 and get the private key
            self.key = ser_key[46:78]
        elif ser_key[:4] == MAINNET_PUBLIC:
            # Get x coordinate of the public point
            x = bytes2int(ser_key[46:78])
            # Calculate y coordinate of the public point
            y = ECPoint.get_secp256k1_y(x)
            # Choice even y if prefix = 02, else choice odd y
            if ser_key[45] == 2:
                y = ECPoint.get_secp256k1_p() - y if y % 2 != 0 else y
            else:
                y = ECPoint.get_secp256k1_p() - y if y % 2 == 0 else y

            self.key = ECPoint(x, y)
        else:
            raise ValueError(
                "Invalid serialized extended key: {:s}".format(ser_key.hex())
            )


    @staticmethod
    def get_fingerprint(public_key):
        """Return first 4 bytes of HASH160 of the public_key"""
        return ripemd160(sha256(public_key))[:4]


    @staticmethod
    def seed_to_master_key(seed):
        """Return an extended master (root) private key (BIP32) for a seed"""
        hash = hmac_sha512(b"Bitcoin seed", seed)
        key = hash[:32]
        chain_code = hash[32:]

        # Check key
        key_int = bytes2int(key)
        if key_int == 0 or key_int >= ECPoint.get_secp256k1_order():
            raise ValueError("Wrong master key")

        return ExtendedKey(key, chain_code)


class BIP32:
    """Represents hierarchy deterministic keys BIP32"""

    def __init__(self, master: ExtendedKey, level_indexes=None):
        """Construct an object.

        Parameters:
        master -- an extended master (root) key (ExtendedKey)
        level_indexes -- a path to the last key level (list),
                         ex. for m/0/1/0/<list> - [0, 1, 0]
        """
        self.level_indexes = [] if level_indexes is None else level_indexes

        if master.is_private():
            self.master_prv = master
            self.master_pub = \
                ExtendedKey(
                    KeysBTC(self.master_prv.key).get_public_point(),
                    self.master_prv.chain_code
                )
        elif master.is_public():
            self.master_prv = None
            self.master_pub = master
        else:
            raise ValueError("Invalid master key: {:s}", master)


    @staticmethod
    def hardened_index(index):
        """Return a hardened key index for a key index"""
        return index + 2 ** 31


    @staticmethod
    def prv_to_child(parent_prv: ExtendedKey, index: int):
        """Return an extended child private key.

        Parameters:
            parent_prv -- a parent private key,
            index -- an index of a parent private key
        """
        cur_key = KeysBTC(parent_prv.key)
        ser32_index = int2bytes(index, 4)

        # If a hardened index the take the private key,
        # otherwise, take the public key.
        if index >= 2 ** 31:
            data = b"\x00" + cur_key.get_private_key() + ser32_index
        else:
            data = cur_key.get_public_key() + ser32_index

        child_hash = hmac_sha512(parent_prv.chain_code, data)

        child_hash_left = bytes2int(child_hash[:32])
        k_i = (child_hash_left + cur_key.get_private_key_int()) % \
              ECPoint.get_secp256k1_order()
        # Check the left part
        if child_hash_left >= ECPoint.get_secp256k1_order() or k_i == 0:
            raise ValueError("The resulting key is invalid")

        # The right part is child_hash[32:]
        return ExtendedKey(
            int2bytes(k_i),
            child_hash[32:],
            parent_prv.level + 1,    # increase level
            index,
            ExtendedKey.get_fingerprint(cur_key.get_public_key())
        )


    @staticmethod
    def pub_to_child(parent_pub: ExtendedKey, index: int):
        """Return an extended child public key.

        Parameters:
            parent_pub -- a parent public key,
            index -- an index of a parent public key
        """
        # Check if index is not a hardened key
        if index >= 2 ** 31:
            raise ValueError(
                "Cannot generate a child public key because "
                "it is a hardened key"
            )

        ser32_index = int2bytes(index, 4)
        public_key = KeysBTC.point_to_publickey(parent_pub.key)

        data = public_key + ser32_index

        child_hash = hmac_sha512(parent_pub.chain_code, data)

        child_hash_left = bytes2int(child_hash[:32])
        K_i = KeysBTC.get_generator_point() * child_hash_left + parent_pub.key
        # Check the left part
        if child_hash_left >= ECPoint.get_secp256k1_order() or \
                K_i == ECPoint.infinity():
            raise ValueError(
                "The resulting key is invalid for index {:d}".format(index)
            )

        # The right part is child_hash[32:]
        return ExtendedKey(
            K_i,
            child_hash[32:],
            parent_pub.level + 1,    # increase level
            index,
            ExtendedKey.get_fingerprint(public_key)
        )


    def build_keys_path(self, level_indexes = None):
        """Build a key chain to the last key level.

        Parameters:
            level_indexes -- a path to the last key level (list),
                             ex. for m/0/1/0/<list> - [0, 1, 0].
        """
        self.level_indexes = level_indexes \
            if level_indexes else self.level_indexes

        parent_prv = self.master_prv
        parent_pub = self.master_pub
        # keys_path -- the list with the couples of extended keys
        # {"private", "public"} for each level in level_indexes.
        #
        # The root is the couple of the master keys
        self.keys_path = [
            {"private": parent_prv,
             "public": parent_pub}
        ]

        # For each level calculate an apropriate key
        for level in range(0, len(self.level_indexes)):
            index = self.level_indexes[level]
            if not isinstance(index, int):
                raise ValueError("Wrong index in the list")

            # Calculate a child private key if the parent private key exists
            try:
                if parent_prv is not None:
                    parent_prv = self.prv_to_child(parent_prv, index)
            except ValueError as err:
                parent_prv = str(err)

            # Calculate a child public key if the parent public key exists
            try:
                if parent_pub is not None:
                    parent_pub = self.pub_to_child(parent_pub, index)
            except ValueError as err:
                parent_pub = str(err)

            self.keys_path.append(
                {"private": parent_prv,
                 "public": parent_pub}
            )
        return


    def ckd_priv(self, index_list):
        """Children key derivation (CKDpriv) from the master (root) private.

        Parameters:
            index_list -- a list with indexes for derivating child keys.
        Return -- a list with the extended private child keys (the last level).
        """
        self.build_keys_path()

        # Derivate child keys from the last key in the key_path
        return [
            self.prv_to_child(self.keys_path[-1]["private"], index)
                for index in index_list
        ]


    def ckd_pub(self, index_list):
        """Children key derivation (CKDpub) from the master (root) public.

        Parameters:
            index_list -- a list with indexes for derivating child keys.
        Return -- a list with the extended public child keys (the last level).
        """
        self.build_keys_path()

        # Derivate child keys from the last key in the key_path
        return [
            self.pub_to_child(self.keys_path[-1]["public"], index)
                for index in index_list
        ]


Represents an extended private (k,c) or public key (K,c) (BIP32) Construct an object.          Parameters:             key -- bytes if an extended private key, an ECPoint if an extended                    public key (left 32 bytes of HMAC-SHA512),             chain_code -- a chain code (right 32 bytes of HMAC-SHA512),             level -- a depth of the key,             index -- an index of the key,             fingerprint -- checksum for the parent public key. Return True if this is an extended public key Return True if this is an extended private key Return serialized format (str) of this key (xprv / xpub) Deserialize a serialized key into self # Checksum # Miss 00 and get the private key # Get x coordinate of the public point # Calculate y coordinate of the public point # Choice even y if prefix = 02, else choice odd y Return first 4 bytes of HASH160 of the public_key Return an extended master (root) private key (BIP32) for a seed # Check key Represents hierarchy deterministic keys BIP32 Construct an object.          Parameters:         master -- an extended master (root) key (ExtendedKey)         level_indexes -- a path to the last key level (list),                          ex. for m/0/1/0/<list> - [0, 1, 0] Return a hardened key index for a key index Return an extended child private key.          Parameters:             parent_prv -- a parent private key,             index -- an index of a parent private key # If a hardened index the take the private key, # otherwise, take the public key. # Check the left part # The right part is child_hash[32:] # increase level Return an extended child public key.          Parameters:             parent_pub -- a parent public key,             index -- an index of a parent public key # Check if index is not a hardened key # Check the left part # The right part is child_hash[32:] # increase level Build a key chain to the last key level.          Parameters:             level_indexes -- a path to the last key level (list),                              ex. for m/0/1/0/<list> - [0, 1, 0]. # keys_path -- the list with the couples of extended keys # {"private", "public"} for each level in level_indexes. # # The root is the couple of the master keys # For each level calculate an apropriate key # Calculate a child private key if the parent private key exists # Calculate a child public key if the parent public key exists Children key derivation (CKDpriv) from the master (root) private.          Parameters:             index_list -- a list with indexes for derivating child keys.         Return -- a list with the extended private child keys (the last level). # Derivate child keys from the last key in the key_path Children key derivation (CKDpub) from the master (root) public.          Parameters:             index_list -- a list with indexes for derivating child keys.         Return -- a list with the extended public child keys (the last level). # Derivate child keys from the last key in the key_path