<?php

/*
 * Copyright (C) 2015 IRSTEA
 * All rights reserved.
 */

namespace Irstea\FileUploadBundle\Entity;

use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Gaufrette\Exception\FileNotFound;
use Gaufrette\Filesystem;
use Gaufrette\StreamMode;
use InvalidArgumentException;
use Irstea\FileUploadBundle\Model\UploadedFileInterface;
use Irstea\FileUploadBundle\Utils\MimeTypeIcon;
use Rhumsaa\Uuid\Uuid;

/**
 * @ORM\Entity(repositoryClass="Irstea\FileUploadBundle\Entity\Repository\UploadedFileRepository")
 * @ORM\EntityListeners({"Irstea\FileUploadBundle\Listener\UploadedFileListener"})
 */
class UploadedFile implements UploadedFileInterface
{
    // Taille de bloc utilisé pour les copies
    static public $copyBlockSize = 8192;

    const ORPHAN_PREFIX = "orphan/";

    /**
     * @ORM\Id
     * @ORM\Column(type="guid")
     * @var string
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=1024)
     * @var string
     */
    private $displayName;

    /**
     * @ORM\Column(type="string", length=1024)
     * @var string
     */
    private $path;

    /**
     *
     * @ORM\Column(type="string", length=255, nullable=true)
     * @var string
     */
    private $mimeType = null;

    /**
     * @ORM\Column(type="integer", nullable=true)
     * @var int
     */
    private $size = null;

    /**
     * @ORM\Column(type="string", length=64, nullable=true)
     * @var string
     */
    private $checksum = null;

    /**
     * @ORM\Column(type="string", length=10)
     * @var string
     */
    private $etat = self::ETAT_EN_COURS;

    /**
     * @ORM\Column(type="datetime")
     * @var DateTime
     */
    private $createdAt;

    /**
     * @ORM\Column(type="string", nullable=true)
     * @var string
     */
    private $createdBy;

    /**
     * @ORM\Column(type="string", nullable=true)
     * @var string
     */
    private $createdFrom;

    /**
     * @ORM\Column(type="json_array", nullable=true)
     * @var array
     */
    private $metadata = null;

    /**
     * @ORM\Column(type="string", length=256, nullable=true)
     * @var string
     */
    private $description = null;

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

    /** Contient le nom de chemin local.
     *
     * @var string
     */
    private $localTempPath = null;

    /** Crée un UploadedFile.
     *
     * @param string $createdBy Nom du créateur.
     * @param string $createdFrom Adresse UP du créateur.
     *
     * @internal
     */
    public function __construct($createdBy = null, $createdFrom = null)
    {
        $this->id = Uuid::uuid4()->toString();
        $this->path = self::ORPHAN_PREFIX.$this->id;
        $this->createdAt = new DateTime('now');
        $this->createdBy = $createdBy;
        $this->createdFrom = $createdFrom;
    }

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

