from .post_ql import post, get_queries, headers
from typing import Dict, Any, List, Tuple, Callable as Function
from .logging import logging
from .utils.JSDict import JSDict
from replit import db
from datetime import datetime
from datauri import parse
from requests import post as _raw_post, get
from .utils.switch import Switch
from os import environ
from .links import links
from .logging import logging
from .utils.EventEmitter import EventEmitter
from time import sleep
from threading import Thread as _Thread
from .exceptions import InvalidSid

class Track(EventEmitter):
    def __init__(self, func: Function[..., Any], ms: int) -> None:
        super().__init__()
        self.f = func
        self.ms = ms
        self.running = False
        self.t = None
    def start(self) -> None:
        if (self.running): return
        def call_func():
            last = None
            self.running = True
            while self.running:
                n = self.f()
                if (n != last):
                    self.emit('update', n)
                self.last = n
                sleep(self.ms)
        self.t = _Thread(target = call_func)
        self.t.start()
    def stop(self) -> None:
        if (self.t is not None):
            self.running = False
            self.t.join()

class Client(EventEmitter):
    def __init__(self, sid: str) -> None:
        super().__init__()
        logging.log("fetching queries")
        with open("queries.json", "w") as f:
            req = get(f"https://Queries-Whitelist.bigminiboss.repl.co/{sid}")
            f.write(req.text)
        logging.log("finished fetching")
        
        self.sid = sid
        self.users = UserManager(self)
        self.repls = ReplManager(self)
        self.posts = PostManager(self, self)
        self.comments = CommentManager(self, self)
        self.q = get_queries()
        data = post(self.sid, self.q["currentUser"])
        if ("currentUser" not in data or data["currentUser"]): InvalidSid("SID invalid")
        vars = data["currentUser"]
        vars.update({"countryCode": data["country"]})
        self.user = CurrentUser(self)
        self.user.update(vars)
        self.users.cache[self.user.username] = self.user
        data = post(self.sid, self.q["repl"], vars = {
            "id": environ["REPL_ID"]
        })
        self.repl = Repl(self)
        self.repl.update(data["repl"])
        self.repls.cache[self.repl.id] = self.repl

        self.emit("ready", self)

    def search(self, query: str, options: Dict[str, Any] = {}):
        current = {"cache": True, "query": query, "categories": ["Tags"], "onlyCalculatedHits": False}
        current.update(options)
        options = JSDict(options)
        cache = options.cache
        del options["cache"]
        res = post(self.sid, self.q["search"], vars = {"options": options})
        search = res["search"]
        results = {
            "repls": {},
            "templates": {},
            "users": {},
            "posts": {},
            "tags": []
        }
        for r in search["replResults"]["results"]["items"]:
            repl = Repl(self)
            repl.update(r)
            if (options.cache): self.repls.cache[repl.id] = repl
            results["repls"][repl.id] = repl
        for t in search["templateResults"]["results"]["items"]:
            template = Template(self)
            template.update(t)
            results["templates"][template.id] = template
        for u in search["userResults"]["results"]["items"]:
            user = User(self)
            user.update(u)
            if (options.cache): self.users.cache[user.username] = user
            results["users"][user.username] = user
        for p in search["postResults"]["results"]["items"]:
            post = Post(self)
            post.update(u)
            if (options.cache): self.users.cache[post.id] = post
            results["posts"][post.id] = post
        for t in search["tagResults"]["results"]["items"]:
            current = t["tag"]
            current["lastUsed"] = t["timeLastUsed"]
            current["replsCount"] = t["replsCount"]
            results["tag"].append(current)
        return results

