<?php

namespace Mine\SeekerBundle\Topic;

use Doctrine\DBAL\Driver\PDOException;
use Doctrine\ORM\EntityManager;
use Gos\Bundle\WebSocketBundle\Client\ClientManipulatorInterface;
use Gos\Bundle\WebSocketBundle\Topic\TopicInterface;
use Gos\Bundle\WebSocketBundle\Router\WampRequest;
use Mine\SeekerBundle\Entity\Gamer;
use Mine\SeekerBundle\Entity\Step;
use Ratchet\ConnectionInterface;
use Ratchet\Wamp\Topic;
use Symfony\Component\HttpFoundation\RequestStack;

class MineseekerTopic implements TopicInterface
{
    /** @var ClientManipulatorInterface */
    protected $clientManipulator;

    /** @var EntityManager */
    protected $em;

    /** @var RequestStack */
    protected $requestStack;

    /**
     * MineseekerTopic constructor.
     *
     * @param $clientManipulator ClientManipulatorInterface
     * @param EntityManager $entityManager
     * @param RequestStack $requestStack
     */
    public function __construct(ClientManipulatorInterface $clientManipulator, EntityManager $entityManager, RequestStack $requestStack)
    {
        $this->clientManipulator = $clientManipulator;
        $this->em = $entityManager;
        $this->requestStack = $requestStack;
    }

