<?php

/**
 * @package    Grav\Common\Page
 *
 * @copyright  Copyright (c) 2015 - 2026 Trilby Media, LLC. All rights reserved.
 * @license    MIT License; see LICENSE file for details.
 */

namespace Grav\Common\Page;

use FilesystemIterator;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaObjectInterface;
use Grav\Common\Yaml;
use Grav\Common\Page\Medium\AbstractMedia;
use Grav\Common\Page\Medium\GlobalMedia;
use Grav\Common\Page\Medium\MediumFactory;
use RocketTheme\Toolbox\File\File;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Throwable;
use function in_array;

/**
 * Class Media
 * @package Grav\Common\Page
 */
class Media extends AbstractMedia
{
    /** @var GlobalMedia */
    protected static $global;

    /** @var array */
    protected $standard_exif = ['FileSize', 'MimeType', 'height', 'width'];

    /**
     * @param string $path
     * @param array|null $media_order
     * @param bool   $load
     */
    public function __construct($path, ?array $media_order = null, $load = true)
    {
        $this->setPath($path);
        $this->media_order = $media_order;

        $this->__wakeup();
        if ($load) {
            $this->init();
        }
    }

    /**
     * Initialize static variables on unserialize.
     */
    public function __wakeup()
    {
        if (null === static::$global) {
            // Add fallback to global media.
            static::$global = GlobalMedia::getInstance();
        }
    }

    /**
     * Return raw route to the page.
     *
     * @return string|null Route to the page or null if media isn't for a page.
     */
    public function getRawRoute(): ?string
    {
        $path = $this->getPath();
        if ($path) {
            /** @var Pages $pages */
            $pages = $this->getGrav()['pages'];
            $page = $pages->get($path);
            if ($page) {
                return $page->rawRoute();
            }
        }

        return null;
    }

    /**
     * Return page route.
     *
     * @return string|null Route to the page or null if media isn't for a page.
     */
    public function getRoute(): ?string
    {
        $path = $this->getPath();
        if ($path) {
            /** @var Pages $pages */
            $pages = $this->getGrav()['pages'];
            $page = $pages->get($path);
            if ($page) {
                return $page->route();
            }
        }

        return null;
    }

    /**
     * @param string $offset
     * @return bool
     */
    #[\ReturnTypeWillChange]
    public function offsetExists($offset)
    {
        return parent::offsetExists($offset) ?: isset(static::$global[$offset]);
    }

    /**
     * @param string $offset
     * @return MediaObjectInterface|null
     */
    #[\ReturnTypeWillChange]
    public function offsetGet($offset)
    {
        return parent::offsetGet($offset) ?: static::$global[$offset];
    }

    /**
     * Initialize class.
     *
     * @return void
     */
    protected function init()
    {
        $path = $this->getPath();

        // Handle special cases where page doesn't exist in filesystem.
        if (!$path || !is_dir($path)) {
            return;
        }

        $grav = Grav::instance();

        /** @var UniformResourceLocator $locator */
        $locator = $grav['locator'];

        /** @var Config $config */
        $config = $grav['config'];

        $exif_reader = isset($grav['exif']) ? $grav['exif']->getReader() : null;
        $media_types = array_keys($config->get('media.types', []));

        $iterator = new FilesystemIterator($path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS);

        $media = [];

        foreach ($iterator as $file => $info) {
            // Ignore folders and Markdown files.
            $filename = $info->getFilename();
            if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || $filename === 'media.json' || str_starts_with($filename, '.')) {
                continue;
            }

            // Find out what type we're dealing with
            [$basename, $ext, $type, $extra] = $this->getFileParts($filename);

            if (!in_array(strtolower((string) $ext), $media_types, true)) {
                continue;
            }

            if ($type === 'alternative') {
                $media["{$basename}.{$ext}"][$type][$extra] = ['file' => $file, 'size' => $info->getSize()];
            } else {
                $media["{$basename}.{$ext}"][$type] = ['file' => $file, 'size' => $info->getSize()];
            }
        }

