"""
Implementation of encoding and decoding of Zcash
unified addresses according to the ZIP-316.
see: https://zips.z.cash/zip-0316
"""

from typing import TYPE_CHECKING

from trezor.crypto.bech32 import Encoding, convertbits

if TYPE_CHECKING:
    from enum import IntEnum
    from apps.common.coininfo import CoinInfo
else:
    IntEnum = object


# Saves 50 bytes over `def prefix(coin: CoinInfo) -> str`
# (throws KeyError instead of ValueError but it should not matter)
PREFIXES = {
    "Zcash": "u",
    "Zcash Testnet": "utest",
}


class Typecode(IntEnum):
    P2PKH = 0x00
    P2SH = 0x01
    SAPLING = 0x02
    ORCHARD = 0x03


# Byte length of a receiver.
# Saves 30 bytes over `def receiver_length(typecode: Typecode) -> int`
RECEIVER_LENGTHS = {
    Typecode.P2PKH: 20,
    Typecode.P2SH: 20,
    Typecode.SAPLING: 43,
    Typecode.ORCHARD: 43,
}


def padding(hrp: str) -> bytes:
    assert len(hrp) <= 16
    return hrp.encode() + bytes(16 - len(hrp))


def encode(receivers: dict[Typecode, bytes], coin: CoinInfo) -> str:
    from trezor.crypto.bech32 import bech32_encode
    from trezor.utils import empty_bytearray
    from .f4jumble import f4jumble
    from apps.common.writers import write_bytes_fixed, write_compact_size

    # multiple transparent receivers forbidden
    assert not (Typecode.P2PKH in receivers and Typecode.P2SH in receivers)
    # at least one shielded address must be present
    assert Typecode.SAPLING in receivers or Typecode.ORCHARD in receivers

    length = 16  # 16 bytes for padding
    for receiver_bytes in receivers.values():
        length += 2  # typecode (1 byte) + length (1 byte)
        length += len(receiver_bytes)

    w = empty_bytearray(length)

    # receivers in ascending order
    receivers_list = list(receivers.items())
    receivers_list.sort()

    for (typecode, raw_bytes) in receivers_list:
        length = RECEIVER_LENGTHS.get(typecode) or len(raw_bytes)
        write_compact_size(w, typecode)
        write_compact_size(w, length)
        write_bytes_fixed(w, raw_bytes, length)

    hrp = PREFIXES[coin.coin_name]
    write_bytes_fixed(w, padding(hrp), 16)
    f4jumble(memoryview(w))
    converted = convertbits(w, 8, 5)
    return bech32_encode(hrp, converted, Encoding.BECH32M)


def decode(addr_str: str, coin: CoinInfo) -> dict[int, bytes]:
    from trezor.crypto.bech32 import bech32_decode
    from trezor.utils import BufferReader
    from trezor.wire import DataError
    from apps.common.readers import read_compact_size
    from .f4jumble import f4unjumble

    try:
        hrp, data, encoding = bech32_decode(addr_str, 1000)
    except ValueError:
        raise DataError("Bech32m decoding failed.")
    assert hrp is not None  # to satisfy typecheckers
    assert data is not None  # to satisfy typecheckers
    assert encoding is not None  # to satisfy typecheckers
    if hrp != PREFIXES[coin.coin_name]:
        raise DataError("Unexpected address prefix.")
    if encoding != Encoding.BECH32M:
        raise DataError("Bech32m encoding required.")

    decoded = bytearray(convertbits(data, 5, 8, False))
    f4unjumble(memoryview(decoded))

    # check trailing padding bytes
    if decoded[-16:] != padding(hrp):
        raise DataError("Invalid padding bytes")

    r = BufferReader(decoded[:-16])

    last_typecode = None
    receivers: dict[int, bytes] = dict()
    while r.remaining_count() > 0:
        typecode = read_compact_size(r)
        if typecode in receivers:
            raise DataError("Duplicated typecode")
        if typecode > 0x02000000:
            raise DataError("Invalid typecode")
        if last_typecode is not None and typecode < last_typecode:
            raise DataError("Invalid receivers order")
        last_typecode = typecode

        length = read_compact_size(r)
        # if the typecode of the receiver is known, then verify receiver length
        expected_length = RECEIVER_LENGTHS.get(Typecode(typecode))
        if expected_length is not None and length != expected_length:
            raise DataError("Unexpected receiver length")

        if r.remaining_count() < length:
            raise DataError("Invalid receiver length")

        receivers[typecode] = r.read(length)

    if Typecode.P2PKH in receivers and Typecode.P2SH in receivers:
        raise DataError("Multiple transparent receivers")

    if len(receivers) == 1:
        the_receiver = list(receivers.keys())[0]
        if the_receiver in (Typecode.P2PKH, Typecode.P2SH):
            raise DataError("Only transparent receiver")

    return receivers
