<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Core\DataHandling\Model;

use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * CorrelationId representation
 *
 * @todo Check internal state during v10 development
 * @internal
 */
class CorrelationId implements \JsonSerializable
{
    protected const DEFAULT_VERSION = 1;
    protected const PATTERN_V1 = '#^(?P<flags>[[:xdigit:]]{4})\$(?:(?P<scope>[[:alnum:]]+):)?(?P<subject>[[:alnum:]]+)(?P<aspects>(?:\/[[:alnum:]._-]+)*)$#';
    protected int $version = self::DEFAULT_VERSION;
    protected ?string $scope = null;
    protected int $capabilities = 0;
    protected ?string $subject = null;

    /**
     * @var string[]
     */
    protected array $aspects = [];

    public static function forScope(string $scope): self
    {
        $target = static::create();
        $target->scope = $scope;
        return $target;
    }

    public static function forSubject(string $subject, string ...$aspects): self
    {
        return static::create()
            ->withSubject($subject)
            ->withAspects(...$aspects);
    }

    public static function fromString(string $correlationId): self
    {
        if (!preg_match(self::PATTERN_V1, $correlationId, $matches, PREG_UNMATCHED_AS_NULL)) {
            throw new \InvalidArgumentException('Unknown format', 1569620858);
        }

        $flags = hexdec($matches['flags']);
        $aspects = $matches['aspects'] === '' ? [] : explode('/', ltrim($matches['aspects'], '/'));
        $target = static::create()
            ->withSubject($matches['subject'])
            ->withAspects(...$aspects);
        $target->scope = $matches['scope'] ?? null;
        $target->version = $flags >> 10;
        $target->capabilities = $flags & ((1 << 10) - 1);
        return $target;
    }

    protected static function create(): self
    {
        return GeneralUtility::makeInstance(static::class);
    }

    public function __toString(): string
    {
        if ($this->subject === null) {
            throw new \LogicException('Cannot serialize for empty subject', 1569668681);
        }
        return $this->serialize();
    }

    public function jsonSerialize(): string
    {
        return (string)$this;
    }

    public function withSubject(string $subject): self
    {
        if ($this->subject === $subject) {
            return $this;
        }
        $target = clone $this;
        $target->subject = $subject;
        return $target;
    }

    public function withAspects(string ...$aspects): self
    {
        if ($this->aspects === $aspects) {
            return $this;
        }
        $target = clone $this;
        $target->aspects = $aspects;
        return $target;
    }

    public function getScope(): ?string
    {
        return $this->scope;
    }

    public function getSubject(): ?string
    {
        return $this->subject;
    }

    /**
     * @return string[]
     */
    public function getAspects(): array
    {
        return $this->aspects;
    }

    /**
     * v1 specs (eBNF)
     * + FLAGS "$" [ SCOPE ":" ] SUBJECT { "/" ASPECT }
     *   + FLAGS   ::= XDIGIT (* 16-bit integer big-endian)
     *   + SCOPE   ::= ALNUM { ALNUM }
     *   + SUBJECT ::= ALNUM { ALNUM }
     *   + ASPECT  ::= ( ALNUM | '.' | '_' | '-' ) { ( ALNUM | '.' | '_' | '-' ) }
     */
    protected function serialize(): string
    {
        // 6-bit version 10-bit capabilities
        $flags = $this->version << 10 + $this->capabilities;
        return sprintf(
            '%s$%s%s%s',
            bin2hex(pack('n', $flags)),
            $this->scope ? $this->scope . ':' : '',
            $this->subject,
            $this->aspects ? '/' . implode('/', $this->aspects) : ''
        );
    }
}
