<?php

namespace Mautic\AssetBundle\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\CoreBundle\Entity\UuidInterface;
use Mautic\CoreBundle\Entity\UuidTrait;
use Mautic\CoreBundle\Helper\FileHelper;
use Mautic\CoreBundle\Loader\ParameterLoader;
use Mautic\ProjectBundle\Entity\ProjectTrait;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Mapping\ClassMetadata;

#[ApiResource(
    operations: [
        new GetCollection(security: "is_granted('asset:assets:viewown')"),
        new Post(security: "is_granted('asset:assets:create')"),
        new Get(security: "is_granted('asset:assets:viewown')"),
        new Put(security: "is_granted('asset:assets:editown')"),
        new Patch(security: "is_granted('asset:assets:editother')"),
        new Delete(security: "is_granted('asset:assets:deleteown')"),
    ],
    normalizationContext: [
        'groups'                  => ['asset:read'],
        'swagger_definition_name' => 'Read',
        'api_included'            => ['category'],
    ],
    denormalizationContext: [
        'groups'                  => ['asset:write'],
        'swagger_definition_name' => 'Write',
    ]
)]
class Asset extends FormEntity implements UuidInterface
{
    use UuidTrait;

    use ProjectTrait;

    public const ENTITY_NAME = 'asset';

    /**
     * @var int|null
     */
    #[Groups(['asset:read', 'download:read', 'email:read'])]
    private $id;

    /**
     * @var string|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $title;

    /**
     * @var string|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $description;

    /**
     * @var string|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $storageLocation = 'local';

    /**
     * @var string|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $path;

    /**
     * @var string|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $remotePath;

    /**
     * @var string|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $originalFileName;

    /**
     * @var File
     */
    private $file;

    /**
     * Holds upload directory.
     */
    private $uploadDir;

    /**
     * Holds max size of uploaded file.
     */
    private $maxSize;

    /**
     * Temporary location when asset file is beeing updated.
     * We need to keep the old file till we are sure the new
     * one is stored correctly.
     */
    private $temp;

    /**
     * Temporary ID used for file upload and validations
     * before the actual ID is known.
     */
    private $tempId;

    /**
     * Temporary file name used for file upload and validations
     * before the actual ID is known.
     */
    private $tempName;

    /**
     * @var string
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $alias;

    /**
     * @var string
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $language = 'en';

    /**
     * @var \DateTimeInterface|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $publishUp;

    /**
     * @var \DateTimeInterface|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $publishDown;

    /**
     * @var int
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $downloadCount = 0;

    /**
     * @var int
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $uniqueDownloadCount = 0;

    /**
     * @var int
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $revision = 1;

    /**
     * @var \Mautic\CategoryBundle\Entity\Category|null
     **/
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $category;

    /**
     * @var string|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $extension;

    /**
     * @var string|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $mime;

    /**
     * @var int|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $size;

    /**
     * @var string|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $downloadUrl;

    /**
     * @var bool|null
     */
    #[Groups(['asset:read', 'asset:write', 'download:read', 'email:read'])]
    private $disallow = true;

    public function __construct()
    {
        $this->initializeProjects();
    }

    public static function loadMetadata(ORM\ClassMetadata $metadata): void
    {
        $builder = new ClassMetadataBuilder($metadata);

        $builder->setTable('assets')
            ->setCustomRepositoryClass(AssetRepository::class)
            ->addIndex(['alias'], 'asset_alias_search');

        $builder->addIdColumns('title');

        $builder->addField('alias', 'string');

        $builder->createField('storageLocation', 'string')
            ->columnName('storage_location')
            ->nullable()
            ->build();

        $builder->createField('path', 'string')
            ->nullable()
            ->build();

        $builder->createField('remotePath', Types::TEXT)
            ->columnName('remote_path')
            ->nullable()
            ->build();

        $builder->createField('originalFileName', Types::TEXT)
            ->columnName('original_file_name')
            ->nullable()
            ->build();

        $builder->createField('language', 'string')
            ->columnName('lang')
            ->build();

        $builder->addPublishDates();

        $builder->createField('downloadCount', 'integer')
            ->columnName('download_count')
            ->build();

        $builder->createField('uniqueDownloadCount', 'integer')
            ->columnName('unique_download_count')
            ->build();

        $builder->addField('revision', 'integer');

        $builder->addCategory();

        $builder->createField('extension', 'string')
            ->nullable()
            ->build();

        $builder->createField('mime', 'string')
            ->nullable()
            ->build();

        $builder->createField('size', 'integer')
            ->nullable()
            ->build();

        $builder->createField('disallow', 'boolean')
            ->nullable()
            ->build();

        static::addUuidField($builder);
        self::addProjectsField($builder, 'asset_projects_xref', 'asset_id');
    }

