<?php
/**
 * Run in folder where to scan:
 *   php -f /path/to/duplicated_functions.php
 *
 * For more detailed debug information:
 *   DEBUG=1 php -f /path/to/duplicated_functions.php
 */

declare (strict_types=1);

function display(string $message, ?string $prefix = null) {
    if ($prefix) {
        echo "[" . $prefix . "] ";
    }
    echo $message . "\n";
}

function debug(string $message, ?string $prefix = null) {
    if (getenv('DEBUG')) {
        display($message, $prefix);
    }
}

function warning(string $message, ?string $prefix = null) {
    display($message, $prefix ?? 'WARNING');
}

function listfilesin(string $dirname, string $pattern = '@\.(php|inc|module|install)@'): array {
    $handle = opendir($dirname);
    $ret = [];
    while ($filename = readdir($handle)) {
        if ('.' === $filename || '..' === $filename) {
            continue;
        }
        $absolute = $dirname . '/' . $filename;
        if (is_dir($absolute)) {
            foreach (listfilesin($absolute) as $nested) {
                $ret[] = $nested;
            }
        } else if (preg_match($pattern, $filename)) {
            $ret[] = $absolute;
        } else {
            debug($absolute, 'ignoring');
        }
    }
    return $ret;
}

class FunctionOccurence
{
    public function __construct(
        public string $name,
        public string $filename,
        public int $line,
    ) {}
}

class FunctionOccurenceSet
{
    public int $count = 0;
    /** @var FunctionOccurence[] */
    public array $occurences = [];

    public function __construct(
        public string $name
    ) {}

    public function add(string $filename, int $line) {
        $this->occurences[] = new FunctionOccurence($this->name, $filename, $line);
        $this->count++;
    }
}

class FunctionMap
{
    public array $functions = [];

    public function register(string $name, string $filename, int $line)
    {
        $set = ($this->functions[$name] ??= new FunctionOccurenceSet($name));
        assert($set instanceof FunctionOccurenceSet);

        $set->add($filename, $line);
    }

    /** @return FunctionOccurenceSet[] */
    public function findDuplicates(): iterable
    {
        foreach ($this->functions as $set) {
            if (1 < $set->count) {
                yield $set;
            }
        }
    }
}

$map = new FunctionMap();

foreach (listfilesin(getcwd()) as $filename) {
    debug($filename, 'found');

    $data = file_get_contents($filename);
    if ($data) {

        $depth = 0;
        $inFunctionDeclaration = false; $inStringOrComplexExpr = false;

        foreach (PhpToken::tokenize($data) as $token) {
            assert($token instanceof PhpToken);

            if ($token->isIgnorable()) {
                continue;
            }

            // Ignore "${EXPR}"
            if ($token->is(T_DOLLAR_OPEN_CURLY_BRACES) || $token->is(T_CURLY_OPEN)) {
                $inStringOrComplexExpr = true;
                continue;
            }

            $name = $token->getTokenName();
            if ('{' === $name) {
                $depth++;
            } else if ('}' === $name) {
                if ($inStringOrComplexExpr) {
                    $inStringOrComplexExpr = false;
                } else {
                    $depth--;
                }
            }

            if (0 > $depth) {
                warning(sprintf("Missed at least one opening brace '{' in '%s' at line %d", $filename, $token->line));
                $depth = 0; // Avoid repeating the warning.
            }

            if (0 < $depth) {
                // Looking for root namespace functions, ignoring depths.
                continue;
            }

            if ($token->is(T_FUNCTION)) {
                $inFunctionDeclaration = true;
            } else if ($token->is(T_WHITESPACE)) {
                // Ignore that.
            } else if ($token->is(T_STRING)) {
                if ($inFunctionDeclaration) {
                    // Function name.
                    $map->register($token->text, $filename, $token->line);
                    $inFunctionDeclaration = false;
                }
            }
        }
    } else {
        warning(sprintf("Could not read file '%s'", $filename));
    }
}

foreach ($map->findDuplicates() as $set) {
    assert($set instanceof FunctionOccurenceSet);
    echo $set->name . "()\n";
    foreach ($set->occurences as $occurence) {
        assert($occurence instanceof FunctionOccurence);
        echo " - " . $occurence->filename . " at line " . $occurence->line . "\n";
    }
}