<?php
namespace Admidio\Photos\Entity;

use Admidio\Infrastructure\Database;
use Admidio\Infrastructure\Exception;
use Admidio\Infrastructure\Entity\Entity;
use Admidio\Infrastructure\Email;
use Admidio\Infrastructure\Utils\FileSystemUtils;
use Admidio\Changelog\Entity\LogChanges;

/**
 * @brief Class manages access to database table adm_photos
 *
 * With the given id a photo album object is created from the data in the database table **adm_photos**.
 * The class will handle the communication with the database and give easy access to the data. New
 * photo albums could be created or existing photo albums could be edited. Special properties of
 * data like save urls, checks for evil code or timestamps of last changes will be handled within this class.
 *
 * @copyright The Admidio Team
 * @see https://www.admidio.org/
 * @license https://www.gnu.org/licenses/gpl-2.0.html GNU General Public License v2.0 only
 */
class Album extends Entity
{
    /**
     * @var bool|null Flag if this album has child albums
     */
    protected ?bool $hasChildAlbums;

    /**
     * Constructor that will create an object of a recordset of the table adm_photos.
     * If the id is set than the specific photo album will be loaded.
     * @param Database $database Object of the class Database. This should be the default global object **$gDb**.
     * @param int $phoId The recordset of the photo album with this id will be loaded. If id isn't set than an empty object of the table is created.
     * @throws Exception
     */
    public function __construct(Database $database, int $phoId = 0)
    {
        parent::__construct($database, TBL_PHOTOS, 'pho', $phoId);
    }

    /**
     * Initialize all necessary data of this object.
     * @return void
     * @throws Exception
     */
    public function clear()
    {
        parent::clear();

        $this->hasChildAlbums = null;
    }

    /**
     * Recursive function that returns the number of all images including sub-albums.
     * @param int $phoId
     * @return int
     * @throws Exception
     */
    public function countImages(int $phoId = 0): int
    {
        $totalImages = 0;

        // If no phoId is set, calculate the amount of pictures in the current album
        if ($phoId === 0) {
            $phoId = (int) $this->getValue('pho_id');
            $totalImages = (int) $this->getValue('pho_quantity');
        }

        // Get all sub-albums
        $sql = 'SELECT pho_id, pho_quantity
                  FROM '.TBL_PHOTOS.'
                 WHERE pho_pho_id_parent = ? -- $phoId
                   AND pho_locked = false';
        $childAlbumsStatement = $this->db->queryPrepared($sql, array($phoId));

        while ($phoRow = $childAlbumsStatement->fetch()) {
            $totalImages += (int) $phoRow['pho_quantity'] + $this->countImages((int) $phoRow['pho_id']);
        }

        return $totalImages;
    }

    /**
     * Creates the folder for the photo album in the file system.
     * @return array<string,string>|null
     * @throws Exception
     */
    public function createFolder(): ?array
    {
        $folderName = $this->getValue('pho_begin', 'Y-m-d') . '_' . (int) $this->getValue('pho_id');
        try {
            FileSystemUtils::createDirectoryIfNotExists(ADMIDIO_PATH . FOLDER_DATA . '/photos/' . $folderName);
        } catch (\RuntimeException $exception) {
            return array(
                'text' => 'SYS_FOLDER_NOT_CREATED',
                'path' => 'adm_my_files/photos/' . $folderName
            );
        }

        return null;
    }

    /**
     * Deletes the selected photo album and all sub photo albums.
     * After that the class will be initialized.
     * @return bool **true** if no error occurred
     * @throws Exception
     */
    public function delete(): bool
    {
        $returnValue = true;
        $albumId = $this->getValue('pho_id');

        $this->db->startTransaction();

        // First, delete all sub-albums (recursively)
        $sql = 'SELECT pho_id
                  FROM '.TBL_PHOTOS.'
                 WHERE pho_pho_id_parent = ? -- $albumId';
        $childAlbumStatement = $this->db->queryPrepared($sql, array($albumId));

        while ($phoId = $childAlbumStatement->fetchColumn()) {
            if ($returnValue) {
                $subAlbum = new Album($this->db, $phoId);
                $returnValue = $returnValue &&
                    $subAlbum->delete();
            }
        }

        // delete folder and then the database entry
        if ($returnValue) {
            $folder = ADMIDIO_PATH . FOLDER_DATA. '/photos/'.$this->getValue('pho_begin', 'Y-m-d').'_'.$albumId;

            // delete current folder including sub folders and files if it exists.
            try {
                $dirDeleted = FileSystemUtils::deleteDirectoryIfExists($folder, true);

                if ($dirDeleted) {
                    parent::delete();
                }
            } catch (\RuntimeException $exception) {
            }
        }

        $this->db->endTransaction();

        return $returnValue;
    }