    /**
     * {@inheritdoc}
     */
    public function setDisplayName($displayName)
    {
        $this->displayName = $displayName;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getDisplayName()
    {
        return $this->displayName;
    }

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

    /**
     * {@inheritdoc}
     */
    public function setPath($path)
    {
        if(!static::isSafePath($path)) {
            throw new InvalidArgumentException("Unsafe path: $path");
        }
        $this->path = trim($path, '/');

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function moveTo($newDir)
    {
        $this->setPath(rtrim($newDir, '/') . '/' . pathinfo($this->path, PATHINFO_FILENAME));
    }

    /**
     * {@inheritdoc}
     */
    public function setMimeType($mimeType)
    {
        $this->mimeType = $mimeType;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getMimeType()
    {
        return $this->mimeType;
    }

    /**
     * {@inheritdoc}
     */
    public function setSize($size)
    {
        $this->size = $size;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getSize()
    {
        return $this->size;
    }

    /**
     * {@inheritdoc}
     */
    public function setChecksum($checksum)
    {
        $this->checksum = $checksum;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getChecksum()
    {
        return $this->checksum;
    }

    /**
     * {@inheritdoc}
     */
    public function setEtat($etat)
    {
        if(!in_array(
            $etat,
            [
                self::ETAT_CORROMPU,
                self::ETAT_EN_COURS,
                self::ETAT_MANQUANT,
                self::ETAT_NORMAL,
                self::ETAT_ORPHELIN,
                self::ETAT_REJETE
            ]
        )) {
            throw new InvalidArgumentException(sprintf("Etat invalide: '%s'", (string)$etat));
        }

        // Déplace le fichier hors de l'orphelinat quand on passe d'orphelin à nouveau
        if($this->etat === self::ETAT_ORPHELIN && $etat === self::ETAT_NORMAL && 0 === strpos($this->path, self::ORPHAN_PREFIX)) {
            $this->path = substr($this->path, strlen(self::ORPHAN_PREFIX));
        }

        $this->etat = $etat;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getEtat()
    {
        return $this->etat;
    }

    /**
     * {@inheritdoc}
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    /**
     * {@inheritdoc}
     */
    public function setMetadata(array $metadata = null)
    {
        $this->metadata = $metadata;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getMetadata()
    {
        return $this->metadata;
    }

    /**
     * {@inheritdoc}
     */
    public function __toString()
    {
        $unit = "";
        $size = $this->size ?: 0;
        if($size >= 10240) {
            $size /= 1024;
            $unit = "k";
            if($size >= 10240) {
                $size /= 1024;
                $unit = "m";
            }
        }
        return sprintf("%s (%s, %d%so)", $this->displayName, $this->mimeType ?: '?/?', $size, $unit);
    }

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

    /**
     * {@inheritdoc}
     */
    public function validate()
    {
        if (self::ETAT_EN_COURS === $this->getEtat()) {
            return;
        }

        $filesystem = $this->filesystem;
        $path = $this->getPath();

        if (!$filesystem->has($path)) {
            $this->setEtat(self::ETAT_MANQUANT);
            return;
        }

        if ($filesystem->size($path) !== $this->size || $filesystem->checksum($path) !== $this->checksum) {
            $this->setEtat(self::ETAT_CORROMPU);
            return;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function isValid()
    {
        return $this->getEtat() === self::ETAT_ORPHELIN || $this->getEtat() === self::ETAT_NORMAL;
    }

    /**
     * {@inheritdoc}
     */
    public function isOrphelin()
    {
        return $this->getEtat() === self::ETAT_ORPHELIN;
    }

    /**
     * {@inheritdoc}
     */
    public function getLastModified()
    {
        try {
            return new \DateTime(sprintf('@%d', $this->filesystem->mtime($this->getPath())));
        } catch(FileNotFound $ex) {
            return null;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getContent()
    {
        return $this->filesystem->read($this->getPath());
    }

    /**
     * {@inheritdoc}
     */
    public function setContent($content)
    {
        return $this->filesystem->write($this->getPath(), $content, true);
    }

    /**
     * {@inheritdoc}
     */
    public function copyFrom($source, $maxlen = -1, $writeOffset = 0)
    {
        if($maxlen === 0) {
            return 0;
        }

        $stream = $this->filesystem->createStream($this->getPath());
        $stream->open(new StreamMode('cb'));
        $stream->seek($writeOffset);

        if(false !== $fileHandle = $stream->cast(STREAM_CAST_AS_STREAM)) {
            // Utilise stream_copy_to_stream si le Gaufrette\Stream peut nous retourner un filehandle
            $copied = $this->stream_copy_to_stream($source, $fileHandle, $maxlen);
        } else {
            // Sinon fait une copie par blocs (moins performant)
            if($maxlen === -1) {
                $maxlen = PHP_INT_MAX;
            }
            $copied = 0;
            while(!$this->feof($source) && $copied <= $maxlen) {
                $copied += $stream->write($this->fread($source, min(static::$copyBlockSize, $maxlen - $copied)));
            }
        }
        $stream->close();

        return $copied;
    }

    /**
     * {@inheritdoc}
     */
    public function copyTo($dest, $maxlen = -1, $readOffset = 0)
    {
        if($maxlen === -1) {
            $actualLength = $this->getSize() - $readOffset;
        } else {
            $actualLength = min($maxlen, $this->getSize() - $readOffset);
        }

        if ($actualLength <= 0) {
            return 0;
        }

        $stream = $this->filesystem->createStream($this->getPath());
        $stream->open(new StreamMode('rb'));
        $stream->seek($readOffset);

        if(false !== $fileHandle = $stream->cast(STREAM_CAST_AS_STREAM)) {
            // Utilise stream_copy_to_stream si le Stream nous renvoie un filehandle
            $copied = $this->stream_copy_to_stream($fileHandle, $dest, $actualLength);
        } else {
            // Sinon, on fait ça à la main par blocs de 8ko
            $copied = 0;
            while(!$stream->eof() && $copied < $actualLength) {
                $copied += $this->fwrite($dest, $stream->read(min(static::$copyBlockSize, $actualLength - $copied)));
            }
        }

        $stream->close();

        return $copied;
    }

    /** Wrapper de stream_copy_to_stream
     *
     * @param resource $source
     * @param resource $dest
     * @param int $maxlen
     * @param int $offset
     *
     * @return int
     *
     * @internal
     */
    protected function stream_copy_to_stream($source, $dest, $maxlen = -1, $offset = 0)
    {
        return stream_copy_to_stream($source, $dest, $maxlen, $offset);
    }

    /** Wrapper de feof
     *
     * @param resource $filehandle
     *
     * @return boolean
     *
     * @internal
     */
    protected function feof($filehandle)
    {
        return feof($filehandle);
    }

    /** Wrapper de fread
     *
     * @param resource $filehandle
     * @param int $maxlen
     *
     * @return int|boolean
     *
     * @internal
     */
    protected function fread($filehandle, $maxlen = -1)
    {
        return fread($filehandle, $maxlen);
    }

    /** Wrapper de fwrite
     *
     * @param resource $filehandle
     * @param int $maxlen
     *
     * @return int|boolean
     *
     * @internal
     */
    protected function fwrite($filehandle, $maxlen = -1)
    {
        return fwrite($filehandle, $maxlen);
    }

    /** Vérifie si un chemin est "safe".
     *
     * @param string $path
     *
     * @return boolean
     *
     * @internal
     */
    public static function isSafePath($path)
    {    /**
     * @return string
     */
        $parts = explode('/', trim($path, '/'));
        $level = 0;
        foreach($parts as $part) {
            switch($part) {
                case '.':
                    break;
                case '..':
                    $level--;
                    if($level < 0) {
                        return false;
                    }
                    break;
                default:    /**
     * @return string
     */
                    $level++;
            }
        }
        return true;
    }

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

    /**
     * {@inheritdoc}
     */
    public function setDescription($description = null)
    {
        $this->description = $description;
        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function toArray()
    {
        return [
            'id'          => $this->getId(),
            'name'        => $this->getDisplayName(),
            'size'        => $this->getSize(),
            'type'        => $this->getMimeType(),
            'etat'        => $this->getEtat(),
            'description' => $this->getDescription(),
            'checksum'    => $this->getChecksum(),
            'icon'        => MimeTypeIcon::getMimeTypeIcon($this->getMimeType())
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getCreatedBy()
    {
        return $this->createdBy;
    }

    /**
     * {@inheritdoc}
     */
    public function getCreatedFrom()
    {
        return $this->createdFrom;
    }

    /**
     * {@inheritdoc}
     */
    public function getLocalPath()
    {
        if(null !== $this->localTempPath) {
            return $this->localTempPath;
        }

        $stream = $this->filesystem->createStream($this->getPath());
        $stream->open(new StreamMode('rb'));
        $handle = $stream->cast(STREAM_CAST_AS_STREAM);

        if(false !== $handle) {
            if(stream_is_local($handle)) {
                $this->localTempPath = stream_get_meta_data($handle)['uri'];
                fclose($handle);
                return $this->localTempPath;
            }
            fclose($handle);
        }

        $this->localTempPath = tempnam(sys_get_temp_dir(), 'UploadedFile');
        register_shutdown_function('unlink', $this->localTempPath);

        $tmp = fopen($this->localTempPath, 'xb');
        $this->copyTo($tmp);
        fclose($tmp);

        return $this->localTempPath;
    }
}