class UserManager:
    def __init__(self, client) -> None:
        self.c = client
        self.cache = {}
        self.q = get_queries()

    def fetch(self, userResolvable, options: Dict[str, Any] = {}):
        current = {"force": False, "cache": True}
        current.update(options)
        options = JSDict(current)
        query, variables = None, None
        user_type = type(userResolvable)
        if (user_type == str):
            query, variables = ("userByUsername", {"username": userResolvable})
        elif (user_type == int):
            query, variables = ("user", {"id": userResolvable})
        elif (user_type == User):
            if (getattr(userResolvable, "username", None)):
                query, variables = ("userByUsername", {"username": userResolvable.username})
            elif (getattr(userResolvable, "id", None)):
                query, variables = ("user", {"id": userResolvable.id})
        else:
            return None
        if (not options.force):
            property = list(variables.keys())[0]
            value = variables[property]
            if ("user" in self.cache):
                match = str(self.cache["user"][property]).lower()
                return match

        res = post(self.c.sid, self.q[query], variables)
        if (not res[query]): return None
        user = User(self.c)
        user.update(res[query])
        if (options.cache): self.cache[user.username] = user
        return user

    def search(self, query: str, options: Dict[str, Any] = {}):
        current = {"cache": True, "limit": 10}
        current.update(options)
        options = JSDict(current)
        res = post(self.c.sid, self.q["userSearch"], vars = {
            "query": query,
            "limit": options.limit
        })
        users = res['usernameSearch']
        c = {}
        for u in users:
            user = User(self.c)
            user.update(u)
            if (options.cache): self.c.users.cache[user.username] = user
            c[user.username] = user
        return c

class UserEventManager:
    def __init__(self, client) -> None:
        self.c = client
        self.cache = {}
        self.q = get_queries()
    def fetch(self, options: Dict[str, Any] = {}) -> Dict[str, Any]:
        current = {"cache": True, "limit": 10}
        current.update(options)
        options = JSDict(current)
        res = post(self.c.sid, self.q["userEvents"], vars = {
            "count": options.limit,
            "after": ""
        })
        events = res["events"]["items"]
        c = {}
        for e in events:
            event = UserEvent(self.c)
            event.update(e)
            if (options.cache): self.cache[event.id] = event
            c[event.id] = event
        return c

class ReplManager:
    def __init__(self, client, user = None) -> None:
        self.c = client
        self.cache = {}
        self.user = user
        self.q = get_queries()

    def fetch(self, replResolvable = None, **kwargs):
        if (self.user):
            if (self.user == self.c.user):
                options = {'cache': True, 'limit': 10, 'paths': ['']}
                options.update(kwargs)
                options = JSDict(options)
                c = {}
                for path in options.paths:
                    res = post(self.c.sid, self.q["dashboardRepls"], vars = {
                        "path": path,
                        "count": options.limit
                    })
                    repls = res["currentUser"]["replFolderByPath"]["repls"]["items"]
                    for r in repls:
                        repl = Repl(self.c)
                        repl.update(r)
                        if (options.cache): self.cache[repl.id] = repl
                        c[repl.id] = repl
                return c
            else:
                options = {"cache": True, "limit": 10, "search": None}
                options.update(kwargs)
                options = JSDict(options)
                res = post(self.c.sid, self.q["profileRepls"], vars = {
                    "username": self.user.username,
                    "count": options.limit
                })
                repls = res["user"]["profileRepls"]["items"]
                c = {}
                for r in repls:
                    repl = Repl(self.c)
                    repl.update(r)
                    if (options.cache):
                        self.cache[repl.id] = repl
                        self.c.repls.cache[repl.id] = repl
                    c[repl.id] = repl
                return c
        else:
            options = {"force": True, "cache": True, "url": False}
            options.update(kwargs)
            options = JSDict(options)
            vars = {}
            typeof = Switch(type(replResolvable))
            if (typeof.case(str)):
                vars = ({"url": replResolvable} if (options.url) else {"id": replResolvable})
            elif (typeof.case(Repl)):
                if (getattr(replResolvable, "id", False) and replResolvable.id):
                    vars = {"id": replResolvable.id}
            else:
                return None
            if (not options.force and "id" in vars and vars["id"] in self.cache):
                return self.cache[vars["id"]]
            res = post(self.c.sid, self.q["repl"], vars = vars)
            repl = Repl(self.c)
            repl.update(res["repl"])
            if (options.cache): self.cache[repl.id] = repl
            return repl
            

    def generateTitle(self) -> str:
        res = post(self.c.sid, self.q["replTitle"])
        return res["replTitle"]

    def create(self, options: Dict[str, Any] = {}):
        current = {"cache": True, "title": self.generateTitle(), "langauge": "nix"}
        current.update(options)
        options = current
        cache = options["cache"]
        del options["cache"]
        res = post(self.c.sid, self.q["createRepl"], vars = {
            "input": options
        })
        r = res["createRepl"]
        if ("id" not in r or not r["id"]): return None
        repl = Repl(self.c)
        repl.update(r)
        return repl

    def delete(self, replResolvable):
        repl = self.fetch(replResolvable)
        if (repl):
            repl.delete()

