<?php

namespace intec\core\helpers;

/**
 * Класс для обработки видео-ссылок и получения информации о видео на различных хостингах.
 */
class VideoThumb
{
    /**
     * Ссылка на видео.
     * @var string
     */
    protected string $link = '';

    /**
     * Массив, содержащий разобранные части URL.
     * @var array
     */
    protected array $link_parts = [];

    /**
     * Хостинг, на котором размещено видео (например, 'youtube', 'vimeo', 'rutube', 'vk').
     * @var string
     */
    protected string $hosting = '';

    /**
     * Идентификатор видео на хостинге.
     * @var string
     */
    protected string $id = '';

    /**
     * Приватный идентификатор (например, для частных видео на Rutube).
     * @var string
     */
    protected string $privateId = '';

    /**
     * Ссылка на изображение (миниатюру) видео.
     * @var string|null
     */
    protected ?string $image = null;

    /**
     * Название видео.
     * @var string
     */
    protected string $title = '';

    /**
     * Ссылка на видео.
     * @var string
     */
    protected string $video = '';

    /**
     * Ссылка на страницу с видео.
     * @var string
     */
    protected string $pageVideo = '';

    /**
     * API-ссылка для получения информации о видео на Rutube.
     * @var string
     */
    protected string $rutubeAPI = '';

    /**
     * Значение токена для VK
     * @var string
     */
    protected string $token = '';

    /**
     * API-ссылка для получения информации о видео на Vk.
     * @var string
     */
    protected string $vkAPI = '';

    /**
     * Константа для хостинга YouTube.
     * @var string
     */
    const YOUTUBE = 'youtube';

    /**
     * Константа для хостинга Vimeo.
     * @var string
     */
    const VIMEO = 'vimeo';

    /**
     * Константа для хостинга Rutube.
     * @var string
     */
    const RUTUBE = 'rutube';

    /**
     * Константа для хостинга Vk.
     * @var string
     */
    const VK = 'vkvideo';

    /**
     * Константа для хостинга файлов.
     * @var string
     */
    const FILE = 'file';

    /**
     * Константа для качества видео.
     * @var string
     */
    const QUALITY = [
        'mp4_1080',
        'mp4_720',
        'mp4_480',
        'mp4_360',
        'mp4_240',
        'mp4_144'
    ];

    /**
     * Массив регулярных выражений для поиска видео с различных хостингов.
     * @var array
     */
    protected array $regexp = [
        self::YOUTUBE => [
            '/https?+:\/\/(?:www\.|)youtube\.com\/watch\?(?:.*)?v=([a-zA-Z0-9_\-]+)/i',
            '/https?+:\/\/(?:www\.|)youtube\.com\/embed\/([a-zA-Z0-9_\-]+)/i',
            '/https?+:\/\/(?:www\.|)youtu\.be\/([a-zA-Z0-9_\-]+)/i'
        ],
        self::VK => [
            '/https?:\/\/(?:www\.)?vk\.[ru|com]+\/[clip|video]+(-?\d+_\d+)/i', // Домен вк
            '/https?:\/\/(?:www\.)?vkvideo\.ru\/[clip|video]+(-?\d+_\d+)/i', // Домен вк видео
            '/https?:\/\/(?:www\.)?m\.vk\.com\/video(-?\d+_\d+)/i', // Мобильный
            '/https?:\/\/(?:www\.)?[vkvideo|vk]+\.[ru|com]+\/video_ext\.php\?oid=(-?\d+).*id=(\d+)/i', // Встраивание
        ],
        self::VIMEO => [
            '/https?+:\/\/(?:www\.|)vimeo\.com\/([a-zA-Z0-9_\-]+)(&.+)?/i',
            '/https?+:\/\/player\.vimeo\.com\/video\/([a-zA-Z0-9_\-]+)(&.+)?/i'
        ],
        self::RUTUBE => [
            '/https?+:\/\/(?:www\.|)rutube\.ru\/video\/([a-zA-Z0-9_\-]+)\/\?p=([a-zA-Z0-9_\-]+)/i', // Приватная страница
            '/https?+:\/\/(?:www\.|)rutube\.ru\/video\/private\/([a-zA-Z0-9_\-]+)\/\?p=([a-zA-Z0-9_\-]+)/i', // Приватная страница
            '/https?+:\/\/(?:www\.|)rutube\.ru\/[play|video]+\/embed\/([a-zA-Z0-9_\-]+)\/\?p=([a-zA-Z0-9_\-]+)/i', // Приватное встраивание
            '/https?+:\/\/(?:www\.|)rutube\.ru\/video\/([a-zA-Z0-9_\-]+)/i', // Страница
            '/https?+:\/\/(?:www\.|)rutube\.ru\/[play|video]+\/embed\/([a-zA-Z0-9_\-]+)/i', // Встраивание
        ],
        self::FILE => array(
            '/\/(.*\.(?:mp4|avi|ogv|mpeg|webm|mov))/i' // Поддерживаемые видеоформаты
        )
    ];

