<?php declare(strict_types=1);

/*
 * This file is part of the Rapsys TreeBundle 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\TreeBundle\Controller;

use Doctrine\Persistence\ManagerRegistry;

use Psr\Container\ContainerInterface;

use Rapsys\PackBundle\Util\FacebookUtil;
use Rapsys\TreeBundle\Entity\Album;
use Rapsys\TreeBundle\Entity\Element;
use Rapsys\TreeBundle\RapsysTreeBundle;
use Rapsys\UserBundle\RapsysUserBundle;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

use Symfony\Contracts\Translation\TranslatorInterface;

use Twig\Environment;

/**
 * {@inheritdoc}
 */
class TreeController extends AbstractController {
	/**
	 * Alias string
	 */
	protected string $alias;

	/**
	 * Config array
	 */
	protected array $config;

	/**
	 * Context array
	 */
	protected array $context = [];

	/**
	 * Count integer
	 */
	protected int $count;

	/**
	 * Locale string
	 */
	protected string $locale;

	/**
	 * Page integer
	 */
	protected int $page;

	/**
	 * Request instance
	 */
	protected Request $request;

	/**
	 * Route string
	 */
	protected string $route;

	/**
	 * Route params array
	 */
	protected array $routeParams;

	/**
	 * Creates a new tree controller
	 *
	 * @param AuthorizationCheckerInterface $checker The container instance
	 * @param ContainerInterface $container The ContainerInterface instance
	 * @param ManagerRegistry $doctrine The doctrine instance
	 * @param FacebookUtil $facebook The facebook instance
	 * @param RouterInterface $router The router instance
	 * @param Security $security The security instance
	 * @param RequestStack $stack The stack instance
	 * @param TranslatorInterface $translator The translator instance
	 * @param Environment $twig The twig environment instance
	 * @param integer $limit The page limit
	 */
	function __construct(protected AuthorizationCheckerInterface $checker, protected ContainerInterface $container, protected ManagerRegistry $doctrine, protected FacebookUtil $facebook, protected RouterInterface $router, protected Security $security, protected RequestStack $stack, protected TranslatorInterface $translator, protected Environment $twig, protected int $limit = 5) {
		//Retrieve config
		$this->config = $container->getParameter($this->alias = RapsysTreeBundle::getAlias());

		//Get main request
		$this->request = $this->stack->getMainRequest();

		//Get current locale
		$this->locale = $this->request->getLocale();

		//Set canonical
		$canonical = null;

		//Set alternates
		$alternates = [];

		//Get current page
		$this->page = (int) $this->request->query->get('page');

		//With negative page
		if ($this->page < 0) {
			$this->page = 0;
		}

		//Set route
		//TODO: default to not found route ???
		//TODO: pour une url not found, cet attribut n'est pas défini, comment on fait ???
		//XXX: on génère une route bidon par défaut ???
		$this->route = $this->request->attributes->get('_route');

		//Set route params
		$this->routeParams = $this->request->attributes->get('_route_params');

		//With route and routeParams
		if ($this->route !== null && $this->routeParams !== null) {
			//Set canonical
			$canonical = $this->router->generate($this->route, $this->routeParams, UrlGeneratorInterface::ABSOLUTE_URL);

			//Set alternates
			$alternates = [
				substr($this->locale, 0, 2) => [
					'absolute' => $canonical
				]
			];
		}

		//Set the context
		$this->context = [
			'alternates' => $alternates,
			'canonical' => $canonical,
			'contact' => [
				'address' => $this->config['contact']['address'],
				'name' => $this->translator->trans($this->config['contact']['name'])
			],
			'copy' => [
				'by' => $this->translator->trans($this->config['copy']['by']),
				'link' => $this->config['copy']['link'],
				'long' => $this->translator->trans($this->config['copy']['long']),
				'short' => $this->translator->trans($this->config['copy']['short']),
				'title' => $this->config['copy']['title']
			],
			'description' => null,
			'donate' => $this->config['donate'],
			'facebook' => [
				'og:type' => 'article',
				'og:site_name' => $title = $this->translator->trans($this->config['title']),
				'og:url' => $canonical,
				#'fb:admins' => $this->config['facebook']['admins'],
				'fb:app_id' => $this->config['facebook']['apps']
			],
			//XXX: TODO: only generate it when fb robot request the url ???
			'fbimage' => [
				'texts' => [
					$title => [
						'font' => 'irishgrover',
						'size' => 110
					]
				]
			],
			'icon' => $this->config['icon'],
			'keywords' => null,
			'locale' => str_replace('_', '-', $this->locale),
			'logo' => $this->config['logo'],
			'forms' => [],
			'root' => $this->router->generate($this->config['root']),
			'title' => [
				'page' => null,
				'section' => null,
				'site' => $title
			]
		];
	}

