import itertools

HASH_INIT_CONSTANT = 5381
HASH_MULT_CONSTANT = 33


def php_hash(inputstring, hash_table_size, init=HASH_INIT_CONSTANT):
    result = init
    for c in inputstring:
        result *= HASH_MULT_CONSTANT
        result += ord(c)
    result %= 2**32
    return result % hash_table_size


def generate_php_hash_strings(generator, half_length, hash_table_size, init=HASH_INIT_CONSTANT):
    """Returns a dict from hash outputs to strings of half_length. Each string is hashed with the PHP hash with initial
       value init. All strings of length half_length producable by the generator are processed in this way, so the size
       of the result set and amount of time and memory required grows exponentially as half_length increases"""
    keys = {}
    characters = generator.get_char_set()
    for s in itertools.product(characters, repeat=half_length):
        hash_val = php_hash(s, hash_table_size, init)
        keys[hash_val] = ''.join(s)
    return keys


def generate_via_half_string_merge(generator, target, collisions, hash_table_size, forbidden_string, init=HASH_INIT_CONSTANT):
    """Returns a list of strings that meet the required hash target, excluding the forbidden string. Uses the approach
       of generating half of the string and then finding a potential match based on the additive nature of the hash
       function.  Incrementally increases the length of the string to favor small strings.  Can suffer from exponential
       time and memory usage."""
    length = 1  # We could start higher but the first lengths go quickly so why not?
    pre_keys = {0: ''}
    post_keys = generate_php_hash_strings(generator, 1, hash_table_size, init)
    multfactor = HASH_MULT_CONSTANT  # should always equal 33 ** length of post_strings in post_keys
    ret = []

    while True:
        for pre_hash in pre_keys:

            effective_target = (target - (pre_hash * multfactor) + init * multfactor) % hash_table_size
            if effective_target in post_keys:
                pre_string = pre_keys[pre_hash]
                post_string = post_keys[effective_target]
                if pre_string + post_string != forbidden_string:
                    ret.append(pre_string + post_string)
                if len(ret) >= collisions:
                    ret.sort()
                    return ret

        # Update variables for next loop
        length += 1
        pre_keys = post_keys  # pre_keys follows 1 step behind post_keys
        if length % 2 != 0:
            # we update to a longer post_keys at every odd length
            post_keys = generate_php_hash_strings(generator, 1 + length // 2, hash_table_size, init)
            multfactor *= HASH_MULT_CONSTANT
