import argparse
import codecs
import hashlib
import os
import random
import time
import json
from string import Template

from twisted.enterprise import adbapi
from twisted.internet import task, reactor
from twisted.internet.protocol import Factory

try:
    from OpenSSL import crypto
    from OpenSSL.SSL import TLSv1_2_METHOD
    from twisted.internet import ssl
except:
    pass

import syncplay
from syncplay import constants
from syncplay.messages import getMessage
from syncplay.protocols import SyncServerProtocol
from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator, meetsMinVersion, playlistIsValid, truncateText, getListAsMultilineString, convertMultilineStringToList

class SyncFactory(Factory):
    def __init__(self, port='', password='', motdFilePath=None, roomsDbFile=None, permanentRoomsFile=None, isolateRooms=False, salt=None,
                 disableReady=False, disableChat=False, maxChatMessageLength=constants.MAX_CHAT_MESSAGE_LENGTH,
                 maxUsernameLength=constants.MAX_USERNAME_LENGTH, statsDbFile=None, tlsCertPath=None):
        self.isolateRooms = isolateRooms
        syncplay.messages.setLanguage(syncplay.messages.getInitialLanguage())
        print(getMessage("welcome-server-notification").format(syncplay.version))
        self.port = port
        if password:
            password = password.encode('utf-8')
            password = hashlib.md5(password).hexdigest()
        self.password = password
        if salt is None:
            salt = RandomStringGenerator.generate_server_salt()
            print(getMessage("no-salt-notification").format(salt))
        self._salt = salt
        self._motdFilePath = motdFilePath
        self.roomsDbFile = roomsDbFile
        self.disableReady = disableReady
        self.disableChat = disableChat
        self.maxChatMessageLength = maxChatMessageLength if maxChatMessageLength is not None else constants.MAX_CHAT_MESSAGE_LENGTH
        self.maxUsernameLength = maxUsernameLength if maxUsernameLength is not None else constants.MAX_USERNAME_LENGTH
        self.permanentRoomsFile = permanentRoomsFile if permanentRoomsFile is not None and os.path.isfile(permanentRoomsFile) else None
        self.permanentRooms = self.loadListFromMultilineTextFile(self.permanentRoomsFile) if self.permanentRoomsFile is not None else []
        if not isolateRooms:
            self._roomManager = RoomManager(self.roomsDbFile, self.permanentRooms)
        else:
            self._roomManager = PublicRoomManager()
        if statsDbFile is not None:
            self._statsDbHandle = StatsDBManager(statsDbFile)
            self._statsRecorder = StatsRecorder(self._statsDbHandle, self._roomManager)
            statsDelay = 5*(int(self.port)%10 + 1)
            self._statsRecorder.startRecorder(statsDelay)
        else:
            self._statsDbHandle = None
        if tlsCertPath is not None:
            self.certPath = tlsCertPath
            self._TLSattempts = 0
            self._allowTLSconnections(self.certPath)
        else:
            self.certPath = None
            self.options = None
            self.serverAcceptsTLS = False

    def loadListFromMultilineTextFile(self, path):
        if not os.path.isfile(path):
            return []
        with open(path) as f:
            multiline = f.read().splitlines()
        return multiline

    def loadRoom(self):
        rooms = self._roomsDbHandle.loadRooms()

    def buildProtocol(self, addr):
        return SyncServerProtocol(self)

    def sendState(self, watcher, doSeek=False, forcedUpdate=False):
        room = watcher.getRoom()
        if room:
            paused, position = room.isPaused(), room.getPosition()
            setBy = room.getSetBy()
            watcher.sendState(position, paused, doSeek, setBy, forcedUpdate)

    def getFeatures(self):
        features = dict()
        features["isolateRooms"] = self.isolateRooms
        features["readiness"] = not self.disableReady
        features["managedRooms"] = True
        features["persistentRooms"] = self.roomsDbFile is not None
        features["chat"] = not self.disableChat
        features["maxChatMessageLength"] = self.maxChatMessageLength
        features["maxUsernameLength"] = self.maxUsernameLength
        features["maxRoomNameLength"] = constants.MAX_ROOM_NAME_LENGTH
        features["maxFilenameLength"] = constants.MAX_FILENAME_LENGTH

        return features

    def getMotd(self, userIp, username, room, clientVersion):
        oldClient = False
        if constants.WARN_OLD_CLIENTS:
            if not meetsMinVersion(clientVersion, constants.RECENT_CLIENT_THRESHOLD):
                oldClient = True
        if self._motdFilePath and os.path.isfile(self._motdFilePath):
            tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read()
            args = dict(version=syncplay.version, userIp=userIp, username=username, room=room)
            try:
                motd = Template(tmpl).substitute(args)
                if oldClient:
                    motdwarning = getMessage("new-syncplay-available-motd-message").format(clientVersion)
                    motd = "{}\n{}".format(motdwarning, motd)
                return motd if len(motd) < constants.SERVER_MAX_TEMPLATE_LENGTH else getMessage("server-messed-up-motd-too-long").format(constants.SERVER_MAX_TEMPLATE_LENGTH, len(motd))
            except ValueError:
                return getMessage("server-messed-up-motd-unescaped-placeholders")
        elif oldClient:
            return getMessage("new-syncplay-available-motd-message").format(clientVersion)
        else:
            return ""

    def addWatcher(self, watcherProtocol, username, roomName):
        roomName = truncateText(roomName, constants.MAX_ROOM_NAME_LENGTH)
        username = self._roomManager.findFreeUsername(username, self.maxUsernameLength)
        watcher = Watcher(self, watcherProtocol, username)
        self.setWatcherRoom(watcher, roomName, asJoin=True)

    def setWatcherRoom(self, watcher, roomName, asJoin=False):
        roomName = truncateText(roomName, constants.MAX_ROOM_NAME_LENGTH)
        self._roomManager.moveWatcher(watcher, roomName)
        if asJoin:
            self.sendJoinMessage(watcher)
        else:
            self.sendRoomSwitchMessage(watcher)

        room = watcher.getRoom()
        roomSetByName = room.getSetBy().getName() if room.getSetBy() else None
        watcher.setPlaylist(roomSetByName, room.getPlaylist())
        watcher.setPlaylistIndex(roomSetByName, room.getPlaylistIndex())
        if RoomPasswordProvider.isControlledRoom(roomName):
            for controller in room.getControllers():
                watcher.sendControlledRoomAuthStatus(True, controller, roomName)

    def sendRoomSwitchMessage(self, watcher):
        l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None)
        self._roomManager.broadcast(watcher, l)
        self._roomManager.broadcastRoom(watcher, lambda w: w.sendSetReady(watcher.getName(), watcher.isReady(), False))
        if self.roomsDbFile:
            l = lambda w: w.sendList(toGUIOnly=True)
            self._roomManager.broadcast(watcher, l)

    def removeWatcher(self, watcher):
        if watcher and watcher.getRoom():
            self.sendLeftMessage(watcher)
            self._roomManager.removeWatcher(watcher)
            if self.roomsDbFile:
                l = lambda w: w.sendList(toGUIOnly=True)
                self._roomManager.broadcast(watcher, l)

    def sendLeftMessage(self, watcher):
        l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"left": True})
        self._roomManager.broadcast(watcher, l)

    def sendJoinMessage(self, watcher):
        l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True, "version": watcher.getVersion(), "features": watcher.getFeatures()}) if w != watcher else None
        self._roomManager.broadcast(watcher, l)
        self._roomManager.broadcastRoom(watcher, lambda w: w.sendSetReady(watcher.getName(), watcher.isReady(), False))
        if self.roomsDbFile:
            l = lambda w: w.sendList(toGUIOnly=True)
            self._roomManager.broadcast(watcher, l)

    def sendFileUpdate(self, watcher):
        if watcher.getFile():
            l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), watcher.getFile(), None)
            self._roomManager.broadcast(watcher, l)

    def forcePositionUpdate(self, watcher, doSeek, watcherPauseState):
        room = watcher.getRoom()
        if room.canControl(watcher):
            paused, position = room.isPaused(), watcher.getPosition()
            setBy = watcher
            l = lambda w: w.sendState(position, paused, doSeek, setBy, True)
            room.setPosition(watcher.getPosition(), setBy)
            self._roomManager.broadcastRoom(watcher, l)
        else:
            watcher.sendState(room.getPosition(), watcherPauseState, False, watcher, True)  # Fixes BC break with 1.2.x
            watcher.sendState(room.getPosition(), room.isPaused(), True, room.getSetBy(), True)

    def getAllWatchersForUser(self, forUser):
        return self._roomManager.getAllWatchersForUser(forUser)

    def getEmptyPersistentRooms(self):
        return self._roomManager.getEmptyPersistentRooms()

    def authRoomController(self, watcher, password, roomBaseName=None):
        room = watcher.getRoom()
        roomName = roomBaseName if roomBaseName else room.getName()
        try:
            success = RoomPasswordProvider.check(roomName, password, self._salt)
            if success:
                watcher.getRoom().addController(watcher)
            self._roomManager.broadcast(watcher, lambda w: w.sendControlledRoomAuthStatus(success, watcher.getName(), room._name))
        except NotControlledRoom:
            newName = RoomPasswordProvider.getControlledRoomName(roomName, password, self._salt)
            watcher.sendNewControlledRoom(newName, password)
        except ValueError:
            self._roomManager.broadcastRoom(watcher, lambda w: w.sendControlledRoomAuthStatus(False, watcher.getName(), room._name))

    def sendChat(self, watcher, message):
        message = truncateText(message, self.maxChatMessageLength)
        messageDict = {"message": message, "username": watcher.getName()}
        self._roomManager.broadcastRoom(watcher, lambda w: w.sendChatMessage(messageDict))

    def setReady(self, watcher, isReady, manuallyInitiated=True):
        watcher.setReady(isReady)
        self._roomManager.broadcastRoom(watcher, lambda w: w.sendSetReady(watcher.getName(), watcher.isReady(), manuallyInitiated))

    def setPlaylist(self, watcher, files):
        room = watcher.getRoom()
        if room.canControl(watcher) and playlistIsValid(files):
            watcher.getRoom().setPlaylist(files, watcher)
            self._roomManager.broadcastRoom(watcher, lambda w: w.setPlaylist(watcher.getName(), files))
        else:
            watcher.setPlaylist(room.getName(), room.getPlaylist())
            watcher.setPlaylistIndex(room.getName(), room.getPlaylistIndex())

    def setPlaylistIndex(self, watcher, index):
        room = watcher.getRoom()
        if room.canControl(watcher):
            watcher.getRoom().setPlaylistIndex(index, watcher)
            self._roomManager.broadcastRoom(watcher, lambda w: w.setPlaylistIndex(watcher.getName(), index))
        else:
            watcher.setPlaylistIndex(room.getName(), room.getPlaylistIndex())

    def _allowTLSconnections(self, path):
        try:
            privKey = open(path+'/privkey.pem', 'rt').read()
            certif = open(path+'/cert.pem', 'rt').read()
            chain = open(path+'/chain.pem', 'rt').read()

            self.lastEditCertTime = os.path.getmtime(path+'/cert.pem')

            privKeyPySSL = crypto.load_privatekey(crypto.FILETYPE_PEM, privKey)
            certifPySSL = crypto.load_certificate(crypto.FILETYPE_PEM, certif)
            chainPySSL = [crypto.load_certificate(crypto.FILETYPE_PEM, chain)]

            cipherListString = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:"\
                               "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:"\
                               "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"
            accCiphers = ssl.AcceptableCiphers.fromOpenSSLCipherString(cipherListString)

            try:
                contextFactory = ssl.CertificateOptions(privateKey=privKeyPySSL, certificate=certifPySSL,
                                                        extraCertChain=chainPySSL, acceptableCiphers=accCiphers,
                                                        raiseMinimumTo=ssl.TLSVersion.TLSv1_2)
            except AttributeError:
                contextFactory = ssl.CertificateOptions(privateKey=privKeyPySSL, certificate=certifPySSL,
                                                        extraCertChain=chainPySSL, acceptableCiphers=accCiphers,
                                                        method=TLSv1_2_METHOD)

            self.options = contextFactory
            self.serverAcceptsTLS = True
            self._TLSattempts = 0
            print("TLS support is enabled.")
        except Exception as e:
            self.options = None
            self.serverAcceptsTLS = False
            self.lastEditCertTime = None
            print("Error while loading the TLS certificates.")
            print(e)
            print("TLS support is not enabled.")

    def checkLastEditCertTime(self):
        try:
            outTime = os.path.getmtime(self.certPath+'/cert.pem')
        except:
            outTime = None
        return outTime

    def updateTLSContextFactory(self):
        self._allowTLSconnections(self.certPath)
        self._TLSattempts += 1
        if self._TLSattempts < constants.TLS_CERT_ROTATION_MAX_RETRIES:
            self.serverAcceptsTLS = True


