<?php
mb_internal_encoding('UTF-8');
set_time_limit(0);
ini_set('memory_limit', '256M');

if ($_SERVER['argc'] > 1) {
    die(B_file($_SERVER['argv'][1]));
}

// мои файлы для работы с UTF-8, их надо пропустить
$skip = array('Utf.php', 'UtfReverseString.php', 'UtfString.php');

// уровень на котором работать не нужно — текущий уровень,
// файлы на этом уровне будут пропущены (внимание!)
$skipdir = dirname(__FILE__);

$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('.'));

for (;$it->valid(); $it->next()) {
    
    if (dirname(realpath($it->key())) == $skipdir || in_array($it->getBaseName(), $skip, true)) {
        continue;
    }

    $ext = pathinfo($it->getBaseName(), PATHINFO_EXTENSION);

    if ($ext == 'php' || $ext == 'tpl' || $ext == 'inc') {

        echo $it->key(), "\n";
        file_put_contents($it->key(), B_file($it->key()));
    }
}

/*
    обработка единичного файла
*/
function B_file($filename)
{
    $content = file_get_contents($filename);

    $content = iconv('cp1251', 'utf8', $content);
    $content = B_main($content);
    $content = iconv('utf8', 'cp1251', $content);

    return B_dirty_hack($content);
}

/*
    грязный хак одного из багов парсера,
    надо ещё в выходном коде поиска ::Utf::get
*/
function B_dirty_hack($str)
{
    return strtr(
        $str, 
        array(
            'self::Utf::get(' => 'Utf::get(self::',
    ));
}

/*
    точка входа. Есть проблема с ${a[1]} вне кавычек,
    но такой конструкции вне кавычек не бывает.
*/
function B_main($str, &$tech = null)
{
    mb_internal_encoding('UTF-8');
    $tokens = B_tok_fix_finish(token_get_all(B_tok_fix($str)));

    return B_replace($tech = B_ren($tokens));
}

/*
    функция отладки
*/
function B_debug($mix)
{
    echo '<plaintext style="font-size: 10px">'; var_dump($mix); exit;
}

/* 
   разбиваю токены, которые PHP почему-то сам не разбивает
   Хха, кажется не нужно этого делать. $∩ — это не $n, это другой символ
*/
function B_tok_fix($str)
{
    /*$str = str_replace("'", '{$∩}\'{$∩}', $str);
    $str = str_replace('][', ']{$∩}[', $str);
    $str = str_replace(']{', ']{$∩}{', $str);*/

    return $str;
}

/*
   убираем {$∩}, которые я применяю для того, чтобы разбить токены,
   удаляю пустые токены, заменяю "${var}" на "{$var}", потому что
   последнюю конструкцию потом проще обрабатывать.

   А ещё собираю разбитые токены апостроф-что-то-внутри-апостроф
   в один токен.
*/
function B_tok_fix_finish(array $arr)
{
    $out = array();
    $state = $skip = $catch = false;
    $skipped = array();

    foreach ($arr as $token)
    {
        if ($skip) {    
            $skip = false;
        } else {
            list($name, $value) = is_scalar($token) ? array('STRING', $token) : $token;

            if ($value == '{') {
                $state = '{';
                $skipped = $token;
            }
            elseif ($name == T_DOLLAR_OPEN_CURLY_BRACES) {
                $state = '${';
            }
            elseif ($name == T_STRING_VARNAME) {
                if ($state == '${') {
                    $out = array_merge(
                        $out,
                        array(
                            array(T_CURLY_OPEN, '{'),
                            array(T_VARIABLE, '$' . $value),
                        )
                    );
                    $state = false;
                    continue;
                }
            }
            elseif ($name == T_VARIABLE) {
                if ($state == '{') {
                    if ($token[1] == '$∩') {
                        $state = false;
                        $skip = true;
                        continue;
                    } else {
                        $out[] = $skipped;
                        $skip = $state = false;
                    }
                }
            }
            elseif ($value == "'") {
                if ($catch !== false) {
                    $token = array(T_ENCAPSED_AND_WHITESPACE, str_replace('{$∩}', '', $catch . "'"));
                    $catch = false;
                } else {
                    $catch = "'";
                    continue;
                }
            }
            elseif ($value == '') {
                continue;
            }
            elseif ($state) {
                $out[] = $skipped;
                $skip = $state = false;
            }

            if ($state === false) {
                if ($catch === false) {
                    $out[] = $token;
                } else {
                    $catch .= $value;
                }
            }
        }
    }

    foreach ($out as &$token) {
        if (is_array($token)) {
            $token[1] = str_replace('{$∩}', '', $token[1]);
        } else {
            $token = str_replace('{$∩}', '', $token);
        }

        unset($token);
    }

    reset($out);

    return $out;
}

