<?php namespace MapGuesser\Controller;

use DateTime;
use Faker\Factory;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Interfaces\Response\IRedirect;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\Model\UserInChallenge;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use SokoWeb\Response\Redirect;

class GameController implements IAuthenticationRequired
{
    const NUMBER_OF_ROUNDS = 5;

    private MultiConnector $multiConnector;

    private MultiRoomRepository $multiRoomRepository;

    private MapRepository $mapRepository;

    private PlaceRepository $placeRepository;

    private ChallengeRepository $challengeRepository;

    private UserInChallengeRepository $userInChallengeRepository;

    public function __construct()
    {
        $this->multiConnector = new MultiConnector();
        $this->multiRoomRepository = new MultiRoomRepository();
        $this->mapRepository = new MapRepository();
        $this->placeRepository = new PlaceRepository();
        $this->challengeRepository = new ChallengeRepository();
        $this->userInChallengeRepository = new UserInChallengeRepository();
    }

    public function isAuthenticationRequired(): bool
    {
        return empty($_ENV['ENABLE_GAME_FOR_GUESTS']);
    }

    public function getGame(): IContent
    {
        $mapId = (int) \Container::$request->query('mapId');

        return new HtmlContent('game', ['mapId' => $mapId]);
    }

    public function getNewMultiGame(): IRedirect
    {
        $mapId = (int) \Container::$request->query('mapId');
        $map = $this->mapRepository->getById($mapId);
        $roomId = bin2hex(random_bytes(3));
        $token = $this->getMultiToken($roomId);

        $room = new MultiRoom();
        $room->setRoomId($roomId);
        $room->setStateArray([
            'mapId' => $mapId,
            'area' => $map->getArea(),
            'rounds' => [],
            'currentRound' => -1
        ]);
        $room->setMembersArray(['owner' => $token, 'all' => []]);
        $room->setUpdatedDate(new DateTime());

        \Container::$persistentDataManager->saveToDb($room);

        $this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);

