<?php declare(strict_types=1);

/*
 * This file is part of the Rapsys AirBundle package.
 *
 * (c) Raphaël Gertz <symfony@rapsys.eu>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Rapsys\AirBundle\Controller;

use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

use Rapsys\AirBundle\Entity\Dance;
use Rapsys\AirBundle\Entity\Location;
use Rapsys\AirBundle\Entity\Session;
use Rapsys\AirBundle\Entity\Snippet;
use Rapsys\AirBundle\Entity\User;
use Rapsys\AirBundle\Token\AnonymousToken;

/**
 * {@inheritdoc}
 */
class DefaultController extends AbstractController {
	/**
	 * The about page
	 *
	 * Display the about informations
	 *
	 * @param Request $request The request instance
	 * @return Response The rendered view
	 */
	public function about(Request $request): Response {
		//Set page
		$this->context['title']['page'] = $this->translator->trans('About');

		//Set description
		$this->context['description'] = $this->translator->trans('Libre Air about');

		//Set keywords
		$this->context['keywords'] = [
			$this->translator->trans('about'),
			$this->translator->trans('Libre Air')
		];

		//Render template
		$response = $this->render('@RapsysAir/default/about.html.twig', $this->context);
		$response->setEtag(md5($response->getContent()));
		$response->setPublic();
		$response->isNotModified($request);

		//Return response
		return $response;
	}

	/**
	 * The contact page
	 *
	 * Send a contact mail to configured contact
	 *
	 * @param Request $request The request instance
	 *
	 * @return Response The rendered view or redirection
	 */
	public function contact(Request $request): Response {
		//Set page
		$this->context['title']['page'] = $this->translator->trans('Contact');

		//Set description
		$this->context['description'] = $this->translator->trans('Contact Libre Air');

		//Set keywords
		$this->context['keywords'] = [
			$this->translator->trans('contact'),
			$this->translator->trans('Libre Air'),
			$this->translator->trans('outdoor'),
			$this->translator->trans('Argentine Tango'),
			$this->translator->trans('calendar')
		];

		//Set data
		$data = [];

		//With user
		if ($user = $this->security->getUser()) {
			//Set data
			$data = [
				'name' => $user->getRecipientName(),
				'mail' => $user->getMail()
			];
		}

		//Create the form according to the FormType created previously.
		//And give the proper parameters
		$form = $this->factory->create('Rapsys\AirBundle\Form\ContactType', $data, [
			'action' => $this->generateUrl('rapsysair_contact'),
			'captcha' => true,
			'method' => 'POST'
		]);

		if ($request->isMethod('POST')) {
			// Refill the fields in case the form is not valid.
			$form->handleRequest($request);

			if ($form->isSubmitted() && $form->isValid()) {
				//Get data
				$data = $form->getData();

				//Create message
				$message = (new TemplatedEmail())
					//Set sender
					->from(new Address($data['mail'], $data['name']))
					//Set recipient
					->to(new Address($this->context['contact']['address'], $this->context['contact']['name']))
					//Set subject
					->subject($data['subject'])

					//Set path to twig templates
					->htmlTemplate('@RapsysAir/mail/contact.html.twig')
					->textTemplate('@RapsysAir/mail/contact.text.twig')

					//Set context
					->context(
						[
							'subject' => $data['subject'],
							'message' => strip_tags($data['message']),
						]+$this->context
					);

				//Try sending message
				//XXX: mail delivery may silently fail
				try {
					//Send message
					$this->mailer->send($message);

					//Redirect on the same route with sent=1 to cleanup form
					return $this->redirectToRoute($request->get('_route'), ['sent' => 1]+$request->get('_route_params'));
				//Catch obvious transport exception
				} catch(TransportExceptionInterface $e) {
					if ($message = $e->getMessage()) {
						//Add error message mail unreachable
						$form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%: %message%', ['%mail%' => $this->context['contact']['address'], '%message%' => $this->translator->trans($message)])));
					} else {
						//Add error message mail unreachable
						$form->get('mail')->addError(new FormError($this->translator->trans('Unable to contact: %mail%', ['%mail%' => $this->context['contact']['address']])));
					}
				}
			}
		}

		//Render template
		return $this->render('@RapsysAir/form/contact.html.twig', ['form' => $form->createView(), 'sent' => $request->query->get('sent', 0)]+$this->context);
	}

