# pylint: disable=missing-docstring,too-many-function-args

import hashlib
import re

P = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1
A = 0
Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240
Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424
G = (Gx, Gy)


def inv(a, n):
    lm, hm = 1, 0
    low, high = a % n, n
    while low > 1:
        r = high / low
        nm, new = hm - lm * r, high - low * r
        lm, low, hm, high = nm, new, lm, low
    return lm % n


def get_code_string(base):
    if base == 2:
        return '01'
    elif base == 10:
        return '0123456789'
    elif base == 16:
        return "0123456789abcdef"
    elif base == 58:
        return "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
    elif base == 256:
        return ''.join([chr(x) for x in range(256)])
    else:
        raise ValueError("Invalid base!")


def encode(val, base, minlen=0):
    code_string = get_code_string(base)
    result = ""
    while val > 0:
        result = code_string[val % base] + result
        val /= base
    if len(result) < minlen:
        result = code_string[0] * (minlen - len(result)) + result
    return result


def decode(string, base):
    code_string = get_code_string(base)
    result = 0
    if base == 16:
        string = string.lower()
    while string:
        result *= base
        result += code_string.find(string[0])
        string = string[1:]
    return result


def changebase(string, frm, to, minlen=0):
    return encode(decode(string, frm), to, minlen)


def base10_add(a, b):
    if a is None:
        return b[0], b[1]
    if b is None:
        return a[0], a[1]
    if a[0] == b[0]:
        if a[1] == b[1]:
            return base10_double(a[0], a[1])
        return None
    m = ((b[1] - a[1]) * inv(b[0] - a[0], P)) % P
    x = (m * m - a[0] - b[0]) % P
    y = (m * (a[0] - x) - a[1]) % P
    return (x, y)


def base10_double(a):
    if a is None:
        return None
    m = ((3 * a[0] * a[0] + A) * inv(2 * a[1], P)) % P
    x = (m * m - 2 * a[0]) % P
    y = (m * (a[0] - x) - a[1]) % P
    return (x, y)


def base10_multiply(a, n):
    if n == 0:
        return G
    if n == 1:
        return a
    if (n % 2) == 0:
        return base10_double(base10_multiply(a, n / 2))
    if (n % 2) == 1:
        return base10_add(base10_double(base10_multiply(a, n / 2)), a)
    return None


def hex_to_point(h):
    return (decode(h[2:66], 16), decode(h[66:], 16))


def point_to_hex(p):
    return '04' + encode(p[0], 16, 64) + encode(p[1], 16, 64)


def multiply(privkey, pubkey):
    return point_to_hex(base10_multiply(hex_to_point(pubkey), decode(privkey, 16)))


def privtopub(privkey):
    return point_to_hex(base10_multiply(G, decode(privkey, 16)))


def add(p1, p2):
    if len(p1) == 32:
        return encode(decode(p1, 16) + decode(p2, 16) % P, 16, 32)
    return point_to_hex(base10_add(hex_to_point(p1), hex_to_point(p2)))


def hash_160(string):
    intermed = hashlib.sha256(string).digest()
    ripemd160 = hashlib.new('ripemd160')
    ripemd160.update(intermed)
    return ripemd160.digest()


def dbl_sha256(string):
    return hashlib.sha256(hashlib.sha256(string).digest()).digest()


def bin_to_b58check(inp):
    inp_fmtd = '\x00' + inp
    leadingzbytes = len(re.match('^\x00*', inp_fmtd).group(0))
    checksum = dbl_sha256(inp_fmtd)[:4]
    return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58)

# Convert a public key (in hex) to a Bitcoin address


def pubkey_to_address(pubkey):
    return bin_to_b58check(hash_160(changebase(pubkey, 16, 256)))