class StatsRecorder(object):
    def __init__(self, dbHandle, roomManager):
        self._dbHandle = dbHandle
        self._roomManagerHandle = roomManager
        
    def startRecorder(self, delay):
        try:
            self._dbHandle.connect()
            reactor.callLater(delay, self._scheduleClientSnapshot)
        except:
            print("--- Error in initializing the stats database. Server Stats not enabled. ---")
    
    def _scheduleClientSnapshot(self):
        self._clientSnapshotTimer = task.LoopingCall(self._runClientSnapshot)
        self._clientSnapshotTimer.start(constants.SERVER_STATS_SNAPSHOT_INTERVAL)    
    
    def _runClientSnapshot(self):
        try:
            snapshotTime = int(time.time())
            rooms = self._roomManagerHandle.exportRooms()
            for room in rooms.values():
                for watcher in room.getWatchers():
                    self._dbHandle.addVersionLog(snapshotTime, watcher.getVersion())
        except:
            pass

class RoomsRecorder(StatsRecorder):
    def __init__(self, dbHandle, roomManager):
        self._dbHandle = dbHandle
        self._roomManagerHandle = roomManager

    def startRecorder(self, delay):
        try:
            self._dbHandle.connect()
            reactor.callLater(delay, self._scheduleClientSnapshot) # TODO: FIX THIS!
        except:
            print("--- Error in initializing the stats database. Server Stats not enabled. ---")

    def _scheduleClientSnapshot(self):
        self._clientSnapshotTimer = task.LoopingCall(self._runClientSnapshot)
        self._clientSnapshotTimer.start(constants.SERVER_STATS_SNAPSHOT_INTERVAL)

    def _runClientSnapshot(self):
        try:
            snapshotTime = int(time.time())
            rooms = self._roomManagerHandle.exportRooms()
            for room in rooms.values():
                for watcher in room.getWatchers():
                    self._dbHandle.addVersionLog(snapshotTime, watcher.getVersion())
        except:
            pass

