# coding:utf8
import ast
import os
import re
import subprocess
import sys
import threading
import time


from syncplay import constants, utils
from syncplay.players.basePlayer import BasePlayer
from syncplay.messages import getMessage
from syncplay.utils import isMacOS, isWindows


class MplayerPlayer(BasePlayer):
    speedSupported = True
    customOpenDialog = False
    alertOSDSupported = False
    chatOSDSupported = False
    osdMessageSeparator = "; "

    RE_ANSWER = re.compile(constants.MPLAYER_ANSWER_REGEX)
    POSITION_QUERY = 'time_pos'
    OSD_QUERY = 'osd_show_text'

    def __init__(self, client, playerPath, filePath, args):
        from twisted.internet import reactor
        self.reactor = reactor
        self._client = client
        self._paused = None
        self._position = 0.0
        self._duration = None
        self._filename = None
        self._filepath = None
        self.quitReason = None
        self.lastLoadedTime = None
        self.fileLoaded = False
        self.delayedFilePath = None
        try:
            self._listener = self.__Listener(self, playerPath, filePath, args)
        except ValueError:
            self._client.ui.showMessage(getMessage("mplayer-file-required-notification"))
            self._client.ui.showMessage(getMessage("mplayer-file-required-notification/example"))
            self.drop()
            return
        self._listener.setDaemon(True)
        self._listener.start()

        self._durationAsk = threading.Event()
        self._filenameAsk = threading.Event()
        self._pathAsk = threading.Event()

        self._positionAsk = threading.Event()
        self._pausedAsk = threading.Event()

        self._preparePlayer()

    def _fileUpdateClearEvents(self):
        self._durationAsk.clear()
        self._filenameAsk.clear()
        self._pathAsk.clear()

    def _fileUpdateWaitEvents(self):
        self._durationAsk.wait()
        self._filenameAsk.wait()
        self._pathAsk.wait()

    def _onFileUpdate(self):
        self._fileUpdateClearEvents()
        self._getFilename()
        self._getLength()
        self._getFilepath()
        self._fileUpdateWaitEvents()
        self._client.updateFile(self._filename, self._duration, self._filepath)

    def _preparePlayer(self):
        self.setPaused(True)
        self.reactor.callLater(0, self._client.initPlayer, self)
        self._onFileUpdate()

    def askForStatus(self):
        self._positionAsk.clear()
        self._pausedAsk.clear()
        self._getPaused()
        self._getPosition()
        self._positionAsk.wait()
        self._pausedAsk.wait()
        self._client.updatePlayerStatus(self._paused, self._position)

    def _setProperty(self, property_, value):
        self._listener.sendLine("set_property {} {}".format(property_, value))

    def _getProperty(self, property_):
        self._listener.sendLine("get_property {}".format(property_))

    def displayMessage(
        self, message,
        duration=(constants.OSD_DURATION * 1000), OSDType=constants.OSD_NOTIFICATION, mood=constants.MESSAGE_NEUTRAL
    ):
        messageString = self._sanitizeText(message.replace("\\n", "<NEWLINE>")).replace("<NEWLINE>", "\\n")
        self._listener.sendLine('{} "{!s}" {} {}'.format(
            self.OSD_QUERY, messageString, duration, constants.MPLAYER_OSD_LEVEL))

    def displayChatMessage(self, username, message):
        messageString = "<{}> {}".format(username, message)
        messageString = self._sanitizeText(messageString.replace("\\n", "<NEWLINE>")).replace("<NEWLINE>", "\\n")
        duration = int(constants.OSD_DURATION * 1000)
        self._listener.sendLine('{} "{!s}" {} {}'.format(
            self.OSD_QUERY, messageString, duration, constants.MPLAYER_OSD_LEVEL))

    def setSpeed(self, value):
        self._setProperty('speed', "{:.2f}".format(value))

    def _loadFile(self, filePath):
        self._listener.sendLine('loadfile {}'.format(self._quoteArg(filePath)))

    def openFile(self, filePath, resetPosition=False):
        self._filepath = filePath
        self._loadFile(filePath)
        self._onFileUpdate()
        if self._paused != self._client.getGlobalPaused():
            self.setPaused(self._client.getGlobalPaused())
        self.setPosition(self._client.getGlobalPosition())

    def setFeatures(self, featureList):
        pass

    def setPosition(self, value):
        self._position = max(value, 0)
        self._setProperty(self.POSITION_QUERY, "{}".format(value))
        time.sleep(0.03)

    def setPaused(self, value):
        if self._paused != value:
            self._paused = not self._paused
            self._listener.sendLine('pause')

    def _getFilename(self):
        self._getProperty('filename')

    def _getLength(self):
        self._getProperty('length')

    def _getFilepath(self):
        self._getProperty('path')

    def _getPaused(self):
        self._getProperty('pause')

    def _getPosition(self):
        self._getProperty(self.POSITION_QUERY)

    def _sanitizeText(self, text):
        text = text.replace("\r", "")
        text = text.replace("\n", "")
        text = text.replace("\\\"", "<SYNCPLAY_QUOTE>")
        text = text.replace("\"", "<SYNCPLAY_QUOTE>")
        text = text.replace("%", "%%")
        text = text.replace("\\", "\\\\")
        text = text.replace("{", "\\\\{")
        text = text.replace("}", "\\\\}")
        text = text.replace("<SYNCPLAY_QUOTE>", "\\\"")
        return text

    def _quoteArg(self, arg):
        arg = arg.replace('\\', '\\\\')
        arg = arg.replace("'", "\\'")
        arg = arg.replace('"', '\\"')
        arg = arg.replace("\r", "")
        arg = arg.replace("\n", "")
        return '"{}"'.format(arg)

    def _fileIsLoaded(self):
        return True

    def _handleUnknownLine(self, line):
        pass

    def _storePosition(self, value):
        self._position = max(value, 0)

    def _storePauseState(self, value):
        self._paused = value

    def lineReceived(self, line):
        if line:
            self._client.ui.showDebugMessage("player << {}".format(line))
            line = line.replace("[cplayer] ", "")  # -v workaround
            line = line.replace("[term-msg] ", "")  # -v workaround
            line = line.replace("   cplayer: ", "")  # --msg-module workaround
            line = line.replace("  term-msg: ", "")
        if (
            "Failed to get value of property" in line or
            "=(unavailable)" in line or
            line == "ANS_filename=" or
            line == "ANS_length=" or
            line == "ANS_path="
        ):
            if "filename" in line:
                self._getFilename()
            elif "length" in line:
                self._getLength()
            elif "path" in line:
                self._getFilepath()
            return
        match = self.RE_ANSWER.match(line)
        if not match:
            self._handleUnknownLine(line)
            return

        name, value = [m for m in match.groups() if m]
        name = name.lower()

        if name == self.POSITION_QUERY:
            self._storePosition(float(value))
            self._positionAsk.set()
        elif name == "pause":
            self._storePauseState(bool(value == 'yes'))
            self._pausedAsk.set()
        elif name == "length":
            try:
                self._duration = float(value)
            except:
                self._duration = 0
            self._durationAsk.set()
        elif name == "path":
            self._filepath = value
            self._pathAsk.set()
        elif name == "filename":
            self._filename = value
            self._filenameAsk.set()
        elif name == "exiting":
            if value != 'Quit':
                if self.quitReason is None:
                    self.quitReason = getMessage("media-player-error").format(value)
                self.reactor.callFromThread(self._client.ui.showErrorMessage, self.quitReason, True)
            self.drop()

    @staticmethod
    def run(client, playerPath, filePath, args):
        mplayer = MplayerPlayer(client, MplayerPlayer.getExpandedPath(playerPath), filePath, args)
        return mplayer

    @staticmethod
    def getDefaultPlayerPathsList():
        l = []
        for path in constants.MPLAYER_PATHS:
            p = MplayerPlayer.getExpandedPath(path)
            if p:
                l.append(p)
        return l

    @staticmethod
    def getIconPath(path):
        return constants.MPLAYER_ICONPATH

    @staticmethod
    def getStartupArgs(path, userArgs):
        args = []
        if userArgs:
            args.extend(userArgs)
        args.extend(constants.MPLAYER_SLAVE_ARGS)
        return args

    @staticmethod
    def isValidPlayerPath(path):
        if "mplayer" in path and MplayerPlayer.getExpandedPath(path) and "mplayerc.exe" not in path and "smplayer.exe" not in path:  # "mplayerc.exe" is Media Player Classic (not Home Cinema) and smplayer.exe is SMPlayer:
            return True
        return False

    @staticmethod
    def getPlayerPathErrors(playerPath, filePath):
        if not filePath:
            return getMessage("no-file-path-config-error")

    @staticmethod
    def getExpandedPath(playerPath):
        if not os.path.isfile(playerPath):
            if os.path.isfile(playerPath + "mplayer.exe"):
                playerPath += "mplayer.exe"
                return playerPath
            elif os.path.isfile(playerPath + "\\mplayer.exe"):
                playerPath += "\\mplayer.exe"
                return playerPath
        if os.access(playerPath, os.X_OK):
            return playerPath
        for path in os.environ['PATH'].split(':'):
            path = os.path.join(os.path.realpath(path), playerPath)
            if os.access(path, os.X_OK):
                return path

    def notMplayer2(self):
        self.reactor.callFromThread(self._client.ui.showErrorMessage, getMessage("mplayer2-required"), True)
        self.drop()

    def _takeLocksDown(self):
        self._durationAsk.set()
        self._filenameAsk.set()
        self._pathAsk.set()
        self._positionAsk.set()
        self._pausedAsk.set()

    def drop(self):
        self._listener.sendLine('quit')
        self._takeLocksDown()
        self.reactor.callFromThread(self._client.stop, False)

    class __Listener(threading.Thread):
        def __init__(self, playerController, playerPath, filePath, args):
            self.sendQueue = []
            self.readyToSend = True
            self.lastSendTime = None
            self.lastNotReadyTime = None
            self.__playerController = playerController
            if not self.__playerController._client._config["chatOutputEnabled"]:
                self.__playerController.alertOSDSupported = False
                self.__playerController.chatOSDSupported = False
            if self.__playerController.getPlayerPathErrors(playerPath, filePath):
                raise ValueError()
            if filePath and '://' not in filePath:
                if not os.path.isfile(filePath) and 'PWD' in os.environ:
                    filePath = os.environ['PWD'] + os.path.sep + filePath
                filePath = os.path.realpath(filePath)

            call = [playerPath]
            if filePath:
                if isWindows() and not utils.isASCII(filePath):
                    self.__playerController.delayedFilePath = filePath
                    filePath = None
                else:
                    call.extend([filePath])
            call.extend(playerController.getStartupArgs(playerPath, args))
            # At least mpv may output escape sequences which result in syncplay
            # trying to parse something like
            # "\x1b[?1l\x1b>ANS_filename=blah.mkv". Work around this by
            # unsetting TERM.
            env = os.environ.copy()
            if 'TERM' in env:
                del env['TERM']
            # On macOS, youtube-dl requires system python to run. Set the environment
            # to allow that version of python to be executed in the mpv subprocess.
            if isMacOS():
                try:
                    pythonLibs = subprocess.check_output(['/usr/bin/python', '-E', '-c',
                                                          'import sys; print(sys.path)'],
                                                          text=True, env=dict())
                    pythonLibs = ast.literal_eval(pythonLibs)
                    pythonPath = ':'.join(pythonLibs[1:])
                except:
                    pythonPath = None
                if pythonPath is not None:
                    env['PATH'] = '/usr/bin:/usr/local/bin'
                    env['PYTHONPATH'] = pythonPath
            if filePath:
                self.__process = subprocess.Popen(
                    call, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT,
                    cwd=self.__getCwd(filePath, env), env=env, bufsize=0)
            else:
                self.__process = subprocess.Popen(
                    call, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT,
                    env=env, bufsize=0)
            threading.Thread.__init__(self, name="MPlayer Listener")

        def __getCwd(self, filePath, env):
            if not filePath:
                return None
            if os.path.isfile(filePath):
                cwd = os.path.dirname(filePath)
            elif 'HOME' in env:
                cwd = env['HOME']
            elif 'APPDATA' in env:
                cwd = env['APPDATA']
            else:
                cwd = None
            return cwd

        def run(self):
            line = self.__process.stdout.readline()
            line = line.decode('utf-8')
            if "MPlayer 1" in line:
                self.__playerController.notMplayer2()
            else:
                line = line.rstrip("\r\n")
                self.__playerController.lineReceived(line)
            while self.__process.poll() is None:
                line = self.__process.stdout.readline()
                line = line.decode('utf-8')
                line = line.rstrip("\r\n")
                self.__playerController.lineReceived(line)
            self.__playerController.drop()

        def sendChat(self, message):
            if message:
                if message[:1] == "/" and message != "/":
                    command = message[1:]
                    if command and command[:1] == "/":
                        message = message[1:]
                    else:
                        self.__playerController.reactor.callFromThread(
                            self.__playerController._client.ui.executeCommand, command)
                        return
                self.__playerController.reactor.callFromThread(self.__playerController._client.sendChat, message)

        def isReadyForSend(self):
            self.checkForReadinessOverride()
            return self.readyToSend

        def setReadyToSend(self, newReadyState):
            oldState = self.readyToSend
            self.readyToSend = newReadyState
            self.lastNotReadyTime = time.time() if newReadyState == False else None
            if self.readyToSend == True:
                self.__playerController._client.ui.showDebugMessage("<mpv> Ready to send: True")
            else:
                self.__playerController._client.ui.showDebugMessage("<mpv> Ready to send: False")
            if self.readyToSend == True and oldState == False:
                self.processSendQueue()

        def checkForReadinessOverride(self):
            if self.lastNotReadyTime and time.time() - self.lastNotReadyTime > constants.MPV_MAX_NEWFILE_COOLDOWN_TIME:
                self.setReadyToSend(True)

        def sendLine(self, line, notReadyAfterThis=None):
            self.checkForReadinessOverride()
            if self.readyToSend == False and "print_text ANS_pause" in line:
                self.__playerController._client.ui.showDebugMessage("<mpv> Not ready to get status update, so skipping")
                return
            try:
                if self.sendQueue:
                    if constants.MPV_SUPERSEDE_IF_DUPLICATE_COMMANDS:
                        for command in constants.MPV_SUPERSEDE_IF_DUPLICATE_COMMANDS:
                            if line.startswith(command):
                                for itemID, deletionCandidate in enumerate(self.sendQueue):
                                    if deletionCandidate.startswith(command):
                                        self.__playerController._client.ui.showDebugMessage(
                                            "<mpv> Remove duplicate (supersede): {}".format(self.sendQueue[itemID]))
                                        try:
                                            self.sendQueue.remove(self.sendQueue[itemID])
                                        except UnicodeWarning:
                                            self.__playerController._client.ui.showDebugMessage(
                                                "<mpv> Unicode mismatch occured when trying to remove duplicate")
                                            # TODO: Prevent this from being triggered
                                            pass
                                        break
                            break
                    if constants.MPV_REMOVE_BOTH_IF_DUPLICATE_COMMANDS:
                        for command in constants.MPV_REMOVE_BOTH_IF_DUPLICATE_COMMANDS:
                            if line == command:
                                for itemID, deletionCandidate in enumerate(self.sendQueue):
                                    if deletionCandidate == command:
                                        self.__playerController._client.ui.showDebugMessage(
                                            "<mpv> Remove duplicate (delete both): {}".format(self.sendQueue[itemID]))
                                        self.__playerController._client.ui.showDebugMessage(self.sendQueue[itemID])
                                        return
            except:
                self.__playerController._client.ui.showDebugMessage("<mpv> Problem removing duplicates, etc")
            self.sendQueue.append(line)
            self.processSendQueue()
            if notReadyAfterThis:
                self.setReadyToSend(False)

        def processSendQueue(self):
            while self.sendQueue and self.readyToSend:
                if self.lastSendTime and time.time() - self.lastSendTime < constants.MPV_SENDMESSAGE_COOLDOWN_TIME:
                    self.__playerController._client.ui.showDebugMessage(
                        "<mpv> Throttling message send, so sleeping for {}".format(
                            constants.MPV_SENDMESSAGE_COOLDOWN_TIME))
                    time.sleep(constants.MPV_SENDMESSAGE_COOLDOWN_TIME)
                try:
                    lineToSend = self.sendQueue.pop()
                    if lineToSend:
                        self.lastSendTime = time.time()
                        self.actuallySendLine(lineToSend)
                except IndexError:
                    pass

        def actuallySendLine(self, line):
            try:
                # if not isinstance(line, str):
                    # line = line.decode('utf8')
                line = line + "\n"
                self.__playerController._client.ui.showDebugMessage("player >> {}".format(line))
                line = line.encode('utf-8')
                self.__process.stdin.write(line)
            except IOError:
                pass