    /**
     * Prepares the metadata for API usage.
     */
    public static function loadApiMetadata(ApiMetadataDriver $metadata): void
    {
        $metadata->setGroupPrefix('asset')
            ->addListProperties(
                [
                    'id',
                    'title',
                    'alias',
                    'category',
                    'description',
                ]
            )
            ->addProperties(
                [
                    'language',
                    'publishUp',
                    'publishDown',
                    'downloadCount',
                    'uniqueDownloadCount',
                    'revision',
                    'extension',
                    'mime',
                    'size',
                    'downloadUrl',
                    'storageLocation',
                    'disallow',
                ]
            )
            ->build();

        self::addProjectsInLoadApiMetadata($metadata, 'asset');
    }

    /**
     * Clone magic function.
     */
    public function __clone()
    {
        $this->id = null;

        parent::__clone();
    }

    /**
     * Get id.
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Sets file.
     */
    public function setFile(?File $file = null): void
    {
        $this->file = $file;

        // check if we have an old asset path
        if (isset($this->path)) {
            // store the old name to delete after the update
            $this->temp = $this->path;
            $this->path = null;
        }
    }

    /**
     * Get file.
     *
     * @return UploadedFile
     */
    public function getFile()
    {
        // if file is not set, try to find it at temp folder
        if ($this->isLocal() && empty($this->file)) {
            $tempFile = $this->loadFile(true);

            if ($tempFile) {
                $this->setFile($tempFile);
            }
        }

        return $this->file;
    }

    /**
     * Set title.
     *
     * @param string $title
     *
     * @return Asset
     */
    public function setTitle($title)
    {
        $this->isChanged('title', $title);
        $this->title = $title;

        return $this;
    }

    /**
     * Get title.
     *
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * @return mixed
     */
    public function getExtension()
    {
        return $this->extension;
    }

    /**
     * @param mixed $extension
     */
    public function setExtension($extension): void
    {
        $this->extension = $extension;
    }

    /**
     * @return mixed
     */
    public function getMime()
    {
        return $this->mime;
    }

    /**
     * @param mixed $mime
     */
    public function setMime($mime): void
    {
        $this->mime = $mime;
    }

    /**
     * Set originalFileName.
     *
     * @param string $originalFileName
     *
     * @return Asset
     */
    public function setOriginalFileName($originalFileName)
    {
        $this->isChanged('originalFileName', $originalFileName);
        $this->originalFileName = $originalFileName;

        return $this;
    }

    /**
     * Get originalFileName.
     *
     * @return string
     */
    public function getOriginalFileName()
    {
        return $this->originalFileName;
    }

    /**
     * Set storage location.
     *
     * @param string $storageLocation
     *
     * @return Asset
     */
    public function setStorageLocation($storageLocation)
    {
        $this->isChanged('storageLocation', $storageLocation);
        $this->storageLocation = $storageLocation;

        return $this;
    }

    /**
     * Get storage location.
     *
     * @return string
     */
    public function getStorageLocation()
    {
        if (null === $this->storageLocation) {
            $this->storageLocation = 'local';
        }

        return $this->storageLocation;
    }

    /**
     * Set path.
     *
     * @param string $path
     *
     * @return Asset
     */
    public function setPath($path)
    {
        $this->isChanged('path', $path);
        $this->path = $path;

        return $this;
    }