class StatsDBManager(object):
    def __init__(self, dbpath):
        self._dbPath = dbpath
        self._connection = None

    def __del__(self):
        if self._connection is not None:
            self._connection.close()

    def connect(self):
        self._connection = adbapi.ConnectionPool("sqlite3", self._dbPath, check_same_thread=False)
        self._createSchema()

    def _createSchema(self):
        initQuery = 'create table if not exists clients_snapshots (snapshot_time INTEGER, version STRING)'
        return self._connection.runQuery(initQuery)

    def addVersionLog(self, timestamp, version):
        content = (timestamp, version, )
        self._connection.runQuery("INSERT INTO clients_snapshots VALUES (?, ?)", content)

class RoomDBManager(object):
    def __init__(self, dbpath, loadroomscallback):
        self._dbPath = dbpath
        self._connection = None
        self._loadRoomsCallback = loadroomscallback

    def __del__(self):
        if self._connection is not None:
            self._connection.close()

    def connect(self):
        self._connection = adbapi.ConnectionPool("sqlite3", self._dbPath, check_same_thread=False)
        self._createSchema().addCallback(self.loadRooms)

    def _createSchema(self):
        initQuery = 'create table if not exists persistent_rooms (name STRING PRIMARY KEY, playlist STRING, playlistIndex INTEGER, position REAL, lastSavedUpdate INTEGER)'
        return self._connection.runQuery(initQuery)

    def saveRoom(self, name, playlist, playlistIndex, position, lastUpdate):
        content = (name, playlist, playlistIndex, position, lastUpdate)
        self._connection.runQuery("INSERT OR REPLACE INTO persistent_rooms VALUES (?, ?, ?, ?, ?)", content)

    def deleteRoom(self, name):
        self._connection.runQuery("DELETE FROM persistent_rooms where name = ?", [name])

    def loadRooms(self, result=None):
        roomsQuery = "SELECT * FROM persistent_rooms"
        rooms = self._connection.runQuery(roomsQuery)
        rooms.addCallback(self.loadedRooms)

    def loadedRooms(self, rooms):
        self._loadRoomsCallback(rooms)