class PostManager:
    def __init__(self, client, parent) -> None:
        self.c = client
        self.parent = parent
        self.cache = {}
        self.q = get_queries()

    def fetch(self, **kwargs: Dict[str, Any]) -> None:
        res = None
        name = Switch(type(self.parent).__name__)
        if (name.case("Client")):
            id = kwargs["id"]
            options = {"cache": True}
            options.update(kwargs)
            options = JSDict(options)
            res = post(self.c.sid, self.q["post"], vars = {
                "id": id
            })
            if (not res["post"]): return None
            _post = Post(self.c)
            _post.update(res["post"])
            if (options.cache):
                self.cache[_post.id] = _post
            return _post
        elif (name.case("User") or name.case("CurrentUser")):
            options = {"cache": True, "limit": 10, "order": "new"}
            options.update(kwargs)
            options = JSDict(options)
            res = post(self.c.sid, self.q["userPosts"], vars = {
                "username": self.parent.username,
                "count": options.limit
            })
            posts = res["userByUsername"]["posts"]["items"]
            c = {}
            for p in posts:
                _post = Post(self.c)
                _post.update(p)
                if (options.cache): self.cache[_post.id] = _post
                c[_post.id] = _post
            return c
    def trending(self, options: Dict[str, Any] = {}) -> Dict[str, Any]:
        if (type(self.parent).__name__ == "Client"): return None
        options = {"cache": True, "limit": 10, "tags": []}
        res = post(self.c.sid, self.q["trending"], vars = {
            "options": {
                "searchQuery": options.search,
                "count": options.limit,
                "order": options.order,
                "tags": options.tags
            }
        })
        posts = res["replPosts"]["items"]
        c = {}
        for p in posts:
            _post = Post(self.c)
            _post.update(p)
            if (options.cache): self.cache[_post.id] = _post
            c[_post.id] = _post
        return c

class NotificationManager:
    def __init__(self, client) -> None:
        self.c = client
        self.cache = {}
        self.track = Track(lambda: self.c.user.notifications.fetch(), 60)
        def _(notifs) -> None:
            for i in notifs:
                self.c.emit("notification", notifs[i])
            if ("size" in notifs and notifs["size"]):
                self.markAsRead()
        
        self.track.on("update", _)
        self.q = get_queries()

    def fetch(self, options: Dict[str, Any] = {}) -> None:
        current = {"cache": True, "seen": False, "limit": 10}
        current.update(options)
        options = JSDict(current)
        res = post(self.c.sid, self.q["notifications"], vars = {
            "count": options.limit,
            "seen": options.seen
        })
        notifications = res["notifications"]["items"]
        c = {}
        for n in notifications:
            notification = Notification(self.c)
            notification.update(n)
            if (options.cache):
                self.cache[notification.id] = notification
            c[notification.id] = notification
        return c

    def markAsRead(self) -> None:
        post(self.c.sid, self.q["markAsRead"])
        self.c.user.notificationCount = 0

    def startEvents(self) -> None:
        self.track.start()

    def stopEvents(self) -> None:
        self.track.stop()