    /**
     * Дополнительное регулярное выражение для Rutube.
     * @var string
     */
    protected string $regexp_rutube_extra = '/https?+:\/\/(?:www\.|)rutube\.ru\/video\/([a-zA-Z0-9_\-]+)\//i';

    /**
     * Массив тестовых ссылок для проверки работы регулярных выражений.
     * @var array
     */
    protected static array $test = [
        'https://youtube.com/watch?v=ShPq2Dmy6X8',
        'https://www.youtube.com/watch?v=6dwqZw0j_jY&feature=youtu.be',
        'https://www.youtube.com/watch?v=cKZDdG9FTKY&feature=channel',
        'www.youtube.com/watch?v=yZ-K7nCVnBI&playnext_from=TL&videos=osPknwzXEas&feature=sub',
        'https://www.youtube.com/embed/ShPq2Dmy6X8?rel=0',
        'https://youtu.be/ShPq2Dmy6X8',
        'youtu.be/6dwqZw0j_jY',
        'https://www.youtu.be/afa-5HQHiAs',

        'vimeo.com/55028438',
        'https://player.vimeo.com/video/55028438?title=0&byline=0&portrait=0&badge=0&color=e1a931',

        'https://rutube.ru/video/c9715d5b435cb9d9a2673b8aa4b1cbd1/#.UMQYln9yTWQ',
        'https://rutube.ru/video/c9715d5b435cb9d9a2673b8aa4b1cbd1/?ref=top',
        'rutube.ru/tracks/c9715d5b435cb9d9a2673b8aa4b1cbd1.html',
        'https://www.rutube.ru/video/embed/c9715d5b435cb9d9a2673b8aa4b1cbd1',
        'https://rutube.ru/play/embed/caafe83ff1c6ed38d394635b83ece578/?p=IBgzQQrKH4qB1bqm_91x7Q&skinColor=ff00ff',
    ];

    /**
     * Конструктор класса.
     *
     * @param string|null $link Ссылка на видео.
     * @param string|null $token Значение токена.
     * @param bool $autostart Флаг, который определяет, нужно ли сразу обрабатывать ссылку.
     */
    function __construct(string $link = null, string $token = null, bool $autostart = true)
    {
        if ($link) {
            $this->setLink($link);

            if ($token) {
                $this->setToken($token);
            }

            if ($autostart) {
                $this->process();
            }
        }
    }


    /**
     * Получить хостинг видео.
     *
     * @return string Хостинг видео.
     */
    public function getHosting(): string
    {
        return $this->hosting;
    }


    /**
     * Получить ID видео.
     *
     * @return string ID видео.
     */
    public function getId(): string
    {
        return $this->id;
    }


    /**
     * Получить приватный ID видео (если есть).
     *
     * @return string Приватный ID.
     */
    public function getPrivateId(): string
    {
        return $this->privateId;
    }


    /**
     * Получить ссылку на изображение (миниатюру) видео.
     *
     * @return string Ссылка на изображение.
     */
    public function getImage(): string
    {
        return $this->image ?: '';
    }


    /**
     * Получить ссылку на видео.
     *
     * @return string Ссылка на видео.
     */
    public function getVideo(): string
    {
        return $this->video;
    }


