<?php

declare(strict_types=1);

/*
 * This file is part of Oveleon ContaoMemberExtension Bundle.
 *
 * @package     contao-member-extension-bundle
 * @license     MIT
 * @author      Sebastian Zoglowek     <https://github.com/zoglo>
 * @author      Daniele Sciannimanica  <https://github.com/doishub>
 * @author      Fabian Ekert           <https://github.com/eki89>
 * @copyright   Oveleon                <https://www.oveleon.de/>
 */

namespace Oveleon\ContaoMemberExtensionBundle;

use Contao\Config;
use Contao\CoreBundle\Monolog\ContaoContext;
use Contao\Dbafs;
use Contao\File;
use Contao\FilesModel;
use Contao\FileUpload;
use Contao\Frontend;
use Contao\FrontendUser;
use Contao\MemberModel;
use Contao\StringUtil;
use Contao\System;
use Contao\Validator;
use Psr\Log\LogLevel;

/**
 * Class Member
 *
 * @property int $avatar UUID of the avatar
 */
class Member extends Frontend
{
    const DEFAULT_PICTURE = 'bundles/contaomemberextension/avatar.png';

    /**
     * MemberAvatar file name
     */
    protected string $avatarName = 'memberAvatar';

    /**
     * Create avatar for a member | Registration
     */
    public function createAvatar(int $userId, array $arrData): void
    {
        $objMember = MemberModel::findById($userId);
        $this->processAvatar($objMember, $arrData);
    }

    /**
     * Update avatar of a member | Login
     */
    public function updateAvatar(FrontendUser $objUser, array $arrData): void
    {
        $objMember = MemberModel::findById($objUser->id);
        $this->processAvatar($objMember, $arrData);
    }

    /**
     * Process avatar upload for a member
     */
    protected function processAvatar(MemberModel $objMember, ?array $arrData): void
    {
        $objMember = MemberModel::findByPk($objMember->id);

        if ($objMember === null)
        {
            return;
        }

        // ToDo: remove $_SESSION when contao 4.13 support ends (Contao ^5.* is not possible with Contao 4.* support)
        $file = $_SESSION['FILES']['avatar'];
        $maxlength_kb = $this->getMaximumUploadSize();
        $maxlength_kb_readable = $this->getReadableSize($maxlength_kb);

        // Sanitize the filename
        try
        {
            $file['name'] = StringUtil::sanitizeFileName($file['name']);
        }
        catch (\InvalidArgumentException $e)
        {
            // ToDo: add error message for invalid characters
            return;
        }

        // Invalid file name
        if (!Validator::isValidFileName($file['name']))
        {
            // ToDo: add error message for invalid characters
            return;
        }

        // File was not uploaded
        if (!is_uploaded_file($file['tmp_name']))
        {
            // ToDo: Add error messages
            /*if ($file['error'] == 1 || $file['error'] == 2) { // Add error message for maximum file size }
            elseif ($file['error'] == 3) { // Add error message for partial upload }
            elseif ($file['error'] > 0) { // Add error message for failed upload }*/

            unset($_SESSION['FILES']['avatar']);

            return;
        }

        // File is too big
        if ($file['size'] > $maxlength_kb)
        {
            // ToDo: add error message for maximum file size
            unset($_SESSION['FILES']['avatar']);

            return;
        }

        $objFile = new File($file['name']);
        $uploadTypes = StringUtil::trimsplit(',', Config::get('validImageTypes'));

        // File type is not allowed
        if (!\in_array($objFile->extension, $uploadTypes))
        {
            // ToDo: add error message for not allowed file type
            unset($_SESSION['FILES']['avatar']);

            return;
        }

        if ($arrImageSize = @getimagesize($file['tmp_name']))
        {
            $intImageWidth = Config::get('imageWidth');

            // Image exceeds maximum image width
            if ($intImageWidth > 0 && $arrImageSize[0] > $intImageWidth) {
                // ToDo: add error message for exceeding width
                unset($_SESSION['FILES']['avatar']);

                return;
            }

            $intImageHeight = Config::get('imageHeight');

            // Image exceeds maximum image height
            if ($intImageHeight > 0 && $arrImageSize[1] > $intImageHeight) {
                // ToDo: add error message for exceeding height
                unset($_SESSION['FILES']['avatar']);

                return;
            }
        }

        // Upload valid file type with no width and height -> svg

        // Don't upload if no homedir is assigned
        // ToDo: Create homedir?
        if (!$objMember->assignDir || !$objMember->homeDir)
        {
            // ToDo: add error message for no homedir
            return;
        }

        $intUploadFolder = $objMember->homeDir;

        $objUploadFolder = FilesModel::findByUuid($intUploadFolder);

        // The upload folder could not be found
        if ($objUploadFolder === null)
        {
            throw new Exception("Invalid upload folder ID $intUploadFolder");
        }

        $strUploadFolder = $objUploadFolder->path;

        // Store the file if the upload folder exists
        $projectDir = System::getContainer()->getParameter('kernel.project_dir');

        if (!!$strUploadFolder & is_dir($projectDir . '/' . $strUploadFolder))
        {
            // Delete existing avatar if it exists
            $this->deleteAvatar($objMember);

            $this->import('Files');

            // Rename file
            $file['name'] =  $this->avatarName . '.' . $objFile->extension;

            // Move the file to its destination
            $this->Files->move_uploaded_file($file['tmp_name'], $strUploadFolder . '/' . $file['name']);
            $this->Files->chmod($strUploadFolder . '/' . $file['name'], 0666 & ~umask());

            $strUuid = null;
            $strFile = $strUploadFolder . '/' . $file['name'];


            // Generate the DB entries
            if (Dbafs::shouldBeSynchronized($strFile))
            {
                $objModel = FilesModel::findByPath($strFile);

                if ($objModel === null)
                {
                    $objModel = Dbafs::addResource($strFile);
                }

                $strUuid = StringUtil::binToUuid($objModel->uuid);

                // Update the hash of the target folder
                Dbafs::updateFolderHashes($strUploadFolder);

                // Update member avatar
                $objMember->avatar = $objModel->uuid;
                $objMember->save();
            }

            // Add the session entry
            $_SESSION['FILES']['avatar'] = array
            (
                'name'     => $file['name'],
                'type'     => $file['type'],
                'tmp_name' => $projectDir . '/' . $strFile,
                'error'    => $file['error'],
                'size'     => $file['size'],
                'uploaded' => true,
                'uuid'     => $strUuid
            );

            // Add a log entry
            $logger = System::getContainer()->get('monolog.logger.contao');
            $logger->log(LogLevel::INFO, 'File "' . $strUploadFolder . '/' . $file['name'] . '" has been uploaded', ['contao' => new ContaoContext(__METHOD__, TL_FILES)]);
        }

        unset($_SESSION['FILES']['avatar']);
    }