class MultiplayerManager:
    def __init__(self, client, repl) -> None:
        self.c = client
        self.cache = {}
        self.repl = repl
        self.q = get_queries()

    def invite(self, userResolvable) -> None:
        user = self.c.users.fetch(userResolvable)
        if (not user): return None
        post(self.c.sid, self.q["addMultiplayer"], vars = {
            "username": user.username,
            "replId": self.repl.id,
            "type": "rw"
        })

    def remove(self, userResolvable) -> None:
        user = self.c.users.fetch(userResolvable)
        if (not user): return None
        post(self.c.sid, self.q["removeMultiplayer"], vars = {
            "username": user.username,
            "replId": self.repl.id,
        })

class FollowingManager:
    def __init__(self, client, user) -> None:
        self.c = client
        self.user = user
        self.cache = {}
        self.events = UserEventManager(self.c)
        self.q = get_queries()

    def fetch(self, options: Dict[str, Any] = {}) -> Dict[str, Any]:
        current = {"cache": True, "limit": 10}
        current.update(options)
        options = current
        res = post(self.c.sid, self.q["follows"], vars = {
            "username": self.user.username,
            "count": options["limit"]
        })
        users = res["userByUsername"]["follows"]["items"]
        c = {}
        for u in users:
            user = User(self.c)
            user.update(u)
            if (options["cache"]):
                self.cache[user.username] = user
                self.c.users.cache[user.username] = user
            c[user.username] = user
        return c

    def setFollowing(self, userResolvable, should_follow: bool = True, options: Dict[str, Any] = {"cache": True}) -> bool:
        user = self.c.users.fetch(userResolvable)
        if (not user): return False
        res = post(self.c.sid, q["follow"], vars = {
            "input": {
                "targetUserId": user.id,
                "shouldFollow": should_follow
            }
        })
        if (not res["setFollowing"]): return False
        user.update(res["setFollowing"]["targetUser"])
        return user.isFollowingCurrentUser


class FollowersManager:
    def __init__(self, client, user) -> None:
        self.c = client
        self.cache = {}
        self.user = user
        self.q = get_queries()

    def fetch(self, options: Dict[str, Any] = {}) -> Dict[str, Any]:
        current = {"cache": True, "limit": 10}
        current.update(options)
        options = JSDict(current)
        res = post(self.c.sid, self.q["followers"], vars = {
            "username": self.user.username,
            "count": options.limit
        })
        users = res['userByUsername']["followers"]['items']
        c = {}
        for u in users:
            user = User(self.c)
            if (options.cache):
                self.cache[user.username] = user
                self.c.users.cache[user.username] = user
            c[user.username] = user
        return c

class CommentManager:
    def __init__(self, client, parentComment) -> None:
        self.c = client
        self.cache = {}
        self.parent = parentComment
        self.q = get_queries()

    def fetch(self, commentResolvable = None, **arguements: Dict[str, Any]):
        name = Switch(type(self.parent).__name__)
        if (name.case("Client")):
            options = {"cache": True, "force": False}
            options.update(arguements)
            options = JSDict(options)
            id = None
            _ = Switch(type(commentResolvable))
            if (_.case(int)):
                id = commentResolvable
            elif (_.case(Comment)):
                id = commentResolvable.id
            else:
                return None
            if (not options.force):
                if (id in self.cache):
                    _comment = self.cache[id]
                    return _comment
            res = post(self.c.sid, self.q["replComment"], vars = {
                "id": id
            })
            c = res["replComment"]
            if ('message' in c): return None
            comment = Comment(self.c)
            comment.update(c)
            if (options.cache):
                self.cache[comment.id] = comment
                if (getattr(comment, "comments", None) and comment.comments):
                    for reply in comment.comments:
                        self.cache[reply.id] = reply
                if (getattr(comment, "parentComment", None) and comment.parentComment):
                    self.cache[comment.parentComment.id] = comment.parentComment
            return comment
        elif (name.case("Repl")):
            options = {'cache': True, "limit": 10}
            options.update(arguements)
            options = JSDict(options)
            res = post(self.c.sid, self.q["replComments"], vars = {
                "id": self.parent.id,
                "count": options.limit
            })
            comments = res["repl"]["comments"]["items"]
            cn = {}
            for c in comments:
                comment = Comment(self.c, self.parent)
                comment.update(c)
                if (options.cache):
                    self.cache[comment.id] = comment
                    self.c.comments.cache[comment.id] = comment
                cn[comment.id] = comment
            return cn
        elif (name.case("Post")):
            return None
        elif (name.case("CurrentUser") or name.case("User")):
            options = {"cache": True, "limit": 10}
            options.update(arguements)
            res = post(self.c.sid, self.q["userComments"], vars = {
                "input": {
                    "userId": self.parent.id
                }
            })
            return res
        else:
            return {}


