import math
import sys
import time
import random
import hashlib
from typing import Optional


class U3IDFactory:

    def __init__(
            self,
            timestamp_integer_part_length_bits: int = 32,
            timestamp_decimal_part_length_bits: int = 10,
            total_length_bits: int = 128,
    ):

        if total_length_bits < timestamp_integer_part_length_bits + timestamp_decimal_part_length_bits:
            raise ValueError(f"total_length_bits must greater than or equal to timestamp_integer_part_length_bits + timestamp_decimal_part_length_bits."
                             f"Values provided (timestamp_integer_part_length_bits: {timestamp_integer_part_length_bits}, "
                             f"timestamp_decimal_part_length_bits: {timestamp_decimal_part_length_bits},"
                             f"total_length_bits: {total_length_bits}")

        if total_length_bits % 8 != 0:
            raise ValueError("total_length_bits must equate to a whole number of bytes")

        self.timestamp_integer_part_length_bits = timestamp_integer_part_length_bits

        if timestamp_decimal_part_length_bits > 30:
            raise ValueError("timestamp_decimal_part_length_bits must be less than or equal 30 because that is the maximum precision for ns.")

        self.timestamp_decimal_part_length_bits = timestamp_decimal_part_length_bits
        self.total_length_bits = total_length_bits
        self.previous_ordered_component = '0'
        self.previous_chaotic_component = None
        self.chaotic_part_length_bits = total_length_bits-timestamp_integer_part_length_bits-timestamp_decimal_part_length_bits

        if self.chaotic_part_length_bits > 512:
            raise ValueError(f"The chaotic part can be at most 512 bits in length when user is providing seed. (using SHA512 hashing) "
                             f"Values provided (self.chaotic_part_length_bits: {self.chaotic_part_length_bits})")

    def generate(
            self,
            integer_time_part: Optional[int] = None,
            decimal_time_part_nanoseconds: Optional[int] = None,
            chaotic_part_seed: Optional[str] = None

            # ordered_component=int(time.time() * 1000),
            # chaotic_component=None,
    ):

        if integer_time_part is None:
            integer_time_part = int(time.time())

        if decimal_time_part_nanoseconds is None:
            decimal_time_part_nanoseconds = int((time.time() % 1)* 10**9) << 32
            fractional_time_part = int(decimal_time_part_nanoseconds/1000000000)
        else:
            if decimal_time_part_nanoseconds > 999999999 or decimal_time_part_nanoseconds < 0:
                raise ValueError("When providing a time component, the decimal time part must be specified in ns and be between 0 and 999,999,999")

            fractional_time_part = int((decimal_time_part_nanoseconds << 32) / 1000000000)

        if chaotic_part_seed is None:
            chaotic_bits = random.SystemRandom().getrandbits(self.chaotic_part_length_bits)
            chaotic_bits = f'{{0:>0{self.chaotic_part_length_bits}b}}'.format(chaotic_bits)[:self.chaotic_part_length_bits]
        else:
            h = hashlib.sha512()
            h.update(str.encode(chaotic_part_seed))
            chaotic_bytes = h.digest()

            chaotic_bits = ""

            byte_order = sys.byteorder

            if byte_order == "little":
                for i in range(len(chaotic_bytes)-1, -1, -1):
                    byte = chaotic_bytes[i]
                    chaotic_bits += f'{byte:0>8b}'
                chaotic_bits = chaotic_bits[:self.chaotic_part_length_bits]
            else:
                for i in range(0, len(chaotic_bytes), 1):
                    byte = chaotic_bytes[i]
                    chaotic_bits += f'{byte:0>8b}'
                chaotic_bits = chaotic_bits[-1*self.chaotic_part_length_bits:]



        # Format the integer time component into a binary string of set length. crop from left if too long, pad left with 0 if too short.
        if self.timestamp_integer_part_length_bits > 0:
            integer_time_part_binary_string = f'{{0:>0{self.timestamp_integer_part_length_bits}b}}'.format(integer_time_part)[-1*self.timestamp_integer_part_length_bits:]
            integer_time_part_binary_string = integer_time_part_binary_string.zfill(self.timestamp_integer_part_length_bits)
        else:
            integer_time_part_binary_string = ""

        # Format the decimal time component into a binary string of set length
        if self.timestamp_decimal_part_length_bits > 0:
            fractional_time_part_binary_string = f'{{0:>0{32}b}}'.format(fractional_time_part)[:self.timestamp_decimal_part_length_bits]
        else:
            fractional_time_part_binary_string = ""

        time_component = integer_time_part_binary_string + fractional_time_part_binary_string

        full_binary_string = time_component + chaotic_bits

        return U3ID(full_binary_string)

class U3ID:

    def __init__(self, binary_string):
        self.binary_string = binary_string

    @property
    def bytes(self):
        raise NotImplementedError()

    def to_binary_string(self):
        return self.binary_string

    def to_hex(self):
        return '{:0{}x}'.format(int(self.binary_string, 2), len(self.binary_string) // 4)

    def to_uuid(self):
        hex_string = self.to_hex()
        return f'{hex_string[0:8]}-{hex_string[8:12]}-{hex_string[12:16]}-{hex_string[16:20]}-{hex_string[20:32]}'

    def __repr__(self) -> str:
        return f"{type(self).__name__}(<{self.to_hex()}>)"


if __name__ == "__main__":
    test_class = U3IDFactory(
        timestamp_integer_part_length_bits = 32,
        timestamp_decimal_part_length_bits = 10,
        total_length_bits = 128,
    )

    integer_time_part = 100
    decimal_time_part_nanoseconds = 999999999
    chaotic_part_seed = "this is a test"

    u3id = test_class.generate(chaotic_part_seed='this is a test')
    u3id = test_class.generate()

    u3id = test_class.generate(
        integer_time_part=integer_time_part,
        decimal_time_part_nanoseconds=decimal_time_part_nanoseconds,
        chaotic_part_seed=chaotic_part_seed
    )

    u3id_bytes = u3id.bytes
    bits = ""
    for i in range(len(u3id_bytes) - 1):
        byte = u3id_bytes[i]
        bits += f'{byte:0>8b}'
    print("py:")
    print(bits)

    print(u3id)