    /**
     * Returns the name of the photographers. If there is no photographer(s) saved within this
     * album then the method will return the name "unknown".
     * @return string Name of the photographer(s)
     * @throws Exception
     */
    public function getPhotographer(): string
    {
        global $gL10n;

        $photographer = (string) $this->getValue('pho_photographers');

        if ($photographer === '') {
            $photographer = $gL10n->get('SYS_UNKNOWN');
        }

        return $photographer;
    }

    /**
     * Get the value of a column of the database table.
     * If the value was manipulated before with **setValue** than the manipulated value is returned.
     * @param string $columnName The name of the database column whose value should be read
     * @param string $format Returns the field value in a special format **text**, **html**, **database**
     *                                or datetime (detailed description in method description)
     *                                * 'd.m.Y' : a date or timestamp field accepts the format of the PHP date() function
     *                                * 'html'  : returns the value in html-format if this is necessary for that field type.
     *                                * 'database' : returns the value that is stored in database with no format applied
     * @return mixed Returns the value of the database column.
     *                         If the value was manipulated before with **setValue** than the manipulated value is returned.
     * @throws Exception
     */
    public function getValue(string $columnName, string $format = ''): mixed
    {
        if ($columnName === 'pho_description' && $format === 'html') {
            $value = nl2br(parent::getValue($columnName));
        } else {
            $value = parent::getValue($columnName, $format);
        }

        return $value;
    }