class User:
    """user object. Follow and blocking"""
    def __init__(self, client) -> None:
        self.c = client
        self.repls = ReplManager(self.c, self)
        self.followers = FollowersManager(self.c, self)
        self.following = FollowingManager(self.c, self)
        self.posts = PostManager(self.c, self)
        self.q = get_queries()

    def update(self, data: Dict[str, Any]):
        """update profile"""
        time_created, presence_status = (None, None)
        if (getattr(data, "timeCreated", None)):
            time_created = data.timeCreated
            del data["timeCreated"]
        if (getattr(data, "presenceStatus", None)):
            presence_status = data.presenceStatus
            del data["presenceStatus"]
        for key in data:
            setattr(self, key, data[key])
        if (time_created is not None): self.time_created = time_created
        if (presence_status is not None):
            self.last_seen = presence_status["lastSeen"]
            self.online = presence_status["isOnline"]

    def set_follow(self, should_follow: bool = True) -> bool:
        """follow a user and return whether they are following you"""
        res = post(self.c.sid, self.q["follow"], vars = {
            "input": {
                "targetUserId": self.id,
                "shouldFollow": should_follow and True
            }
        })
        if (res["setFollowing"]): return False
        self.update(res["setFollowing"]["targetUser"])
        return self.isFollowedBycurrentUser

    def set_block(self, should_block: bool = True) -> bool:
        """blocks a user and return whether they are blocking you"""
        res = post(self.c.sid, self.q["block"], vars = {
            "input": {
                "targetUserId": self.id,
                "shouldFollow": should_block and True
            }
        })
        if (res["setBlocking"]): return False
        self.update(res["setBlocking"])
        return self.isBlockedBycurrentUser

class CurrentUser(User):
    def __init__(self, client):
        super()
        self.c = client
        self.repls = ReplManager(self.c, self)
        self.notifications = NotificationManager(self.c)
        self.auth = {
            "google": None,
            "github": None,
            "facebook": None
        }
        self.q = get_queries()

    def update(self, data: Dict[str, Any] = {}):
        """update profile"""
        login = {
            "googleAuth": None,
            "cannySSOToken": None,
            "githubAuth": None,
            "facebookAuth": None,
            "clui": None,
            "usernameRepl": None
        }
        properties = {}
        for i in data:
            if (i in login):
                login[i] = data[i]
            else:
                properties[i] = data[i]
        super().update(properties)
        self.cannySSOToken = login["cannySSOToken"]
        if (login["googleAuth"]): self.auth["google"] = login["googleAuth"]["accessToken"]
        if (login["githubAuth"]): self.auth["github"] = login["githubAuth"]["accessToken"]
        if (login["facebookAuth"]): self.auth["facebook"] = login["facebookAuth"]["accessToken"]
        if (login["clui"]): self.clui = login["clui"]
        if (login["usernameRepl"]):
            del login["usernameRepl"]["url"]
            self.usernameRepl = self.c.repls.fetch(login["usernameRepl"]["id"], **login["usernameRepl"])
        return self
    
    def cannySSOToken(self):
        return self.cannySSOToken

    def auth(self):
        return self.auth

    def change(self, options: Dict[str, Any] = {}):
        if ("image" not in options): options["image"] = None
        image = options["image"]
        del options["image"]
        if (image):
            i = parse(image)
            if (i):
                _headers = headers.copy()
                _headers["Cookie"] = f"connect.sid={self.c.sid}"
                data = _raw_post("https://replit.com/data/images/upload", data={
                    "context": 'profile-image',
                    "image": i
                }, headers = _headers)
                if (data.status_code == 200):
                    options["profileImageId"] = data["data"]["id"]
        res = post(self.c.sid, self.q["updateUser"], vars = {
            "input": options
        })
        self.update(res['updateCurrentUser'])
        return self