    /**
     * This will receive any Subscription requests for this topic.
     *
     * @param ConnectionInterface $connection
     * @param Topic $topic
     * @param WampRequest $request
     * @return void
     */
    public function onSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request)
    {
        /** this will broadcast the message to ALL subscribers of this topic. */
        $user = $this->clientManipulator->getClient($connection);
        $userName = is_string($user) ? $user : $user->getUsername();

        /** if more user wants to connect than 2 to one channel */
        if ($topic->count() > 2) {
            $topic->remove($connection);
        } else {
            $users = $this->controlUsers($topic, $userName, $user);

            $topic->broadcast([
                'userTopicId' => $connection->resourceId,
                'channel' => $topic->getId(),
                'user' => $userName,
                'userCnt' => $topic->count(),
                'users' => $users
            ]);
        }
    }

    /**
     * This will receive any UnSubscription requests for this topic.
     *
     * @param ConnectionInterface $connection
     * @param Topic $topic
     * @param WampRequest $request
     * @return void
     */
    public function onUnSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request)
    {
        /** this will broadcast the message to ALL subscribers of this topic. */
        $topic->broadcast(['msg' => $connection->resourceId . " has left " . $topic->getId()]);
    }

    /**
     * This will receive any Publish requests for this topic.
     *
     * @param ConnectionInterface $connection
     * @param Topic $topic
     * @param WampRequest $request
     * @param $event
     * @param array $exclude
     * @param array $eligible
     * @return mixed|void
     * @internal param Topic $Topic
     * @internal param array $eligibles
     */
    public function onPublish(ConnectionInterface $connection, Topic $topic, WampRequest $request, $event, array $exclude, array $eligible)
    {
        $user = $this->clientManipulator->getClient($connection);
        $userName = is_string($user) ? $user : $user->getUsername();

        /** Save every step by user to db */
        if (null === $event['resign']) {
            $this->saveStepToDb($topic, $event);
        } else {
            $this->saveResignToDb($topic, $event['resign']);
        }

        $topic->broadcast([
            'userTopicId' => $connection->resourceId,
            'channel' => $topic->getId(),
            'user' => $userName,
            'userCnt' => $topic->count(),
            'data' => $event
        ]);
    }

    /**
     * Like RPC is will use to prefix the channel
     *
     * @return string
     */
    public function getName()
    {
        return 'mineseeker.topic';
    }

    /**
     * Save Resign event to database
     *
     * @param $topic
     * @param $color
     */
    private function saveResignToDb($topic, $color)
    {
        $this->reConnect();
        $gameAssoc = explode('/', $topic->getId())[2];

        $playedGame = $this->em
            ->getRepository('MineSeekerBundle:PlayedGame')
            ->findOneByGameAssoc($gameAssoc);

        $playedGame->setResign($color);
        $this->em->persist($playedGame);
        $this->em->flush();
    }

    /**
     * Save steps and point information to database
     *
     * @param $topic
     * @param $event
     */
    private function saveStepToDb($topic, $event)
    {
        $this->reConnect();
        $gameAssoc = explode('/', $topic->getId())[2];

        $playedGame = $this->em
            ->getRepository('MineSeekerBundle:PlayedGame')
            ->findOneByGameAssoc($gameAssoc);

        $step = new Step();

        $step->setRow($event['coords'][0]);
        $step->setCol($event['coords'][1]);
        $step->setWBomb($event['bomb']);
        $step->setPlayedGame($playedGame);
        $step->setCreated(new \DateTime());
        $this->em->persist($step);

        $playedGame->setBluePoints($event['bluePoints']);
        $playedGame->setRedPoints($event['redPoints']);
        $playedGame->setBlueExplodedBomb($event['blueExplodedBomb'] ? true : null);
        $playedGame->setRedExplodedBomb($event['redExplodedBomb'] ? true : null);
        $playedGame->setUpdated(new \DateTime());
        $this->em->persist($playedGame);

        $this->em->flush();
    }

    /**
     * Control all users in a channel
     *
     * @param $topic
     * @param $userName
     * @param $user
     * @return array
     */
    private function controlUsers($topic, $userName, $user)
    {
        $this->reConnect();
        $gameAssoc = explode('/', $topic->getId())[2];

        $playedGame = $this->em
            ->getRepository('MineSeekerBundle:PlayedGame')
            ->findOneByGameAssoc($gameAssoc);

        /** @var $users {array} */
        $users = $this->getUserCollection($playedGame);

        $red = "" !== $users['red'] || "" !== $users['redAnon'] ? 1 : 0;
        $blue = "" !== $users['blue'] || "" !== $users['blueAnon'] ? 1 : 0;
        $one = $topic->count() === 1;
        $two = $topic->count() === 2;

        /** This checks it is a reconnection */
        if (($one && ($red + $blue === 0)) || ($two && ($red + $blue === 1))) {
            /** @var $users {array} w/ save users to database */
            $users = $this->saveUserToDb($topic, $userName, $user, $topic->count());
        }

        return $users;
    }

    /**
     * Save user data to database
     *
     * @param $topic
     * @param $userName
     * @param $user
     * @param $count
     * @return array
     */
    private function saveUserToDb($topic, $userName, $user, $count)
    {
        $this->reConnect();
        $gameAssoc = explode('/', $topic->getId())[2];

        $playedGame = $this->em
            ->getRepository('MineSeekerBundle:PlayedGame')
            ->findOneByGameAssoc($gameAssoc);

        /** the user is not anonym */
        if (!is_string($user)) {
            $FOSUser = $this->em
                ->getRepository('JotunheimrUserBundle:User')
                ->findOneByUsername($userName);

            if ($count == 1) {
                /** @var $random {integer} Active player: red: 0, blue: 1 */
                $random = rand(0, 1);
                !$random ? $playedGame->setRed($FOSUser) : $playedGame->setBlue($FOSUser);
            } else {
                null === $playedGame->getRed() && null === $playedGame->getRedAnon()
                    ? $playedGame->setRed($FOSUser)
                    : $playedGame->setBlue($FOSUser);
            }
        } else {
            // $request = $this->requestStack->getCurrentRequest(); // TODO nem megy...

            $anon = new Gamer();
            $anon->setUserName($userName);
            $anon->setConnTimestamp(new \DateTime());
            $this->em->persist($anon);

            if ($count == 1) {
                /** @var $random {integer} Active player: red: 0, blue: 1 */
                $random = rand(0, 1);
                !$random ? $playedGame->setRedAnon($anon) : $playedGame->setBlueAnon($anon);
            } else {
                null === $playedGame->getRed() && null === $playedGame->getRedAnon()
                    ? $playedGame->setRedAnon($anon)
                    : $playedGame->setBlueAnon($anon);
            }
        }

        $this->em->persist($playedGame);
        $this->em->flush();

        return $this->getUserCollection($playedGame);
    }

    /**
     * Get user collection from PlayedGame entity
     *
     * @param $playedGame
     * @return array
     */
    private function getUserCollection($playedGame)
    {
        return array(
            'red' => null !== $playedGame->getRed() ? $playedGame->getRed()->getUsername() : '',
            'blue' => null !== $playedGame->getBlue() ? $playedGame->getBlue()->getUsername() : '',
            'redAnon' => null !== $playedGame->getRedAnon() ? $playedGame->getRedAnon()->getUserName() : '',
            'blueAnon' => null !== $playedGame->getBlueAnon() ? $playedGame->getBlueAnon()->getUserName() : ''
        );
    }

    /**
     * Handle prod MySQL timeout
     */
    private function reConnect()
    {
        try {
            $connection = $this->em->getConnection();

            if (false === $connection->ping()) {
                $connection->close();
                $connection->connect();
            }
        } catch(PDOException $ex) {
            throw PDOException::class;
        }
    }
}
