<?php

function cli_error(string $message): void
{
    $stderr = fopen('php://stderr', 'w');
    if ($stderr !== false) {
        fwrite($stderr, $message);
        fclose($stderr);
        return;
    }
    echo $message;
}

function usage(string $argv0): void
{
    cli_error("Usage: {$argv0} <input-executable> <output.stacktrace-map> <input.vala> [input.vala ...]\n");
}

function stacktrace_unescape_vala_string(string $value): string
{
    return stripcslashes($value);
}

function add_symbol_alias(array &$symbols, string $symbol, string $display): void
{
    $symbol = trim($symbol);
    $display = trim($display);
    if ($symbol === '' || $display === '') {
        return;
    }
    $symbols[$symbol] = $display;
    if (str_contains($symbol, '.')) {
        $native = str_replace('.', '_', $symbol);
        $symbols[$native] = $display;
        $lowered = strtolower($native);
        if ($lowered !== $native) {
            $symbols[$lowered] = $display;
        }
    }
    if (str_ends_with($symbol, '__handler')) {
        add_symbol_alias($symbols, substr($symbol, 0, -strlen('__handler')), $display);
    }
}

function collect_vala_symbols(array $valaFiles): array
{
    $symbols = [];
    foreach ($valaFiles as $valaFile) {
        $text = @file_get_contents($valaFile);
        if ($text === false) {
            cli_error("Could not read Vala source: {$valaFile}\n");
            exit(1);
        }
        if (preg_match_all('/PhpefRuntimeCore\.register_stacktrace_frame\s*\(\s*"((?:\\\\.|[^"])*)"\s*,\s*"((?:\\\\.|[^"])*)"\s*\)/', $text, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                add_symbol_alias($symbols, stacktrace_unescape_vala_string($match[1]), stacktrace_unescape_vala_string($match[2]));
            }
        }
        if (preg_match_all('/PhpefRuntimeCore\.register_function\s*\(\s*"((?:\\\\.|[^"])*)"\s*,\s*[^,]+\s*,\s*"((?:\\\\.|[^"])*)"/s', $text, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                add_symbol_alias($symbols, stacktrace_unescape_vala_string($match[2]), stacktrace_unescape_vala_string($match[1]));
            }
        }
        if (preg_match_all('/PhpefRuntimeCore\.register_static_method\s*\(\s*"((?:\\\\.|[^"])*)"\s*,\s*"((?:\\\\.|[^"])*)"\s*,\s*[^,]+\s*,\s*"((?:\\\\.|[^"])*)"/s', $text, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                add_symbol_alias($symbols, stacktrace_unescape_vala_string($match[3]), stacktrace_unescape_vala_string($match[1]) . '::' . stacktrace_unescape_vala_string($match[2]));
            }
        }
        if (preg_match_all('/PhpefRuntimeCore\.register_instance_method\s*\(\s*"((?:\\\\.|[^"])*)"\s*,\s*"((?:\\\\.|[^"])*)"\s*,\s*[^,]+\s*,\s*"((?:\\\\.|[^"])*)"/s', $text, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                add_symbol_alias($symbols, stacktrace_unescape_vala_string($match[3]), stacktrace_unescape_vala_string($match[1]) . '::' . stacktrace_unescape_vala_string($match[2]));
            }
        }
    }
    return $symbols;
}

function normalize_nm_symbol(string $symbol): string
{
    foreach (['.isra.', '.constprop.', '.part.', '.cold'] as $marker) {
        $pos = strpos($symbol, $marker);
        if ($pos !== false && $pos > 0) {
            return substr($symbol, 0, $pos);
        }
    }
    return $symbol;
}

function stacktrace_text_field(string $value): string
{
    return str_replace(["\\", "\t", "\r", "\n"], ["\\\\", "\\t", "\\r", "\\n"], $value);
}

const STACKTRACE_MIN_GLOSSARY_DEDUP_SUBSTRING_LENGTH = 4;

function stacktrace_glossary_debug_enabled(): bool
{
    $value = getenv("PHPEF_STACKTRACE_GLOSSARY_DEBUG");
    return $value !== false && $value !== "" && $value !== "0";
}