    /**
     * Получить ссылку на страницу с видео.
     *
     * @return string Ссылка на страницу с видео.
     */
    public function getPageVideo(): string
    {
        return $this->pageVideo;
    }


    /**
     * Получить API для Rutube.
     *
     * @return string Ссылка на API для Rutube.
     */
    public function getRutubeAPI(): string
    {
        return $this->rutubeAPI;
    }

    /**
     * Получить ссылку на поток HLS для видео Rutube (если доступно).
     *
     * @return ?string Ссылка на HLS поток или null, если поток не найден.
     */
    public function getRutubeHLS(): ?string
    {
        $curlRequest = function (string $url): ?string {
            $curl = curl_init();
            curl_setopt_array($curl, [
                CURLOPT_URL => $url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_ENCODING => "",
                CURLOPT_MAXREDIRS => 10,
                CURLOPT_TIMEOUT => 30,
                CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2,
                CURLOPT_CUSTOMREQUEST => "GET",
                CURLOPT_HTTPHEADER => [
                    "Content-Type: application/json",
                    "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0"
                ],
            ]);

            $response = curl_exec($curl);
            $err = curl_error($curl);
            $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
            curl_close($curl);

            if ($err || $httpCode !== 200) {
                error_log("cURL Error: $err, HTTP Code: $httpCode");
                return null;
            }

            return $response;
        };

        $response = $curlRequest($this->rutubeAPI);
        if ($response === null) {
            return null;
        }
        $videoBalancerObj = json_decode($response);
        if (isset($videoBalancerObj->video_balancer->json)) {
            $hlsJsonUrl = $videoBalancerObj->video_balancer->json;

            $hlsJsonResponse = $curlRequest($hlsJsonUrl);
            if ($hlsJsonResponse === null) {
                return null;
            }

            $HLSObj = json_decode($hlsJsonResponse);
            return $HLSObj->results[0] ?? $HLSObj->results[1] ?? null;
        }

        return null;
    }

    /**
     * Получить ссылку на поток HLS для видео Vk (если доступно).
     *
     * @return ?string Ссылка на HLS поток или null, если поток не найден.
     */
    public function getVkHLS(): ?string
    {
        $curlRequest = function (string $url): ?string {
            $curl = curl_init();

            curl_setopt_array($curl, [
                CURLOPT_URL => $url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_ENCODING => "",
                CURLOPT_MAXREDIRS => 10,
                CURLOPT_TIMEOUT => 30,
                CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2,
                CURLOPT_CUSTOMREQUEST => "GET",
                CURLOPT_HTTPHEADER => [
                    "Content-Type: application/json",
                    "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0"
                ],
            ]);

            $response = curl_exec($curl);
            $err = curl_error($curl);
            $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
            curl_close($curl);

            if ($err || $httpCode !== 200) {
                error_log("cURL Error: $err, HTTP Code: $httpCode");
                return null;
            }

            return $response;
        };

        $response = $curlRequest($this->vkAPI);
        if ($response === null) {
            return null;
        }
        $videoDecode = json_decode($response);
        if (isset($videoDecode->response->items[0]->files)) {
            $filesQuality = $videoDecode->response->items[0]->files;

            foreach ($this::QUALITY as $quality) {
                if (isset($filesQuality->$quality)) {
                    $hlsUrl = $filesQuality->$quality;
                    break;
                }
            }
            return $hlsUrl;
        }

        return null;
    }

    /**
     * Получить название видео.
     *
     * @return string Название видео.
     */
    public function getTitle(): string
    {
        return $this->title;
    }


    /**
     * Установить ссылку на видео.
     *
     * @param string $link Ссылка на видео.
     * @return self Возвращает текущий объект для цепочки вызовов.
     */
    public function setLink(string $link): self
    {
        $this->link = $link;

        return $this;
    }

    /**
     * Установить токен.
     *
     * @param string $token Значение токена.
     * @return self Возвращает текущий объект для цепочки вызовов.
     */
    public function setToken(string $token): self
    {
        $this->token = $token;

        return $this;
    }


