<?php declare(strict_types=1);
/*
 * Copyright (C) 2015-2017 IRSTEA
 * All rights reserved.
 */

namespace Irstea\FileUploadBundle\Entity\Repository;

use DateTime;
use Doctrine\ORM\EntityRepository;
use Gaufrette\Filesystem;
use Gaufrette\StreamMode;
use InvalidArgumentException;
use Irstea\FileUploadBundle\Entity\UploadedFile;
use Irstea\FileUploadBundle\Event\FileUploadCompleteEvent;
use Irstea\FileUploadBundle\Exception\RejectedFileException;
use Irstea\FileUploadBundle\FileUploadEvents;
use Irstea\FileUploadBundle\Model\FileManagerInterface;
use Irstea\FileUploadBundle\Model\UploadedFileInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LogLevel;
use Rhumsaa\Uuid\Uuid;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Class UploadedFileRepository.
 */
class UploadedFileRepository extends EntityRepository implements FileManagerInterface
{
    use LoggerAwareTrait;

    /**
     * @var Filesystem
     */
    protected $filesystem;

    /**
     * @var EventDispatcher
     */
    protected $eventDispatcher;

    /**
     * @param Filesystem $filesystem
     */
    public function setFilesystem(Filesystem $filesystem)
    {
        $this->filesystem = $filesystem;
    }

    /**
     * @param EventDispatcherInterface $eventDispatcher
     */
    public function setEventDispatcher(EventDispatcherInterface $eventDispatcher)
    {
        $this->eventDispatcher = $eventDispatcher;
    }

    /**
     * @param string $filename
     * @param int    $size
     * @param string $mimeType
     * @param null   $lastModified
     *
     * @return UploadedFile
     */
    public function create($filename, $size, $mimeType, $lastModified = null)
    {
        $file = new UploadedFile();

        $file
            ->setFilesystem($this->filesystem)
            ->setDisplayName($filename)
            ->setMetadata(
                [
                    'client' => [
                        'filename'     => $filename,
                        'size'         => $size,
                        'mimeType'     => $mimeType,
                        'lastModified' => $lastModified,
                    ],
                ]
            );

        $this->_em->persist($file);
        $this->_em->flush();

        $this->log(LogLevel::INFO, 'File created', ['file' => $file]);

        return $file;
    }

    /**
     * @param UploadedFileInterface $original
     *
     * @return UploadedFile
     */
    public function duplicate(UploadedFileInterface $original)
    {
        if (!$original->isValid()) {
            throw new InvalidArgumentException('Impossible de dupliquer le fichier ' . $original->getId() . ' car il est invalide !');
        }

        $new = new UploadedFile();

        $metadata = $original->getMetadata();
        if (!isset($metadata['duplicateOf'])) {
            $metadata['duplicateOf'] = [$original->getId()];
        } else {
            array_unshift($metadata['duplicateOf'], $original->getId());
        }

        $new
            ->setFilesystem($this->filesystem)
            ->setMetadata($metadata)
            ->setDisplayName($original->getDisplayName())
            ->setDescription($original->getDescription())
            ->setMimeType($original->getMimeType())
            ->setChecksum($original->getChecksum())
            ->setSize($original->getSize());

        $stream = $this->filesystem->createStream($new->getPath());
        $stream->open(new StreamMode('cb'));
        $original->copyTo($stream->cast(STREAM_CAST_AS_STREAM));
        $stream->close();

        $this->_em->persist($new);

        return $new;
    }

    /**
     * @param UploadedFileInterface $file
     */
    public function delete(UploadedFileInterface $file)
    {
        $this->_em->remove($file);
        $this->_em->flush();

        $this->log(LogLevel::INFO, 'File deleted', ['file' => $file]);
    }

    /**
     * @param string $uuid
     */
    public function get($uuid)
    {
        if (!$uuid) {
            return null;
        }
        if (!is_string($uuid) || !Uuid::isValid($uuid)) {
            throw new InvalidArgumentException(sprintf('Identifiant invalide: %s', (string) $uuid));
        }

        return $this->findOneById($uuid);
    }

    /**
     * @param UploadedFileInterface $file
     */
    public function completed(UploadedFileInterface $file)
    {
        $path = $file->getPath();
        $filesystem = $this->filesystem;

        $file
            ->setChecksum($filesystem->checksum($path))
            ->setSize($filesystem->size($path))
            ->setMimeType($filesystem->mimeType($path))
            ->setEtat(UploadedFileInterface::ETAT_ORPHELIN);

        $this->_em->persist($file);

        try {
            $this->eventDispatcher->dispatch(FileUploadEvents::UPLOAD_COMPLETE, new FileUploadCompleteEvent($file));

            $this->_em->flush();
            $this->log(LogLevel::INFO, 'File completed', ['file' => $file]);
        } catch (RejectedFileException $ex) {
            $file->setEtat(UploadedFileInterface::ETAT_REJETE);

            $this->_em->flush();
            $this->log(LogLevel::WARNING, 'File rejected', ['file' => $file, 'exception' => $ex]);

            throw $ex;
        }
    }

    /**
     * @param string $level
     * @param string $message
     * @param array  $context
     */
    protected function log($level, $message, array $context = [])
    {
        if (null !== $this->logger) {
            $this->logger->log($level, $message, $context);
        }
    }

    /**
     * @return array
     */
    public function findGarbage()
    {
        $files = $this->findBy(['etat' => [UploadedFileInterface::ETAT_EN_COURS, UploadedFileInterface::ETAT_ORPHELIN]]);

        $limit = new DateTime('now');
        $limit->modify('- 1 hour');

        return array_filter(
            $files,
            function (UploadedFileInterface $file) use ($limit) {
                $mtime = $file->getLastModified();

                return $mtime === null || $mtime < $limit;
            }
        );
    }

    /**
     * @return array
     */
    public function findFilesToValidate()
    {
        return $this->findBy(['etat' => [UploadedFileInterface::ETAT_ORPHELIN, UploadedFileInterface::ETAT_NORMAL]]);
    }
}