class RoomManager(object):
    def __init__(self, roomsdbfile=None, permanentRooms=[]):
        self._roomsDbFile = roomsdbfile
        self._rooms = {}
        self._permanentRooms = permanentRooms
        if self._roomsDbFile is not None:
            self._roomsDbHandle = RoomDBManager(self._roomsDbFile, self.loadRooms)
            self._roomsDbHandle.connect()
        else:
            self._roomsDbHandle = None

    def loadRooms(self, rooms):
        roomsLoaded = []
        for roomDetails in rooms:
            roomName = truncateText(roomDetails[0], constants.MAX_ROOM_NAME_LENGTH)
            room = Room(roomDetails[0], self._roomsDbHandle)
            room.loadRoom(roomDetails)
            if roomName in self._permanentRooms:
                room.setPermanent(True)
            self._rooms[roomName] = room
            roomsLoaded.append(roomName)
        for roomName in self._permanentRooms:
            if roomName not in roomsLoaded:
                roomDetails = (roomName, "", 0, 0, 0)
                room = Room(roomName, self._roomsDbHandle)
                room.loadRoom(roomDetails)
                room.setPermanent(True)
                self._rooms[roomName] = room

    def broadcastRoom(self, sender, whatLambda):
        room = sender.getRoom()
        if room and room.getName() in self._rooms:
            for receiver in room.getWatchers():
                whatLambda(receiver)

    def broadcast(self, sender, whatLambda):
        for room in self._rooms.values():
            for receiver in room.getWatchers():
                whatLambda(receiver)

    def getAllWatchersForUser(self, sender):
        watchers = []
        for room in self._rooms.values():
            for watcher in room.getWatchers():
                watchers.append(watcher)
        return watchers

    def getPersistentRooms(self, sender):
        persistentRooms = []
        for room in self._rooms.values():
            if room.isPersistent():
                persistentRooms.append(room.getName())
        return persistentRooms

    def getEmptyPersistentRooms(self):
        emptyPersistentRooms = []
        for room in self._rooms.values():
            if len(room.getWatchers()) == 0:
                emptyPersistentRooms.append(room.getName())
        return emptyPersistentRooms

    def moveWatcher(self, watcher, roomName):
        roomName = truncateText(roomName, constants.MAX_ROOM_NAME_LENGTH)
        self.removeWatcher(watcher)
        room = self._getRoom(roomName)
        room.addWatcher(watcher)

    def removeWatcher(self, watcher):
        oldRoom = watcher.getRoom()
        if oldRoom:
            oldRoom.removeWatcher(watcher)
            self._deleteRoomIfEmpty(oldRoom)

    def _getRoom(self, roomName):
        if roomName in self._rooms:
            return self._rooms[roomName]
        else:
            if RoomPasswordProvider.isControlledRoom(roomName):
                room = ControlledRoom(roomName, self._roomsDbHandle)
            else:
                if roomName in self._rooms:
                    self._deleteRoomIfEmpty(self._rooms[roomName])
                room = Room(roomName, self._roomsDbHandle)
            self._rooms[roomName] = room
            return room

    def _deleteRoomIfEmpty(self, room):
        if room.isEmpty() and room.getName():
            if self._roomsDbHandle and room.isPermanent():
                return
            if self._roomsDbHandle and room.isNotPermanent():
                if room.isPersistent() and not room.isPlaylistEmpty():
                    return
                self._roomsDbHandle.deleteRoom(room.getName())
            del self._rooms[room.getName()]

    def findFreeUsername(self, username, maxUsernameLength=constants.MAX_USERNAME_LENGTH):
        username = truncateText(username, maxUsernameLength)
        allnames = []
        for room in self._rooms.values():
            for watcher in room.getWatchers():
                allnames.append(watcher.getName().lower())
        while username.lower() in allnames:
            username += '_'
        return username
    
    def exportRooms(self):
        return self._rooms