class UserEvent:
    def __init__(self, client) -> None:
        self.c = client

    def update(self, data: Dict[str, Any]):
        user_events = {
            "timeUpdated": None,
            "user": None,
            "repl": None,
            "comment": None,
            "following": None
        }
        for i in data:
            if (i in user_events):
                user_events[i] = data[i]
                del data[i]
        for key in data:
            setattr(self, key, data[key])
        if (user_events["timeUpdated"]): self.timeUpdated = user_events["timeUpdated"]
        if (user_events["user"]): self.user = self.c.users.fetch(user_events["user"])
        if (user_events["repl"]): self.repl = self.c.repls.fetch(user_events["repl"])
        if (user_events["following"]): self.following = self.c.users.fetch(user_events["following"])
        if (user_events["comment"]): self.comment = self.c.comments.fetch(user_events["comment"])
        return self

class ThreadMessage:
    def __init__(self, client) -> None:
        self.c = client

    def update(self, data: Dict[str, Any]):
        thread_message_data = {
            "timeCreated": None,
            "timeUpdated": None,
            "user": None
        }
        for key in data:
            if (key in thread_message_data):
                thread_message_data[key] = data[key]
            else:
                setattr(self, key, data[key])
        if (thread_message_data["timeCreated"]): self.timeCreated = thread_message_data["timeCreated"]
        if (thread_message_data["timeUpdated"]): self.timeUpdate = thread_message_data["timeUpdated"]
        if (thread_message_data["user"]): self.c.users.fetch(thread_message_data["user"])
        return self

class Thread:
    def __init__(self, client) -> None:
        self.c = client
        self.participants = {}
        self.messages = []

    def update(self, data: Dict[str, Any]):
        thread_data = {
            "timeCreated": None,
            "timeUpdated": None,
            "messages": None,
            "participants": None
        }
        for key in data:
            if (key in thread_data):
                thread_data[key] = data[key]
            else:
                setattr(self, key, data[key])
        if (thread_data["timeCreated"]): self.timeCreated = thread_data["timeCreated"]
        if (thread_data["timeUpdated"]): self.timeUpdated = thread_data["timeUpdated"]
        if (thread_data["messages"]):
            for m in thread_data["messages"]:
                message = ThreadMessage(self.c)
                message.update(m)
                self.messages.append(message)
        if (thread_data["participants"]):
            for u in thread_data["participants"]:
                user = self.c.users.fetch(u)
                self.participants[user.username] =  user
        return self

class Template:
    def __init__(self, client) -> None:
        self.c = client

    def update(self, data: Dict[str, Any]):
        for key in data:
            setattr(self, key, data[key])
        return self

class Team:
    def __init__(self, client) -> None:
        self.c = client

    def update(self, data: Dict[str, Any]):
        for key in data:
            setattr(self, key, data[key])
        return self

class Env:
    def __init__(self, pyDict: Dict[str, Any] = {}) -> None:
        self._env = pyDict
    def __getitem__(self, key):
        return self._env[key]
    def __setitem__(self, key, val) -> None:
        self._env[key] = val
    def __delitem__(self, key) -> None:
        del self._env[key]

