#!/usr/bin/env python3
"""Parallel reverse-MD5 demo for Parallel Python.

Website: https://www.parallelpython.com
License: Apache-2.0 (see LICENSE and NOTICE)

Computes the MD5 of a known number, then fans the search space out to the
job server in equal slices to find which integer reproduces the hash.

Usage: python reverse_md5.py [ncpus]
    [ncpus] - the number of workers to run in parallel,
    if omitted it will be set to the number of processors in the system
"""

import sys

import pp


def md5test(hash_value, start, end):
    """Return the integer in [start, end) whose MD5 matches *hash_value*."""
    import hashlib

    for x in range(start, end):
        if hashlib.md5(str(x).encode("utf-8")).hexdigest() == hash_value:
            return x


def main():
    print("Usage: python reverse_md5.py [ncpus]")
    print("    [ncpus] - the number of workers to run in parallel,")
    print("    if omitted it will be set to the number of processors in the system")

    # tuple of all parallel python servers to connect with
    # ppservers = ("*",)              # auto-discover
    # ppservers = ("10.0.0.1",)      # static IPs
    ppservers = ()

    if len(sys.argv) > 1:
        ncpus = int(sys.argv[1])
        # Creates jobserver with ncpus workers
        job_server = pp.Server(ncpus, ppservers=ppservers)
    else:
        # Creates jobserver with automatically detected number of workers
        job_server = pp.Server(ppservers=ppservers)

    print("Starting pp with", job_server.get_ncpus(), "workers")

    import hashlib

    # Calculate the md5 hash of the target number
    hash_value = hashlib.md5(b"1829182").hexdigest()
    print("hash =", hash_value)

    start = 1
    end = 2000000

    # Jobs are not equal in execution time, so dividing the problem into
    # many small subproblems leads to better load balancing.
    parts = 128
    step = (end - start) // parts + 1

    jobs = []
    for index in range(parts):
        starti = start + index * step
        endi = min(start + (index + 1) * step, end)
        # Submit a job that checks whether a number in [starti, endi)
        # has the given md5 hash.
        jobs.append(job_server.submit(md5test, (hash_value, starti, endi)))

    # Retrieve results of all submitted jobs
    result = None
    for job in jobs:
        result = job()
        if result:
            break

    if result:
        print("Reverse md5 for", hash_value, "is", result)
    else:
        print("Reverse md5 for", hash_value, "has not been found")

    job_server.print_stats()

    # Properly finalize all tasks (not strictly necessary)
    job_server.wait()
    job_server.destroy()


if __name__ == "__main__":
    main()
