#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Dusan Klinec, ph4r05, 2018
# base58 for Monero - working in 8-byte blocks
#
# This gives us eight full-sized blocks and one 5-byte block.
# Eight bytes converts to 11 or less Base58 characters; if a particular block converts to <11 characters,
# the conversion pads it with "1"s (1 is 0 in Base58). Likewise, the final 5-byte block can convert to 7 or
# less Base58 digits; the conversion will ensure the result is 7 digits. Due to the conditional padding,
# the 69-byte string will always convert to 95 Base58 characters (8 * 11 + 7).
# [ https://xmr.llcoins.net/addresstests.html ]

# MoneroPy - A python toolbox for Monero
# Copyright (C) 2016 The MoneroPy Developers.
#
# MoneroPy is released under the BSD 3-Clause license. Use and redistribution of
# this software is subject to the license terms in the LICENSE file found in the
# top-level directory of this distribution.

__alphabet = [ord(s) for s in '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz']
__b58base = 58
__UINT64MAX = 2**64
__encodedBlockSizes = [0, 2, 3, 5, 6, 7, 9, 10, 11]
__fullBlockSize = 8
__fullEncodedBlockSize = 11


def _hexToBin(hex):
    if len(hex) % 2 != 0:
        return "Hex string has invalid length!"
    return [int(hex[i*2:i*2+2], 16) for i in range(len(hex)//2)]


def _binToHex(bin):
    return "".join([("0" + hex(int(bin[i])).split('x')[1])[-2:] for i in range(len(bin))])


def _strToBin(a):
    return [ord(s) for s in a]


def _binToStr(bin):
    return ''.join([chr(bin[i]) for i in range(len(bin))])


def _uint8be_to_64(data):
    l_data = len(data)

    if l_data < 1 or l_data > 8:
        return "Invalid input length"

    res = 0
    switch = 9 - l_data
    for i in range(l_data):
        if switch == 1:
            res = res << 8 | data[i]
        elif switch == 2:
            res = res << 8 | data[i]
        elif switch == 3:
            res = res << 8 | data[i]
        elif switch == 4:
            res = res << 8 | data[i]
        elif switch == 5:
            res = res << 8 | data[i]
        elif switch == 6:
            res = res << 8 | data[i]
        elif switch == 7:
            res = res << 8 | data[i]
        elif switch == 8:
            res = res << 8 | data[i]
        else:
            return "Impossible condition"
    return res


def _uint64_to_8be(num, size):
    res = [0] * size;
    if size < 1 or size > 8:
        return "Invalid input length"

    twopow8 = 2**8
    for i in range(size-1,-1,-1):
        res[i] = num % twopow8
        num = num // twopow8

    return res


def encode_block(data, buf, index):
    l_data = len(data)

    if l_data < 1 or l_data > __fullEncodedBlockSize:
        return "Invalid block length: " + str(l_data)

    num = _uint8be_to_64(data)
    i = __encodedBlockSizes[l_data] - 1

    while num > 0:
        remainder = num % __b58base
        num = num // __b58base
        buf[index+i] = __alphabet[remainder];
        i -= 1

    return buf


def b58encode(data_hex=None, data_bin=None):
    """
    Encode hexadecimal string as base58 (ex: encoding a Monero address).

    :param data_hex:
    :param data_bin:
    :return:
    """
    data = bytearray(data_bin) if data_bin else _hexToBin(data_hex)
    l_data = len(data)

    if l_data == 0:
        return ""

    full_block_count = l_data // __fullBlockSize
    last_block_size = l_data % __fullBlockSize
    res_size = full_block_count * __fullEncodedBlockSize + __encodedBlockSizes[last_block_size]

    res = [0] * res_size
    for i in range(res_size):
        res[i] = __alphabet[0]

    for i in range(full_block_count):
        res = encode_block(data[(i*__fullBlockSize):(i*__fullBlockSize+__fullBlockSize)], res, i * __fullEncodedBlockSize)

    if last_block_size > 0:
        res = encode_block(data[(full_block_count*__fullBlockSize):(full_block_count*__fullBlockSize+last_block_size)], res, full_block_count * __fullEncodedBlockSize)

    return bytes(res)


def decode_block(data, buf, index):
    l_data = len(data)

    if l_data < 1 or l_data > __fullEncodedBlockSize:
        return "Invalid block length: " + l_data

    res_size = __encodedBlockSizes.index(l_data)
    if res_size <= 0:
        return "Invalid block size"

    res_num = 0
    order = 1
    for i in range(l_data-1, -1, -1):
        digit = __alphabet.index(data[i])
        if digit < 0:
            return "Invalid symbol"

        product = order * digit + res_num
        if product > __UINT64MAX:
            return "Overflow"

        res_num = product
        order = order * __b58base

    if res_size < __fullBlockSize and 2**(8 * res_size) <= res_num:
        return "Overflow 2"

    tmp_buf = _uint64_to_8be(res_num, res_size)
    for i in range(len(tmp_buf)):
        buf[i+index] = tmp_buf[i]

    return buf


def b58decode(data_hex=None, data_bin=None):
    """
    Decode a base58 string (ex: a Monero address) into hexidecimal form.

    :param data_hex:
    :param data_bin:
    :return:
    """
    enc = bytearray(data_bin) if data_bin else _strToBin(data_hex)
    l_enc = len(enc)

    if l_enc == 0:
        return ""

    full_block_count = l_enc // __fullEncodedBlockSize
    last_block_size = l_enc % __fullEncodedBlockSize
    last_block_decoded_size = __encodedBlockSizes.index(last_block_size)

    if last_block_decoded_size < 0:
        return "Invalid encoded length"

    data_size = full_block_count * __fullBlockSize + last_block_decoded_size

    data = [0] * data_size
    for i in range(full_block_count):
        data = decode_block(enc[(i*__fullEncodedBlockSize):(i*__fullEncodedBlockSize+__fullEncodedBlockSize)], data, i * __fullBlockSize)

    if last_block_size > 0:
        data = decode_block(enc[(full_block_count*__fullEncodedBlockSize):(full_block_count*__fullEncodedBlockSize+last_block_size)], data, full_block_count * __fullBlockSize)

    return bytes(data)


