<?php

// debug
set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
    throw new ErrorException($errstr, $errno, 0, $errfile, $errline);
});

$charset = "abcdefghijklmnopqrstuvwxyz0123456789";
$alphabet = PassCracker::createAlphabet($charset);
$hash = "e7806b130b429fc9b5890608a2c60675";

$cracker = new PassCracker(4, $alphabet, false);
$answer = $cracker->crack($hash);
echo $answer !== null ? 
    "Answer is $answer\n" : 
    "This time we're out of luck. Try googling: http://google.com/?q=$hash\n";

class PassCracker
{
    private $length;
    private $alphabet;

    private $passwordIndexes;
    private $passwordLetters;

    public function __construct($length, array $alphabet)
    {
        $this->length = $length;
        $this->alphabet = $alphabet;
    }

    public static function createAlphabet($string)
    {
        return preg_split("//u", $string, null, PREG_SPLIT_NO_EMPTY);
    }

    private function initCode()
    {
        $this->passwordIndexes = array_fill(0, $this->length, 0);
        $this->generatePassword();
    }

    private function generatePassword()
    {
        $this->passwordLetters = array_map(function ($index) {
            return $this->alphabet[$index];
        }, $this->passwordIndexes);
    }

    public function crack($hash)
    {
        $this->initCode();
        $hash = mb_strtolower(trim($hash)); // everything might happen

        do {

            $password = implode('', $this->passwordLetters);
            if (md5($password) == $hash) {
                return $password;
            }            

            if (!$this->generateNextCode()) {
                return null;
            };

        } while (true);

    }

    private function generateNextCode() {
        $max = count($this->alphabet);
        for ($pos = $this->length - 1; $pos >= 0; $pos--) {

            $newIndex = $this->passwordIndexes[$pos] = ($this->passwordIndexes[$pos] + 1) % $max;
            $this->passwordLetters[$pos] = $this->alphabet[$newIndex];

            if ($newIndex > 0) {
                return true;
            }
        }

        // No more tries
        return false;
    }
}