/*
   замена в цикле конструкций взятия по индексу, начиная с тех, у которых нет вложенных.
   Кроме того грязные фиксы вложенных замен {{…}} → {…}

   впереди не могут идти констуркции isset, unset, empty и взятие по ссылке
   сзади не могут быть ++, --, += и им подобные
*/
function B_replace($str)
{
    $regex = '(& | (?: (isset | unset | empty) \(\s* ) )?
        (?(?=\x00«)
            \x00«(?:(?!\x00[«»„“]).)+\x00» (?: \s*\+\+ | \s*\-\- | (?: \s* (?: \+ | \- | \* | / | \. | % | & | \| | ^ | << | >> )? =) )? |
                (?=\x00„)
                    \x00„(?:(?!\x00[«»“„]).)+\x00“
        )';

    do {
        $str = preg_replace_callback("@$regex@xusi", 'B_replace_helper', $str, -1, $count);
    }
    while ($count);

    $str = strtr(
        $str,
        array(
            "{\0{" => '{',
            "\0}}" => '}',
            "\0{"  => '{',
            "\0}"  => '}',
        )
    );

    return $str;
}

/*
    замена собственно конструкций. Внутри строки и вне разные замены.
*/
function B_replace_helper(array $m)
{
    if (in_array(substr($m[0], -1), array('=', '-', '+'))) {
        return B_clear_0(preg_replace('/\x00«|\x00»/sux', '', $m[0]));
    }

    /* isset и unset перед переменной */
    if (isset($m[1])) {
        return B_clear_0(preg_replace('/\x00«|\x00»/sux', '', $m[0]));
    }

    $str = mb_substr($m[0], 2, mb_strlen($m[0]) - 4);

    if (strpos($str, "\0‹") === false) {
        return $str;
    }

    $params = B_replace_param($str);

    if (mb_substr($m[0], 1, 1) == '„') { // замена в строке
        return B_clear_0("\x00{\$_ENV[0x5f3759df]->get(" . $params . ")\x00}");
    }
    else {
        /*
            простая проверка — является ли второй параметр строкой, справа я всё равно
            проверяю, несмотря на то, что может быть такое +"500",
            но я надеюсь на адекватность программиста
        */
        if (preg_match('/\x00ₑ\s*["\']\D | \D["\']\x00ₔ/xus', $params)) {
            return B_clear_0($str);
        }

        return B_clear_0('Utf::get(' . $params . ')');
    }
}

/*
    вычистка «хвостов» — удаляю маркеры у конструкций, замена которых не требуется
*/
function B_clear_0($str)
{
    return preg_replace('/\x00[‹›].|\x00[ₔₑ]/us', '', $str);
}

/*
    замена второго параметра функции utf::get
*/
function B_replace_param($str)
{
    if (preg_match('/‹(\w)\[.*?›\1\]$/xusi', $str)) {
        /* не ясно — строка или нет […], ставим маркер второго параметра */
        return preg_replace('/\x00‹(\w)\[((?:(?!\x00›\1\]).)*)\x00›\1\]$/xusi', ", \0ₑ\$2\0ₔ", $str);
    } else {
        /* строковая замена {…} */
        return preg_replace('/\x00‹(\w)\{((?:(?!\x00›\1\}).)*)\x00›\1\}$/xusi', ', $2', $str);
    }
}

/*
    разные маркеры для переменных внутри строки и вне
*/
function B_embrace($str, $in_quotes = false)
{
    if ($in_quotes) {
        return "\0„" . $str . "\0“";
    } else {
        return "\0«" . $str . "\0»";
    }
}