	/**
	 * The index page
	 *
	 * Display session calendar
	 *
	 * @param Request $request The request instance
	 * @return Response The rendered view
	 */
	public function index(Request $request): Response {
		//Add cities
		$this->context['cities'] = $this->doctrine->getRepository(Location::class)->findCitiesAsArray($this->period);

		//Add calendar
		$this->context['calendar'] = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period, !$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED'));

		//Add dances
		$this->context['dances'] = $this->doctrine->getRepository(Dance::class)->findNamesAsArray();

		//Set modified
		$this->modified = max(array_map(function ($v) { return $v['modified']; }, array_merge($this->context['calendar'], $this->context['cities'], $this->context['dances'])));

		//Create response
		$response = new Response();

		//With logged user
		if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
			//Set last modified
			$response->setLastModified(new \DateTime('-1 year'));

			//Set as private
			$response->setPrivate();
		//Without logged user
		} else {
			//Set etag
			//XXX: only for public to force revalidation by last modified
			$response->setEtag(md5(serialize(array_merge($this->context['calendar'], $this->context['cities'], $this->context['dances']))));

			//Set last modified
			$response->setLastModified($this->modified);

			//Set as public
			$response->setPublic();

			//Without role and modification
			if ($response->isNotModified($request)) {
				//Return 304 response
				return $response;
			}
		}

		//With cities
		if (!empty($this->context['cities'])) {
			//Set locations
			$locations = [];

			//Iterate on each cities
			foreach($this->context['cities'] as $city) {
				//Iterate on each locations
				foreach($city['locations'] as $location) {
					//Add location
					$locations[$location['id']] = $location;
				}
			}

			//Add multi
			$this->context['multimap'] = $this->map->getMultiMap($this->translator->trans('Libre Air cities sector map'), $this->modified->getTimestamp(), $locations);

			//Set cities
			$cities = array_map(function ($v) { return $v['in']; }, $this->context['cities']);

			//Set dances
			$dances = array_map(function ($v) { return $v['name']; }, $this->context['dances']);
		} else {
			//Set cities
			$cities = [];

			//Set dances
			$dances = [];
		}

		//Set keywords
		//TODO: use splice instead of that shit !!!
		//TODO: handle smartly indoor and outdoor !!!
		$this->context['keywords'] = array_values(
			array_merge(
				$dances,
				$cities,
				[
					$this->translator->trans('indoor'),
					$this->translator->trans('outdoor'),
					$this->translator->trans('calendar'),
					$this->translator->trans('Libre Air')
				]
			)
		);