function stacktrace_glossary_debug_write(string $text): void
{
    if (!stacktrace_glossary_debug_enabled()) {
        return;
    }
    if (defined('STDERR')) {
        fwrite(STDERR, $text);
        return;
    }
    static $stderr = null;
    if ($stderr === null) {
        $stderr = fopen("php://stderr", "a");
    }
    if ($stderr !== false) {
        fwrite($stderr, $text);
    }
}

function stacktrace_glossary_debug(string $message, mixed $value = null): void
{
    stacktrace_glossary_debug_write($message . "\n");
    if ($value !== null) {
        stacktrace_glossary_debug_write(print_r($value, true) . "\n");
    }
}

function stacktrace_glossary_from_phrases(array $phrases): array
{
    $seen = [];
    $glossary = [];
    foreach ($phrases as $parts) {
        foreach ($parts as $part) {
            if (!isset($seen[$part])) {
                $seen[$part] = true;
                $glossary[] = $part;
            }
        }
    }
    return $glossary;
}

function stacktrace_glossary_debug_string_list(int $loop, array $phrases): void
{
    if (!stacktrace_glossary_debug_enabled()) {
        return;
    }
    stacktrace_glossary_debug_write("String-List on Loop " . $loop . ":\n");
    foreach (stacktrace_glossary_from_phrases($phrases) as $string) {
        stacktrace_glossary_debug_write(" " . $string . "\n");
    }
    stacktrace_glossary_debug_write("\n");
}

function stacktrace_endless_debug_write_string_pool(int $loop, string $stringPool): void
{
    if (!stacktrace_glossary_debug_enabled()) {
        return;
    }
    stacktrace_glossary_debug_write("String-List on Loop " . $loop . ":\n");
    stacktrace_glossary_debug_write(" Endless-String (" . strlen($stringPool) . " bytes): ");
    stacktrace_glossary_debug_write($stringPool === '' ? "<empty>\n\n" : $stringPool . "\n\n");
}

function stacktrace_endless_debug_references(array $references, string $stringPool): array
{
    $debug = [];
    foreach ($references as $reference) {
        [$position, $length] = $reference;
        $debug[] = [
            'source' => $reference[2] ?? 'unknown',
            'position' => $position,
            'length' => $length,
            'text' => substr($stringPool, $position, $length),
        ];
    }
    return $debug;
}

function stacktrace_find_best_endless_string_plan(string $phrase, string $stringPool): ?array
{
    $phraseLength = strlen($phrase);
    $poolLength = strlen($stringPool);
    if ($phraseLength === 0) {
        return ['cost' => 0, 'parts' => []];
    }

    $bestCost = array_fill(0, $phraseLength + 1, PHP_INT_MAX);
    $bestParts = array_fill(0, $phraseLength + 1, null);
    $bestCost[$phraseLength] = 0;
    $bestParts[$phraseLength] = [];

    for ($phraseOffset = $phraseLength - 1; $phraseOffset >= 0; $phraseOffset--) {
        // Option 1: append a fragment from the current phrase to the Endless-String.
        for ($length = 1; $length <= $phraseLength - $phraseOffset; $length++) {
            if ($bestParts[$phraseOffset + $length] === null) {
                continue;
            }
            $text = substr($phrase, $phraseOffset, $length);
            $candidateCost = 4 + $length + $bestCost[$phraseOffset + $length];
            $candidateParts = array_merge([['source' => 'append', 'text' => $text, 'length' => $length]], $bestParts[$phraseOffset + $length]);
            if (stacktrace_plan_is_better($candidateCost, $candidateParts, $bestCost[$phraseOffset], $bestParts[$phraseOffset])) {
                $bestCost[$phraseOffset] = $candidateCost;
                $bestParts[$phraseOffset] = $candidateParts;
            }
        }

        // Option 2: reuse a fragment already present in the Endless-String.
        for ($poolOffset = 0; $poolOffset < $poolLength; $poolOffset++) {
            $maxLength = 0;
            while (
                $phraseOffset + $maxLength < $phraseLength
                && $poolOffset + $maxLength < $poolLength
                && $phrase[$phraseOffset + $maxLength] === $stringPool[$poolOffset + $maxLength]
            ) {
                $maxLength++;
            }
            for ($length = 1; $length <= $maxLength; $length++) {
                if ($bestParts[$phraseOffset + $length] === null) {
                    continue;
                }
                $candidateCost = 4 + $bestCost[$phraseOffset + $length];
                $candidateParts = array_merge([['source' => 'reuse', 'position' => $poolOffset, 'length' => $length]], $bestParts[$phraseOffset + $length]);
                if (stacktrace_plan_is_better($candidateCost, $candidateParts, $bestCost[$phraseOffset], $bestParts[$phraseOffset])) {
                    $bestCost[$phraseOffset] = $candidateCost;
                    $bestParts[$phraseOffset] = $candidateParts;
                }
            }
        }
    }

    if ($bestParts[0] === null) {
        return null;
    }
    $dpPlan = stacktrace_optimize_plan_with_virtual_appends($bestParts[0], $stringPool);
    $greedyVirtualPlan = stacktrace_find_greedy_virtual_append_plan($phrase, $stringPool);
    if (stacktrace_plan_is_better($greedyVirtualPlan['cost'], $greedyVirtualPlan['parts'], $dpPlan['cost'], $dpPlan['parts'])) {
        return $greedyVirtualPlan;
    }
    return $dpPlan;
}

