import argparse
import codecs
import hashlib
import os
import time
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"))
        self._argparser.add_argument('--ipv4-only', action='store_true', help=getMessage("server-listen-only-on-ipv4"))
        self._argparser.add_argument('--ipv6-only', action='store_true', help=getMessage("server-listen-only-on-ipv6"))
        self._argparser.add_argument('--interface-ipv4', metavar='interfaceIPv4', type=str, nargs='?', help=getMessage("server-interface-ipv4"), default='')
        self._argparser.add_argument('--interface-ipv6', metavar='interfaceIPv6', type=str, nargs='?', help=getMessage("server-interface-ipv6"), default='')