class Repl:
    def __init__(self, client) -> None:
        self.c = client
        self.threads = []
        self.multiplayer = MultiplayerManager(self.c, self)
        self.comments = CommentManager(self.c, self)
        self.q = get_queries()
        self.env = Env()

    def update(self, data: Dict[str, Any] = {}):
        repl_data = {
            "layoutState": None,
            "owner": None,
            "timeCreated": None,
            "timeUpdated": None,
            "currentUserPermissions": None,
            "annotationAnchors": None,
            "multiplayers": None,
            "database": None
        }
        for key in data:
            if (key in repl_data):
                repl_data[key] = data[key]
            else:
                setattr(self, key, data[key])
        if (repl_data["layoutState"]): self._layoutState = repl_data["layoutState"]
        if (repl_data["owner"]):
            u = User(self.c)
            u.update(repl_data["owner"])
            self.owner = u
            if (repl_data["database"]):
                self.database = db
                self.database.db_url = repl_data["database"]["jwt"]
            # if (self.owner.username != self.c.user.username):
            #     for p in ['folderId', 'multiplayerInvites', 'pinnedToProfile', 'powerUpCosts', 'delete', 'change', 'files', 'database']:
            #         delattr(self, p)
        if (repl_data["timeCreated"]): self.timeCreated = repl_data["timeCreated"]
        if (repl_data["timeUpdated"]): self.timeUpdated = repl_data["timeUpdated"]
        if (repl_data["currentUserPermissions"]): self.currentUserPermissions = repl_data["currentUserPermissions"]
        if (repl_data["multiplayers"]):
            for u in repl_data["multiplayers"]:
                user = self.c.users.fetch(u["username"])
                self.multiplayers.cache[user.username] = user
        return self

    def layoutState(self):
        return self._layoutState

    def fetchThreads(self, options: Dict[str, Any] = {"cache": True}) -> Dict[str, Any]:
        options = JSDict(options)
        res = post(self.c.sid, self.q["replThreads"], vars = {
            "id": self.id
        })
        threads = res["repl"]["annotationAnchors"]
        c = {}
        for t in threads:
            thread = Thread(self.c)
            thread.update(t)
            if (options.cache): self.threads[thread.id] = thread
            c[thread.id] = thread
        return c

    def fork(self, options: Dict[str, Any] = {}):
        current = {
            "cache": True,
            "title": self.title,
            "description": self.description,
            "language": (self.templateInfo.label if getattr(self.templateInfo, "label", False) else self.templateInfo["label"]),
            "isPrivate": self.isPrivate,
            "originId": self.id
        }
        current.update(options)
        options = current
        cache = options["cache"]
        del options["cache"]
        res = post(self.c.sid, self.q["createRepl"], vars = {"input": options})
        r = res["createRepl"]
        if (not r["id"]): return None
        repl = Repl(self.c)
        repl.update()
        if (cache): self.c.repls.cache[repl.id] = repl
        return repl

    def delete(self) -> None:
        post(self.c.sid, self.q["deleteRepl"], vars = {
            "id": self.id
        })

    def change(self, options: Dict[str, Any] = {}):
        options.update({"id": self.id})
        res = post(self.c.sid, self.q["updateRepl"], vars = {
            "input": options
        })
        self.update(res["updateRepl"]["repl"])
        return self

    def comment(self, body: str, options: Dict[str, Any] = {"cache": True}):
        options = JSDict(options)
        res = post(self.c.sid, self.q["sendReplComment"], vars = {
            "input": {
                "body": body,
                "replId": self.id
            }
        })
        if (not res): return None
        comment = Comment(self.comment)
        comment.update(res["createReplComment"])
        if (options.cache): self.c.comments.cache[comment.id] = comment
        return comment