function stacktrace_plan_is_better(int $candidateCost, array $candidateParts, int $bestCost, ?array $bestParts): bool
{
    if ($candidateCost < $bestCost) {
        return true;
    }
    if ($candidateCost > $bestCost || $bestParts === null) {
        return false;
    }

    $candidateReuseBytes = stacktrace_plan_reuse_bytes($candidateParts);
    $bestReuseBytes = stacktrace_plan_reuse_bytes($bestParts);
    if ($candidateReuseBytes !== $bestReuseBytes) {
        return $candidateReuseBytes > $bestReuseBytes;
    }

    return count($candidateParts) < count($bestParts);
}

function stacktrace_plan_reuse_bytes(array $parts): int
{
    $bytes = 0;
    foreach ($parts as $part) {
        if (in_array(($part['source'] ?? ''), ['reuse', 'virtual-reuse'], true)) {
            $bytes += $part['length'];
        }
    }
    return $bytes;
}

function stacktrace_plan_append_bytes(array $parts): int
{
    $bytes = 0;
    foreach ($parts as $part) {
        if (($part['source'] ?? '') === 'append') {
            $bytes += strlen($part['text']);
        }
    }
    return $bytes;
}

function stacktrace_plan_reuse_count(array $parts): int
{
    $count = 0;
    foreach ($parts as $part) {
        if (in_array(($part['source'] ?? ''), ['reuse', 'virtual-reuse'], true)) {
            $count++;
        }
    }
    return $count;
}

function stacktrace_longest_pool_match_at(string $phrase, int $phraseOffset, string $pool): ?array
{
    $best = null;
    $phraseLength = strlen($phrase);
    $poolLength = strlen($pool);
    for ($poolOffset = 0; $poolOffset < $poolLength; $poolOffset++) {
        $length = 0;
        while (
            $phraseOffset + $length < $phraseLength
            && $poolOffset + $length < $poolLength
            && $phrase[$phraseOffset + $length] === $pool[$poolOffset + $length]
        ) {
            $length++;
        }
        if ($length > 0 && ($best === null || $length > $best['length'])) {
            $best = ['position' => $poolOffset, 'length' => $length];
        }
    }
    return $best;
}

function stacktrace_next_pool_match_offset(string $phrase, int $phraseOffset, string $pool): ?int
{
    $phraseLength = strlen($phrase);
    for ($offset = $phraseOffset + 1; $offset < $phraseLength; $offset++) {
        if (stacktrace_longest_pool_match_at($phrase, $offset, $pool) !== null) {
            return $offset;
        }
    }
    return null;
}

