<?php
// {{{ license
/**
 * SyberIsle Code Generator
 * Copyright 2011, SyberIsle Productions, <http://www.syberisle.net>
 *
 * Redistributions of files is expressly prohibited without prior written consent
 * of SyberIsle Productions.
 *
 * @copyright     Copyright 2011, SyberIsle Productions, http://www.syberisle.net>
 * @package       SyberIsle_Code_Generator
 * @author        David Lundgren <dlundgren@syberisle.net>
 * @license       Please contact SyberIsle Productions or David Lundgren for licensing details.
 */
// }}}
// {{{ definitions

define('SIP_CODE_GENERATOR_TYPE_NUMBER',            'n');
define('SIP_CODE_GENERATOR_TYPE_SEQUENCE',          's');
define('SIP_CODE_GENERATOR_TYPE_REGULAR_CHARSET',   'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
define('SIP_CODE_GENERATOR_TYPE_CONSONANTS',        'bcdfghjklmnpqrstvwxyz');
define('SIP_CODE_GENERATOR_TYPE_VOWELS',            'aeiou');
define('SIP_CODE_GENERATOR_TYPE_CONSONANTS_NO_Y',   'bcdfghjklmnpqrstvwxz');
define('SIP_CODE_GENERATOR_TYPE_VOWELS_Y',          'aeiouy');
define('SIP_CODE_GENERATOR_TYPE_CONSONANTS_U',      'BCDFGHJKLMNPQRSTVWXYZ');
define('SIP_CODE_GENERATOR_TYPE_VOWELS_U',          'AEIOU');

define('SIP_CODE_GENERATOR_TYPE_CONSONANTS_M',      'bcdfghjkmnpqrstvwxyz');
define('SIP_CODE_GENERATOR_TYPE_VOWELS_M',          'aeu');
define('SIP_CODE_GENERATOR_TYPE_CONSONANTS_M_NO_Y''bcdfghjkmnpqrstvwxz');
define('SIP_CODE_GENERATOR_TYPE_VOWELS_M_Y',        'aeuy');

// }}}
// {{{ SyberIsle_Code_Generator class
/**
 * Generates codes based on the given string using a format mask.
 *
 * The mask formats are as follows:
 * {W:....} = comma separated list of words to use
 * {#:x,?,?} = random number
 *   x = n number
 *       s series (start - end)
 *   ? = starting number (default: SYSTEM SET)
 *   ? = ending number (default: SYSTEM SET)
 * {L:x,?,?} = random string using temp characters
 *   x = temporary character set
 *   ? = min length (default: SYSTEM SET)
 *   ? = max length (default: SYSTEM SET)
 * {N:x,?,?} = random string
 *   x = character sets (vVcCRS)
 *   ? = min length (default: SYSTEM SET)
 *   ? = max length (default: SYSTEM SET)
 * {P:x,?,?} = pronouncable character starting with vVcC
 *   x = character sets (vVcC)
 *   ? = min length (default: SYSTEM SET)
 *   ? = max length (default: SYSTEM SET)
 * {S:?,?} = insert STRING into string
 *   ? = string to insert
 *   ? = insert every so many
 * {C:x,?,?} = modify the case
 *   x = l lowercase
 *       u uppercase
 *       m mixedcase
 *   ? = start character
 *   ? = end character
 *
 */
final class SyberIsle_Code_Generator
{
    
// {{{ properties

    /**
     * Any error messages are place here.
     * @var string
     */
    
public $error;

    
/**
     * The current position in a sequence.
     * @var integer
     */
    
private $_sequence = array();

    
/**
     * Has the MT PRNG been seeded.
     * @var boolean
     */
    
private $_seeded false;

    
/**
     * Caches the code formats
     *
     * @var array
     */
    
private $_cache = array();

    
/**
     * The current code that is being generated.
     * @var string
     */
    
private $_current null;

    
// }}}
    // {{{ functions: borrowed
    /**
     * @author Oleg Butuzov butuzov at made dot com dot ua
     * @date   01-Feb-2011 03:26
     * @url    http://www.php.net/manual/en/function.str-replace.php#102186
     */
    
private function str_replace_once($in_search$in_replace$in_string)
    {
        
$s_pos strpos($in_string$in_search);
        if (
$s_pos !== false){
            return 
substr_replace($in_string$in_replace$s_posstrlen($in_search));
        }
        return 
$in_string;
    }

    
// }}}
    // {{{ functions: private

    /**
     * Generates the seed information.
     *
     * TODO: detect CLI and modify accordingly.
     *
     * @returns void
     */
    
private function do_seed()
    {
        if (
$this->_seeded) {
            return;
        }

        
$s_id session_id();
        list(
$usec$sec) = explode(' 'microtime());
          
$t1 = (float) $sec + ((float) $usec 100000);
        
$s1 implode(unpack('L*',$_SERVER['SERVER_NAME'].$_SERVER['SERVER_ADDR'].$s_id.$t1),'');
        
$s2 implode(unpack('L*',$_SERVER['REMOTE_ADDR'].$_SERVER['REQUEST_TIME'].$t1),'');

        
// get a SHA256 seed based on the current values
        
$s_seed implode(unpack('L*',mhash(Constant('MHASH_SHA256'),$s1$s2)),'');

        
mt_srand($s_seed);
        
$this->_seeded true;
    }

    
// }}}
    // {{{ functions
    /**
     * Checks if the code format is valid.
     *
     * @param   string  $in_string The string to be checked.
     * @returns boolean True on valid, False otherwise.
     */
    
public function isValidCodeFormat($in_string)
    {
        
// Unmatched brackets
        
if ( ! preg_match('/^((?:[^{}]|\{(?1)\})*+)$/'$in_string)) {
            
$this->error "Unmatched brackets: <strong>$in_string</strong>";
            
trigger_error($this->errorE_USER_WARNING);
            return 
false;
        }

        
// Nothing to generate
        
if ( ! preg_match('/{(([#CLNPSW])(:([^}]*))?)}/'$in_string$a_matches)) {
            
$this->error "No generator rules";
            return 
false;
        }

        
$this->_format[$in_string] = true;
        return 
true;
    }
    
/**
     * Generates a code based on the string and returns it.
     *
     * @param   string $in_string The string to generate a code from.
     * @returns string The generated code.
     */
    
public function generateCode($in_string)
    {
        if ( ! 
$this->isValidCodeFormat($in_string)) {
            return 
false;
        }

        if ( ! empty(
$this->_cache[$in_string])) {
            
$a_matches $this->_cache[$in_string];
        }
        else {
            
preg_match_all('/{(([#CLNPSW])(:([^}]*))?)}/'$in_string$a_matches);
            
$this->_cache[$in_string] = $a_matches;
        }

        
$this->_current $in_string;

        
// cycle through the matches and make replacements as necessary
        
foreach($a_matches[2] as $s_key => $s_val) {
            
$tmp false;
            
$s_params $a_matches[4][$s_key];
            
$s_replace $a_matches[0][$s_key];

            switch(
$s_val) {
                case 
'#'// Number
                    
$tmp $this->generateNumber($s_params);
                    break;
                case 
'C'// Modify case of current string
                    
$s_pos strpos($in_string$a_matches[0][$s_key]);
                    
$s_string substr($in_string0$s_pos);
                    
$tmp $this->modifyCase($s_params$s_string);
                    
$s_replace $s_string $a_matches[0][$s_key];
                    break;
                case 
'L'// Insert temporary character string
                    
$tmp $this->generateString($s_paramstrue);
                    break;
                case 
'N'// Insert normal random string
                    
$tmp $this->generateString($s_params);
                    break;
                case 
'P'// Insert pronouncable string
                    
$tmp $this->generatePronouncableString($s_params);
                    break;
                case 
'S'// Insert STRING into string
                    
$s_pos strpos($in_string$a_matches[0][$s_key]);
                    
$s_string substr($in_string0$s_pos);
                    
$tmp $this->insertString($s_params$s_string);
                    
$s_replace $s_string $a_matches[0][$s_key];
                    break;
                case 
'W'// insert one of the words
                    
$tmp $this->insertWord($s_params);
                    break;
            }

            if (
false === $tmp && $this->error) {
                return 
false;
            }
            elseif (
false === $tmp) {
                continue;
            }
            
$in_string $this->str_replace_once($s_replace$tmp$in_string);
        }
        return 
$in_string;
    }

    
/**
     * Generates a random number. Uses the following rules
     *
     * {#:x,?,?} = random number
     *   x = n number
     *       s series (start - end)
     *   ? = starting number (default: SYSTEM SET)
     *   ? = ending number (default: SYSTEM SET)
     *
     * @param   string $in_params The parameters from the code string.
     * @returns string The generated number.
     */
    
protected function generateNumber($in_params 'n,0,100')
    {
        if (empty(
$this->_sequence[$this->_current])) {
            
$this->_sequence[$this->_current] = 0;
        }
        
$s_sequence =& $this->_sequence[$this->_current];

        
// parse the parameters
        
list($in_type$in_min$in_max) = explode(','$in_params);
        
$in_type = empty($in_type) ? SIP_CODE_GENERATOR_TYPE_NUMBER $in_type;
        
$in_min  = empty($in_min) ? $in_min;        
        
$in_max  = empty($in_max) ? 100 $in_max;

        
// generate the number
        
if (SIP_CODE_GENERATOR_TYPE_SEQUENCE == $in_type && $s_sequence <= $in_max) {
            return 
$s_sequence++;
        }
        elseif (
SIP_CODE_GENERATOR_TYPE_NUMBER == $in_type) {
            
$this->do_seed();
            return 
mt_rand($in_min$in_max);
        }
        else {
            
// there was a problem
            
return false;
        }
    }

    
/**
     * Generates a random string
     *
     * {L:x,?,?} = random string using temp characters
     *   x = temporary character set
     *   ? = min length (default: SYSTEM SET)
     *   ? = max length (default: SYSTEM SET)
     * {N:x,?,?} = random string
     *   x = character sets (vVcCRS)
     *   ? = min length (default: SYSTEM SET)
     *   ? = max length (default: SYSTEM SET)
     *
     * @param   string  $in_params        The parameters from the code string.
     * @param   boolean $in_isTempCharset Whether or not this is an L string
     * @returns string  The generated string.
     */
    
protected function generateString($in_params$in_isTempCharset false)
    {
        
// parse the parameters
        
list($in_charset$in_min$in_max) = explode(','$in_params);
        
$in_min  = empty($in_min) ? $in_min;        
        
$in_max  = empty($in_max) ? $in_max;

        
// setup the caracter sets
        
if ($in_isTempCharset) {
            if (empty(
$in_charset)) {
                
$this->error "Missing Arguments for <strong>\{L:$in_params\}</strong>.";
                return 
false;
            }
            
$s_charset $in_charset;
        }
        else {
            
$in_charset = empty($in_charset) ? 'cCvV' $in_charset;
            
// determine the characters that are valid
            
if ('cCvV' == $in_charset) {
                
$s_charset SIP_CODE_GENERATOR_TYPE_REGULAR_CHARSET;
            }
            else {
                
$s_charset '';
                foreach(
str_split($in_charset) as $s_char) {
                    switch(
$s_char) {
                        case 
'c':
                            
$s_charset .= SIP_CODE_GENERATOR_TYPE_CONSONANTS;
                            break;
                        case 
'C':
                            
$s_charset .= SIP_CODE_GENERATOR_TYPE_CONSONANTS_U;
                            break;
                        case 
'v':
                            
$s_charset .= SIP_CODE_GENERATOR_TYPE_VOWELS;
                            break;
                        case 
'V':
                            
$s_charset .= SIP_CODE_GENERATOR_TYPE_VOWELS_U;
                            break;
                        case 
'm':
                            
$s_charset .= SIP_CODE_GENERATOR_TYPE_CONSONANTS_M;
                            break;
                        case 
'M':
                            
$s_charset .= SIP_CODE_GENERATOR_TYPE_VOWELS_M;
                            break;
                    }
                }
            }

            if ( ! isset(
$s_charset)) {
                
$s_charset SIP_CODE_GENERATOR_TYPE_REGULAR_CHARSET;
            }
        }
        
$a_charset str_split($s_charset);

        
$this->do_seed();

        
// determine the random size string
        
if (>= $in_max) {
            
$s_size $in_min;
        }
        elseif (
$in_max) {
            
$s_size mt_rand($in_min$in_max);
        }

        
// generate the characters
        
$s_count count($a_charset);
        
$s_string '';
        for (
$i 0$i $s_size; ++$i) {
            
$s_string .= $a_charset[mt_rand(0$s_count 1)];
        }

        return 
$s_string;
    }

    
/**
     * Generate a random pronouncable string.
     *
     * {P:x,?,?} = pronouncable character starting with vVcC
     *   x = character sets (vVcC)
     *   ? = min length (default: SYSTEM SET)
     *   ? = max length (default: SYSTEM SET)
     *
     * NOTE: Currently this is limited to cvc or vc style pronouncable strings.
     *
     * @param   string $in_params The parameters from the code string.
     * @returns string The generated pronouncable string.
     */
    
protected function generatePronouncableString($in_params)
    {
        
// parse the parameters
        
list($in_charset$in_groups) = explode(','$in_params);
        
$in_charset = empty($in_charset) ? 'c' $in_charset;

        
// default to cvc format
        
$s_format 'cvc';
        
$s_groups    2;
        foreach(
str_split($in_charset) as $s_char) {
            if (
'v' == $s_char || 'V' == $s_char || 'M' == $s_char) {
                
$s_format 'vc';
                
$s_groups    3;
                break;
            }
            if (
'm' == $s_char) {
                
$s_format 'cvc';
                
$s_groups    2;
            }
        }
        
$in_groups  = empty($in_groups) ? $s_groups $in_groups;

        if (
$s_char == 'M' || $s_char == 'm') {
            
$a_v     str_split(SIP_CODE_GENERATOR_TYPE_VOWELS_M_Y);
            
$a_c     str_split(SIP_CODE_GENERATOR_TYPE_CONSONANTS_M_NO_Y);
        }
        else {
            
$a_v     str_split(SIP_CODE_GENERATOR_TYPE_VOWELS_Y);
            
$a_c     str_split(SIP_CODE_GENERATOR_TYPE_CONSONANTS_NO_Y);
        }

        
$s_vSize count($a_v) - 1;
        
$s_cSize count($a_c) - 1;

        
// determine the random size string
        
$this->do_seed();
        
$s_string '';
        for(
$i 0$i $in_groups; ++$i) {
            
$s_group '';
            if (
'cvc' == $s_format) {
                
$s_group .= $a_c[mt_rand(0$s_vSize)];
            }
            
$s_group .= $a_v[mt_rand(0$s_vSize)];
            
$s_group .= $a_c[mt_rand(0$s_vSize)];

            if (
mt_rand(01) && mt_rand(0100) % 2) {
                
$s_string .= strtoupper($s_group);
            }
            else {
                
$s_string .= $s_group;
            }
        }

        return 
$s_string;

    }

    
/**
     * Inserts a word from the comma separate list that is passed in.
     *
     * {W:....} = comma separated list of words to use
     *
     * @params  string $in_words A list of comma separated words.
     * @returns string One of the words from the list.
     */
    
protected function insertWord($in_words)
    {
        
$a_words explode(','$in_words);
        if (
== count($a_words)) {
            return 
$a_words[0];
        }

        
$s_count count($a_words) - 1;
        
$this->do_seed();

        return 
$a_words[mt_rand(0$s_count)];
    }

    
/**
     * Converts the currently generated string into cases
     *
     * {C:x,?,?} = modify the case
     *   x = l lowercase
     *       u uppercase
     *       m mixedcase
     *   ? = start character
     *   ? = end character
     */
    
protected function modifyCase($in_params$in_string)
    {
        list(
$in_type$in_start$in_end) = explode(','$in_params);
        
$in_start  = empty($in_start) ? $in_start;
        
$in_end  = empty($in_end) ? strlen($in_string) : $in_end;

        
$s_count $in_end $in_start;
        
$s_replace substr($in_string$in_start$s_count);

        if (
'l' == $in_type) {
            
$s_replace strtolower($s_replace);
        }
        elseif (
'u' == $in_type) {
            
$s_replace strtoupper($s_replace);
        }
        elseif (
'm' == $in_type) {
            
// random generation
            
$a_str str_split($s_replace);
            
$s_replace '';

            
$s_method 'strtolower';
            if (
strtolower($a_str[0]) == $a_str[0]) {
                
$s_method 'strtoupper';
            }

            foreach(
$a_str as $s_chr) {
                
$s_replace .= $s_method($s_chr);
                if (
$s_method == 'strtolower') {
                    
$s_method 'strtoupper';
                }
                elseif (
$s_method == 'strtoupper') {
                    
$s_method 'strtolower';
                }
            }            
        }
        return 
substr_replace($in_string$s_replace$in_start$s_count);
        return 
false;
    }

    
/**
     * Inserts the given string into the passed in string at the given intervals.
     *
     * {S:?,?} = insert STRING into string
     *   ? = string to insert
     *   ? = insert every so many characters
     *
     * @param   string $in_params The parameters for inserting.
     * @param   string $in_string The string to insert into.
     * @returns string The modified string.
     */    
    
protected function insertString($in_params$in_string)
    {
        list(
$in_insert$in_count) = explode(','$in_params);

        
$s_len strlen($in_string);
        
$s_string '';
        for(
$i 0$i $s_len$i += $in_count) {
            
$ary[] = substr($in_string$i$in_count);
        }

        return 
join($in_insert$ary);
    }

    
// }}}
}
// }}}