<?php

    function stack(\Throwable|array $context = null, string $prune = 'vendor', int $text = 64, int $arr = 4): void {
        $currentTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
        $current = array_shift($currentTrace);
        $caller = implode(':', [$current['file'] ?? '', $current['line'] ?? '']);
        $exception = $context instanceof \Throwable ? get_class($context) : '';
        $message = $context instanceof \Throwable ? $context->getMessage() : '';
        $trace = $context instanceof \Throwable ? $context->getTrace() : ($context ?: $currentTrace);

        if ($context instanceof \Throwable) {
            array_unshift($trace, ['line' => $context->getFile() . ':' . $context->getLine()]);
        }

        $trace = array_map(function (array $line) use ($text, $arr): array {
            $args = array_map($self = function (mixed $arg) use (&$self, $text, $arr): string {
                static $depth = 0;
                $depth++;

                $getItemTypes = function (array $items) use ($self, $depth, $text, $arr): string {
                    $count = (string)count($items);

                    if ($depth > 1) {
                        return $count;
                    }

                    $sample = array_splice($items, 0, $arr);
                    $types = array_map('gettype', $sample);
                    $types = (array_is_list($sample) && count(array_unique($types)) == 1)
                        ? current($types) : array_map($self, $sample);

                    $type = fn (string $type, string|int $index): string => "$index: $type";
                    $types = is_array($types) ? array_map($type, $types, array_keys($types)) : [$types . "[$count]"];

                    return implode(', ', $items ? array_merge($types, ['...+' . count($items)]) : $types);
                };

                $response = match (gettype($arg)) {
                    'string' => '"' . (($l = strlen($arg)) > ($t = $text + 3)
                            ? substr($arg, 0, $text) . '...+' . $l - $t : $arg) . '"',
                    'object' => $arg instanceof \Closure ? 'closure' : get_class($arg),
                    'resource' => 'resource',
                    'array' => (array_is_list($arg) ? 'array' : 'hash') . '(' . $getItemTypes($arg) . ')',
                    'NULL' => 'null',
                    'boolean' => $arg ? 'true' : 'false',
                    'integer', 'double' => (string)$arg,
                    default => $arg
                };

                $depth--;

                return $response;
            }, $line['args'] ?? []);

            $function = implode('::', array_filter([$line['class'] ?? null, $line['function'] ?? null]));
            $function .= '(' . implode(', ', $args) . ')';

            $line = implode(':', array_filter([$line['file'] ?? null, $line['line'] ?? null]));

            return array_filter(compact('function', 'line'));
        }, $trace);

        if ($prune) {
            $truncate = function (array $line, int $key) use ($trace, $prune) : array|string {
                $isEnd = !$key || $key == array_key_last($trace);
                $isRegex = ($delim = substr($prune, 0, 1)) == substr($prune, -1, 1) && in_array($delim, ['\\', '~']);
                $prune = $isRegex ? $prune : '~' . preg_quote($prune, '~') . '~i';

                return !$isEnd && preg_match($prune, $line['line'] ?? '') ? '...' : $line;
            };

            $trace = array_map($truncate, $trace, array_keys($trace));
            $truncate = fn (array|string $line, int $key): bool => $line != '...' || ($trace[$key - 1] ?? '') != '...';
            $trace = array_filter($trace, $truncate, ARRAY_FILTER_USE_BOTH);
        }

        die(print_r(array_filter(compact('caller', 'exception', 'message', 'trace')), true));
    }