    /**
     * Get path.
     *
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * Set remote path.
     *
     * @param string $remotePath
     *
     * @return Asset
     */
    public function setRemotePath($remotePath)
    {
        $this->isChanged('remotePath', $remotePath);
        $this->remotePath = $remotePath;

        return $this;
    }

    /**
     * Get remote path.
     *
     * @return string
     */
    public function getRemotePath()
    {
        return $this->remotePath;
    }

    /**
     * Set alias.
     *
     * @param string $alias
     *
     * @return Asset
     */
    public function setAlias($alias)
    {
        $this->isChanged('alias', $alias);
        $this->alias = $alias;

        return $this;
    }

    /**
     * Get alias.
     *
     * @return string
     */
    public function getAlias()
    {
        return $this->alias;
    }

    /**
     * Set publishUp.
     *
     * @param \DateTime $publishUp
     *
     * @return Asset
     */
    public function setPublishUp($publishUp)
    {
        $this->isChanged('publishUp', $publishUp);
        $this->publishUp = $publishUp;

        return $this;
    }

    /**
     * Get publishUp.
     *
     * @return \DateTimeInterface
     */
    public function getPublishUp()
    {
        return $this->publishUp;
    }

    /**
     * Set publishDown.
     *
     * @param \DateTimeInterface $publishDown
     *
     * @return Asset
     */
    public function setPublishDown($publishDown)
    {
        $this->isChanged('publishDown', $publishDown);
        $this->publishDown = $publishDown;

        return $this;
    }

    /**
     * Get publishDown.
     *
     * @return \DateTimeInterface
     */
    public function getPublishDown()
    {
        return $this->publishDown;
    }

    /**
     * Set downloadCount.
     *
     * @param int $downloadCount
     *
     * @return Asset
     */
    public function setDownloadCount($downloadCount)
    {
        $this->downloadCount = $downloadCount;

        return $this;
    }

    /**
     * Get downloadCount.
     *
     * @return int
     */
    public function getDownloadCount()
    {
        return $this->downloadCount;
    }

    /**
     * Set revision.
     *
     * @param int $revision
     *
     * @return Asset
     */
    public function setRevision($revision)
    {
        $this->revision = $revision;

        return $this;
    }

    /**
     * Get revision.
     *
     * @return int
     */
    public function getRevision()
    {
        return $this->revision;
    }

    /**
     * Set language.
     *
     * @param string $language
     *
     * @return Asset
     */
    public function setLanguage($language)
    {
        $this->isChanged('language', $language);
        $this->language = $language;

        return $this;
    }

    /**
     * Get language.
     *
     * @return string
     */
    public function getLanguage()
    {
        return $this->language;
    }

    /**
     * Set category.
     *
     * @return Asset
     */
    public function setCategory(?\Mautic\CategoryBundle\Entity\Category $category = null)
    {
        $this->isChanged('category', $category);
        $this->category = $category;

        return $this;
    }

    /**
     * Get category.
     *
     * @return \Mautic\CategoryBundle\Entity\Category
     */
    public function getCategory()
    {
        return $this->category;
    }

    /**
     * Set uniqueDownloadCount.
     *
     * @param int $uniqueDownloadCount
     *
     * @return Asset
     */
    public function setUniqueDownloadCount($uniqueDownloadCount)
    {
        $this->uniqueDownloadCount = $uniqueDownloadCount;

        return $this;
    }

    /**
     * Get uniqueDownloadCount.
     *
     * @return int
     */
    public function getUniqueDownloadCount()
    {
        return $this->uniqueDownloadCount;
    }

    public function setFileNameFromRemote(): void
    {
        $fileName = basename($this->getRemotePath());

        $this->setOriginalFileName($fileName);

        // set the asset title as original file name if title is missing
        if (null === $this->getTitle()) {
            $this->setTitle($fileName);
        }
    }

