<?php

declare(strict_types=1);

namespace App\Framework\Tokenizer\ValueObjects;

use ArrayIterator;
use Countable;
use IteratorAggregate;
use Traversable;

/**
 * Collection of tokens with utility methods
 */
final readonly class TokenCollection implements IteratorAggregate, Countable
{
    /**
     * @param array<Token> $tokens
     */
    public function __construct(
        private array $tokens = []
    ) {
    }

    /**
     * Get iterator for the collection
     */
    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->tokens);
    }

    /**
     * Count tokens in collection
     */
    public function count(): int
    {
        return count($this->tokens);
    }

    /**
     * Get all tokens as array
     * @return array<Token>
     */
    public function toArray(): array
    {
        return $this->tokens;
    }

    /**
     * Get token at index
     */
    public function get(int $index): ?Token
    {
        return $this->tokens[$index] ?? null;
    }

    /**
     * Filter tokens by predicate
     */
    public function filter(callable $predicate): self
    {
        return new self(array_values(array_filter($this->tokens, $predicate)));
    }

    /**
     * Filter by token type
     */
    public function filterByType(TokenType ...$types): self
    {
        return $this->filter(fn (Token $token) => in_array($token->type, $types, true));
    }

    /**
     * Filter by PHP token ID
     */
    public function filterById(int ...$ids): self
    {
        return $this->filter(fn (Token $token) => in_array($token->id, $ids, true));
    }

    /**
     * Get only structural tokens
     */
    public function getStructural(): self
    {
        return $this->filter(fn (Token $token) => $token->isStructural());
    }

    /**
     * Get only identifiers
     */
    public function getIdentifiers(): self
    {
        return $this->filter(fn (Token $token) => $token->isIdentifier());
    }

    /**
     * Get tokens in line range
     */
    public function getInLineRange(int $startLine, int $endLine): self
    {
        return $this->filter(
            fn (Token $token) =>
            $token->line >= $startLine && $token->line <= $endLine
        );
    }

    /**
     * Get tokens at specific line
     */
    public function getAtLine(int $line): self
    {
        return $this->filter(fn (Token $token) => $token->line === $line);
    }

    /**
     * Find first token matching predicate
     */
    public function findFirst(callable $predicate): ?Token
    {
        foreach ($this->tokens as $token) {
            if ($predicate($token)) {
                return $token;
            }
        }

        return null;
    }

    /**
     * Find first token of type
     */
    public function findFirstOfType(TokenType $type): ?Token
    {
        return $this->findFirst(fn (Token $token) => $token->type === $type);
    }

    /**
     * Map tokens to another form
     */
    public function map(callable $mapper): array
    {
        return array_map($mapper, $this->tokens);
    }

    /**
     * Extract all values
     */
    public function getValues(): array
    {
        return $this->map(fn (Token $token) => $token->value);
    }

    /**
     * Extract all clean values
     */
    public function getCleanValues(): array
    {
        return $this->map(fn (Token $token) => $token->getCleanValue());
    }

    /**
     * Get tokens grouped by type
     * @return array<string, array<Token>>
     */
    public function groupByType(): array
    {
        $groups = [];
        foreach ($this->tokens as $token) {
            $type = $token->type->value;
            if (! isset($groups[$type])) {
                $groups[$type] = [];
            }
            $groups[$type][] = $token;
        }

        return $groups;
    }

    /**
     * Get tokens grouped by line
     * @return array<int, array<Token>>
     */
    public function groupByLine(): array
    {
        $groups = [];
        foreach ($this->tokens as $token) {
            if (! isset($groups[$token->line])) {
                $groups[$token->line] = [];
            }
            $groups[$token->line][] = $token;
        }

        return $groups;
    }

    /**
     * Check if collection is empty
     */
    public function isEmpty(): bool
    {
        return empty($this->tokens);
    }

    /**
     * Get first token
     */
    public function first(): ?Token
    {
        return $this->tokens[0] ?? null;
    }

    /**
     * Get last token
     */
    public function last(): ?Token
    {
        return empty($this->tokens) ? null : $this->tokens[count($this->tokens) - 1];
    }

    /**
     * Slice collection
     */
    public function slice(int $offset, ?int $length = null): self
    {
        return new self(array_slice($this->tokens, $offset, $length));
    }

    /**
     * Merge with another collection
     */
    public function merge(self $other): self
    {
        return new self(array_merge($this->tokens, $other->tokens));
    }

    /**
     * Convert to string (concatenate all values)
     */
    public function toString(): string
    {
        return implode('', $this->getValues());
    }
}