class PublicRoomManager(RoomManager):
    def broadcast(self, sender, what):
        self.broadcastRoom(sender, what)

    def getAllWatchersForUser(self, sender):
        return sender.getRoom().getWatchers()

    def moveWatcher(self, watcher, room):
        oldRoom = watcher.getRoom()
        l = lambda w: w.sendSetting(watcher.getName(), oldRoom, None, {"left": True})
        self.broadcast(watcher, l)
        RoomManager.moveWatcher(self, watcher, room)
        watcher.setFile(watcher.getFile())


class Room(object):
    STATE_PAUSED = 0
    STATE_PLAYING = 1

    def __init__(self, name, roomsdbhandle):
        self._name = name
        self._roomsDbHandle = roomsdbhandle
        self._watchers = {}
        self._playState = self.STATE_PAUSED
        self._setBy = None
        self._playlist = []
        self._playlistIndex = None
        self._lastUpdate = time.time()
        self._lastSavedUpdate = 0
        self._position = 0
        self._permanent = False

    def __str__(self, *args, **kwargs):
        return self.getName()

    def roomsCanPersist(self):
        return self._roomsDbHandle is not None

    def isPersistent(self):
        return self.roomsCanPersist() and not self.isMarkedAsTemporary()

    def isMarkedAsTemporary(self):
        roomName = self.getName().lower()
        return roomName.endswith("-temp") or "-temp:" in roomName

    def isPlaylistEmpty(self):
        return len(self._playlist) == 0

    def isPermanent(self):
        return self._permanent

    def isNotPermanent(self):
        return not self.isPermanent()

    def sanitizeFilename(self, filename, blacklist="<>:/\\|?*\"", placeholder="_"):
        return ''.join([c if c not in blacklist and ord(c) >= 32 else placeholder for c in filename])

    def writeToDb(self):
        if not self.isPersistent():
            return
        processed_playlist = getListAsMultilineString(self._playlist)
        self._roomsDbHandle.saveRoom(self._name, processed_playlist, self._playlistIndex, self._position, self._lastSavedUpdate)

    def loadRoom(self, room):
        name, playlist, playlistindex, position, lastupdate = room
        self._name = name
        self._playlist = convertMultilineStringToList(playlist)
        self._playlistIndex = playlistindex
        self._position = position
        self._lastSavedUpdate = lastupdate

    def getName(self):
        return self._name

    def getPosition(self):
        age = time.time() - self._lastUpdate
        if self._watchers and age > 1:
            watcher = min(self._watchers.values())
            self._setBy = watcher
            self._position = watcher.getPosition()
            self._lastSavedUpdate = self._lastUpdate = time.time()
            return self._position
        elif self._position is not None:
            return self._position + (age if self._playState == self.STATE_PLAYING else 0)
        else:
            return 0

    def setPaused(self, paused=STATE_PAUSED, setBy=None):
        self._playState = paused
        self._setBy = setBy
        self.writeToDb()

    def setPosition(self, position, setBy=None):
        self._position = position
        for watcher in self._watchers.values():
            watcher.setPosition(position)
            self._setBy = setBy
        self.writeToDb()

    def setPermanent(self, newState):
        self._permanent = newState

    def isPlaying(self):
        return self._playState == self.STATE_PLAYING

    def isPaused(self):
        return self._playState == self.STATE_PAUSED

    def getWatchers(self):
        return list(self._watchers.values())

    def addWatcher(self, watcher):
        if self._watchers or self.isPersistent():
            watcher.setPosition(self.getPosition())
        self._watchers[watcher.getName()] = watcher
        watcher.setRoom(self)

    def removeWatcher(self, watcher):
        if watcher.getName() not in self._watchers:
            return
        del self._watchers[watcher.getName()]
        watcher.setRoom(None)
        if not self._watchers and not self.isPersistent():
            self._position = 0
        self.writeToDb()

    def isEmpty(self):
        return not bool(self._watchers)

    def getSetBy(self):
        return self._setBy

    def canControl(self, watcher):
        return True

    def setPlaylist(self, files, setBy=None):
        self._playlist = files
        self.writeToDb()

    def setPlaylistIndex(self, index, setBy=None):
        self._playlistIndex = index
        self.writeToDb()

    def getPlaylist(self):
        return self._playlist

    def getPlaylistIndex(self):
        return self._playlistIndex

    def getControllers(self):
        return []