    /**
     * Обработать ссылку на видео.
     * Определить хостинг и другие параметры.
     *
     * @return string|false Хостинг или false, если ссылка не поддерживается.
     */
    public function process(): string|false
    {
        if ($this->maybeFile()) {
            return self::FILE;
        } else if ($this->cleanLink()) {
            match (true) {
                $this->maybeYoutube() => self::YOUTUBE,
                $this->maybeVimeo() => self::VIMEO,
                $this->maybeVk() => self::VK,
                $this->maybeRutube() => $this->maybeRutube(),
                default => false
            };
        }

        return false;
    }


    /**
     * Скачать изображение (миниатюру) видео.
     *
     * @param string|null $filename Имя файла для сохранения изображения.
     * @return string|false Содержимое изображения или false при ошибке.
     */
    public function fetchImage(string $filename = null): string|false
    {
        if (!$url = $this->getImage()) {
            return false;
        }

        if (!$res = $this->fetchPage($url)) {
            return false;
        }

        return $filename
            ? file_put_contents($filename, $res)
            : $res;
    }


    /**
     * Очистить ссылку, приведя её к стандартному виду (с https).
     *
     * @return string|false Строка с очищенной ссылкой или false, если ссылка невалидна.
     */
    protected function cleanLink(): string|bool
    {
        if (!preg_match('/^https?:\/\//i', $this->link)) {

            $this->link = 'https://' . $this->link;

        }

        if (parse_url($this->link) === false) {
            return false;
        } else {
            $this->link_parts = parse_url($this->link);
        }

        return true;
    }


    /**
     * Проверить, является ли ссылка на видео с YouTube.
     *
     * @return string|false Ссылка на видео с YouTube или false.
     */
    protected function maybeYoutube(): string|false
    {
        $h = str_replace('www.', '', $this->link_parts['host']);
        $p = isset($this->link_parts['path']) ? $this->link_parts['path'] : false;

        if ('youtube.com' == $h) {
            parse_str($this->link_parts['query'], $q);

            if ('/watch' == $p && !empty($q['v'])) {
                return $this->foundYoutube($q['v']);
            }
            if (0 === strpos($p, '/embed/')) {
                return $this->foundYoutube(str_replace('/embed/', '', $p));
            }
        } elseif ('youtu.be' == $h) {
            return $this->foundYoutube(trim($p, '/'));
        }

        return false;
    }


    /**
     * Проверить, является ли ссылка на видео с Vimeo.
     *
     * @return string|false Ссылка на видео с Vimeo или false.
     */
    protected function maybeVimeo(): string|false
    {
        $h = str_replace('www.', '', $this->link_parts['host']);
        $p = isset($this->link_parts['path']) ? $this->link_parts['path'] : false;

        if ('vimeo.com' == $h) {
            return $this->foundVimeo(trim($p, '/'));
        } elseif ('player.vimeo.com' == $h && 0 === strpos($p, '/video/')) {
            return $this->foundVimeo(str_replace('/video/', '', $p));
        }

        return false;
    }


    /**
     * Проверить, является ли ссылка на видео с Rutube.
     *
     * @return string|false Ссылка на видео с Rutube или false.
     */
    protected function maybeRutube(string $html = null): string|false
    {
        $link = $html ?: $this->link;

        foreach ($this->regexp[self::RUTUBE] as $regexp) {
            if (preg_match($regexp, $link, $matches)) {
                return $this->foundRutube($matches[1], $matches[2]);
            }
        }

        // Проверка на особенную ссылку RUtube`a
        if (is_null($html) && preg_match($this->regexp_rutube_extra, $this->link, $matches)) {
            $html = $this->fetchPage($matches[0]);
            if ($r = $this->maybeRutube($html)) {
                return $r;
            }
        }

        return false;
    }


    /**
     * Проверить, является ли ссылка на видео с VK.
     *
     * @return string|false Ссылка на видео с VK или false.
     */
    protected function maybeVk(string $html = null): string|false
    {
        $link = $html ?: $this->link;

        foreach ($this->regexp[self::VK] as $regexp) {
            if (preg_match($regexp, $link, $matches)) {
                return $this->foundVk($matches[1], $matches[2]);
            }
        }

        return false;
    }


