<?php

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

namespace Irstea\FileUploadBundle\Controller;

use Irstea\FileUploadBundle\Entity\UploadedFile;
use Irstea\FileUploadBundle\Http\UploadedFileResponse;
use Irstea\FileUploadBundle\Service\FileManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\RouterInterface;

/**
 * @Route("/files", service="irstea_file_upload.upload_controller")
 */
class UploadController extends Controller
{
    const CSRF_READ_INTENTION = "uploaded_file_read";
    const CSRF_WRITE_INTENTION = "uploaded_file_write_%s";

    /**
     *
     * @var FileManagerInterface
     */
    protected $fileManager;

    /**
     *
     * @var RouterInterface
     */
    protected $router;

    /**
     * @var CsrfProviderInterface
     */
    protected $csrfProvider;

    /**
     *
     */
    public function __construct(FileManagerInterface $fileManager, RouterInterface $router, CsrfProviderInterface $csrfProvider)
    {
        $this->fileManager = $fileManager;
        $this->router = $router;
        $this->csrfProvider = $csrfProvider;
    }

    /**
     * @Route("", name="file_upload_create")
     * @Method("POST")
     * @param Request $request
     */
    public function createAction(Request $request)
    {
        $data = $request->request->get('file');

        $file = $this->fileManager->create(
            $data['name'],
            $data['size'],
            $data['type'],
            isset($data['lastModified']) ? $data['lastModified'] : null
        );

        $token = $this->generateCsrfToken(self::CSRF_WRITE_INTENTION, $file);

        $parameters = [
            'id' => $file->getId(),
            'token' => $token
        ];

        $deleteUrl = $this->router->generate('file_upload_delete', $parameters);

        return $this->createResponse(
            Response::HTTP_CREATED,
            'New file created',
            [
                'id'          => $file->getId(),
                'put_url'     => $this->router->generate('file_upload_put_content', $parameters),
                'delete_type' => 'DELETE',
                'delete_url'  => $deleteUrl,
            ],
            // On a pas de get pour l'instant, le DELETE et ce qui y ressemble le plus
            [ 'Location' => $deleteUrl ]
        );
    }

    /**
     * @Route("/{id}/content", name="file_upload_put_content")
     * @Method("PUT")
     * @param Request $request
     * @param UploadedFile $file
     */
    public function putContentAction(Request $request, UploadedFile $file)
    {
        $this->validateCsrfToken($request, self::CSRF_WRITE_INTENTION, $file);

        if(null !== $range = $request->headers->get('Content-Range', null)) {
            $matches = [];
            if(!preg_match('@^bytes (\d+)-(\d+)/(\d+)$@', $range, $matches)) {
                throw new BadRequestHttpException("Invalid Content-Range");
            }

            $start = intval($matches[1]);
            $end   = intval($matches[2]);
            $total = intval($matches[3]);

            if($start < 0 || $start >= $end || $end >= $total) {
                throw new HttpException(Response::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE);
            }

            $offset = $start;
            $maxlen = 1 + ($end - $start);
            $complete = $end === ($total-1);
        } else {
            $offset = 0;
            $maxlen = PHP_INT_MAX;
            $complete = true;
        }

        // Demande un filehandle plutôt que charger le contenu en mémoire
        $input = $request->getContent(true);
        $file->copyFrom($input, $maxlen, $offset);
        fclose($input);

        if(!$complete) {
            return $this->createResponse(Response::HTTP_OK, 'Chunk received');
        }

        $this->fileManager->completed($file);

        $parameters = [
            'id'    => $file->getId(),
            'token' => $request->query->get('token')
        ];

        return $this->createResponse(
            Response::HTTP_OK,
            'File uploaded',
            [
                'files' => [
                    [
                        'id'          => $file->getId(),
                        'name'        => $file->getDisplayName(),
                        'type'        => $file->getMimeType(),
                        'size'        => $file->getSize(),
                        'icon'        => MimeTypeIcon::getMimeTypeIcon($file->getMimeType()),
                        'url'         => $this->router->generate('file_upload_get_content', $parameters),
                        'delete_type' => 'DELETE',
                        'delete_url'  => $this->router->generate('file_upload_delete', $parameters),
                    ]
                ]
            ]
        );
    }

    /**
     * @Route("/{id}/content", name="file_upload_get_content")
     * @Method("GET")
     * @param Request $request
     * @param UploadedFile $file
     */
    public function getContentAction(Request $request, UploadedFile $file)
    {
        $this->validateCsrfToken($request, self::CSRF_READ_INTENTION);

        if(!$file->isValid()) {
            throw new NotFoundHttpException();
        }

        $response = UploadedFileResponse::create($file, 200, [], false, ResponseHeaderBag::DISPOSITION_ATTACHMENT);
        $response->isNotModified($request);
        return $response;
    }

    /**
     * @Route("/{id}", name="file_upload_delete")
     * @Method("DELETE")
     * @param Request $request
     * @param UploadedFile $file
     */
    public function deleteAction(Request $request, UploadedFile $file)
    {
        $this->validateCsrfToken($request, self::CSRF_WRITE_INTENTION, $file);

        $this->fileManager->delete($file);

        return $this->createResponse();
    }

    /**
     *
     * @param type $intention
     * @param UploadedFile $file
     * @return string
     */
    protected function generateCsrfToken($intention, UploadedFile $file = null)
    {
        if(null !== $file) {
            $intention = sprintf($intention, $file->getId());
        }
        return $this->csrfProvider->generateCsrfToken($intention);
    }

    /**
     *
     * @param Request $request
     * @param type $intention
     * @param UploadedFile $file
     * @throws HttpException
     */
    protected function validateCsrfToken(Request $request, $intention, UploadedFile $file = null)
    {
        if(null !== $file) {
            $intention = sprintf($intention, $file->getId());
        }
        if(!$this->csrfProvider->isCsrfTokenValid($intention, $request->query->get('token', null))) {
            throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid CSRF token');
        }
    }

    /**
     *
     * @param int $status
     * @param string $message
     * @param array $data
     * @param array $headers
     * @return JsonResponse
     */
    protected function createResponse($status = Response::HTTP_OK, $message = 'OK', array $data = [], array $headers = [])
    {
        $data['status'] = $status;
        $data['message'] = $message;
        return new JsonResponse($data, $status, $headers);
    }
}