    /**
     * Check if this album has one or more child albums.
     * @return bool Return **true** if child albums exists.
     * @throws Exception
     */
    public function hasChildAlbums(): ?bool
    {
        if ($this->hasChildAlbums === null) {
            $sql = 'SELECT COUNT(*) AS count
                      FROM '.TBL_PHOTOS.'
                     WHERE pho_pho_id_parent = ? -- $this->getValue(\'pho_id\')';
            $countChildAlbums = $this->db->queryPrepared($sql, array((int) $this->getValue('pho_id')));

            $this->hasChildAlbums = $countChildAlbums->fetchColumn() > 0;
        }

        return $this->hasChildAlbums;
    }

    /**
     * This method checks if the current user is allowed to edit this photo album. Therefore,
     * the photo album must be visible to the user and must be of the current organization.
     * The user must be a member of at least one role that have the right to manage photo albums.
     * @return bool Return true if the current user is allowed to edit this photo album
     * @throws Exception
     */
    public function isEditable(): bool
    {
        global $gCurrentUser;

        return $gCurrentUser->isAdministratorPhotos() && ($this->isVisible() || (int) $this->getValue('pho_id') === 0);
    }

    /**
     * This method checks if the current user is allowed to view this photo album. Therefore,
     * the album must be from the current organization and should not be locked or the user
     * is a module administrator.
     * @return bool Return true if the current user is allowed to view this photo album
     * @throws Exception
     */
    public function isVisible(): bool
    {
        // current photo album must belong to current organization
        if ($this->getValue('pho_id') > 0 && (int) $this->getValue('pho_org_id') !== $GLOBALS['gCurrentOrgId']) {
            return false;
        }
        // locked photo album could only be viewed by module administrators
        elseif ($this->getValue('pho_locked') && !$GLOBALS['gCurrentUser']->isAdministratorPhotos()) {
            return false;
        }

        return true;
    }

    /**
     * Save all changed columns of the recordset in table of database. Therefore, the class remembers if it's
     * a new record or if only an update is necessary. The update statement will only update
     * the changed columns. If the table has columns for creator or editor than these column
     * with their timestamp will be updated.
     * The current organization will be set per default.
     * @param bool $updateFingerPrint Default **true**. Will update the creator or editor of the recordset if table has columns like **usr_id_create** or **usr_id_changed**
     * @return bool If an update or insert into the database was done then return true, otherwise false.
     * @throws Exception
     */
    public function save(bool $updateFingerPrint = true): bool
    {
        if ($this->newRecord) {
            $this->setValue('pho_org_id', $GLOBALS['gCurrentOrgId']);
        }

        return parent::save($updateFingerPrint);
    }

    /**
     * Send a notification email that a new photo album was created or an existing photo album was changed
     * to all members of the notification role. This role is configured within the global preference
     * **system_notifications_role**. The email contains the photo album title, the name of the current user,
     * the timestamp and the url to this photo album.
     * @return bool Returns **true** if the notification was sent
     * @throws Exception 'SYS_EMAIL_NOT_SEND'
     * @throws Exception
     */
    public function sendNotification(): bool
    {
        global $gCurrentOrganization, $gCurrentUser, $gSettingsManager, $gL10n;

        if ($gSettingsManager->getBool('system_notifications_new_entries')) {
            $notification = new Email();

            if ($this->isNewRecord()) {
                $messageTitleText = 'SYS_ALBUM_CREATED_TITLE';
                $messageUserText = 'SYS_CREATED_BY';
                $messageDateText = 'SYS_CREATED_AT';
            } else {
                $messageTitleText = 'SYS_ALBUM_CHANGED_TITLE';
                $messageUserText = 'SYS_CHANGED_BY';
                $messageDateText = 'SYS_CHANGED_AT';
            }

            $message = $gL10n->get($messageTitleText, array($gCurrentOrganization->getValue('org_longname'))) . '<br /><br />'
                . $gL10n->get('SYS_ALBUM') . ': ' . $this->getValue('pho_name') . '<br />'
                . $gL10n->get('SYS_START') . ': ' . $this->getValue('pho_begin') . '<br />'
                . $gL10n->get('SYS_END') . ': ' . $this->getValue('pho_end') . '<br />'
                . $gL10n->get($messageUserText) . ': ' . $gCurrentUser->getValue('FIRST_NAME') . ' ' . $gCurrentUser->getValue('LAST_NAME') . '<br />'
                . $gL10n->get($messageDateText) . ': ' . date($gSettingsManager->getString('system_date') . ' ' . $gSettingsManager->getString('system_time')) . '<br />'
                . $gL10n->get('SYS_URL') . ': ' . ADMIDIO_URL . FOLDER_MODULES . '/photos/photos.php?photo_uuid=' . $this->getValue('pho_uuid') . '<br />';
            return $notification->sendNotification(
                $gL10n->get($messageTitleText, array($gCurrentOrganization->getValue('org_longname'))),
                $message
            );
        }
        return false;
    }

    /**
     * Recursive function to select a sample image from an album as high as possible.
     * Return an array with all the necessary info to create the link.
     * @param int $phoId
     * @return array
     * @throws Exception
     */
    public function shuffleImage(int $phoId = 0): array
    {
        $shuffleImage = array('shuffle_pho_id' => 0, 'shuffle_img_nr' => 0, 'shuffle_img_begin' => '');

        // if no ID is given, try to take the random picture from the current album
        if ($phoId === 0) {
            $phoId = (int) $this->getValue('pho_id');
            $shuffleImage['shuffle_pho_id']    = $phoId;
            $shuffleImage['shuffle_pho_uuid']  = $this->getValue('pho_uuid');
            $shuffleImage['shuffle_img_begin'] = $this->getValue('pho_begin', 'Y-m-d');

            if ($this->getValue('pho_quantity') > 0) {
                $shuffleImage['shuffle_img_nr'] = mt_rand(1, (int) $this->getValue('pho_quantity'));
            }
        }

        if ($shuffleImage['shuffle_img_nr'] === 0) {
            // kein Bild vorhanden, dann in einem Unteralbum suchen
            $sql = 'SELECT pho_id, pho_uuid, pho_begin, pho_quantity
                      FROM '.TBL_PHOTOS.'
                     WHERE pho_pho_id_parent = ? -- $phoId
                       AND pho_locked = false
                  ORDER BY pho_quantity DESC';
            $childAlbumsStatement = $this->db->queryPrepared($sql, array($phoId));

            while ($phoRow = $childAlbumsStatement->fetch()) {
                if ($shuffleImage['shuffle_img_nr'] === 0) {
                    $shuffleImage['shuffle_pho_id']    = (int) $phoRow['pho_id'];
                    $shuffleImage['shuffle_pho_uuid']  = $phoRow['pho_uuid'];
                    $shuffleImage['shuffle_img_begin'] = $phoRow['pho_begin'];

                    if ($phoRow['pho_quantity'] > 0) {
                        $shuffleImage['shuffle_img_nr'] = mt_rand(1, $phoRow['pho_quantity']);
                    } else {
                        $shuffleImage = $this->shuffleImage((int) $phoRow['pho_id']);
                    }
                }
            }
        }

        return $shuffleImage;
    }
    /**
     * Adjust the changelog entry for this db record: Add the parent Album as a related object
     *
     * @param LogChanges $logEntry The log entry to adjust
     *
     * @return void
     */
    protected function adjustLogEntry(LogChanges $logEntry): void {
        if (!empty($this->getValue('pho_pho_id_parent'))) {
            $parEntry = new Album($this->db, $this->getValue('pho_pho_id_parent'));
            $logEntry->setLogRelated($parEntry->getValue('pho_uuid'), $parEntry->getValue('pho_name'));
        }
    }
}