class ControlledRoom(Room):
    def __init__(self, name, roomsdbhandle):
        Room.__init__(self, name, roomsdbhandle)
        self._controllers = {}

    def getPosition(self):
        age = time.time() - self._lastUpdate
        if self._controllers and age > 1:
            watcher = min(self._controllers.values())
            self._setBy = watcher
            self._position = watcher.getPosition()
            self._lastUpdate = time.time()
            return self._position
        elif self._position is not None:
            return self._position + (age if self._playState == self.STATE_PLAYING else 0)
        else:
            return 0

    def addController(self, watcher):
        self._controllers[watcher.getName()] = watcher

    def removeWatcher(self, watcher):
        Room.removeWatcher(self, watcher)
        if watcher.getName() in self._controllers:
            del self._controllers[watcher.getName()]
        self.writeToDb()

    def setPaused(self, paused=Room.STATE_PAUSED, setBy=None):
        if self.canControl(setBy):
            Room.setPaused(self, paused, setBy)

    def setPosition(self, position, setBy=None):
        if self.canControl(setBy):
            Room.setPosition(self, position, setBy)

    def setPlaylist(self, files, setBy=None):
        if self.canControl(setBy) and playlistIsValid(files):
            self._playlist = files

    def setPlaylistIndex(self, index, setBy=None):
        if self.canControl(setBy):
            self._playlistIndex = index

    def canControl(self, watcher):
        return watcher.getName() in self._controllers

    def getControllers(self):
        return {}