    /**
     * Проверить, является ли ссылка на файл (например, mp4, avi, ogv, mpeg, webm).
     *
     * @return string|false Ссылка на файл или false.
     */
    protected function maybeFile($html = null): string|false
    {
        $link = $html ?: $this->link;

        foreach ($this->regexp[self::FILE] as $regexp) {
            if (preg_match($regexp, $link)) {
                return $this->foundFile($link);
            }
        }

        return false;
    }


    /**
     * Определяет видео как видео с YouTube по переданному ID.
     *
     * @param string $id ID видео на YouTube.
     * @return bool Возвращает true, если ID валидный и видео найдено, иначе false.
     */
    protected function foundYoutube($id): bool
    {
        if (empty($id) || strlen($id) != 11) {
            return false;
        }

        $this->hosting = self::YOUTUBE;
        $this->id = $id;
        $this->image = 'https://img.youtube.com/vi/' . $id . '/0.jpg';
        $this->video = 'https://www.youtube.com/embed/' . $id;

        $this->getYoutubeInfo($id);

        return true;
    }


    /**
     * Определяет видео как видео с Vimeo по переданному ID.
     *
     * @param string $id ID видео на Vimeo.
     * @return bool Возвращает true, если ID валидный и видео найдено, иначе false.
     */
    protected function foundVimeo($id): bool
    {
        if (empty($id) || !is_numeric($id)) {
            return false;
        }

        $this->hosting = self::VIMEO;
        $this->id = $id;
        $this->video = 'https://player.vimeo.com/video/' . $id;

        $this->getVimeoInfo($id);

        return true;
    }


    /**
     * Определяет видео как видео с VK по переданному ID.
     *
     * @param string $id ID видео на VK.
     * @return bool Возвращает true, если ID валидный и видео найдено, иначе false.
     */
    protected function foundVk($id, $embedId): bool
    {
        $this->hosting = self::VK;

        if (isset($embedId)) {
            $separatedLink[0] = $id;
            $separatedLink[1] = $embedId;
            $this->id = implode('_',[$id,$embedId]);
        } else {
            $this->id = $id;
            $separatedLink = explode("_", $id);
        }
        $this->video = 'https://vkvideo.ru/video_ext.php?oid=' . $separatedLink[0] . '&id=' . $separatedLink[1];
        $this->vkAPI = 'https://api.vk.com/method/video.get?v=5.81&videos=' . $this->id . '&access_token=' . $this->token;

        $this->getVkInfo($this->id);
        return true;
    }
    /**
     * Определяет видео как видео с Rutube по переданному ID.
     * Также поддерживает приватные видео через приватный ID.
     *
     * @param string $id ID видео на Rutube.
     * @param string $privateId Приватный ID видео (необязателен).
     * @return bool Возвращает true, если ID валидный и видео найдено, иначе false.
     */
    protected function foundRutube($id, $privateId = ''): bool
    {
        $this->hosting = self::RUTUBE;
        $this->id = $id;
        $this->privateId = '';

        if (empty($privateId)) {
            $this->video = 'https://rutube.ru/play/embed/' . $id;
            $this->pageVideo = 'https://rutube.ru/video/' . $id;
            $this->rutubeAPI = 'https://rutube.ru/api/play/options/' . $this->id;

            $this->getRutubeInfo($id);
        } else {
            $this->privateId = $privateId;
            $this->video = 'https://rutube.ru/play/embed/' . $id . '/?p=' . $this->privateId;
            $this->pageVideo = 'https://rutube.ru/video/' . $id . '/?p=' . $this->privateId;
            $this->rutubeAPI = 'https://rutube.ru/api/play/options/' . $this->id . '/?p=' . $this->privateId;

            $this->getRutubeInfo($id, $this->privateId);
        }

        return true;
    }


    /**
     * Определяет видео как обычный файл (например, mp4) по переданной ссылке.
     *
     * @param string $link Ссылка на видеофайл.
     * @return bool Возвращает true, если ссылка валидная, иначе false.
     */
    protected function foundFile($link): bool
    {
        $this->hosting = self::FILE;
        $this->video = $link;

        return true;
    }


