| 
<?php
 /*
 * This file is part of Chevere.
 *
 * (c) Rodolfo Berrios <[email protected]>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
 
 declare(strict_types=1);
 
 namespace Chevere\Parameter;
 
 use BadMethodCallException;
 use Chevere\DataStructure\Interfaces\VectorInterface;
 use Chevere\DataStructure\Map;
 use Chevere\DataStructure\Traits\MapTrait;
 use Chevere\DataStructure\Vector;
 use Chevere\Parameter\Interfaces\ArgumentsInterface;
 use Chevere\Parameter\Interfaces\ParameterCastInterface;
 use Chevere\Parameter\Interfaces\ParameterInterface;
 use Chevere\Parameter\Interfaces\ParametersInterface;
 use InvalidArgumentException;
 use OverflowException;
 use function Chevere\Message\message;
 
 final class Parameters implements ParametersInterface
 {
 /**
 * @template-use MapTrait<ParameterInterface>
 */
 use MapTrait;
 
 /**
 * @var Map<ParameterInterface>
 */
 private Map $map;
 
 /**
 * @var Vector<string>
 */
 private VectorInterface $requiredKeys;
 
 /**
 * @var Vector<string>
 */
 private VectorInterface $optionalKeys;
 
 private int $optionalMinimum = 0;
 
 private bool $isVariadic = false;
 
 /**
 * @param ParameterInterface $parameter Required parameters
 */
 public function __construct(ParameterInterface ...$parameter)
 {
 $this->map = new Map();
 $this->requiredKeys = new Vector();
 $this->optionalKeys = new Vector();
 foreach ($parameter as $name => $item) {
 $name = strval($name);
 $this->addProperty('requiredKeys', $name, $item);
 }
 }
 
 public function __invoke(mixed ...$argument): ArgumentsInterface
 {
 return new Arguments($this, $argument);
 }
 
 public function withIsVariadic(bool $flag): ParametersInterface
 {
 $new = clone $this;
 $new->isVariadic = $flag;
 
 return $new;
 }
 
 public function isVariadic(): bool
 {
 return $this->isVariadic;
 }
 
 public function withRequired(string $name, ParameterInterface $parameter): ParametersInterface
 {
 $new = clone $this;
 $new->addProperty('requiredKeys', $name, $parameter);
 
 return $new;
 }
 
 public function withOptional(string $name, ParameterInterface $parameter): ParametersInterface
 {
 $new = clone $this;
 $new->addProperty('optionalKeys', $name, $parameter);
 
 return $new;
 }
 
 public function withMakeOptional(string ...$name): ParametersInterface
 {
 $new = clone $this;
 if ($name === []) {
 $name = $this->requiredKeys->toArray();
 }
 foreach ($name as $key) {
 if (! $new->requiredKeys->contains($key)) {
 throw new InvalidArgumentException(
 (string) message(
 'Parameter `%name%` is not required',
 name: $key
 )
 );
 }
 $parameter = $new->get($key);
 $new->remove($key);
 $new->addProperty('optionalKeys', $key, $parameter);
 }
 
 return $new;
 }
 
 public function withMakeRequired(string ...$name): ParametersInterface
 {
 $new = clone $this;
 if ($name === []) {
 $name = $this->optionalKeys->toArray();
 }
 foreach ($name as $key) {
 if (! $new->optionalKeys()->contains($key)) {
 throw new InvalidArgumentException(
 (string) message(
 'Parameter `%name%` is not optional',
 name: $key
 )
 );
 }
 $parameter = $new->get($key);
 $new->remove($key);
 $new->addProperty('requiredKeys', $key, $parameter);
 }
 
 return $new;
 }
 
 public function without(string ...$name): ParametersInterface
 {
 $new = clone $this;
 $new->remove(...$name);
 
 return $new;
 }
 
 public function withMerge(ParametersInterface $parameters): ParametersInterface
 {
 $new = clone $this;
 foreach ($parameters as $name => $parameter) {
 $container = match ($parameters->requiredKeys()->contains($name)) {
 true => 'requiredKeys',
 default => 'optionalKeys',
 };
 $new->addProperty($container, $name, $parameter);
 }
 
 return $new;
 }
 
 public function withOptionalMinimum(int $count): ParametersInterface
 {
 match (true) {
 $count < 0 => throw new InvalidArgumentException(
 (string) message('Count must be greater or equal to 0')
 ),
 $this->optionalKeys()->count() === 0 => throw new BadMethodCallException(
 (string) message('No optional parameters found')
 ),
 default => null,
 };
 $new = clone $this;
 $new->optionalMinimum = $count;
 $new->assertMinimumOptional();
 
 return $new;
 }
 
 public function requiredKeys(): VectorInterface
 {
 return $this->requiredKeys;
 }
 
 public function optionalKeys(): VectorInterface
 {
 return $this->optionalKeys;
 }
 
 public function optionalMinimum(): int
 {
 return $this->optionalMinimum;
 }
 
 public function assertHas(string ...$name): void
 {
 $this->map->assertHas(...$name);
 }
 
 public function has(string ...$name): bool
 {
 return $this->map->has(...$name);
 }
 
 public function get(string $name): ParameterInterface
 {
 return $this->map->get($name);
 }
 
 public function required(string $name): ParameterCastInterface
 {
 $parameter = $this->get($name);
 if ($this->optionalKeys()->contains($name)) {
 throw new InvalidArgumentException(
 (string) message(
 'Parameter `%name%` is optional',
 name: $name
 )
 );
 }
 
 return new ParameterCast($parameter);
 }
 
 public function optional(string $name): ParameterCastInterface
 {
 $parameter = $this->get($name);
 if (! $this->optionalKeys()->contains($name)) {
 throw new InvalidArgumentException(
 (string) message(
 'Parameter `%name%` is required',
 name: $name
 )
 );
 }
 
 return new ParameterCast($parameter);
 }
 
 private function remove(string ...$name): void
 {
 $this->map = $this->map->without(...$name);
 $requiredDiff = array_diff($this->requiredKeys->toArray(), $name);
 $optionalDiff = array_diff($this->optionalKeys->toArray(), $name);
 $this->requiredKeys = new Vector(...$requiredDiff);
 $this->optionalKeys = new Vector(...$optionalDiff);
 $this->assertMinimumOptional();
 }
 
 private function assertMinimumOptional(): void
 {
 if ($this->optionalMinimum > $this->optionalKeys()->count()) {
 throw new InvalidArgumentException(
 (string) message(
 'Count must be less or equal to `%optional%`',
 optional: strval($this->optionalMinimum)
 )
 );
 }
 }
 
 private function addProperty(string $property, string $name, ParameterInterface $parameter): void
 {
 if ($this->has($name)) {
 throw new OverflowException(
 (string) message(
 'Parameter `%name%` has been already added',
 name: $name
 )
 );
 }
 $this->{$property} = $this->{$property}->withPush($name);
 $this->map = $this->map->withPut($name, $parameter);
 }
 }
 
 |