    public function preUpload(): void
    {
        if (null !== $this->getFile()) {
            // set the asset title as original file name if title is missing
            if (null === $this->getTitle()) {
                $this->setTitle($this->file->getClientOriginalName());
            }

            $filename  = sha1(uniqid(mt_rand(), true));
            $extension = $this->getFile()->guessExtension();

            if (empty($extension)) {
                // get it from the original name
                $extension = pathinfo($this->originalFileName, PATHINFO_EXTENSION);
            }
            $this->path = $filename.'.'.$extension;
        } elseif ($this->isRemote() && null !== $this->getRemotePath()) {
            $this->setFileNameFromRemote();
        }
    }

    public function upload(): void
    {
        // the file property can be empty if the field is not required
        if (null === $this->getFile()) {
            // check for the remote and set type data
            if ($this->isRemote()) {
                $this->setFileInfoFromFile();
            }

            return;
        }

        // move takes the target directory and then the
        // target filename to move to
        $this->getFile()->move($this->getUploadDir(), $this->path);
        $filePath = $this->getUploadDir().'/'.$this->temp;

        $this->setFileInfoFromFile();

        // check if we have an old asset
        if (isset($this->temp) && file_exists($filePath)) {
            // delete the old asset
            unlink($filePath);
            // clear the temp asset path
            $this->temp = null;
        }

        // Remove temporary folder and files
        $fs = new Filesystem();
        $fs->remove($this->getAbsoluteTempDir());

        // clean up the file property as you won't need it anymore
        $this->file = null;
    }

    /**
     * Remove a file.
     */
    public function setFileInfoFromFile(): void
    {
        // get some basic information about the file type
        $fileInfo = $this->getFileInfo();

        if (!is_array($fileInfo)) {
            return;
        }

        // set the mime and extension column values
        $this->setExtension($fileInfo['extension']);
        $this->setMime($fileInfo['mime']);
        $this->setSize($fileInfo['size']);
    }

    /**
     * Remove a file.
     *
     * @param bool $temp >> regular uploaded file or temporary
     */
    public function removeUpload($temp = false): void
    {
        if ($temp) {
            $file = $this->getAbsoluteTempPath();
        } else {
            $file = $this->getAbsolutePath();
        }

        if ($file && file_exists($file)) {
            unlink($file);
        }
    }

    /**
     * Returns absolute path to the file.
     *
     * @return string
     */
    public function getAbsolutePath()
    {
        return null === $this->path
            ? null
            : $this->getUploadDir().'/'.$this->path;
    }

    /**
     * Returns absolute path to temporary file.
     *
     * @return string
     */
    public function getAbsoluteTempPath()
    {
        return null === $this->tempId || null === $this->tempName
            ? null
            : $this->getAbsoluteTempDir().'/'.$this->tempName;
    }

    /**
     * Returns absolute path to temporary file.
     *
     * @return string
     */
    public function getAbsoluteTempDir()
    {
        return null === $this->tempId
            ? null
            : $this->getUploadDir().'/tmp/'.$this->tempId;
    }

    /**
     * Returns absolute path to upload dir.
     *
     * @return string
     */
    protected function getUploadDir()
    {
        if ($this->uploadDir) {
            return $this->uploadDir;
        }

        return 'media/files';
    }

    /**
     * Set uploadDir.
     *
     * @param string $uploadDir
     *
     * @return Asset
     */
    public function setUploadDir($uploadDir)
    {
        $this->uploadDir = $uploadDir;

        return $this;
    }

    /**
     * Returns maximal uploadable size in bytes.
     * If not set, 6000000 is default.
     *
     * @return string
     */
    protected function getMaxSize()
    {
        if ($this->maxSize) {
            return $this->maxSize;
        }

        return 6_000_000;
    }

    /**
     * Set max size.
     *
     * @param string $maxSize
     *
     * @return Asset
     */
    public function setMaxSize($maxSize)
    {
        $this->maxSize = $maxSize;

        return $this;
    }