function stacktrace_find_greedy_virtual_append_plan(string $phrase, string $stringPool): array
{
    $virtualPool = $stringPool;
    $originalPoolLength = strlen($stringPool);
    $phraseLength = strlen($phrase);
    $offset = 0;
    $parts = [];
    $cost = 0;

    while ($offset < $phraseLength) {
        $match = stacktrace_longest_pool_match_at($phrase, $offset, $virtualPool);
        if ($match !== null) {
            $parts[] = [
                'source' => $match['position'] < $originalPoolLength ? 'reuse' : 'virtual-reuse',
                'position' => $match['position'],
                'length' => $match['length'],
            ];
            $cost += 4;
            $offset += $match['length'];
            continue;
        }

        $nextMatchOffset = stacktrace_next_pool_match_offset($phrase, $offset, $virtualPool);
        $appendLength = ($nextMatchOffset ?? $phraseLength) - $offset;
        $text = substr($phrase, $offset, $appendLength);
        $position = strlen($virtualPool);
        $virtualPool .= $text;
        $parts[] = [
            'source' => 'append',
            'position' => $position,
            'text' => $text,
            'length' => $appendLength,
        ];
        $cost += 4 + $appendLength;
        $offset += $appendLength;
    }

    return ['cost' => $cost, 'parts' => $parts];
}

function stacktrace_optimize_plan_with_virtual_appends(array $planParts, string $stringPool): array
{
    $virtualPool = $stringPool;
    $optimizedParts = [];
    $cost = 0;

    foreach ($planParts as $part) {
        if ($part['source'] === 'reuse') {
            $optimizedParts[] = $part;
            $cost += 4;
            continue;
        }

        $text = $part['text'];
        $length = strlen($text);
        $existingPosition = strpos($virtualPool, $text);
        if ($existingPosition !== false) {
            $optimizedParts[] = ['source' => 'virtual-reuse', 'position' => $existingPosition, 'length' => $length, 'text' => $text];
            $cost += 4;
            continue;
        }

        $position = strlen($virtualPool);
        $virtualPool .= $text;
        $optimizedParts[] = ['source' => 'append', 'position' => $position, 'text' => $text, 'length' => $length];
        $cost += 4 + $length;
    }

    return ['cost' => $cost, 'parts' => $optimizedParts];
}