		//Get textual cities
		$cities = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($cities, 0, -1))], array_slice($cities, -1)), 'strlen'));

		//Get textual dances
		$dances = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($dances, 0, -1))], array_slice($dances, -1)), 'strlen'));

		//Set title
		$this->context['title']['page'] = $this->translator->trans('%dances% %cities%', ['%dances%' => $dances, '%cities%' => $cities]);

		//Set description
		//TODO: handle french translation when city start with a A, change à in en !
		$this->context['description'] = $this->translator->trans('%dances% indoor and outdoor calendar %cities%', ['%dances%' => $dances, '%cities%' => $cities]);

		//Set facebook type
		//XXX: only valid for home page
		$this->context['facebook']['metas']['og:type'] = 'website';

		//Render the view
		return $this->render('@RapsysAir/default/index.html.twig', $this->context, $response);
	}

	/**
	 * The organizer regulation page
	 *
	 * Display the organizer regulation policy
	 *
	 * @param Request $request The request instance
	 * @return Response The rendered view
	 */
	public function organizerRegulation(Request $request): Response {
		//Set page
		$this->context['title']['page'] = $this->translator->trans('Organizer regulation');

		//Set description
		$this->context['description'] = $this->translator->trans('Libre Air organizer regulation');

		//Set keywords
		$this->context['keywords'] = [
			$this->translator->trans('organizer regulation'),
			$this->translator->trans('Libre Air')
		];

		//Render template
		$response = $this->render('@RapsysAir/default/organizer_regulation.html.twig', $this->context);

		//Set as cachable
		$response->setEtag(md5($response->getContent()));
		$response->setPublic();
		$response->isNotModified($request);

		//Return response
		return $response;
	}

	/**
	 * The terms of service page
	 *
	 * Display the terms of service policy
	 *
	 * @param Request $request The request instance
	 * @return Response The rendered view
	 */
	public function termsOfService(Request $request): Response {
		//Set page
		$this->context['title']['page'] = $this->translator->trans('Terms of service');

		//Set description
		$this->context['description'] = $this->translator->trans('Libre Air terms of service');

		//Set keywords
		$this->context['keywords'] = [
			$this->translator->trans('terms of service'),
			$this->translator->trans('Libre Air')
		];

		//Render template
		$response = $this->render('@RapsysAir/default/terms_of_service.html.twig', $this->context);

		//Set as cachable
		$response->setEtag(md5($response->getContent()));
		$response->setPublic();
		$response->isNotModified($request);

		//Return response
		return $response;
	}

	/**
	 * The frequently asked questions page
	 *
	 * Display the frequently asked questions
	 *
	 * @param Request $request The request instance
	 * @return Response The rendered view
	 */
	public function frequentlyAskedQuestions(Request $request): Response {
		//Set page
		$this->context['title']['page'] = $this->translator->trans('Frequently asked questions');

		//Set description
		$this->context['description'] = $this->translator->trans('Libre Air frequently asked questions');

		//Set keywords
		$this->context['keywords'] = [
			$this->translator->trans('frequently asked questions'),
			$this->translator->trans('faq'),
			$this->translator->trans('Libre Air')
		];

		//Render template
		$response = $this->render('@RapsysAir/default/frequently_asked_questions.html.twig', $this->context);

		//Set as cachable
		$response->setEtag(md5($response->getContent()));
		$response->setPublic();
		$response->isNotModified($request);

		//Return response
		return $response;
	}

	/**
	 * List all users
	 *
	 * Display all user with a group listed as users
	 *
	 * @param Request $request The request instance
	 *
	 * @return Response The rendered view
	 */
	public function userIndex(Request $request): Response {
		//With admin role
		if ($this->checker->isGranted('ROLE_ADMIN')) {
			//Set title
			$this->context['title']['page'] = $this->translator->trans('Libre Air user list');

			//Set section
			$this->context['title']['section'] = $this->translator->trans('User');

			//Set description
			$this->context['description'] = $this->translator->trans('Lists Libre air users');
		//Without admin role
		} else {
			//Set title
			$this->context['title']['page'] = $this->translator->trans('Libre Air organizer list');

			//Set section
			$this->context['title']['section'] = $this->translator->trans('Organizer');

			//Set description
			$this->context['description'] = $this->translator->trans('Lists Libre air organizers');
		}

		//Set keywords
		$this->context['keywords'] = [
			$this->translator->trans('users'),
			$this->translator->trans('user list'),
			$this->translator->trans('listing'),
			$this->translator->trans('Libre Air')
		];

		//Fetch users
		$users = $this->doctrine->getRepository(User::class)->findIndexByGroupId();

		//With admin role
		if ($this->checker->isGranted('ROLE_ADMIN')) {
			//Display all users
			$this->context['groups'] = $users;
		//Without admin role
		} else {
			//Only display senior organizers
			$this->context['users'] = $users[$this->translator->trans('Senior')];
		}

		//Render the view
		return $this->render('@RapsysAir/user/index.html.twig', $this->context);
	}

	/**
	 * List all sessions for the user
	 *
	 * Display all sessions for the user with an application or login form
	 *
	 * @param Request $request The request instance
	 * @param int $id The user id
	 *
	 * @return Response The rendered view
	 */
	public function userView(Request $request, int $id, ?string $user): Response {
		//Get user
		if (empty($this->context['user'] = $this->doctrine->getRepository(User::class)->findOneByIdAsArray($id, $this->locale))) {
			//Throw not found
			throw new NotFoundHttpException($this->translator->trans('Unable to find user: %id%', ['%id%' => $id]));
		}

		//Create token
		$token = new AnonymousToken($this->context['user']['roles']);

		//Prevent access when not admin, user is not guest and not currently logged user
		if (!($isAdmin = $this->checker->isGranted('ROLE_ADMIN')) && !($isGuest = $this->decision->decide($token, ['ROLE_GUEST']))) {
			//Throw access denied
			throw new AccessDeniedException($this->translator->trans('Unable to access user: %id%', ['%id%' => $id]));
		}

		//With invalid user slug
		if ($this->context['user']['slug'] !== $user) {
			//Redirect to cleaned url
			return $this->redirectToRoute('rapsysair_user_view', ['id' => $id, 'user' => $this->context['user']['slug']]);
		}

		//Fetch calendar
		$this->context['calendar'] = $this->doctrine->getRepository(Session::class)->findAllByPeriodAsCalendarArray($this->period, !$this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED'), null, null, $id);

		//Get locations at less than 2 km
		$this->context['locations'] = $this->doctrine->getRepository(Location::class)->findAllByUserIdAsArray($id, $this->period, 2);

		//Set ats
		$ats = [];

		//Set dances
		$dances = [];

		//Set indoors
		$indoors = [];

		//Set ins
		$ins = [];

		//Set insides
		$insides = [];

		//Set locations
		$locations = [];

		//Set types
		$types = [];

		//Iterate on each calendar
		foreach($this->context['calendar'] as $date => $calendar) {
			//Iterate on each session
			foreach($calendar['sessions'] as $sessionId => $session) {
				//Add dance
				$dances[$session['application']['dance']['name']] = $session['application']['dance']['name'];

				//Add types
				$types[$session['application']['dance']['type']] = lcfirst($session['application']['dance']['type']);

				//Add indoors
				$indoors[$session['location']['indoor']?'indoor':'outdoor'] = $this->translator->trans($session['location']['indoor']?'indoor':'outdoor');

				//Add insides
				$insides[$session['location']['indoor']?'inside':'outside'] = $this->translator->trans($session['location']['indoor']?'inside':'outside');

				//Add ats
				$ats[$session['location']['id']] = $session['location']['at'];

				//Add ins
				$ins[$session['location']['id']] = $session['location']['in'];

				//Session with application user id
				if (!empty($session['application']['user']['id']) && $session['application']['user']['id'] == $id) {
					//Add location
					$locations[$session['location']['id']] = $session['location'];
				}
			}
		}

		//Set modified
		//XXX: dance modified is already computed inside calendar modified
		$this->modified = max(array_merge([$this->context['user']['modified']], array_map(function ($v) { return $v['modified']; }, array_merge($this->context['calendar'], $this->context['locations']))));

		//Create response
		$response = new Response();

		//With logged user
		if ($this->checker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
			//Set last modified
			$response->setLastModified(new \DateTime('-1 year'));

			//Set as private
			$response->setPrivate();
		//Without logged user
		} else {
			//Set etag
			//XXX: only for public to force revalidation by last modified
			$response->setEtag(md5(serialize(array_merge($this->context['user'], $this->context['calendar'], $this->context['locations']))));

			//Set last modified
			$response->setLastModified($this->modified);

			//Set as public
			$response->setPublic();

			//Without role and modification
			if ($response->isNotModified($request)) {
				//Return 304 response
				return $response;
			}
		}

		//Add multi map
		$this->context['multimap'] = $this->map->getMultiMap($this->context['user']['multimap'], $this->modified->getTimestamp(), $this->context['locations']);

		//Set keywords
		$this->context['keywords'] = [
			$this->context['user']['pseudonym'],
			$this->translator->trans('calendar'),
			$this->translator->trans('Libre Air')
		];

		//Set cities
		$cities = array_unique(array_map(function ($v) { return $v['city']; }, $locations));

		//Set titles
		$titles = array_map(function ($v) { return $v['title']; }, $locations);

		//Insert dances in keywords
		array_splice($this->context['keywords'], 1, 0, array_merge($types, $dances, $indoors, $insides, $titles, $cities));

		//Deduplicate ins
		$ins = array_unique($ins);

		//Get textual dances
		$dances = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($dances, 0, -1))], array_slice($dances, -1)), 'strlen'));

		//Get textual types
		$types = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($types, 0, -1))], array_slice($types, -1)), 'strlen'));

		//Get textual indoors
		$indoors = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($indoors, 0, -1))], array_slice($indoors, -1)), 'strlen'));

		//Get textual ats
		$ats = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($ats, 0, -1))], array_slice($ats, -1)), 'strlen'));

		//Get textual ins
		$ins = implode($this->translator->trans(' and '), array_filter(array_merge([implode(', ', array_slice($ins, 0, -1))], array_slice($ins, -1)), 'strlen'));

		//Set title
		$this->context['title']['page'] = $this->translator->trans('%pseudonym% organizer', ['%pseudonym%' => $this->context['user']['pseudonym']]);

		//Set section
		$this->context['title']['section'] = $this->translator->trans('User');

		//With locations
		if (!empty($locations)) {
			//Set description
			$this->context['description'] = ucfirst($this->translator->trans('%dances% %types% %indoors% calendar %ats% %ins% %pseudonym%', ['%dances%' => $dances, '%types%' => $types, '%indoors%' => $indoors, '%ats%' => $ats, '%ins%' => $ins, '%pseudonym%' => $this->translator->trans('by %pseudonym%', ['%pseudonym%' => $this->context['user']['pseudonym']])]));
		//Without locations
		} else {
			//Set description
			$this->context['description'] = $this->translator->trans('%pseudonym% calendar', ['%pseudonym%' => $this->context['user']['pseudonym']]);
		}

		//Set user description
		$this->context['locations_description'] = $this->translator->trans('Libre Air %pseudonym% location list', ['%pseudonym%' => $this->translator->trans('by %pseudonym%', ['%pseudonym%' => $this->context['user']['pseudonym']])]);

		//Set alternates
		$this->context['alternates'] += $this->context['user']['alternates'];

		//Create snippet forms for role_guest
		//TODO: optimize this call
		if ($isAdmin || $isGuest && $this->security->getUser() && $this->context['user']['id'] == $this->security->getUser()->getId()) {
			//Fetch all user snippet
			$snippets = $this->doctrine->getRepository(Snippet::class)->findByUserIdLocaleIndexByLocationId($id, $this->locale);

			//Get user
			$user = $this->doctrine->getRepository(User::class)->findOneById($id);

			//Iterate on locations
			foreach($this->context['locations'] as $locationId => $location) {
				//With existing snippet
				if (isset($snippets[$location['id']])) {
					//Set existing in current
					$current = $snippets[$location['id']];
				//Without existing snippet
				} else {
					//Init snippet
					$current = new Snippet($this->locale, $this->doctrine->getRepository(Location::class)->findOneById($location['id']), $user);
				}

				//Create SnippetType form
				$form = $this->factory->createNamed(
					//Set form id
					'snippet_'.$locationId.'_'.$id.'_'.$this->locale,
					//Set form type
					'Rapsys\AirBundle\Form\SnippetType',
					//Set form data
					$current
				);

				//Refill the fields in case of invalid form
				$form->handleRequest($request);

				//Handle submitted and valid form
				//TODO: add a delete snippet ?
				if ($form->isSubmitted() && $form->isValid()) {
					//Get snippet
					$snippet = $form->getData();

					//Queue snippet save
					$this->manager->persist($snippet);

					//Flush to get the ids
					$this->manager->flush();

					//Add notice
					$this->addFlash('notice', $this->translator->trans('Snippet for %user% %location% updated', ['%location%' => $location['at'], '%user%' => $this->context['user']['pseudonym']]));

					//Redirect to cleaned url
					return $this->redirectToRoute('rapsysair_user_view', ['id' => $id, 'user' => $this->context['user']['slug']]);
				}

				//Add form to context
				$this->context['forms']['snippets'][$locationId] = $form->createView();

				//With location user source image
				if (($isFile = is_file($source = $this->config['path'].'/location/'.$location['id'].'/'.$id.'.png')) && ($mtime = stat($source)['mtime'])) {
					//Set location image
					$this->context['locations'][$locationId]['image'] = $this->image->getThumb($location['miniature'], $mtime, $source);
				}

				//Create ImageType form
				$form = $this->factory->createNamed(
					//Set form id
					'image_'.$locationId.'_'.$id,
					//Set form type
					'Rapsys\AirBundle\Form\ImageType',
					//Set form data
					[
						//Set location
						'location' => $location['id'],
						//Set user
						'user' => $id
					],
					//Set form attributes
					[
						//Enable delete with image
						'delete' => isset($this->context['locations'][$locationId]['image'])
					]
				);

				//Refill the fields in case of invalid form
				$form->handleRequest($request);

				//Handle submitted and valid form
				if ($form->isSubmitted() && $form->isValid()) {
					//With delete
					if ($form->has('delete') && $form->get('delete')->isClicked()) {
						//With source and mtime
						if ($isFile && !empty($source) && !empty($mtime)) {
							//Clear thumb
							$this->image->remove($mtime, $source);

							//Unlink file
							unlink($this->config['path'].'/location/'.$location['id'].'/'.$id.'.png');

							//Add notice
							$this->addFlash('notice', $this->translator->trans('Image for %user% %location% deleted', ['%location%' => $location['at'], '%user%' => $this->context['user']['pseudonym']]));

							//Redirect to cleaned url
							return $this->redirectToRoute('rapsysair_user_view', ['id' => $id, 'user' => $this->context['user']['slug']]);
						}
					}

					//With image
					if ($image = $form->get('image')->getData()) {
						//Check source path
						if (!is_dir($dir = dirname($source))) {
							//Create filesystem object
							$filesystem = new Filesystem();

							try {
								//Create dir
								//XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
								$filesystem->mkdir($dir, 0775);
							} catch (IOExceptionInterface $e) {
								//Throw error
								throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
							}
						}

						//Set source
						$source = realpath($dir).'/'.basename($source);

						//Create imagick object
						$imagick = new \Imagick();

						//Read image
						$imagick->readImage($image->getRealPath());

						//Save image
						if (!$imagick->writeImage($source)) {
							//Throw error
							throw new \Exception(sprintf('Unable to write image "%s"', $source));
						}

						//Add notice
						$this->addFlash('notice', $this->translator->trans('Image for %user% %location% updated', ['%location%' => $location['at'], '%user%' => $this->context['user']['pseudonym']]));

						//Redirect to cleaned url
						return $this->redirectToRoute('rapsysair_user_view', ['id' => $id, 'user' => $this->context['user']['slug']]);
					}
				}

				//Add form to context
				$this->context['forms']['images'][$locationId] = $form->createView();
			}
		}

		//Render the view
		return $this->render('@RapsysAir/user/view.html.twig', ['id' => $id]+$this->context);
	}
}