class Watcher(object):
    def __init__(self, server, connector, name):
        self._ready = None
        self._server = server
        self._connector = connector
        self._name = name
        self._room = None
        self._file = None
        self._position = None
        self._lastUpdatedOn = time.time()
        self._sendStateTimer = None
        self._connector.setWatcher(self)
        reactor.callLater(0.1, self._scheduleSendState)

    def setFile(self, file_):
        if file_ and "name" in file_:
            file_["name"] = truncateText(file_["name"], constants.MAX_FILENAME_LENGTH)
        self._file = file_
        self._server.sendFileUpdate(self)

    def setRoom(self, room):
        self._room = room
        if room is None:
            self._deactivateStateTimer()
        else:
            self._resetStateTimer()
            self._askForStateUpdate(True, True)

    def setReady(self, ready):
        self._ready = ready

    def getFeatures(self):
        features = self._connector.getFeatures()
        return features

    def isReady(self):
        if self._server.disableReady:
            return None
        return self._ready

    def getRoom(self):
        return self._room

    def getName(self):
        return self._name

    def getVersion(self):
        return self._connector.getVersion()

    def getFile(self):
        return self._file

    def setPosition(self, position):
        self._position = position

    def getPosition(self):
        if self._position is None:
            return None
        if self._room.isPlaying():
            timePassedSinceSet = time.time() - self._lastUpdatedOn
        else:
            timePassedSinceSet = 0
        return self._position + timePassedSinceSet

    def sendSetting(self, user, room, file_, event):
        self._connector.sendUserSetting(user, room, file_, event)

    def sendNewControlledRoom(self, roomBaseName, password):
        self._connector.sendNewControlledRoom(roomBaseName, password)

    def sendControlledRoomAuthStatus(self, success, username, room):
        self._connector.sendControlledRoomAuthStatus(success, username, room)

    def sendChatMessage(self, message):
        if self._connector.meetsMinVersion(constants.CHAT_MIN_VERSION):
            self._connector.sendMessage({"Chat": message})

    def sendList(self, toGUIOnly=False):
        if toGUIOnly and self.isGUIUser(self._connector.getFeatures()):
            clientFeatures = self._connector.getFeatures()
            if "uiMode" in clientFeatures:
                if clientFeatures["uiMode"] == constants.CONSOLE_UI_MODE:
                    return
            else:
                return
        self._connector.sendList()

    def isGUIUser(self, clientFeatures):
        clientFeatures = self._connector.getFeatures()
        uiMode = clientFeatures["uiMode"] if "uiMode" in clientFeatures else constants.UNKNOWN_UI_MODE
        if uiMode == constants.UNKNOWN_UI_MODE:
            uiMode = constants.FALLBACK_ASSUMED_UI_MODE
        return uiMode == constants.GRAPHICAL_UI_MODE

    def sendSetReady(self, username, isReady, manuallyInitiated=True):
        self._connector.sendSetReady(username, isReady, manuallyInitiated)

    def setPlaylistIndex(self, username, index):
        self._connector.setPlaylistIndex(username, index)

    def setPlaylist(self, username, files):
        self._connector.setPlaylist(username, files)

    def __lt__(self, b):
        if self.getPosition() is None or self._file is None:
            return False
        if b.getPosition() is None or b.getFile() is None:
            return True
        return self.getPosition() < b.getPosition()

    def _scheduleSendState(self):
        self._sendStateTimer = task.LoopingCall(self._askForStateUpdate)
        self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL)

    def _askForStateUpdate(self, doSeek=False, forcedUpdate=False):
        self._server.sendState(self, doSeek, forcedUpdate)

    def _resetStateTimer(self):
        if self._sendStateTimer:
            if self._sendStateTimer.running:
                self._sendStateTimer.stop()
            self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL)

    def _deactivateStateTimer(self):
        if self._sendStateTimer and self._sendStateTimer.running:
            self._sendStateTimer.stop()

    def sendState(self, position, paused, doSeek, setBy, forcedUpdate):
        if self._connector.isLogged():
            self._connector.sendState(position, paused, doSeek, setBy, forcedUpdate)
        if time.time() - self._lastUpdatedOn > constants.PROTOCOL_TIMEOUT:
            self._server.removeWatcher(self)
            self._connector.drop()

    def __hasPauseChanged(self, paused):
        if paused is None:
            return False
        return self._room.isPaused() and not paused or not self._room.isPaused() and paused

    def _updatePositionByAge(self, messageAge, paused, position):
        if not paused:
            position += messageAge
        return position

    def updateState(self, position, paused, doSeek, messageAge):
        pauseChanged = self.__hasPauseChanged(paused)
        self._lastUpdatedOn = time.time()
        if pauseChanged:
            self.getRoom().setPaused(Room.STATE_PAUSED if paused else Room.STATE_PLAYING, self)
        if position is not None:
            position = self._updatePositionByAge(messageAge, paused, position)
            self.setPosition(position)
        if doSeek or pauseChanged:
            self._server.forcePositionUpdate(self, doSeek, paused)

    def isController(self):
        return RoomPasswordProvider.isControlledRoom(self._room.getName()) \
            and self._room.canControl(self)


