<?php

namespace App\Services;

use ProPresenter\Parser\Song;
use Rv\Data\Action\ActionType;
use Rv\Data\Action\LayerType;
use Rv\Data\AlphaType;
use Rv\Data\Cue;
use Rv\Data\FileProperties;
use Rv\Data\Graphics\Element as GraphicsElement;
use Rv\Data\Graphics\Fill;
use Rv\Data\Graphics\Path;
use Rv\Data\Graphics\Path\BezierPoint;
use Rv\Data\Graphics\Path\Shape;
use Rv\Data\Graphics\Path\Shape\Type as ShapeType;
use Rv\Data\Graphics\Point;
use Rv\Data\Graphics\Rect;
use Rv\Data\Graphics\Size;
use Rv\Data\Media;
use Rv\Data\Media\DrawingProperties;
use Rv\Data\Media\ImageTypeProperties;
use Rv\Data\Media\Metadata;
use Rv\Data\Media\ScaleBehavior;
use Rv\Data\Slide as BaseSlide;
use Rv\Data\Slide\Element as SlideElement;
use Rv\Data\URL;
use Rv\Data\URL\LocalRelativePath;
use Rv\Data\URL\Platform as UrlPlatform;
use Rv\Data\UUID;

/**
 * Converts a generated Song's FOREGROUND image media ACTIONS (uploaded slide
 * images from the Information/Moderation/Sermon blocks and the abspann info
 * images) into full-frame slide-content fill.media ELEMENTS, and strips those
 * foreground actions so none survives.
 *
 * BACKGROUND-layer image media actions — the service background image AND the
 * key-visual — are LEFT UNTOUCHED. They stay as media ACTIONS on the BACKGROUND
 * layer so ProPresenter renders them behind the whole slide-content stack. Text,
 * translation, subtitle, macro and completion actions are never touched.
 *
 * Combined rule (content vs background):
 *
 *   - Background image / key-visual  → media ACTION, layer BACKGROUND (kept).
 *   - Uploaded content slide image   → fill.media content ELEMENT (converted).
 *   - Songtext / name-tag text       → text content elements (kept).
 *
 * Layering contract for the resulting base-slide element array (index 0 = bottom):
 *
 *   [0 .. n-1]   text elements from ProFileGenerator::generate()
 *   [n]          uploaded (foreground) image element, appended on top
 *
 * The background image is NOT an element; it is a media action on the BACKGROUND
 * layer and renders beneath the entire element stack automatically.
 *
 * Every converted foreground image is referenced bundle-relative
 * (ROOT_CURRENT_RESOURCE) by its bare filename so it resolves against the bytes
 * embedded into the .pro/.probundle archive — never by an absolute path.
 */
class SlideMediaElementBuilder
{
    /**
     * Build a full-frame (0,0 → 1920×1080) content element whose fill is the
     * given bundle-relative image.
     *
     * Images are pre-sized to 1920×1080 (16:9) before export, so FILL and FIT are
     * equivalent; FILL is used as the default scale behaviour.
     */
    public function mediaFillElement(
        string $bundleRelativeFilename,
        string $elementName = '',
        int $width = 1920,
        int $height = 1080,
        int $scaleBehavior = ScaleBehavior::SCALE_BEHAVIOR_FILL,
    ): SlideElement {
        $naturalSize = new Size;
        $naturalSize->setWidth($width);
        $naturalSize->setHeight($height);

        $drawing = new DrawingProperties;
        $drawing->setScaleBehavior($scaleBehavior);
        $drawing->setNaturalSize($naturalSize);
        $drawing->setAlphaType(AlphaType::ALPHA_TYPE_STRAIGHT);

        $fileProperties = new FileProperties;
        $fileProperties->setLocalUrl($this->bundleRelativeUrl($bundleRelativeFilename));

        $image = new ImageTypeProperties;
        $image->setDrawing($drawing);
        $image->setFile($fileProperties);

        $metadata = new Metadata;
        $metadata->setFormat('JPG');

        $media = new Media;
        $media->setUuid($this->newUuid());
        $media->setUrl($this->bundleRelativeUrl($bundleRelativeFilename));
        $media->setMetadata($metadata);
        $media->setImage($image);

        $fill = new Fill;
        $fill->setEnable(true);
        $fill->setMedia($media);

        $graphicsElement = new GraphicsElement;
        $graphicsElement->setUuid($this->newUuid());
        $graphicsElement->setName($elementName);
        $graphicsElement->setBounds($this->fullFrameBounds($width, $height));
        $graphicsElement->setOpacity(1.0);
        $graphicsElement->setPath($this->rectanglePath());
        $graphicsElement->setFill($fill);

        $slideElement = new SlideElement;
        $slideElement->setElement($graphicsElement);
        $slideElement->setInfo(0);

        return $slideElement;
    }

