from hashlib import blake2b, shake_128, md5, sha256, sha1
from itertools import combinations
from urllib.parse import urlencode
import random
import time
from base64 import b64encode, b64decode, b85encode, b85decode
import base32_lib as b32

DIGEST_SIZE_BYTES = 3
DIGEST_SIZE_BITMASK = 0xffffff
DIGEST_SIZE_SIGNED_TO_UNSIGNED_BITMASK = 0x1ffffff
DIGEST_SIZE_SIGNED_TO_UNSIGNED_BIT = 0x1000000
DIGEST_SIZE_NIBBLES = DIGEST_SIZE_BYTES * 2
B64ALTCHARS = b'.-'

def sha1hash(data: str):
    return sha1(data.encode("utf-8"), usedforsecurity=False).hexdigest()[:DIGEST_SIZE_BYTES]

def sha256hash(data: str):
    return sha256(data.encode("utf-8"), usedforsecurity=False).hexdigest()[:DIGEST_SIZE_BYTES]

def md5hash(data: str):
    return md5(data.encode("utf-8"), usedforsecurity=False).hexdigest()[:DIGEST_SIZE_BYTES]

def shake(data: str):
    return shake_128(data.encode("utf-8"), usedforsecurity=False).hexdigest(DIGEST_SIZE_BYTES)

def blake(data: bytes, person: bytes = None) -> bytes:
    return blake2b(
        data,
        usedforsecurity=False,
        digest_size=DIGEST_SIZE_BYTES,
        person=person
    ).digest()

def python(data: str):
    return hash(data)

def normalize_hash(_hash: int) -> int:
    #hex = hash_to_hex(_hash)
    #return int(hex, 16)
    #_bytes = _hash.to_bytes(8, byteorder='big', signed=True)
    #return bytes_to_hash(_bytes)
    return _hash & DIGEST_SIZE_BITMASK

def normalize_bytes(_bytes: bytes) -> bytes:
    return (b'\x00' * DIGEST_SIZE_BYTES + _bytes)[-DIGEST_SIZE_BYTES:]

def normalize_hex(_hex: str) -> str:
    #_bytes = hex_to_bytes(hex)
    #return _bytes.hex()
    return _hex.zfill(DIGEST_SIZE_NIBBLES)[-DIGEST_SIZE_NIBBLES:]

def hex_to_bytes(_hex: str) -> bytes:
    _bytes = bytes.fromhex(_hex.zfill(DIGEST_SIZE_NIBBLES))
    return normalize_bytes(_bytes)

def bytes_to_hex(_bytes: bytes) -> str:
    return normalize_bytes(_bytes).hex()

def hash_to_bytes(_hash: int) -> bytes:
    _bytes = _hash.to_bytes(8, byteorder='big', signed=True)
    return normalize_bytes(_bytes)

def bytes_to_hash(_bytes: bytes) -> int:
    norm = normalize_bytes(_bytes)
    return int.from_bytes(norm, byteorder='big', signed=False)

def hash_to_hex(_hash: int) -> str:
    #return hash_to_bytes(_hash).hex()
    #return normalize_hex(
    #return f"{_hash + (1 << 64):x}"[-4:]
    #return hex(_hash + (1<<64))[2:][-4:]
    #return f"{_hash & 0xffff:04x}"
    return hex((_hash|DIGEST_SIZE_SIGNED_TO_UNSIGNED_BIT) & DIGEST_SIZE_SIGNED_TO_UNSIGNED_BITMASK)[3:]

def hex_to_hash(_hex: str) -> int:
    #_bytes = bytes.fromhex(hex.zfill(4))
    #return bytes_to_hash(_bytes)
    #return int(normalize_hex(hex), 16)
    return int(_hex, 16) & DIGEST_SIZE_BITMASK

def remove_padding(f):
    def wrap(*args, **kwargs):
        return f(*args, **kwargs).split('=')[0]
    return wrap

def fix_padding(f):
    def wrap(_b64, *args, **kwargs):
        pad = (4 - len(_b64)) % 4
        fixed = _b64 + '='*pad
        return f(fixed, *args, **kwargs)
    return wrap