    /**
     * Return the maximum upload file size in bytes
     */
    protected function getMaximumUploadSize()
    {
        if ($this->maxlength > 0)
        {
            return $this->maxlength;
        }

        return FileUpload::getMaxUploadSize();
    }

    /**
     * Parses an avatar to the template
     */
    public static function parseMemberAvatar(?MemberModel $objMember, &$objTemplate, ?string $imgSize): void
    {
        $objTemplate->addImage= true;

        $objTemplate->singleSRC = self::DEFAULT_PICTURE;
        $objTemplate->addFallbackImage = true;

        $projectDir = System::getContainer()->getParameter('kernel.project_dir');

        // Check if member avatar exists
        if (null === $objMember || null === $objMember->avatar || null === ($objFile = FilesModel::findByUuid($objMember->avatar)) || !\is_file($projectDir.'/'. $objFile->path))
        {
            $objFile = !!($uuidDefault = Config::get('defaultAvatar')) ? FilesModel::findByUuid($uuidDefault) : null;
        }

        // Check if config avatar exists
        if (null === $objFile || !\is_file($projectDir . '/' . $objFile->path))
        {
            return;
        }

        $objTemplate->addFallbackImage = false;
        $imgSize = $imgSize ?? null;

        $figureBuilder = System::getContainer()
            ->get('contao.image.studio')
            ->createFigureBuilder()
            ->from($objFile->path)
            ->setSize($imgSize)
        ;

        if (null !== ($figure = $figureBuilder->buildIfResourceExists()))
        {
            $figure->applyLegacyTemplateData($objTemplate);
        }
    }

    /**
     * Gets the url for a member avatar
     */
    public static function getMemberAvatarURL(?MemberModel $objMember): string
    {
        // ToDo: Merge logic with parseMemberAvatar
        $projectDir = System::getContainer()->getParameter('kernel.project_dir');

        if (null === $objMember || null === $objMember->avatar || null === ($objFile = FilesModel::findByUuid($objMember->avatar)) || !\is_file($projectDir.'/'. $objFile->path))
        {
            $objFile = !!($uuidDefault = Config::get('defaultAvatar')) ? FilesModel::findByUuid($uuidDefault) : null;
        }

        // Check if config avatar exists
        if (null === $objFile || !\is_file($projectDir . '/' . $objFile->path))
        {
            return self::DEFAULT_PICTURE;
        }

        return $objFile->path;
    }

    /**
     * Deletes an avatar
     */
    public static function deleteAvatar(MemberModel $objMember): void
    {
        if (!!$objMember->avatar)
        {
            $objFile = FilesModel::findByUuid($objMember->avatar) ?: '';
            $projectDir = System::getContainer()->getParameter('kernel.project_dir');

            // Only delete if file exists
            if (!!$objFile && file_exists($projectDir . '/' . $objFile->path))
            {
                $file = new File($objFile->path);
                $file->delete();
            }
        }
    }
}