<?php /** @noinspection PhpIllegalPsrClassPathInspection */

namespace Kicken\Chat;


use InvalidArgumentException;
use RuntimeException;
use stdClass;

class ChatServer extends StreamSocketServer {
    private array $mMessages = [];
    /** @var ChatUser[] */
    private array $mUsers = [];
    /** @var callable[] */
    private array $mNewMessageListeners = [];

    public function getUsers() : array{
        return $this->mUsers;
    }

    public function getUserByUID(string $uid) : ?ChatUser{
        return $this->mUsers[$uid] ?? null;
    }

    public function cancelOnNewMessage(callable $cb) : void{
        foreach ($this->mNewMessageListeners as $k => $cb2){
            if ($cb === $cb2){
                unset($this->mNewMessageListeners[$k]);
            }
        }
    }

    public function onNewMessage(callable $cb) : void{
        $this->mNewMessageListeners[] = $cb;
    }

    public function registerNick(string $nick) : string{
        if (strlen($nick) == 0){
            throw new InvalidArgumentException('Nickname cannot be blank', ERR_NO_NICK);
        }

        $inuse = null;
        foreach ($this->mUsers as $u){
            if ($u->getNick() === $nick){
                $inuse = $u;
            }
        }

        if ($inuse && !$inuse->isActive()){
            return $inuse->getUid();
        } else if ($inuse){
            throw new InvalidArgumentException('Nickname already in use', ERR_NICK_TAKEN);
        } else {
            $try = 0;
            do {
                $uid = uniqid('u');
            } while (isset($this->mUsers[$uid]) && ++$try < 25);

            if (isset($this->mUsers[$uid])){
                throw new RuntimeException('Unable to generate new unique id', ERR_SYSTEM);
            }

            $user = new ChatUser($uid, $this);
            $user->setNick($nick);
            $this->mUsers[$uid] = $user;

            return $uid;
        }
    }

    private function generateNewMessageId(stdClass $json) : string{
        if (!isset($json->timestamp)){
            throw new InvalidArgumentException('No timestamp on message object', ERR_NO_TIMESTAMP);
        }

        if (!isset($this->mMessages[$json->timestamp])){
            $this->mMessages[$json->timestamp] = [];
        }

        $idx = count($this->mMessages[$json->timestamp]);

        return sprintf("%d:%d", $json->timestamp, $idx);
    }

    public function saveMessage(stdClass $json) : string{
        $newId = $this->generateNewMessageId($json);
        [$ts, $idx] = explode(':', $newId, 2);

        $json->messageId = $newId;
        $this->mMessages[$ts][$idx] = $json;

        $this->fireOnNewMessage($newId);

        return $newId;
    }

    private function fireOnNewMessage(string $id) : void{
        foreach ($this->mNewMessageListeners as $cb){
            $cb($id);
        }
    }

    public function getNewestMessageId(){
        $last = end($this->mMessages);
        $last = end($last);

        return $last->messageId;
    }

    public function getMessagesAfter(?string $id) : array{
        $output = [];
        [$ts, $idx] = explode(':', $id ?: '0:0', 2);
        $idx = intval($idx);

        $newerTs = array_filter(array_keys($this->mMessages), function($k) use ($ts){
            return $k >= $ts;
        });

        foreach ($newerTs as $k){
            $list = $this->mMessages[$k];

            if ($k == $ts && isset($list[$idx + 1])){
                $slice = array_slice($list, $idx + 1);
                $output = array_merge($output, $slice);
            } else if ($k > $ts){
                $output = array_merge($output, $list);
            }
        }

        return $output;
    }
}