    /**
     * Получает информацию о видео с Rutube по переданному ID.
     * Если указан приватный ID, запрос будет выполнен с ним.
     *
     * @param string $id ID видео на Rutube.
     * @param string $privateId Приватный ID видео (необязателен).
     * @return void Не возвращает значения, но заполняет свойства объекта.
     */
    protected function getRutubeInfo($id, $privateId = ''): void
    {
        if (empty($privateId)) {
            $APIJson = file_get_contents("https://rutube.ru/api/video/" . $id);
            $APIObj = json_decode($APIJson);
            $this->title = iconv('UTF-8//IGNORE', Encoding::getDefault() . '//IGNORE', $APIObj->title);
            $this->image = $APIObj->thumbnail_url;
        } else {
            $APIJson = file_get_contents("https://rutube.ru/api/video/" . $id . "/?p=" . $privateId);
            $APIObj = json_decode($APIJson);
            $this->title = iconv('UTF-8//IGNORE', Encoding::getDefault() . '//IGNORE', $APIObj->title);
            $this->image = $APIObj->thumbnail_url;
        }
    }

    /**
     * Получает информацию о видео с Vk по переданному ID.
     *
     *
     * @param string $id ID видео на Vk.
     * @return void Не возвращает значения, но заполняет свойства объекта.
     */
    protected function getVkInfo($id): void
    {
        $APIJson = file_get_contents("https://api.vk.com/method/video.get?v=5.81&videos=". $id . "&access_token=" . $this->token);
        $APIObj = json_decode($APIJson);

        $this->title = iconv('UTF-8//IGNORE', Encoding::getDefault() . '//IGNORE', $APIObj->response->items[0]->title);
        $this->image = $APIObj->response->items[0]->photo_1280;
    }


    /**
     * Получает информацию о видео с Vimeo по переданному ID.
     * Загружает XML-данные с API Vimeo.
     *
     * @param string $id ID видео на Vimeo.
     * @return void Не возвращает значения, но заполняет свойства объекта.
     */
    protected function getVimeoInfo($id): void
    {
        if (@$xml = simplexml_load_file('https://vimeo.com/api/v2/video/' . $id . '.xml')) {
            $this->title = (string)$xml->video->title;
            $this->image = (string)$xml->video->thumbnail_large ?: $xml->video->thumbnail_medium;
        }
    }


    /**
     * Получает информацию о видео с YouTube по переданному ID.
     * Запрашивает данные через внешний API noembed.
     *
     * @param string $id ID видео на YouTube.
     * @return void Не возвращает значения, но заполняет свойства объекта.
     */
    protected function getYoutubeInfo($id): void
    {
        $APIJson = file_get_contents("https://noembed.com/embed?dataType=json&url=https://www.youtube.com/embed/$id");
        $APIObj = json_decode($APIJson);
        $this->title = iconv('UTF-8//IGNORE', Encoding::getDefault() . '//IGNORE', $APIObj->title);
    }


    /**
     * Загружает содержимое страницы по переданному URL.
     * Использует cURL для получения данных с удаленного ресурса.
     *
     * @param string $url URL страницы для загрузки.
     * @return string|false Возвращает содержимое страницы в случае успеха, либо false при ошибке.
     */
    protected function fetchPage($url): string|false
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_FAILONERROR, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 3);

        return curl_exec($ch);
    }


    /**
     * Выполняет тестирование на основе переданных ссылок.
     * Для каждой ссылки выводит информацию о видео: хостинг, видео, название и изображение.
     *
     * @param array|null $links Массив ссылок для тестирования. Если не передан, используется статический массив.
     * @return void Не возвращает значений, выводит информацию на экран.
     */
    public static function RunTest($links = null): void
    {
        if (!is_array($links)) {
            $links = static::$test;
        }

        foreach ($links as $link) {
            $v = new static($link);
            echo "<h1>$link</h1>\n"
                . "<h3>" . $v->getHosting() . "</h3>"
                . "<b>Video:</b> " . $v->getVideo() . "<br />\n"
                . "<b>Name:</b> " . $v->getTitle() . "<br />\n"
                . "<b>Picture:</b> " . $v->getImage() . "<hr />\n";
        }
    }
}