        foreach ($media as $name => $types) {
            try {
                // First prepare the alternatives in case there is no base medium
                if (!empty($types['alternative'])) {
                    /**
                     * @var string|int $ratio
                     * @var array $alt
                     */
                    foreach ($types['alternative'] as $ratio => &$alt) {
                        $alt['file'] = $this->createFromFile($alt['file']);

                        if (empty($alt['file'])) {
                            unset($types['alternative'][$ratio]);
                        } else {
                            $alt['file']->set('size', $alt['size']);
                        }
                    }
                    unset($alt);
                }

                $file_path = null;

                // Create the base medium
                if (empty($types['base'])) {
                    if (!isset($types['alternative'])) {
                        continue;
                    }

                    $max = max(array_keys($types['alternative']));
                    $medium = $types['alternative'][$max]['file'];
                    $file_path = $medium->path();
                    $medium = MediumFactory::scaledFromMedium($medium, $max, 1)['file'];
                } else {
                    $medium = $this->createFromFile($types['base']['file']);
                    if ($medium) {
                        $medium->set('size', $types['base']['size']);
                        $file_path = $medium->path();
                    }
                }

                if (empty($medium)) {
                    continue;
                }

                // metadata file
                $meta_path = $file_path . '.meta.yaml';

                if (file_exists($meta_path)) {
                    $types['meta']['file'] = $meta_path;
                } elseif ($file_path && $exif_reader && $medium->get('mime') === 'image/jpeg' && empty($types['meta']) && $config->get('system.media.auto_metadata_exif')) {
                    try {
                        $meta = $exif_reader->read($file_path);
                    } catch (Throwable $e) {
                        // The reader chokes on damaged files and on Exif values it does not expect.
                        $grav['log']->error($file_path . ' - ' . $e->getMessage());
                        $meta = null;
                    }

                    if ($meta) {
                        $meta_data = $meta->getData();
                        $meta_trimmed = array_diff_key($meta_data, array_flip($this->standard_exif));
                        if ($meta_trimmed) {
                            if ($locator->isStream($meta_path)) {
                                $file = File::instance($locator->findResource($meta_path, true, true));
                            } else {
                                $file = File::instance($meta_path);
                            }
                            $file->save(Yaml::dump($meta_trimmed));
                            $types['meta']['file'] = $meta_path;
                        }
                    }
                }

                if (!empty($types['meta'])) {
                    $medium->addMetaFile($types['meta']['file']);
                }

                if (!empty($types['thumb'])) {
                    // We will not turn it into medium yet because user might never request the thumbnail
                    // not wasting any resources on that, maybe we should do this for medium in general?
                    $medium->set('thumbnails.page', $types['thumb']['file']);
                }

                // Build missing alternatives
                if (!empty($types['alternative'])) {
                    $alternatives = $types['alternative'];
                    $max = max(array_keys($alternatives));

                    for ($i=$max; $i > 1; $i--) {
                        if (isset($alternatives[$i])) {
                            continue;
                        }

                        // Only raster images can be scaled (not a gif or svg): skip the step when there is no scaled copy.
                        $scaled = MediumFactory::scaledFromMedium($alternatives[$max]['file'], $max, $i);
                        if (!empty($scaled['file'])) {
                            $types['alternative'][$i] = $scaled;
                        }
                    }

                    foreach ($types['alternative'] as $altMedium) {
                        if ($altMedium['file'] != $medium) {
                            $altWidth = $altMedium['file']->get('width');
                            $medWidth = $medium->get('width');
                            if ($altWidth && $medWidth) {
                                $ratio = $altWidth / $medWidth;
                                $medium->addAlternative($ratio, $altMedium['file']);
                            }
                        }
                    }
                }

                $this->add($name, $medium);
            } catch (\Throwable $e) {
                $grav['log']->error(sprintf('Skipping media "%s" in %s: %s', $name, $path, $e->getMessage()));
            }
        }
    }

    /**
     * @return string|null
     * @deprecated 1.6 Use $this->getPath() instead.
     */
    public function path(): ?string
    {
        return $this->getPath();
    }
}
