<?php

declare(strict_types=1);

namespace Dhii\Versions;

use Dhii\Package\Version\VersionInterface;
use DomainException;
use Exception;
use RangeException;
use RuntimeException;
use Stringable;

/**
 * A value object containing information about a SemVer-compliant version.
 */
class Version implements VersionInterface
{
    /**
     * @var int
     */
    protected $major;
    /**
     * @var int
     */
    protected $minor;
    /**
     * @var int
     */
    protected $patch;
    /**
     * @var string[]
     */
    protected $preRelease;
    /**
     * @var string[]
     */
    protected $build;

    /**
     * @param int $major The major version number. See {@see getMajor()}.
     * @param int $minor The minor version number See {@see getMinor()}.
     * @param int $patch The patch version number. See {@see getPatch()}.
     * @param array<string|Stringable> $preRelease A list of pre-release identifiers. See {@see getPreRelease()}.
     * @param array<string|Stringable> $build A list of build identifiers. See {@see getBuild()}.
     *
     * @throws RangeException If an identifier is malformed
     * @throws Exception If problem creating.
     */
    public function __construct(
        int $major,
        int $minor,
        int $patch,
        array $preRelease,
        array $build
    ) {
        $this->major = $major;
        $this->minor = $minor;
        $this->patch = $patch;
        $this->preRelease = $this->normalizePreRelease($preRelease);
        $this->build = $this->normalizeBuild($build);
    }

    /**
     * @inheritDoc
     */
    public function getMajor(): int
    {
        return $this->major;
    }

    /**
     * @inheritDoc
     */
    public function getMinor(): int
    {
        return $this->minor;
    }

    /**
     * @inheritDoc
     */
    public function getPatch(): int
    {
        return $this->patch;
    }

    /**
     * @inheritDoc
     */
    public function getPreRelease(): array
    {
        return $this->preRelease;
    }

    /**
     * @inheritDoc
     */
    public function getBuild(): array
    {
        return $this->build;
    }

    /**
     * Normalizes an identifier.
     *
     * @param string $identifier The identifier to normalize.
     *                           Must be a non-empty alphanumeric+hyphen string.
     *
     * @return string An identifier with all disallowed characters removed.
     *
     * @throws DomainException If identifier is malformed
     * @throws Exception If problem normalizing.
     */
    protected function normalizeIdentifier(string $identifier): string
    {
        $origIdentifier = $identifier;

        $identifier = $this->replace('![^\d\w-]!', '', $identifier);

        if (!strlen($identifier)) {
            throw new DomainException(sprintf('Identifier "%1$s" normalized to "%2$s" is empty', $origIdentifier, $identifier));
        }

        return $identifier;
    }

    /**
     * Normalizes a series of pre-release identifiers.
     *
     * Will remove all illegal characters.
     *
     * @param iterable|array<string|Stringable> $preRelease The series of identifiers to normalize.
     *                             Each is a non-empty alphanumeric+hyphen string.
     *                             If numeric, leading zeroes are not allowed.
     *
     * @return string[] A series of normalized pre-release identifiers.
     *
     * @throws RangeException If could not normalize.
     * @throws Exception If problem normalizing.
     */
    protected function normalizePreRelease(iterable $preRelease): array
    {
        $normalized = [];

        foreach ($preRelease as $idx => $identifier) {
            $identifier = (string) $identifier;

            try {
                $identifier = $this->normalizeIdentifier($identifier);
            } catch (DomainException $e) {
                throw new RangeException(sprintf('Pre-release identifier #%1$d "%2$s" cannot be normalized', $idx, $identifier), 0, $e);
            }
            if (is_numeric($identifier)) {
                $identifier = (string) intval($identifier);
            }

            $normalized[] = $identifier;
        }

        return $normalized;
    }

    /**
     * Normalizes a series of build identifiers.
     *
     * Will remove all illegal characters.
     *
     * @param iterable|array<string|Stringable> $build The series of identifiers to normalize.
     *                             Each is a non-empty alphanumeric+hyphen string.
     *
     * @return string[] A series of normalized build identifiers.
     *
     * @throws RangeException If could not normalize.
     * @throws Exception If problem normalizing.
     */
    protected function normalizeBuild(iterable $build): array
    {
        $normalized = [];

        foreach ($build as $idx => $identifier) {
            $identifier = (string) $identifier;

            try {
                $identifier = $this->normalizeIdentifier($identifier);
            } catch (DomainException $e) {
                throw new RangeException(sprintf('Build identifier #%1$d "%2$s" cannot be normalized', $idx, $identifier), 0, $e);
            }

            $normalized[] = $identifier;
        }

        return $normalized;
    }

    /**
     * Replaces occurrences of $pattern in $subject.
     *
     * @param string $pattern The pattern to use for replacing.
     * @param string $replacement The replacement.
     * @param string $subject The subject.
     *
     * @return string The result of replacement.
     *
     * @throws Exception If problem replacing.
     */
    protected function replace(string $pattern, string $replacement, string $subject): string
    {
        $result = preg_replace($pattern, $replacement, $subject);

        if ($result === null) {
            $code = preg_last_error();
            $code = $code ? $code : 0;
            $message = preg_last_error_msg();
            $message = !empty($message) ? $message : 'Could not replace';

            throw new RuntimeException($message, $code);
        }

        return $result;
    }

    /**
     * @inheritDoc
     */
    public function __toString()
    {
        $version = "{$this->major}.{$this->minor}.{$this->patch}";

        if (count($this->preRelease)) {
            $version .= '-' . implode('.', $this->preRelease);
        }

        if (count($this->build)) {
            $version .= '+' . implode('.', $this->build);
        }

        return $version;
    }
}
