import os
import hashlib
import binascii
import itertools
import bisect

from .wordlist import WORDS as wordlist
from .exceptions import FailedChecksumError
from jumpscale.core import exceptions


def generate_mnemonic(strength=256, wordlist=wordlist):
    """Generate random mnemonic

    Args:
        strength (int, optional): The number of bits of the generated key. Defaults to 256.
        wordlist (list[str], optional): The Word list used to generate the mnemonic. Defaults to wordlist.

    Returns:
        str: A string of space separated words representing the mnemonic.
    """
    ent = entropy(strength)
    return key_to_mnemonic(ent)


def entropy(strength=256):
    """Returns random bytes of length strengh/8.

    Args:
        strength (int, optional): The number of bits in the randomly created array. Defaults to 256.

    Returns:
        bytes: The generated bytes.
    """
    return os.urandom(strength // 8)


def mnemonic_to_key(mnemonic, wordlist=wordlist):
    """Convert mnemonic to key

    Args:
        mnemonic (str): A string of space separated words representing the mnemonic.
        wordlist (list[string], optional): The Word list used to generate the mnemonic. Defaults to wordlist.

    Raises:
        FailedChecksumError: Thrown when the checksum validation fails.

    Returns:
        bytes: The key
    """
    words = mnemonic.split(" ")
    concatLenBits = len(words) * 11
    concatBits = [False] * concatLenBits
    wordindex = 0
    use_binary_search = True
    for word in words:
        # Find the words index in the wordlist
        ndx = binary_search(wordlist, word) if use_binary_search else wordlist.index(word)
        if ndx < 0:
            raise exceptions.NotFound('Unable to find "%s" in word list.' % word)
        # Set the next 11 bits to the value of the index.
        for ii in range(11):
            concatBits[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0
        wordindex += 1
    checksumLengthBits = concatLenBits // 33
    entropyLengthBits = concatLenBits - checksumLengthBits
    # Extract original entropy as bytes.
    entropy = bytearray(entropyLengthBits // 8)
    for ii in range(len(entropy)):
        for jj in range(8):
            if concatBits[(ii * 8) + jj]:
                entropy[ii] |= 1 << (7 - jj)
    # Take the digest of the entropy.
    hashBytes = hashlib.sha256(entropy).digest()
    hashBits = list(itertools.chain.from_iterable(([c & (1 << (7 - i)) != 0 for i in range(8)] for c in hashBytes)))
    # Check all the checksum bits.
    for i in range(checksumLengthBits):
        if concatBits[entropyLengthBits + i] != hashBits[i]:
            raise exceptions.Value("Failed checksum.")
    return bytes(entropy)


def key_to_mnemonic(key, wordlist=wordlist):
    """Convert the passed key to memorizable mnemonic.

    Args:
        key (bytes): The key to be encoded.
        wordlist (list[str], optional): The wordlist used to generate the mnemonic. Defaults to wordlist.

    Returns:
        str: A string of space separated words representing the mnemonic
    """
    h = hashlib.sha256(key).hexdigest()
    b = (
        bin(int(binascii.hexlify(key), 16))[2:].zfill(len(key) * 8)
        + bin(int(h, 16))[2:].zfill(256)[: len(key) * 8 // 32]
    )
    result = []
    for i in range(len(b) // 11):
        idx = int(b[i * 11 : (i + 1) * 11], 2)
        result.append(wordlist[idx])
    result_phrase = " ".join(result)
    return result_phrase


def to_bin(arr, bytelen=8):
    """Converts an iterable object containing ascii codes to a string of the joint binary representation of each item where each of them is of length bytelen

    Args:
        arr (iterable): The array to be converted
        bytelen (int, optional): The length of the to-be-created binary string of each item. Defaults to 8.

    Returns:
        string: The binary string representing the array
    """
    result = ""
    for c in arr:
        result += ("0" * bytelen + bin(c)[2:])[-bytelen:]
    return result


def binary_search(a, x, lo=0, hi=None):  # can't use a to specify default for hi
    hi = hi if hi is not None else len(a)  # hi defaults to len(a)
    pos = bisect.bisect_left(a, x, lo, hi)  # find insertion position
    return pos if pos != hi and a[pos] == x else -1  # don't walk off the end