        return new Redirect(
            \Container::$routeCollection
                ->getRoute('multiGame')
                ->generateLink(['roomId' => $roomId]),
            IRedirect::TEMPORARY
        );
    }

    public function getMultiGame(): IContent
    {
        $roomId = \Container::$request->query('roomId');

        return new HtmlContent('game', ['roomId' => $roomId]);
    }

    public function getChallenge(): IContent
    {
        $challengeToken = \Container::$request->query('challengeToken');

        return new HtmlContent('game', ['challengeToken' => $challengeToken]);
    }

    public function createNewChallenge(): IContent
    {
        // create Challenge
        do {
            // initiliaze or if a challenge with the same token already exists
            $challengeToken = mt_rand();
        } while ($this->challengeRepository->getByToken($challengeToken));

        $challenge = new Challenge();
        $challenge->setToken($challengeToken);
        $challenge->setCreatedDate(new DateTime());

        if (\Container::$request->post('timerEnabled') !== null && \Container::$request->post('timeLimit') !== null) {
            $challenge->setTimeLimit(\Container::$request->post('timeLimit'));
        }
        if (\Container::$request->post('timeLimitType') !== null) {
            $challenge->setTimeLimitType(\Container::$request->post('timeLimitType'));
        }
        if (\Container::$request->post('noMove') !== null) {
            $challenge->setNoMove(true);
        }
        if (\Container::$request->post('noPan') !== null) {
            $challenge->setNoPan(true);
        }
        if (\Container::$request->post('noZoom') !== null) {
            $challenge->setNoZoom(true);
        }

        \Container::$persistentDataManager->saveToDb($challenge);

        // save owner/creator

        $session = \Container::$request->session();
        $userId = $session->get('userId');

        $userInChallenge = new UserInChallenge();
        $userInChallenge->setUserId($userId);
        $userInChallenge->setChallenge($challenge);
        $userInChallenge->setTimeLeft($challenge->getTimeLimit());
        $userInChallenge->setIsOwner(true);

        \Container::$persistentDataManager->saveToDb($userInChallenge);

        // select places

        $mapId = (int) \Container::$request->post('mapId');
        // $map = $this->mapRepository->getById($mapId);

        $places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);

        $round = 0;
        foreach ($places as $place) {
            $placeInChallenge = new PlaceInChallenge();
            $placeInChallenge->setPlace($place);
            $placeInChallenge->setChallenge($challenge);
            $placeInChallenge->setRound($round++);
            \Container::$persistentDataManager->saveToDb($placeInChallenge);
        }

        return new JsonContent(['challengeToken' => dechex($challengeToken)]);
    }

    public function prepareGame(): IContent
    {
        $mapId = (int) \Container::$request->query('mapId');
        $map = $this->mapRepository->getById($mapId);
        $session = \Container::$request->session();

        if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
            $session->set('state', [
                'mapId' => $mapId,
                'area' => $map->getArea(),
                'rounds' => [],
                'currentRound' => -1
            ]);
        } else { // update the area of the map in the session in any case
            $state['area'] = $map->getArea();
            $session->set('state', $state);
        }

        return new JsonContent([
            'mapId' => $mapId,
            'mapName' => $map->getName(),
            'bounds' => $map->getBounds()->toArray()
        ]);
    }

    public function prepareMultiGame(): IContent
    {
        $roomId = \Container::$request->query('roomId');
        $userName = \Container::$request->post('userName');
        if (empty($userName)) {
            $faker = Factory::create();
            $userName = $faker->userName;
        }

        $room = $this->multiRoomRepository->getByRoomId($roomId);

        if (!isset($room)) {
            return new JsonContent(['error' => 'game_not_found']);
        }

        $state = $room->getStateArray();
        $map = $this->mapRepository->getById($state['mapId']);
        $token = $this->getMultiToken($roomId);

        $members = $room->getMembersArray();

        if (!in_array($token, $members['all'])) {
            if ($state['currentRound'] >= 0) {
                return new JsonContent(['error' => 'game_already_started']);
            }

            $members['all'][] = $token;
        }

        $room->setMembersArray($members);
        $room->setUpdatedDate(new DateTime());

        \Container::$persistentDataManager->saveToDb($room);

        $this->multiConnector->sendMessage('join_room', [
            'roomId' => $roomId,
            'token' => $token,
            'userName' => $userName
        ]);

        return new JsonContent([
            'roomId' => $roomId,
            'token' => $token,
            'owner' => $members['owner'] == $token,
            'mapId' => $state['mapId'],
            'mapName' => $map->getName(),
            'bounds' => $map->getBounds()->toArray()
        ]);
    }

    public function prepareChallenge(): IContent
    {
        $challengeToken_str = \Container::$request->query('challengeToken');
        $session = \Container::$request->session();
        $userId = $session->get('userId');

        if (!isset($userId))
        {
            return new JsonContent(['error' => 'anonymous_user']);
        }

        $challenge = $this->challengeRepository->getByTokenStr($challengeToken_str);

        if (!isset($challenge))
        {
            return new JsonContent(['error' => 'game_not_found']);
        }

        if (!$this->userInChallengeRepository->isUserParticipatingInChallenge($userId, $challenge)) {
            // new player is joining
            $userInChallenge = new UserInChallenge();
            $userInChallenge->setUserId($userId);
            $userInChallenge->setChallenge($challenge);
            $userInChallenge->setTimeLeft($challenge->getTimeLimit());
            \Container::$persistentDataManager->saveToDb($userInChallenge);
        }

        $map = $this->mapRepository->getByChallenge($challenge);

        return new JsonContent([
            'mapId' => $map->getId(),
            'mapName' => $map->getName(),
            'bounds' => $map->getBounds()->toArray()
        ]);
    }

    private function getMultiToken(string $roomId): string
    {
        $session = \Container::$request->session();

        if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
            $token = bin2hex(random_bytes(16));

            $session->set('multiState', [
                'roomId' => $roomId,
                'token' => $token
            ]);
        } else {
            $token = $multiState['token'];
        }

        return $token;
    }
}