    /**
     * Returns file extension.
     *
     * @return string
     */
    public function getFileType()
    {
        if (!empty($this->extension) && empty($this->changes['originalFileName'])) {
            return $this->extension;
        }

        if ($this->isRemote()) {
            return pathinfo(parse_url($this->getRemotePath(), PHP_URL_PATH), PATHINFO_EXTENSION);
        }

        if (null === $this->loadFile()) {
            return '';
        }

        return $this->loadFile()->guessExtension();
    }

    /**
     * Returns some file info.
     *
     * @return array
     */
    public function getFileInfo()
    {
        $fileInfo = [];

        if ($this->isRemote()) {
            $ch = curl_init($this->getRemotePath());
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch, CURLOPT_HEADER, true);
            curl_setopt($ch, CURLOPT_NOBODY, true);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
            curl_exec($ch);

            // build an array of handy info
            $fileInfo['mime']      = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
            $fileInfo['extension'] = $this->getFileType();
            $fileInfo['size']      = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);

            return $fileInfo;
        }

        if (null === $this->loadFile()) {
            return '';
        }

        // return an array of file type info
        $fileInfo['mime']      = $this->loadFile()->getMimeType();
        $fileInfo['extension'] = $this->getFileType();
        $fileInfo['size']      = $this->getSize(false, true);

        return $fileInfo;
    }

    /**
     * Returns file mime type.
     *
     * @return string
     */
    public function getFileMimeType()
    {
        if ($this->isRemote()) {
            $ch = curl_init($this->getRemotePath());
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch, CURLOPT_HEADER, true);
            curl_setopt($ch, CURLOPT_NOBODY, true);
            curl_exec($ch);

            return curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
        }

        if (null === $this->loadFile()) {
            return '';
        }

        return $this->loadFile()->getMimeType();
    }

    /**
     * Returns icon class based on file type.
     */
    public function getIconClass(): string
    {
        $fileType = $this->getFileType();

        // return missing file icon if file type is empty
        if (!$fileType) {
            return 'ri-prohibited-line';
        }

        $fileTypes = $this->getFileExtensions();

        // Search for icon name by file extension.
        foreach ($fileTypes as $icon => $extensions) {
            if (in_array($fileType, $extensions)) {
                return 'ri-file-'.$icon.'-line';
            }
        }

        // File extension is unknown, display general file icon.
        return 'ri-file-line';
    }

    /**
     * Decides if an asset is image displayable by browser.
     */
    public function isImage(): bool
    {
        $fileType = strtolower($this->getFileType());

        if (!$fileType) {
            return false;
        }

        $imageTypes = ['jpg', 'jpeg', 'png', 'gif'];

        if (in_array($fileType, $imageTypes)) {
            return true;
        }

        return false;
    }

    /**
     * Returns array of common extensions.
     *
     * @return array<string, string[]>
     */
    public function getFileExtensions(): array
    {
        return [
            'excel' => [
                'xlsx',
                'xlsm',
                'xlsb',
                'xltx',
                'xltm',
                'xls',
                'xlt',
            ],
            'word' => [
                'doc',
                'docx',
                'docm',
                'dotx',
            ],
            'pdf' => [
                'pdf',
            ],
            'audio' => [
                'mp3',
            ],
            'zip' => [
                'zip',
                'rar',
                'iso',
                'tar',
                'gz',
                '7z',
            ],
            'image' => [
                'jpg',
                'jpeg',
                'png',
                'gif',
                'ico',
                'bmp',
                'psd',
            ],
            'text' => [
                'txt',
                'pub',
            ],
            'code' => [
                'php',
                'js',
                'json',
                'yaml',
                'xml',
                'html',
                'htm',
                'sql',
            ],
            'ppt' => [
                'ppt',
                'pptx',
                'pptm',
                'xps',
                'potm',
                'potx',
                'pot',
                'pps',
                'odp',
            ],
            'video' => [
                'wmv',
                'avi',
                'mp4',
                'mkv',
                'mpeg',
            ],
        ];
    }

    /**
     * Load the file object from it's path.
     *
     * @return File|null
     */
    public function loadFile($temp = false)
    {
        if ($temp) {
            $path = $this->getAbsoluteTempPath();
        } else {
            $path = $this->getAbsolutePath();
        }

        if (!$path || !file_exists($path)) {
            return null;
        }

        try {
            $file = new File($path);
        } catch (FileNotFoundException) {
            $file = null;
        }

        return $file;
    }

    /**
     * Load content of the file from it's path.
     */
    public function getFileContents(): string|bool
    {
        $path = $this->getFilePath();

        return file_get_contents($path);
    }

    /**
     * Get the path to the file; a URL if remote or full file path if local.
     *
     * @return string
     */
    public function getFilePath()
    {
        return $this->isRemote() ? $this->getRemotePath() : $this->getAbsolutePath();
    }

    /**
     * @return mixed
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * @param mixed $description
     */
    public function setDescription($description): void
    {
        $this->description = $description;
    }

    public static function loadValidatorMetadata(ClassMetadata $metadata): void
    {
        // Add a constraint to manage the file upload data
        $metadata->addConstraint(new Assert\Callback([self::class, 'validateFile']));
    }

    /**
     * Validator to ensure proper data for the file fields.
     *
     * @param Asset                     $object  Entity object to validate
     * @param ExecutionContextInterface $context Context object
     */
    public static function validateFile($object, ExecutionContextInterface $context): void
    {
        if ($object->isLocal()) {
            $tempName = $object->getTempName();
            $path     = $object->getPath();

            // If the object is stored locally, we should have file data
            if ($object->isNew() && null === $tempName && null === $path) {
                $context->buildViolation('mautic.asset.asset.error.missing.file')
                    ->atPath('tempName')
                    ->setTranslationDomain('validators')
                    ->addViolation();
            }

            if (null === $object->getTitle()) {
                $context->buildViolation('mautic.asset.asset.error.missing.title')
                    ->atPath('title')
                    ->setTranslationDomain('validators')
                    ->addViolation();
            }
            $parameters        = (new ParameterLoader())->getParameterBag();
            $extensionsAllowed = $parameters->get('allowed_extensions');
            $mimeTypesMap      = $parameters->get('allowed_mimetypes');
            $mimeTypesAllowed  = array_intersect_key($mimeTypesMap, array_flip($extensionsAllowed));

            $fileMimeType        = $object->getFileMimeType();
            $fileExtension       = strtolower($object->getExtension() ?? '');
            $lowercaseMimeTypes  = array_change_key_case($mimeTypesAllowed, CASE_LOWER);
            $lowercaseMimeValues = array_map('strtolower', $mimeTypesAllowed);

            if (!empty($fileMimeType) && array_key_exists($fileExtension, $lowercaseMimeTypes) && !in_array(strtolower($fileMimeType), $lowercaseMimeValues, true)) {
                $context->buildViolation('mautic.asset.asset.error.invalid.mimetype', [
                    '%fileMimetype%'=> $object->getFileMimeType(),
                    '%mimetypes%'   => implode(', ', $mimeTypesAllowed),
                ])->atPath('file')
                    ->setTranslationDomain('validators')
                    ->addViolation();
            }

            $fileType = $object->getExtension();
            if (null !== $fileType && !in_array(strtolower($fileType), array_map('strtolower', $extensionsAllowed), true)) {
                $context->buildViolation('mautic.asset.asset.error.file.extension', [
                    '%fileExtension%'=> $object->getExtension(),
                    '%extensions%'   => implode(', ', $extensionsAllowed),
                ])->atPath('file')
                    ->setTranslationDomain('validators')
                    ->addViolation();
            }

            // Unset any remote file data
            $object->setRemotePath(null);
        } elseif ($object->isRemote()) {
            // If the object is stored remotely, we should have a remote path
            if (null === $object->getRemotePath()) {
                $context->buildViolation('mautic.asset.asset.error.missing.remote.path')
                    ->atPath('remotePath')
                    ->setTranslationDomain('validators')
                    ->addViolation();
            }

            // Unset any local file data
            $object->setPath(null);
        }
    }

    /**
     * Set temporary ID.
     *
     * @param string $tempId
     *
     * @return Asset
     */
    public function setTempId($tempId)
    {
        $this->tempId = $tempId;

        return $this;
    }

    /**
     * Get temporary ID.
     *
     * @return string
     */
    public function getTempId()
    {
        return $this->tempId;
    }

    /**
     * Set temporary file name.
     *
     * @param string $tempName
     *
     * @return Asset
     */
    public function setTempName($tempName)
    {
        $this->tempName = $tempName;

        return $this;
    }

    /**
     * Get temporary file name.
     *
     * @return string
     */
    public function getTempName()
    {
        return $this->tempName;
    }

    /**
     * @param bool   $humanReadable
     * @param bool   $forceUpdate
     * @param string $inUnit
     *
     * @return float|string
     */
    public function getSize($humanReadable = true, $forceUpdate = false, $inUnit = '')
    {
        if (empty($this->size) || $forceUpdate) {
            // Try to fetch it
            if ($this->isRemote()) {
                $ch = curl_init($this->getRemotePath());
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
                curl_setopt($ch, CURLOPT_HEADER, true);
                curl_setopt($ch, CURLOPT_NOBODY, true);
                curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);

                curl_exec($ch);

                $this->setSize(round(curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD)));
            }

            if (null === $this->loadFile()) {
                return 0;
            }

            $this->setSize(round($this->loadFile()->getSize()));
        }

        return ($humanReadable) ? static::convertBytesToHumanReadable($this->size, $inUnit) : $this->size;
    }

    /**
     * @param mixed $size
     *
     * @return Asset
     */
    public function setSize($size)
    {
        $this->size = $size;

        return $this;
    }

    /**
     * Get value from PHP configuration with special handling of -1.
     *
     * @param string    $setting
     * @param bool|true $convertToBytes
     */
    public static function getIniValue($setting, $convertToBytes = true): int
    {
        $value = ini_get($setting);

        if (-1 == $value || 0 === $value) {
            return PHP_INT_MAX;
        }

        if ($convertToBytes) {
            $value = FileHelper::convertPHPSizeToBytes($value);
        }

        return (int) $value;
    }

    /**
     * @param string $unit
     */
    public static function convertBytesToHumanReadable($size, $unit = ''): string
    {
        [$number, $unit] = self::convertBytesToUnit($size, $unit);

        // Format number
        $number = number_format($number, 2);

        // Remove trailing .00
        $number = str_contains($number, '.') ? rtrim(rtrim($number, '0'), '.') : $number;

        return $number.' '.$unit;
    }

    /**
     * @param string $unit
     */
    public static function convertBytesToUnit($size, $unit = ''): array
    {
        $unit = strtoupper($unit);

        if ((!$unit && $size >= 1 << 30) || 'GB' == $unit || 'G' == $unit) {
            return [$size / (1 << 30), 'GB'];
        }
        if ((!$unit && $size >= 1 << 20) || 'MB' == $unit || 'M' == $unit) {
            return [$size / (1 << 20), 'MB'];
        }
        if ((!$unit && $size >= 1 << 10) || 'KB' == $unit || 'K' == $unit) {
            return [$size / (1 << 10), 'KB'];
        }

        // Add zero to remove useless .00
        return [$size, 'bytes'];
    }

    /**
     * @return string|null
     */
    public function getDownloadUrl()
    {
        return $this->downloadUrl;
    }

    /**
     * @param string|null $downloadUrl
     *
     * @return Asset
     */
    public function setDownloadUrl($downloadUrl)
    {
        $this->downloadUrl = $downloadUrl;

        return $this;
    }

    public function isLocal(): bool
    {
        return 'local' === $this->storageLocation;
    }

    public function isRemote(): bool
    {
        return 'remote' === $this->storageLocation;
    }

    /**
     * @return bool
     */
    public function getDisallow()
    {
        return $this->disallow;
    }

    /**
     * @param mixed $disallow
     */
    public function setDisallow($disallow): void
    {
        $this->disallow = $disallow;
    }
}