class ConfigurationGetter(object):
    def getConfiguration(self):
        self._prepareArgParser()
        args = self._argparser.parse_args()
        if args.port is None:
            args.port = constants.DEFAULT_PORT
        return args

    def _prepareArgParser(self):
        self._argparser = argparse.ArgumentParser(
            description=getMessage("server-argument-description"),
            epilog=getMessage("server-argument-epilog"))
        self._argparser.add_argument('--port', metavar='port', type=str, nargs='?', help=getMessage("server-port-argument"))
        self._argparser.add_argument('--password', metavar='password', type=str, nargs='?', help=getMessage("server-password-argument"), default=os.environ.get('SYNCPLAY_PASSWORD'))
        self._argparser.add_argument('--isolate-rooms', action='store_true', help=getMessage("server-isolate-room-argument"))
        self._argparser.add_argument('--disable-ready', action='store_true', help=getMessage("server-disable-ready-argument"))
        self._argparser.add_argument('--disable-chat', action='store_true', help=getMessage("server-chat-argument"))
        self._argparser.add_argument('--salt', metavar='salt', type=str, nargs='?', help=getMessage("server-salt-argument"), default=os.environ.get('SYNCPLAY_SALT'))
        self._argparser.add_argument('--motd-file', metavar='file', type=str, nargs='?', help=getMessage("server-motd-argument"))
        self._argparser.add_argument('--rooms-db-file', metavar='rooms', type=str, nargs='?', help=getMessage("server-rooms-argument"))
        self._argparser.add_argument('--permanent-rooms-file', metavar='permanentrooms', type=str, nargs='?', help=getMessage("server-permanent-rooms-argument"))
        self._argparser.add_argument('--max-chat-message-length', metavar='maxChatMessageLength', type=int, nargs='?', help=getMessage("server-chat-maxchars-argument").format(constants.MAX_CHAT_MESSAGE_LENGTH))
        self._argparser.add_argument('--max-username-length', metavar='maxUsernameLength', type=int, nargs='?', help=getMessage("server-maxusernamelength-argument").format(constants.MAX_USERNAME_LENGTH))
        self._argparser.add_argument('--stats-db-file', metavar='file', type=str, nargs='?', help=getMessage("server-stats-db-file-argument"))
        self._argparser.add_argument('--tls', metavar='path', type=str, nargs='?', help=getMessage("server-startTLS-argument"))
