# bitcoin.py - functions for handling Bitcoin addresses
#
# Copyright (C) 2018 Arthur de Jong
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

"""Bitcoin address.

A Bitcoin address is an identifier that is used as destination in a Bitcoin
transaction. It is based on a hash of the public portion of a key pair.

There are currently three address formats in use:

* P2PKH: pay to pubkey hash
* P2SH: pay to script hash
* Bech32

More information:

* https://en.bitcoin.it/wiki/Address

>>> validate('1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD')
'1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD'
>>> validate('BC1QARDV855YJNGSPVXUTTQ897AQCA3LXJU2Y69JCE')
'bc1qardv855yjngspvxuttq897aqca3lxju2y69jce'
>>> validate('1NEDqZPvTWRaoho48qXuLLsrYomMXPABfX')
Traceback (most recent call last):
    ...
InvalidChecksum: ...
"""

from __future__ import annotations

import hashlib
import struct
from collections.abc import Iterable
from functools import reduce

from stdnum.exceptions import *
from stdnum.util import clean


def compact(number: str) -> str:
    """Convert the number to the minimal representation. This strips the
    number of any valid separators and removes surrounding whitespace."""
    number = clean(number, ' ').strip()
    if number[:3].lower() == 'bc1':
        number = number.lower()
    return number


# Base58 encoding character set as used in Bitcoin addresses
_base58_alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'


def b58decode(s: str) -> bytes:
    """Decode a Base58 encoded string to a bytestring."""
    value = reduce(lambda a, c: a * 58 + _base58_alphabet.index(c), s, 0)
    result = b''
    while value >= 256:
        value, mod = divmod(value, 256)
        result = struct.pack('B', mod) + result
    result = struct.pack('B', value) + result
    return struct.pack('B', 0) * (len(s) - len(s.lstrip('1'))) + result


# Bech32 character set as used in Bitcoin addresses
_bech32_alphabet = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'


# The Bech32 generator tests and values for checksum calculation
_bech32_generator = (
    (1 << 0, 0x3b6a57b2), (1 << 1, 0x26508e6d), (1 << 2, 0x1ea119fa),
    (1 << 3, 0x3d4233dd), (1 << 4, 0x2a1462b3))


def bech32_checksum(values: Iterable[int]) -> int:
    """Calculate the Bech32 checksum."""
    chk = 1
    for value in values:
        top = chk >> 25
        chk = (chk & 0x1ffffff) << 5 | value
        for test, val in _bech32_generator:
            chk ^= val if top & test else 0
    return chk


def b32decode(data: Iterable[int]) -> bytes:
    """Decode a list of Base32 values to a bytestring."""
    acc, bits = 0, 0
    result = b''
    for value in data:
        acc = ((acc << 5) | value) & 0xfff
        bits += 5
        if bits >= 8:
            bits -= 8
            result = result + struct.pack('B', (acc >> bits) & 0xff)
    if bits >= 5 or acc & ((1 << bits) - 1):
        raise InvalidComponent()
    return result


def _expand_hrp(hrp: str) -> list[int]:
    """Convert the human-readable part to format for checksum calculation."""
    return [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]


def validate(number: str) -> str:
    """Check if the number provided is valid. This checks the length and
    check digit."""
    number = compact(number)
    if number.startswith('1') or number.startswith('3'):
        # P2PKH (pay to pubkey hash) or P2SH (pay to script hash) address
        if not all(x in _base58_alphabet for x in number):
            raise InvalidFormat()
        address = b58decode(number)
        if len(address) != 25:
            raise InvalidLength()
        if hashlib.sha256(hashlib.sha256(address[:-4]).digest()).digest()[:4] != address[-4:]:
            raise InvalidChecksum()
    elif number.startswith('bc1'):
        # Bech32 type address
        if not all(x in _bech32_alphabet for x in number[3:]):
            raise InvalidFormat()
        if len(number) < 11 or len(number) > 90:
            raise InvalidLength()
        data = [_bech32_alphabet.index(x) for x in number[3:]]
        if bech32_checksum(_expand_hrp('bc') + data) != 1:
            raise InvalidChecksum()
        witness_version = data[0]
        witness_program = b32decode(data[1:-6])
        if witness_version > 16:
            raise InvalidComponent()
        if len(witness_program) < 2 or len(witness_program) > 40:
            raise InvalidLength()
        if witness_version == 0 and len(witness_program) not in (20, 32):
            raise InvalidLength()
    else:
        raise InvalidComponent()
    return number


def is_valid(number: str) -> bool:
    """Check if the number provided is valid. This checks the length and
    check digit."""
    try:
        return bool(validate(number))
    except ValidationError:
        return False