	/**
	 * The album page
	 *
	 * Display album
	 *
	 * @param Request $request The request instance
	 * @param int $id The album id
	 * @param string $path The album path
	 * @param string $slug The album slug
	 * @return Response The rendered view
	 */
	public function album(Request $request, int $id, string $path, string $slug): Response {
		//Get user
		$user = $this->security->getUser();

		//Check admin role
		if (!$this->checker->isGranted('ROLE_'.strtoupper($this->container->getParameter(RapsysUserBundle::getAlias().'.default.admin')))) {
			//Throw access denied
			//XXX: prevent slugger reverse engineering by not displaying decoded mail
			throw $this->createAccessDeniedException($this->translator->trans('Unable to access album', [], $this->alias));
		}

		//Without album
		if (!($this->context['album'] = $this->doctrine->getRepository(Album::class)->findOneByIdPathAsArray($id, $path))) {
			//Throw 404
			throw $this->createNotFoundException($this->translator->trans('Unable to find album'));
		}

		//With slug not matching
		if ($this->context['album']['slug'] !== $slug) {
			//Redirect on clean slug
			return $this->redirectToRoute($this->route, [ 'slug' => $this->context['album']['slug'] ]+$this->routeParams);
		}

		//Set modified
		$this->modified = $this->context['album']['modified'];

		//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($this->context['album'])));

			//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;
			}
		}

		//Set keywords
		/*$this->context['head']['keywords'] = implode(
			', ',
			//Use closure to extract each unique article keywords sorted
			(function ($t) {
				//Return array
				$r = [];

				//Iterate on articles
				foreach($t as $a) {
					//Non empty keywords
					if (!empty($a['keywords'])) {
						//Iterate on keywords
						foreach($a['keywords'] as $k) {
							//Set keyword
							$r[$k['title']] = $k['title'];
						}
					}
				}

				//Sort array
				sort($r);

				//Return array
				return $r;
			})($this->context['articles'])
		);
		//Get albums
		$this->context['albums'] = $this->config['albums'];*/

		//Return rendered response
		return $this->render('@RapsysTree/album.html.twig', $this->context, $response);
	}


	/**
	 * The element page
	 *
	 * Display element
	 *
	 * @param Request $request The request instance
	 * @param int $id The element id
	 * @param ?string $path The element path
	 * @return Response The rendered view
	 */
	public function element(Request $request, int $id, string $path): Response {
		//Get user
		$user = $this->security->getUser();

		//Without element
		if (!($this->context['element'] = $this->doctrine->getRepository(Element::class)->findOneByUidIdPathAsArray($user?->getId(), $id, $path))) {
			//Throw 404
			throw $this->createNotFoundException($this->translator->trans('Unable to find element'));
		}

		//With realpath not matching path
		//XXX: extra slashes removed by rewrite rule
		if (($this->context['path'] = substr($this->context['element']['realpath'], strlen($this->context['element']['album']['path'])+1)) !== $path) {
			//Redirect on clean path
			return $this->redirectToRoute($this->route, [ 'path' => $this->context['path'] ]+$this->routeParams);
		}

		//Set modified
		$this->modified = $this->context['element']['modified'];

		//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($this->context['element'])));

			//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;
			}
		}

		//TODO: move that in a shared function to use in album member function too

		/*
		//With directory
		if (is_dir($realpath)) {
			//Set element directories
			$this->context['element'] += [
				//Directories
				'directories' => [],
				//Files
				'files' => []
			];

			//Iterate on directory
			foreach(array_diff(scandir($realpath), ['.', '..']) as $item) {
				//Check item
				if (
					//Without item realpath
					!($itempath = realpath($realpath.'/'.$item)) ||
					//With item realpath not matching element path
					(
						$this->context['element']['album']['path'].$this->context['element']['path'] !==
						substr($itempath, 0, strlen($this->context['element']['album']['path'].$this->context['element']['path']))
					)
				) {
					//Skip
					continue;
				}

				//With directory
				if (is_dir($itempath)) {
					//Append directory
					$this->context['element']['directories'][$item] = $this->router->generate($this->route, [ 'path' => $this->context['path'].'/'.$item ]+$this->routeParams);
				//With file
				} elseif (is_file($itempath)) {
					//Append file
					$this->context['element']['files'][$item] = [
						//Set mime
						'mime' => mime_content_type($itempath),
						//Set size
						'size' => filesize($itempath),
						//Set link
						'link' => $this->router->generate($this->route, [ 'path' => $this->context['path'].'/'.$item ]+$this->routeParams)
					];
				//With unknown type
				} else {
					//Throw 404
					throw $this->createNotFoundException($this->translator->trans('Unable to process element'));
				}
			}
		//With file
		} elseif (is_file($realpath)) {
			//Append file
			$this->context['element']['file'] = [
				//Set mime
				'mime' => mime_content_type($realpath),
				//Set size
				'size' => filesize($realpath),
				//TODO: extra fields ? (preview, miniature, etc ?)
			];
			//TODO: mimetype decided extra fields ? (.pdf for doc, webm preview, img preview, etc ?)
			//TODO: XXX: finish this !!!
		//With unknown type
		} else {
			//Throw 404
			throw $this->createNotFoundException($this->translator->trans('Unable to process element'));
		}
		 */

		//Set keywords
		/*$this->context['head']['keywords'] = implode(
			', ',
			//Use closure to extract each unique article keywords sorted
			(function ($t) {
				//Return array
				$r = [];

				//Iterate on articles
				foreach($t as $a) {
					//Non empty keywords
					if (!empty($a['keywords'])) {
						//Iterate on keywords
						foreach($a['keywords'] as $k) {
							//Set keyword
							$r[$k['title']] = $k['title'];
						}
					}
				}

				//Sort array
				sort($r);

				//Return array
				return $r;
			})($this->context['articles'])
		);
		//Get albums
		$this->context['albums'] = $this->config['albums'];*/

		#header('Content-Type: text/plain');
		#var_dump($this->context['element']);
		#exit;

		//Return rendered response
		return $this->render('@RapsysTree/element.html.twig', $this->context, $response);
	}

	/**
	 * The index page
	 *
	 * Display index
	 *
	 * @param Request $request The request instance
	 * @return Response The rendered view
	 */
	public function index(Request $request): Response {
		//Get user
		$user = $this->security->getUser();

		//With not enough albums
		if (($this->count = $this->doctrine->getRepository(Album::class)->countByUidAsInt($user?->getId())) < $this->page * $this->limit) {
			//Throw 404
			throw $this->createNotFoundException($this->translator->trans('Unable to find albums'));
		}

		//Get albums
		if ($this->context['albums'] = $this->doctrine->getRepository(Album::class)->findByUidAsArray($user?->getId(), $this->page, $this->limit)) {
			//Set modified
			$this->modified = max(array_map(function ($v) { return $v['modified']; }, $this->context['albums']));
		//Without albums
		} else {
			//Set empty modified
			$this->modified = new \DateTime('-1 year');
		}

		//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($this->context['albums'])));

			//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;
			}
		}

		//Set keywords
		/*$this->context['head']['keywords'] = implode(
			', ',
			//Use closure to extract each unique article keywords sorted
			(function ($t) {
				//Return array
				$r = [];

				//Iterate on articles
				foreach($t as $a) {
					//Non empty keywords
					if (!empty($a['keywords'])) {
						//Iterate on keywords
						foreach($a['keywords'] as $k) {
							//Set keyword
							$r[$k['title']] = $k['title'];
						}
					}
				}

				//Sort array
				sort($r);

				//Return array
				return $r;
			})($this->context['articles'])
		);
		//Get albums
		$this->context['albums'] = $this->config['albums'];*/

		//Return rendered response
		return $this->render('@RapsysTree/index.html.twig', $this->context, $response);
	}

	/**
	 * The directory page
	 *
	 * Display directory
	 *
	 * @param Request $request The request instance
	 * @param string $path The directory path
	 * @return Response The rendered view
	 */
	public function directory(Request $request, string $path): Response {
		header('Content-Type: text/plain');
		var_dump($path);
		exit;

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

		$response->setEtag(md5($response->getContent()));
		$response->setPublic();
		$response->isNotModified($request);

		//Return response
		return $response;
	}

	/**
	 * The document page
	 *
	 * Display document
	 *
	 * @param Request $request The request instance
	 * @param string $path The directory path
	 * @return Response The rendered view
	 */
	public function document(Request $request, string $path): Response {
		//Render template
		$response = $this->render('@RapsysTree/document.html.twig', $this->context);

		$response->setEtag(md5($response->getContent()));
		$response->setPublic();
		$response->isNotModified($request);

		//Return response
		return $response;
	}

	/**
	 * Renders a view
	 *
	 * {@inheritdoc}
	 */
	protected function render(string $view, array $parameters = [], Response $response = null): Response {
		//Create response when null
		$response ??= new Response();

		//Without alternates
		if (count($parameters['alternates']) <= 1) {
			//Set routeParams
			$routeParams = $this->routeParams;

			//Iterate on locales excluding current one
			foreach($this->config['locales'] as $locale) {
				//With current locale
				if ($locale !== $this->locale) {
					//Set titles
					$titles = [];

					//Set route params locale
					$routeParams['_locale'] = $locale;

					//Iterate on other locales
					foreach(array_diff($this->config['locales'], [$locale]) as $other) {
						//Set other locale title
						$titles[$other] = $this->translator->trans($this->config['languages'][$locale], [], null, $other);
					}

					//Set locale locales context
					$parameters['alternates'][str_replace('_', '-', $locale)] = [
						'absolute' => $this->router->generate($this->route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
						'relative' => $this->router->generate($this->route, $routeParams),
						'title' => implode('/', $titles),
						'translated' => $this->translator->trans($this->config['languages'][$locale], [], null, $locale)
					];

					//Add shorter locale
					if (empty($parameters['alternates'][$shortCurrent = substr($locale, 0, 2)])) {
						//Set locale locales context
						$parameters['alternates'][$shortCurrent] = $parameters['alternates'][str_replace('_', '-', $locale)];
					}
				}
			}
		}

		//With canonical
		if (!empty($parameters['canonical'])) {
			//Set facebook url
			$parameters['facebook']['og:url'] = $parameters['canonical'];
		}

		//With empty facebook title and title
		if (empty($parameters['facebook']['og:title']) && !empty($parameters['title']['page'])) {
			//Set facebook title
			$parameters['facebook']['og:title'] = $parameters['title']['page'];
		}

		//With empty facebook description and description
		if (empty($parameters['facebook']['og:description']) && !empty($parameters['description'])) {
			//Set facebook description
			$parameters['facebook']['og:description'] = $parameters['description'];
		}

		//With locale
		if (!empty($this->locale)) {
			//Set facebook locale
			$parameters['facebook']['og:locale'] = $this->locale;

			//With alternates
			//XXX: locale change when fb_locale=xx_xx is provided is done in FacebookSubscriber
			//XXX: see https://stackoverflow.com/questions/20827882/in-open-graph-markup-whats-the-use-of-oglocalealternate-without-the-locati
			if (!empty($parameters['alternates'])) {
				//Iterate on alternates
				foreach($parameters['alternates'] as $lang => $alternate) {
					if (strlen($lang) == 5) {
						//Set facebook locale alternate
						$parameters['facebook']['og:locale:alternate'] = str_replace('-', '_', $lang);
					}
				}
			}
		}

		//Without facebook image defined and texts
		if (empty($parameters['facebook']['og:image']) && !empty($this->request) && !empty($parameters['fbimage']['texts']) && !empty($this->modified)) {
			//Get facebook image
			$parameters['facebook'] += $this->facebook->getImage($this->request->getPathInfo(), $parameters['fbimage']['texts'], $this->modified->getTimestamp());
		}

		//Call twig render method
		$content = $this->twig->render($view, $parameters);

		//Invalidate OK response on invalid form
		if (200 === $response->getStatusCode()) {
			foreach ($parameters as $v) {
				if ($v instanceof FormInterface && $v->isSubmitted() && !$v->isValid()) {
					$response->setStatusCode(422);
					break;
				}
			}
		}

		//Store content in response
		$response->setContent($content);

		//Return response
		return $response;
	}
}