<?php

declare(strict_types=1);

namespace Intervention\Image\Drivers;

use Intervention\Image\Alignment;
use Intervention\Image\Exceptions\InvalidArgumentException;
use Intervention\Image\Geometry\Point;
use Intervention\Image\Geometry\Polygon;
use Intervention\Image\Interfaces\FontInterface;
use Intervention\Image\Interfaces\FontProcessorInterface;
use Intervention\Image\Interfaces\PointInterface;
use Intervention\Image\Size;
use Intervention\Image\Typography\Line;
use Intervention\Image\Typography\TextBlock;

abstract class AbstractFontProcessor implements FontProcessorInterface
{
    /**
     * {@inheritdoc}
     *
     * @see FontProcessorInterface::textBlock()
     *
     * @throws InvalidArgumentException
     */
    public function textBlock(string $text, FontInterface $font, PointInterface $position): TextBlock
    {
        $lines = $this->wrapTextBlock(new TextBlock($text), $font);
        $pivot = $this->buildPivot($lines, $font, $position);

        $leading = $this->leading($font);
        $blockWidth = $this->boxSize((string) $lines->longestLine(), $font)->width();

        $x = $pivot->x();
        $y = $font->hasFile() ? $pivot->y() + $this->capHeight($font) : $pivot->y();
        $xAdjustment = 0;

        // adjust line positions according to alignment
        $horizontalAlignment = $font->alignmentHorizontal();
        foreach ($lines as $line) {
            $lineBoxSize = $this->boxSize((string) $line, $font);
            $lineWidth = $lineBoxSize->width() + $lineBoxSize->pivot()->x();
            $xAdjustment = $horizontalAlignment === Alignment::LEFT ? 0 : $blockWidth - $lineWidth;
            $xAdjustment = $horizontalAlignment === Alignment::RIGHT ? intval(round($xAdjustment)) : $xAdjustment;
            $xAdjustment = $horizontalAlignment === Alignment::CENTER ? intval(round($xAdjustment / 2)) : $xAdjustment;
            $position = new Point($x + $xAdjustment, $y);
            $position->rotate($font->angle(), $pivot);
            $line->setPosition($position);
            $y += $leading;
        }

        return $lines;
    }

    /**
     * {@inheritdoc}
     *
     * @see FontProcessorInterface::nativeFontSize()
     */
    public function nativeFontSize(FontInterface $font): float
    {
        return $font->size();
    }

    /**
     * {@inheritdoc}
     *
     * @see FontProcessorInterface::typographicalSize()
     */
    public function typographicalSize(FontInterface $font): int
    {
        return $this->boxSize('Hy', $font)->height();
    }

    /**
     * {@inheritdoc}
     *
     * @see FontProcessorInterface::capHeight()
     */
    public function capHeight(FontInterface $font): int
    {
        return $this->boxSize('T', $font)->height();
    }

    /**
     * {@inheritdoc}
     *
     * @see FontProcessorInterface::leading()
     */
    public function leading(FontInterface $font): int
    {
        return intval(round($this->typographicalSize($font) * $font->lineHeight()));
    }

    /**
     * Reformat a text block by wrapping each line before the given maximum width.
     */
    protected function wrapTextBlock(TextBlock $block, FontInterface $font): TextBlock
    {
        $newLines = [];
        foreach ($block as $line) {
            foreach ($this->wrapLine($line, $font) as $newLine) {
                $newLines[] = $newLine;
            }
        }

        return $block->setLines($newLines);
    }

    /**
     * Check if a line exceeds the given maximum width and wrap it if necessary.
     * The output will be an array of formatted lines that are all within the
     * maximum width.
     *
     * @return array<Line>
     */
    protected function wrapLine(Line $line, FontInterface $font): array
    {
        // no wrap width - no wrapping
        if (is_null($font->wrapWidth())) {
            return [$line];
        }

        $wrapped = [];
        $formattedLine = new Line();

        foreach ($line as $word) {
            // calculate width of newly formatted line
            $lineWidth = $this->boxSize(match ($formattedLine->count()) {
                0 => $word,
                default => $formattedLine . ' ' . $word,
            }, $font)->width();

            // decide if word fits on current line or a new line must be created
            if ($line->count() === 1 || $lineWidth <= $font->wrapWidth()) {
                $formattedLine->add($word);
            } else {
                if ($formattedLine->count() !== 0) {
                    $wrapped[] = $formattedLine;
                }
                $formattedLine = new Line($word);
            }
        }

        $wrapped[] = $formattedLine;

        return $wrapped;
    }

    /**
     * Build pivot point of textblock according to the font settings and based on given position.
     *
     * @throws InvalidArgumentException
     */
    protected function buildPivot(TextBlock $block, FontInterface $font, PointInterface $position): PointInterface
    {
        // bounding box
        $box = Polygon::fromSize(new Size(
            $this->boxSize((string) $block->longestLine(), $font)->width(),
            $this->leading($font) * ($block->count() - 1) + $this->capHeight($font)
        ));

        // set position
        $box->setPivot($position);

        // alignment
        $box->alignHorizontally($font->alignmentHorizontal());
        $box->alignVertically($font->alignmentVertical());
        $box->rotate($font->angle());

        return $box->last();
    }
}