    /**
     * Self-contained REPLACE: derive the per-cue plan from the Song's own
     * FOREGROUND image media actions, then strip those actions and inject content
     * elements. This is the method the export services call after
     * ProFileGenerator::generate(). BACKGROUND image actions are left in place.
     */
    public function replaceImageActionsWithElements(Song $song): void
    {
        $this->injectIntoSong($song, $this->planFromSong($song));
    }

    /**
     * Convert each cue's FOREGROUND (uploaded) image media action into a
     * full-frame content fill.media element appended on top of the generated
     * text/content elements, and strip that foreground action. The BACKGROUND
     * media action (service background / key-visual) is left untouched so it
     * keeps rendering as a background layer beneath all slide content.
     *
     * $plan is an ordered list (one entry per cue, in cue order). Each entry:
     *   ['foreground' => ?string, 'foregroundName' => ?string]
     * A null/absent foreground leaves the cue exactly as generated (text-only
     * songs, key-visual-only slides, name-tag slides).
     *
     * @param  array<int, array{foreground?: ?string, foregroundName?: ?string}>  $plan
     */
    public function injectIntoSong(Song $song, array $plan): void
    {
        $index = 0;

        foreach ($song->getPresentation()->getCues() as $cue) {
            $entry = $plan[$index] ?? [];
            $index++;

            $baseSlide = $this->baseSlideForCue($cue);
            if ($baseSlide === null) {
                continue;
            }

            $foreground = $entry['foreground'] ?? null;

            if ($foreground === null || $foreground === '') {
                // No foreground image: leave the cue (text elements, BACKGROUND
                // media action, macro) exactly as the generator produced it.
                continue;
            }

            $this->stripForegroundImageMediaActions($cue);

            $elements = [];
            foreach ($baseSlide->getElements() as $element) {
                $elements[] = $element;
            }

            $elements[] = $this->mediaFillElement($foreground, $entry['foregroundName'] ?? 'Bild');

            $baseSlide->setElements($elements);
        }
    }

    /**
     * Derive the injection plan from the FOREGROUND image media actions the
     * generator added. BACKGROUND-layer image actions (service background /
     * key-visual) are intentionally ignored so they remain media actions.
     *
     * @return array<int, array{foreground: ?string, foregroundName: ?string}>
     */
    private function planFromSong(Song $song): array
    {
        $plan = [];

        foreach ($song->getPresentation()->getCues() as $cue) {
            $foreground = null;
            $foregroundName = null;

            foreach ($cue->getActions() as $action) {
                if ($action->getType() !== ActionType::ACTION_TYPE_MEDIA) {
                    continue;
                }

                $mediaType = $action->getMedia();
                $mediaElement = $mediaType?->getElement();
                if ($mediaElement === null || ! $mediaElement->hasImage()) {
                    continue;
                }

                // Only FOREGROUND images become content elements. BACKGROUND-layer
                // images (service background / key-visual) stay as media actions.
                if ($mediaType->getLayerType() === LayerType::LAYER_TYPE_BACKGROUND) {
                    continue;
                }

                $filename = $mediaElement->getUrl()?->getAbsoluteString();
                if ($filename === null || $filename === '') {
                    continue;
                }

                $foreground = $filename;
                $foregroundName = $action->getName() !== '' ? $action->getName() : null;
            }

            $plan[] = [
                'foreground' => $foreground,
                'foregroundName' => $foregroundName,
            ];
        }

        return $plan;
    }