class Post:
    def __init__(self, client) -> None:
        self.c = client

    def update(self, data: Dict[str, Any]):
        posts = {
            "user": None,
            "repl": None,
            "replComment": None,
            "timeUpdated": None,
            "timeCreated": None,
            "board": None
        }
        for i in data:
            if (i in posts):
                posts[i] = data[i]
                del data[i]
        for key in data:
            setattr(self, key, data[key])
        if (posts["user"]): self.user = self.c.users.fetch(posts['user'])
        if (posts["repl"]): self.repl = self.c.repls.fetch(posts["repl"])
        if (posts["replComment"]):
            comment = Comment(self.c, self.repl)
            comment.update(posts["replComment"])
            self.comment = comment
            self.comment.post = self
        if (posts["timeCreated"]): self.timeCreated = posts["timeCreated"]
        if (posts["timeUpdated"]): self.timeUpdated = posts["timeUpdated"]
        return self

    def reply(self, body: str) -> Dict[str, Any]:
        return self.comment.reply(body)

class Notification:
    def __init__(self, client) -> None:
        self.c = client

    def update(self, data: Dict[str, Any]):
        notif_data = {
            "timeCreated": None,
            "creator": None,
            "replComment": None
        }
        for key in data:
            if (key in notif_data):
                notif_data[key] = data[key]
            else:
                setattr(self, key, data[key])
        if (notif_data["timeCreated"]): self.timeCreated = notif_data["timeCreated"]
        if (notif_data["creator"]): self.creator = self.c.users.fetch(notif_data["creator"])
        if (notif_data["replComment"]):
            self.comment = self.c.comments.fetch(notif_data["replComment"]["id"], **notif_data["replComment"])
        return self

class Comment:
    def __init__(self, client, parent = None) -> None:
        self.c = client
        self.comments = []
        self.q = get_queries()
        self.parentComment = parent

    def update(self, data: Dict[str, Any]):
        comment_data = {
            "timeCreated": None,
            "timeUpdated": None,
            "user": None,
            "repl": None,
            "post": None,
            "parentComment": None,
            "comments": None
        }
        for i in data:
            if (i in comment_data):
                comment_data[i] = data[i]
            else:
                setattr(self, i, data[i])
        if (comment_data["timeCreated"]): self.timeCreated = comment_data["timeCreated"]
        if (comment_data["timeUpdated"]): self.timeUpdated = comment_data["timeUpdated"]
        if (comment_data["user"]): self.user = self.c.users.fetch(comment_data["user"])
        if (comment_data["repl"]): self.repl = self.c.repls.fetch(comment_data["repl"])
        if (comment_data["post"]): self.post = self.c.post.fetch(comment_data["post"])
        if (comment_data["parentComment"]):
            parent = comment_data["parentComment"]
            if (parent["id"] in self.c.comments.cache):
                self.parentComment = self.c.comments.cache[parent["id"]]
            else:
                # recursively get the root comment
                comment = Comment(self.c)
                comment.update(parent)
                self.parentComment = comment
        if (comment_data["comments"]):
            for c in comment_data["comments"]:
                if (c["id"] in self.c.comments.cache):
                    self.comments.append(self.c.comments.cache[c["id"]])
                else:
                    comment = Comment(self.c)
                    parentComment = None
                    if ("parentComment" in c):
                        parentComment = c["parentComment"]
                        del c["parentComment"]
                    comment.update(c)
                    comment.parentComment = self
                    self.comments.append(comment)
        return self

    def delete(self) -> None:
        """delete comment"""
        if ("delete" not in self.currentUserPermission): self.currentUserPermission["delete"] = None
        if (self.currentUserPermission["delete"] or self.isAuthor or self.repl.isOwner):
            post(self.c.sid, self.q["deleteComment"], vars = {
                "id": self.id
            })

    def reply(self, body: str, options: Dict[str, Any] = {}):
        """reply comment"""
        current = {"cache": True}
        current.update(options)
        options = JSDict(current)
        res = post(self.c.sid, self.q["sendReplCommentReply"], vars = {
            "input": {
                "body": (f"@{self.user.username} {body}" if (getattr(options, "mention", False) and options.mention) else body),
                "replCommentId": self.id
            }
        })
        if (not res): return None
        comment = Comment(self.c)
        comment.update(res['createReplCommentReply'])
        if (options.cache):
            self.c.comments.cache[comment.id] = comment
        return comment

