<?php

declare(strict_types=1);

namespace Intervention\Image\Colors;

use Intervention\Image\Colors\Hsl\Channels\Luminance;
use Intervention\Image\Colors\Hsl\Channels\Saturation;
use Intervention\Image\Colors\Hsl\Colorspace as HslColorspace;
use Intervention\Image\Colors\Rgb\Channels\Blue;
use Intervention\Image\Colors\Rgb\Channels\Green;
use Intervention\Image\Colors\Rgb\Channels\Red;
use Intervention\Image\Colors\Rgb\Colorspace as RgbColorspace;
use Intervention\Image\Exceptions\ColorException;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Interfaces\ColorChannelInterface;
use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface;
use ReflectionClass;
use Stringable;

abstract class AbstractColor implements ColorInterface, Stringable
{
    /**
     * Color channels.
     *
     * @var array<ColorChannelInterface>
     */
    protected array $channels;

    /**
     * {@inheritdoc}
     *
     * @see ColorInterface::channels()
     */
    public function channels(): array
    {
        return $this->channels;
    }

    /**
     * {@inheritdoc}
     *
     * @see ColorInterface::channel()
     *
     * @throws InvalidArgumentException
     */
    public function channel(string $classname): ColorChannelInterface
    {
        $channels = array_filter(
            $this->channels(),
            fn(ColorChannelInterface $channel): bool => $channel::class === $classname,
        );

        if (count($channels) === 0) {
            throw new InvalidArgumentException('Color channel ' . $classname . ' could not be found');
        }

        return reset($channels);
    }

    /**
     * {@inheritdoc}
     *
     * @see ColorInterface::toColorspace()
     *
     * @throws InvalidArgumentException
     */
    public function toColorspace(string|ColorspaceInterface $colorspace): ColorInterface
    {
        if (is_string($colorspace) && !class_exists($colorspace)) {
            throw new InvalidArgumentException('Unknown color space (' . $colorspace . ') as conversion target');
        }

        $colorspace = is_string($colorspace) ? new $colorspace() : $colorspace;

        if (!$colorspace instanceof ColorspaceInterface) {
            throw new InvalidArgumentException('Given color space must implement ' . ColorspaceInterface::class);
        }

        return $colorspace->importColor($this);
    }

    /**
     * {@inheritdoc}
     *
     * @see ColorInterface::isTransparent()
     */
    public function isTransparent(): bool
    {
        return $this->alpha()->value() < $this->alpha()->max();
    }

    /**
     * {@inheritdoc}
     *
     * @see ColorInterface::isClear()
     */
    public function isClear(): bool
    {
        return floatval($this->alpha()->value()) === 0.0;
    }

    /**
     * {@inheritdoc}
     *
     * @see ColorInterface::withTransparency()
     *
     * @throws InvalidArgumentException
     */
    public function withTransparency(float $transparency): ColorInterface
    {
        $color = clone $this;

        $color->channels = array_map(
            fn(ColorChannelInterface $channel): ColorChannelInterface =>
            $channel instanceof AlphaChannel ? $channel::fromNormalized($transparency) : $channel,
            $this->channels,
        );

        return $color;
    }

    /**
     * {@inheritdoc}
     *
     * @see ColorInterface::withBrightness()
     *
     * @throws InvalidArgumentException
     */
    public function withBrightness(int $level): ColorInterface
    {
        $hsl = clone $this->toColorspace(HslColorspace::class);
        $hsl->channel(Luminance::class)->scale($level);

        return $hsl->toColorspace($this->colorspace());
    }

    /**
     * {@inheritdoc}
     *
     * @see ColorInterface::withSaturation()
     *
     * @throws InvalidArgumentException
     */
    public function withSaturation(int $level): ColorInterface
    {
        $hsl = clone $this->toColorspace(HslColorspace::class);
        $hsl->channel(Saturation::class)->scale($level);

        return $hsl->toColorspace($this->colorspace());
    }

    /**
     * {@inheritdoc}
     *
     * @see ColorInterface::withInversion()
     *
     * @throws ColorException
     */
    public function withInversion(): ColorInterface
    {
        try {
            $rgb = $this->toColorspace(RgbColorspace::class);
        } catch (InvalidArgumentException) {
            throw new ColorException('Failed to invert color');
        }

        try {
            $inverted = new \Intervention\Image\Colors\Rgb\Color(
                255 - $rgb->channel(Red::class)->value(),
                255 - $rgb->channel(Green::class)->value(),
                255 - $rgb->channel(Blue::class)->value(),
                $rgb->alpha()->normalized(),
            );
            return $inverted->toColorspace($this->colorspace());
        } catch (InvalidArgumentException) {
            throw new ColorException('Failed to invert color');
        }
    }

    /**
     * Show debug info for the current color.
     *
     * @return array<string, string>
     */
    public function __debugInfo(): array
    {
        return array_reduce($this->channels(), function (array $result, ColorChannelInterface $item) {
            $key = strtolower((new ReflectionClass($item))->getShortName());
            $result[$key] = $item->toString();
            return $result;
        }, []);
    }

    /**
     * Clone color.
     */
    public function __clone(): void
    {
        foreach ($this->channels as $key => $channel) {
            $this->channels[$key] = clone $channel;
        }
    }

    /**
     * {@inheritdoc}
     *
     * @see ColorInterface::__toString()
     */
    public function __toString(): string
    {
        return $this->toString();
    }
}