function stacktrace_materialize_endless_string_plan(array $planParts, string &$stringPool): array
{
    $references = [];
    foreach ($planParts as $part) {
        if ($part['source'] === 'reuse' || $part['source'] === 'virtual-reuse') {
            $references[] = [$part['position'], $part['length'], $part['source']];
            continue;
        }

        $text = $part['text'];
        $position = strlen($stringPool);
        $length = strlen($text);
        if ($position + $length > 0xffff || $length > 0xffff) {
            cli_error("PHPEF stacktrace string pool exceeds v3 uint16 limits while adding fragment: {$text}
");
            exit(1);
        }
        $stringPool .= $text;
        $references[] = [$position, $length, 'append'];
    }
    return $references;
}

function stacktrace_build_phrase_glossary(array $strings): array
{
    $stringPool = '';
    $phraseIndexByString = [];
    $phraseParts = [];
    $loop = 0;

    $phrases = $strings;
    usort($phrases, static function (string $a, string $b): int {
        return strlen($b) <=> strlen($a) ?: strcmp($a, $b);
    });

    foreach ($phrases as $phrase) {
        stacktrace_endless_debug_write_string_pool($loop, $stringPool);
        $phraseLength = strlen($phrase);
        $appendWholeCost = 4 + $phraseLength;
        $plan = stacktrace_find_best_endless_string_plan($phrase, $stringPool);
        $planCost = $plan['cost'] ?? null;

        stacktrace_glossary_debug("Phrase: \"" . $phrase . "\"");
        if ($plan !== null && $planCost < $appendWholeCost) {
            $reuseCount = stacktrace_plan_reuse_count($plan['parts']);
            $appendBytes = stacktrace_plan_append_bytes($plan['parts']);
            $decision = $appendBytes > 0 ? 'Reuse+Append' : 'Reuse';
            stacktrace_glossary_debug("=> " . $decision . ": " . count($plan['parts']) . " refs, " . $appendBytes . " append bytes, " . $planCost . " bytes < " . $appendWholeCost . " append-whole bytes");
            stacktrace_glossary_debug("Plan", $plan['parts']);
            $references = stacktrace_materialize_endless_string_plan($plan['parts'], $stringPool);
            stacktrace_glossary_debug("References", stacktrace_endless_debug_references($references, $stringPool));
        } else {
            $position = strlen($stringPool);
            if ($position + $phraseLength > 0xffff || $phraseLength > 0xffff) {
                cli_error("PHPEF stacktrace string pool exceeds v3 uint16 limits while adding phrase: {$phrase}\n");
                exit(1);
            }
            $stringPool .= $phrase;
            $references = [[$position, $phraseLength, 'append']];
            if ($planCost === null) {
                stacktrace_glossary_debug("=> Append to Endless-String at position " . $position . " length " . $phraseLength . " (no cheaper plan found)");
            } else {
                stacktrace_glossary_debug("=> Append to Endless-String at position " . $position . " length " . $phraseLength . " (best plan cost " . $planCost . " bytes >= " . $appendWholeCost . " append-whole bytes)");
            }
            stacktrace_glossary_debug("References", stacktrace_endless_debug_references($references, $stringPool));
        }
        $phraseIndexByString[$phrase] = count($phraseParts);
        $phraseParts[] = $references;
        $loop++;
    }

    stacktrace_endless_debug_write_string_pool($loop, $stringPool);
    stacktrace_glossary_debug("=> No more phrases left; Endless-String complete (" . strlen($stringPool) . " bytes)");

    return [$phraseIndexByString, $phraseParts, $stringPool];
}

function stacktrace_string_table_offset(array &$offsets, string &$stringTable, string $value): int
{
    if (isset($offsets[$value])) {
        return $offsets[$value];
    }
    $offset = strlen($stringTable);
    $offsets[$value] = $offset;
    $stringTable .= $value . "\0";
    return $offset;
}

if ($argc < 4) {
    usage($argv[0]);
    exit(2);
}

$inputExecutable = $argv[1];
$outputFile = $argv[2];
$valaFiles = array_slice($argv, 3);
$symbolDisplays = collect_vala_symbols($valaFiles);

$cmd = 'nm -n ' . escapeshellarg($inputExecutable) . ' 2>/dev/null';
$nmLines = [];
$exitCode = 1;
exec($cmd, $nmLines, $exitCode);
if ($exitCode !== 0) {
    cli_error("Could not read symbols from executable: {$inputExecutable}\n");
    exit(1);
}

$allTextSymbols = [];
$matched = [];
foreach ($nmLines as $line) {
    if (!preg_match('/^([0-9A-Fa-f]+)\s+([tTwW])\s+(\S+)$/', $line, $match)) {
        continue;
    }
    $address = intval(hexdec($match[1]));
    $symbol = $match[3];
    $allTextSymbols[] = [$address, $symbol];
    $normalized = normalize_nm_symbol($symbol);
    $candidates = [$symbol, $normalized];
    foreach (['_phpef_runtime_core_instance_method_handler', '_phpef_runtime_core_user_function_handler'] as $delegateSuffix) {
        if (str_ends_with($normalized, $delegateSuffix)) {
            $handler = substr($normalized, 0, -strlen($delegateSuffix));
            $handler = ltrim($handler, '_');
            $candidates[] = $handler;
        }
    }
    $display = null;
    foreach ($candidates as $candidate) {
        if (isset($symbolDisplays[$candidate])) {
            $display = $symbolDisplays[$candidate];
            break;
        }
    }
    if ($display !== null) {
        $matched[] = [$address, $symbol, $display];
    }
}

usort($allTextSymbols, static fn($a, $b) => $a[0] <=> $b[0]);
usort($matched, static fn($a, $b) => $a[0] <=> $b[0]);

$resolvedEntries = [];
$allMapStrings = [];
$textLines = ['phpef-stacktrace-map-v3-text-view'];
foreach ($matched as $entry) {
    [$start, $symbol, $display] = $entry;
    $end = $start + 1;
    foreach ($allTextSymbols as $candidate) {
        if ($candidate[0] > $start) {
            $end = $candidate[0];
            break;
        }
    }
    $nativeSymbol = normalize_nm_symbol($symbol);
    $resolvedEntries[] = [$start, $end, $display, $nativeSymbol];
    $allMapStrings[$display] = true;
    $allMapStrings[$nativeSymbol] = true;
    $textLines[] = dechex($start) . "\t" . dechex($end) . "\t" . stacktrace_text_field($display) . "\t" . stacktrace_text_field($nativeSymbol);
}

[$phraseIndexByString, $phraseParts, $stringPool] = stacktrace_build_phrase_glossary(array_keys($allMapStrings));

$entries = '';
foreach ($resolvedEntries as $entry) {
    [$start, $end, $display, $nativeSymbol] = $entry;
    $entries .= pack('P2V2', $start, $end, $phraseIndexByString[$display], $phraseIndexByString[$nativeSymbol]);
}

$phraseTable = '';
$phrasePartReferences = '';
$phrasePartOffset = 0;
foreach ($phraseParts as $parts) {
    $phraseTable .= pack('V2', $phrasePartOffset, count($parts));
    foreach ($parts as $part) {
        [$position, $length] = $part;
        if ($position > 0xffff || $length > 0xffff) {
            cli_error("PHPEF stacktrace v3 phrase reference exceeds uint16 limits\n");
            exit(1);
        }
        $phrasePartReferences .= pack('v2', $position, $length);
        $phrasePartOffset++;
    }
}

$header = 'PHPEFSTK' . pack('V7', 3, 0, count($resolvedEntries), count($phraseParts), $phrasePartOffset, 0, strlen($stringPool));
$binaryMap = $header . $entries . $phraseTable . $phrasePartReferences . $stringPool;
$textOutputFile = $outputFile . '.txt';

$originalPhraseIndexByString = [];
$originalPhraseParts = [];
$originalStringPool = "";
foreach (array_keys($allMapStrings) as $phrase) {
    $position = strlen($originalStringPool);
    $length = strlen($phrase);
    if ($position + $length > 0xffff || $length > 0xffff) {
        cli_error("PHPEF original stacktrace byte map exceeds v3 uint16 limits while adding phrase: {$phrase}\n");
        exit(1);
    }
    $originalPhraseIndexByString[$phrase] = count($originalPhraseParts);
    $originalPhraseParts[] = [[$position, $length]];
    $originalStringPool .= $phrase;
}

$originalEntries = "";
foreach ($resolvedEntries as $entry) {
    [$start, $end, $display, $nativeSymbol] = $entry;
    $originalEntries .= pack('P2V2', $start, $end, $originalPhraseIndexByString[$display], $originalPhraseIndexByString[$nativeSymbol]);
}

$originalPhraseTable = "";
$originalPhrasePartReferences = "";
$originalPhrasePartOffset = 0;
foreach ($originalPhraseParts as $parts) {
    $originalPhraseTable .= pack('V2', $originalPhrasePartOffset, count($parts));
    foreach ($parts as $part) {
        [$position, $length] = $part;
        $originalPhrasePartReferences .= pack('v2', $position, $length);
        $originalPhrasePartOffset++;
    }
}
$originalHeader = 'PHPEFSTK' . pack('V7', 3, 1, count($resolvedEntries), count($originalPhraseParts), $originalPhrasePartOffset, 0, strlen($originalStringPool));
$originalByteMap = $originalHeader . $originalEntries . $originalPhraseTable . $originalPhrasePartReferences . $originalStringPool;
$originalByteMapOutputFile = $outputFile . '.original-bytes';

if (file_put_contents($outputFile, $binaryMap) === false) {
    cli_error("Could not write stacktrace map: {$outputFile}\n");
    exit(1);
}

if (file_put_contents($textOutputFile, implode("\n", $textLines) . "\n") === false) {
    cli_error("Could not write stacktrace map text view: {$textOutputFile}\n");
    exit(1);
}

if (file_put_contents($originalByteMapOutputFile, $originalByteMap) === false) {
    cli_error("Could not write original stacktrace byte map: {$originalByteMapOutputFile}\n");
    exit(1);
}

echo "Wrote binary PHPEF stacktrace map v3 to {$outputFile} with " . count($resolvedEntries) . " entries, " . count($phraseParts) . " phrases, " . $phrasePartOffset . " phrase refs, and " . strlen($stringPool) . " endless-string bytes\n";
echo "Wrote PHPEF stacktrace map text view to {$textOutputFile}\n";
echo "Wrote original PHPEF stacktrace byte map to {$originalByteMapOutputFile} with " . count($originalPhraseParts) . " phrases, " . $originalPhrasePartOffset . " phrase refs, and " . strlen($originalStringPool) . " original-string bytes\n";