/*
    основная функция, расставляющие маркеры внутри конструкций,
    максимальная вложенность конструкций — 36
*/
function B_ren(array &$tokens, $in_quotes = false, $level = 0)
{
    $pairs = array(
        '(' => ')',
        '{' => '}',
        '[' => ']',
    );

    $state = false;
    $body  = '';
    $inner_body = array();
    $stack = array();
    $out = '';

    $was_quotes = $prev = false;

    while (list(,$token) = each($tokens)) {
        $token = list($type, $value) = is_array($token) ?
            array(is_int($token[0]) ? token_name($token[0]) : $token[0], $token[1]) :
            array('STRING', $token);

        if (!$state) {
            switch ($type) {
                case 'STRING':
                    if ($value == '$') {
                        $state = 'START';
                    }
                    elseif ($value == '"') {
                        $was_quotes = !$was_quotes;
                    }
                    break;

                case 'T_STRING':
                    if (is_array($nexttoken = current($tokens)) && $nexttoken[0] == T_DOUBLE_COLON) {
                        $state = 'TAIL';
                    }
                    break;


                case 'T_DOLLAR_OPEN_CURLY_BRACES': // ${
                    $state = 'INDEX';
                    $stack[] = '}';
                    break;

                case 'T_VARIABLE':
                    $state = 'TAIL';
                    break;
            }

            if ($state) {
                $body .= $value;
            } else {
                $out .= $value;
            }

        } else {
            if ($state == 'START') {
                if ($type == 'STRING' || $type == 'T_STRING') {
                    if (preg_match('/^[a-zA-Z0-9_\x7f-\xff]+$/s', $value)) {
                        $state = 'TAIL';
                        $body .= $value;
                    }
                    elseif ($type == 'STRING' && $value == '{') {
                        $state = 'INDEX';
                        $stack[] = '}';
                        $body .= $value;
                    } else {
                        $state = false;
                        $out .= B_embrace($body, $in_quotes);
                        $body = $body_inner = '';
                    }
                }
                elseif ($type == 'T_STRING_VARNAME') {
                    $state = 'INDEX';
                    $body .= $value;
                    $stack[] = '}';
                }
                else {
                    $body .= $value;
                    $state = 'TAIL';
                }

            }
            elseif ($state == 'TAIL') {
                if ($type == 'T_OBJECT_OPERATOR' || $type == 'T_DOUBLE_COLON') {
                    $state = 'START';
                }
                else switch ($value) {
                    case '[' :
                    case '{' :
                        $stack[] = $pairs[$value];
                        $body .= "\0‹" . base_convert($level, 10, 36);
                        $state = 'INDEX';
                        break;

                    default:
                        $state = false;
                        $out .= B_embrace($body, $in_quotes || $was_quotes);
                        $body = $body_inner = '';

                        if ($value == '$' || $type == 'T_DOLLAR_OPEN_CURLY_BRACES' || $type == 'T_VARIABLE') {
                            prev($tokens);
                            $prev = true;
                        }

                        if ($value == '"') {
                            $was_quotes = !$was_quotes;
                        }
                }

                if ($prev) {
                    $prev = false;
                } else {
                    if ($state) {
                        $body .= $value;
                    } else {
                        $out .= $value;
                    }
                }
            }
            elseif ($state == 'INDEX' || $state == 'INDEX-QUOTE' ) {

                if ($state == 'INDEX') {
                    switch ($value) {
                        case '[':
                        case '{':
                        case '(':
                            $stack[] = $pairs[$value];

                            /*if ($value == '[' || $value == '{') {
                                $inner_body[] = array('START_OF', "\0‹" . base_convert($level, 10, 36));
                            }*/

                            break;

                        case ']':
                        case '}':
                        case ')':
                            if (array_pop($stack) == $value) {
                                if (!$stack) {
                                    $state = 'TAIL';
                                }

                                if ($value == ']' || $value == '}') {
                                    $inner_body[] = array('END_OF', "\0›" . base_convert($level, 10, 36));
                                }

                            } else {
                                trigger_error("Unpaired $value", E_USER_ERROR);
                            }
                            break;

                        case '"':
                            $state = 'INDEX-QUOTE';
                            $was_quotes = true;
                            break;
                    }
                }
                elseif ($state == 'INDEX-QUOTE') {
                    if ($value == '"') {
                        $state = 'INDEX';
                        $was_quotes = false;
                    }
                }

                if ($state <> 'INDEX' && $state <> 'INDEX-QUOTE') {
                    $body .= B_ren($inner_body, $was_quotes, $level + 1) . $value;
                    $inner_body = array();

                    if ($state == 'INDEX-QUOTE') {
                        $was_quotes = false;
                    }
                } else {
                    $inner_body[] = $token;
                }
            }
        }

        if (defined('B_DEBUG') && B_DEBUG) {
            echo '<pre style="font:10px">',
                htmlspecialchars(var_export(array_merge($token, array($state)), true)),
                '</pre>';
        }
    }

    if ($body !== '') {
        $out .= B_embrace($body, $in_quotes);
    }

    return $out;
}