    /** Resolve the base slide via Cue → SLIDE action → presentation → base slide. */
    private function baseSlideForCue(Cue $cue): ?BaseSlide
    {
        foreach ($cue->getActions() as $action) {
            if ($action->getType() === ActionType::ACTION_TYPE_PRESENTATION_SLIDE) {
                return $action->getSlide()?->getPresentation()?->getBaseSlide();
            }
        }

        return null;
    }

    /**
     * Remove only FOREGROUND-layer media actions whose element carries an image.
     * BACKGROUND-layer image actions (service background / key-visual) are kept.
     */
    private function stripForegroundImageMediaActions(Cue $cue): void
    {
        $kept = [];
        $stripped = false;

        foreach ($cue->getActions() as $action) {
            if ($action->getType() === ActionType::ACTION_TYPE_MEDIA) {
                $mediaType = $action->getMedia();
                $mediaElement = $mediaType?->getElement();
                if (
                    $mediaElement !== null
                    && $mediaElement->hasImage()
                    && $mediaType->getLayerType() !== LayerType::LAYER_TYPE_BACKGROUND
                ) {
                    $stripped = true;

                    continue;
                }
            }

            $kept[] = $action;
        }

        if ($stripped) {
            $cue->setActions($kept);
        }
    }

    private function bundleRelativeUrl(string $filename): URL
    {
        $local = new LocalRelativePath;
        $local->setRoot(LocalRelativePath\Root::ROOT_CURRENT_RESOURCE);
        $local->setPath($filename);

        $url = new URL;
        $url->setAbsoluteString($filename);
        $url->setLocal($local);
        $url->setPlatform(UrlPlatform::PLATFORM_MACOS);

        return $url;
    }

    private function fullFrameBounds(int $width, int $height): Rect
    {
        $origin = new Point;
        $origin->setX(0);
        $origin->setY(0);

        $size = new Size;
        $size->setWidth($width);
        $size->setHeight($height);

        $rect = new Rect;
        $rect->setOrigin($origin);
        $rect->setSize($size);

        return $rect;
    }

    private function rectanglePath(): Path
    {
        $points = [];
        foreach ([[0, 0], [1, 0], [1, 1], [0, 1]] as [$x, $y]) {
            $point = new Point;
            $point->setX((float) $x);
            $point->setY((float) $y);

            $bezier = new BezierPoint;
            $bezier->setPoint($point);
            $bezier->setQ0($point);
            $bezier->setQ1($point);
            $bezier->setCurved(false);

            $points[] = $bezier;
        }

        $path = new Path;
        $path->setClosed(true);
        $path->setPoints($points);

        $shape = new Shape;
        $shape->setType(ShapeType::TYPE_RECTANGLE);
        $path->setShape($shape);

        return $path;
    }

    private function newUuid(): UUID
    {
        $bytes = random_bytes(16);
        $bytes[6] = chr((ord($bytes[6]) & 0x0F) | 0x40);
        $bytes[8] = chr((ord($bytes[8]) & 0x3F) | 0x80);
        $hex = bin2hex($bytes);

        $uuid = new UUID;
        $uuid->setString(strtoupper(sprintf(
            '%s-%s-%s-%s-%s',
            substr($hex, 0, 8),
            substr($hex, 8, 4),
            substr($hex, 12, 4),
            substr($hex, 16, 4),
            substr($hex, 20, 12),
        )));

        return $uuid;
    }
}