def normalize_base32(_b32: str):
    return _b32.upper().zfill(DIGEST_SIZE_BYTES*8//5+1)

def add_padding_base32(f):
    def wrap(*args, **kwargs):
        return normalize_base32(f(*args, **kwargs))
    return wrap

@remove_padding
def hash_to_base64(_hash: int) -> str:
    return b64encode(hash_to_bytes(_hash), altchars=B64ALTCHARS).decode("utf-8")

@remove_padding
def hex_to_base64(_hex: str) -> str:
    return b64encode(hex_to_bytes(_hex), altchars=B64ALTCHARS).decode("utf-8")

@remove_padding
def bytes_to_base64(_bytes: str) -> str:
    return b64encode(normalize_bytes(_bytes), altchars=B64ALTCHARS).decode("utf-8")

@fix_padding
def base64_to_hash(_b64: str) -> str:
    return bytes_to_hash(b64decode(_b64, altchars=B64ALTCHARS))

@fix_padding
def base64_to_hex(_b64: str) -> str:
    return bytes_to_hex(b64decode(_b64, altchars=B64ALTCHARS))

@fix_padding
def base64_to_bytes(_b64: str) -> str:
    return normalize_bytes(b64decode(_b64, altchars=B64ALTCHARS))

#@remove_padding
def hash_to_base85(_hash: int) -> str:
    return b85encode(hash_to_bytes(_hash)).decode("utf-8")

#@remove_padding
def hex_to_base85(_hex: str) -> str:
    return b85encode(hex_to_bytes(_hex)).decode("utf-8")

#@remove_padding
def bytes_to_base85(_bytes: str) -> str:
    return b85encode(normalize_bytes(_bytes)).decode("utf-8")

#@fix_padding
def base85_to_hash(_b64: str) -> str:
    return bytes_to_hash(b85decode(_b64))

#@fix_padding
def base85_to_hex(_b64: str) -> str:
    return bytes_to_hex(b85decode(_b64))

#@fix_padding
def base85_to_bytes(_b64: str) -> str:
    return normalize_bytes(b85decode(_b64))

@add_padding_base32
def hash_to_base32(_hash: int) -> str:
    return b32.encode(_hash & DIGEST_SIZE_BITMASK)

@add_padding_base32
def hex_to_base32(_hex: str) -> str:
    return b32.encode(hex_to_hash(_hex))

@add_padding_base32
def bytes_to_base32(_bytes: bytes) -> str:
    return b32.encode(bytes_to_hash(_bytes))

#@fix_padding
def base32_to_hash(_b64: str) -> str:
    return b32.decode(_b64)

#@fix_padding
def base32_to_hex(_b64: str) -> str:
    return hash_to_hex(base32_to_hash(_b64))

#@fix_padding
def base32_to_bytes(_b64: str) -> str:
    return hash_to_bytes(base32_to_hash(_b64))

print(105, hash(105))
print(base64_to_hex('aa'))
print(hash_to_base64(105))
print(hex_to_base64('69'))

print(1421958803217889556, hash(1421958803217889556))
print(hash_to_base64(1421958803217889556))
print(hex_to_base64('13bbcfff670ab914'))

print('a', hash('a'))
print(hash_to_base64(9675337))
print(hex_to_base64('0093a249'))
print(hex_to_base64('93a249'))
print(bytes_to_base64(b'\x93\xa2I'))
print(bytes_to_base64(b'\x00\x93\xa2I'))
print(hash_to_base85(9675337))
print(base85_to_hex('lcGr'))
print(hash_to_base32(9675337))
print(base32_to_hash('978J9'))
print(base32_to_hex('978J9'))
print(hash_to_base32(0))
print(hash_to_base32(10))
print(hash_to_base32(100))
print(hash_to_base32(1000))
print(hash_to_base32(10000))

products = { k for k in """
Adzuki Beans|Alberts Eggs|Almonds|Apple Cider Vinegar|Apples|Apricots|Arrowroot Powder|Avocado|Baking Soda|Bananas|Barn Eggs|Bars|Beans Canned|Beef Chuck|Beef Jerky|Beef Mince|Beef Patties|Beef Roast|Beef Sausages|Beef Scotch Fillet|Beef Shin|Beef Stir Fry|Beef Topside|Beetroot|Beetroot Canned|Blade Steak|Blueberries Frozen|Brazil Nuts|Brewers Yeast|Brisket|Broccoli|Brown Lentils|Brown Rice|Brown Sugar|Buckwheat Groats|Buns|Burger Patties|Butter|Butternut Squash|Cabbage|Cacao Powder|Cage Free Eggs|Candied Ginger|Canned Apricots|Canned Peaches|Canned Salmon|Canned Tomatoes|Canned Tuna|Capsicum|Cardamon|Carrots|Cashews|Cauliflower|Cayenne Powder|Celery|Chicken Drumsticks|Chicken Frames|Chicken Mince|Chicken Nibbles|Chicken Stir Fry|Chicken Tender|Chicken Thighs|Chicken Thighs BL|Chickpeas|Chocolate Chips|Cinnamon, Ground|Coconut Cream|Coconut Desiccated|Coconut Oil|Coconut Water|Coconut Yoghurt|Coffee Beans|Colby Cheese|Cookie Pack|Cookie Time|Corn Chips|Corned Beef|Cornflour|Courgettes|Crackers|Cream|Cruskits|Cucumber|Curry Paste|Dark Chocolate|Demerara Sugar|Doritos|Dried Dates|Dried Figs|Dry Peas|Edam Cheese|Eggplant|FR Chicken Breast|FR Chicken Drumsticks|FR Chicken Mince|FR Chicken Thighs|FR Chicken Whole|Feta|Fish Fillets|Fish Frames|Fish Heads|Fish Sauce|Flax Seeds|Free Range Eggs|French Lentils|Frozen Cherries|Frozen Green Beans|Frozen Peas|Frozen Pop|Fruit Bars|Garam Masala|Garlic|Ginger|Ginger Beer|Ginger Ground|Hemp Seeds|Herbal Tea|Honey|Hot Chilli|Hummus|Ice Cream|Jalapenos Canned|Jar of Chillis|Kassori Methi|Kelp|Kiwifruit|Kumara|Lamb Cubes|Lamb Mince|Lamb Pieces|Lamb Shank|Lamb Shoulder|M&Ms Mini|Magnesium Fizz|Mandarins|Mango|Maple Syrup|Marmite|Mayo|Meatballs|Mild Cheese|Milk Standard|Mixed Fruit Frozen|Mixed Nuts|Molasses|Monkfruit|Mozzarella Cheese|Mung Beans|Mushrooms|Mutton Chops|Neck Chops|Nut Bars|Nutritional Yeast|OSM bars|Oat Bars|Olive Oil|Olives Bag|Olives Jar|Onions|Orange Juice|Oranges|Oreo Cookies|Organic Cheese|Organic Chicken|Paprika|Parsnips|Pasta|Peaches|Peanutbutter|Peanuts|Pears|Pinenuts|Pizza Bases|Plums|Potato Crisps|Potatoes|Powdered Milk|Prunes|Puffed Rice|Pukka Tea|Pumpkin Seeds|Quinoa|Raw Milk|Red Lentils|Rice Crackers|Rice Noodles|Rooibos Tea|Rye Flour|Ryecorn|Salad Bag|Salami|Salsa|Salt|Saurkraut|Seaweed|Smoked Salmon|Snack Balls|Soda Syrup|Soup Refrigerated|Soy Sauce|Squash|Starter|Strawberries|Strawberries Frozen|Sugar, Coconut|Sultanas|Sunflower Oil|Sunflower Seeds|TNCC Lollies|Tasty Cheese|Tomato Fresh|Tomato Juice|Tomato Paste|Tomato Sauce|Tonic Water|Tulsi Tea|Tumeric|Vanilla|Veggie Crisps|Vinegar|Vintage Cheese|Walnut|Water|Watermelon|Wheat Berries|White Flour|White Rice|White Sugar|Whittakers Chocolate|Whole Chicken|Whole Fish|Whole Oats|Wholemeal Flour|Xcut Blade Steak|Yoghurt Dairy
""".strip().split('|')
}

def find_collision(f=python, norm=normalize_hash, how_many=None, **kwargs):
    seen = dict()
    random.shuffle(how_many)
    for i in how_many:
        for idx, j in enumerate(combinations(products, i)):
            if idx > 10000:
                break
            url = urlencode([
                ('product', k) for k in j
            ])
            _hash = f(url, **kwargs)
            _hash = norm(_hash) if callable(norm) else _hash
            if _hash in seen:
                return url, _hash, seen
            #assert _hash not in seen, len(seen)
            #assert _hash not in seen, f"{url} ({_hash}) == {seen[_hash]} ({len(seen)})"
            seen[_hash] = url
    return None, None, seen

# trials = 20
# for name, args, kwargs in [
#     ("Python", [], dict()),
#     ("Shake", [shake], {'norm': hex_to_hash}),
#     ("Blake", [blake], {'norm': hex_to_hash, 'person': 'trend'.encode("utf-8")}),
# ]:
#     start = time.time()
#     random.seed(0)
#     score = 0
#     _min = 0
#     print(name)
#     for i in range(trials):
#         url, _hash, seen = find_collision(*args, **kwargs, how_many=list(range(len(products))))
#         n = len(seen)
#         print(n)
#         if n < 30000:
#             print(url)
#             print(seen[_hash])
#         score += n
#         _min = _min if _min < n and _min else n
#     end = time.time()
#     print(f"Score: {_min} / {score/trials}")
#     print(f"Time: {end - start}")

# exit(0)


if __name__ == '__main__':
    import timeit
    # For Python>=3.5 one can also write:
    #print(timeit.timeit("normalize_bytes(b'a')", globals=locals(), number=5000000))
    for method, arg in [
        ('normalize_hash', "hash('a')"),
        ('normalize_hex', "'a'"),
        ('normalize_bytes', r"b'\x0a'"),
        #('hex_to_hash', "'a'"),
        #('hex_to_bytes', "'a'"),
        #('bytes_to_hash', r"b'\n'"),
        #('bytes_to_hex', r"b'\n'"),
        #('hash_to_bytes', "10"),
        #('hash_to_hex', "10"),
        # ('base64_to_hash', "'aa'"),
        # ('base64_to_bytes', "'aa'"),
        # ('base64_to_hex', "'aa'"),
        # ('bytes_to_base64', r"b'\n'"),
        # ('hex_to_base64', "'a'"),
        # ('hash_to_base64', "10"),
        # ('base85_to_hash', "'aa'"),
        # ('base85_to_bytes', "'aa'"),
        # ('base85_to_hex', "'aa'"),
        # ('bytes_to_base85', r"b'\n'"),
        # ('hex_to_base85', "'a'"),
        # ('hash_to_base85', "10"),
        ('base32_to_hash', "'aa'"),
        ('base32_to_bytes', "'aa'"),
        ('base32_to_hex', "'aa'"),
        ('bytes_to_base32', r"b'\n'"),
        ('hex_to_base32', "'a'"),
        ('hash_to_base32', "10"),
        #('shake', "'abc1234567890&|abcdefghijklmnopqrstuvwxyz'"),
        #('blake', "'abc1234567890&|abcdefghijklmnopqrstuvwxyz'"),
        #('md5hash', "'abc1234567890&|abcdefghijklmnopqrstuvwxyz'"),
        #('sha256hash', "'abc1234567890&|abcdefghijklmnopqrstuvwxyz'"),
        #('sha1hash', "'abc1234567890&|abcdefghijklmnopqrstuvwxyz'"),
        #('python', "'abc1234567890&|abcdefghijklmnopqrstuvwxyz'"),
    ]:
        test = f"{method}({arg})"
        elapsed = timeit.timeit(test, globals=locals(), number=5000000)
        print(f"{method}: {elapsed}")