import json
import os
import sys
import threading
import time
import appdirs
import confuse
from configparser import ConfigParser
from pathlib import Path
from queue import Queue
from trakt_scrobbler import logger
from trakt_scrobbler.player_monitors.monitor import Monitor

if os.name == 'posix':
    import select
    import socket
elif os.name == 'nt':
    import win32file


class MPVMon(Monitor):
    name = 'mpv'
    exclude_import = True
    WATCHED_PROPS = ['pause', 'path', 'working-directory',
                     'duration', 'time-pos']
    CONFIG_TEMPLATE = {
        "ipc_path": confuse.String(default="auto-detect"),
        "poll_interval": confuse.Number(default=10),
    }

    def __init__(self, scrobble_queue):
        try:
            self.ipc_path = self.config['ipc_path']
        except KeyError:
            logger.exception('Check config for correct MPV params.')
            return
        super().__init__(scrobble_queue)
        self.buffer = ''
        self.lock = threading.Lock()
        self.poll_timer = None
        self.write_queue = Queue()
        self.sent_commands = {}
        self.command_counter = 1
        self.vars = {}

    @classmethod
    def read_player_cfg(cls, auto_keys=None):
        if sys.platform == "darwin":
            conf_path = Path.home() / ".config" / "mpv" / "mpv.conf"
        else:
            conf_path = (
                Path(appdirs.user_config_dir("mpv", roaming=True, appauthor=False))
                / "mpv.conf"
            )
        mpv_conf = ConfigParser(
            allow_no_value=True, strict=False, inline_comment_prefixes="#"
        )
        mpv_conf.optionxform = lambda option: option
        mpv_conf.read_string("[root]\n" + conf_path.read_text())
        return {
            "ipc_path": lambda: mpv_conf.get("root", "input-ipc-server")
        }

    def run(self):
        while True:
            if self.can_connect():
                self.update_vars()
                self.conn_loop()
                if self.poll_timer:
                    self.poll_timer.cancel()
                time.sleep(1)
            else:
                logger.info('Unable to connect to MPV. Check ipc path.')
                time.sleep(10)

    def update_status(self):
        fpath = Path(self.vars['working-directory']) / Path(self.vars['path'])

        # Update last known position if player is stopped
        pos = self.vars['time-pos']
        if self.vars['state'] == 0 and self.status['state'] == 2:
            pos += round(time.time() - self.status['time'], 3)
        self.status = {
            'state': self.vars['state'],
            'filepath': str(fpath),
            'position': pos,
            'duration': self.vars['duration'],
            'time': time.time()
        }
        self.handle_status_update()

    def update_vars(self):
        """Query mpv for required properties."""
        self.updated_props_count = 0
        for prop in self.WATCHED_PROPS:
            self.send_command(['get_property', prop])
        if self.poll_timer:
            self.poll_timer.cancel()
        self.poll_timer = threading.Timer(10, self.update_vars)
        self.poll_timer.name = 'mpvpoll'
        self.poll_timer.start()

    def handle_event(self, event):
        if event == 'end-file':
            self.vars['state'] = 0
            self.is_running = False
            self.update_status()
        elif event == 'pause':
            self.vars['state'] = 1
            self.update_vars()
        elif event == 'unpause' or event == 'playback-restart':
            self.vars['state'] = 2
            self.update_vars()

    def handle_cmd_response(self, resp):
        command = self.sent_commands[resp['request_id']]['command']
        del self.sent_commands[resp['request_id']]
        if resp['error'] != 'success':
            logger.error(f'Error with command {command!s}. Response: {resp!s}')
            return
        elif command[0] != 'get_property':
            return
        param = command[1]
        data = resp['data']
        if param == 'pause':
            self.vars['state'] = 1 if data else 2
        if param in self.WATCHED_PROPS:
            self.vars[param] = data
            self.updated_props_count += 1
        if self.updated_props_count == len(self.WATCHED_PROPS):
            self.update_status()

    def on_data(self, data):
        self.buffer = self.buffer + data.decode('utf-8')
        while True:
            line_end = self.buffer.find('\n')
            if line_end == -1:
                # partial line received
                # self.on_line() is called in next data batch
                break
            else:
                self.on_line(self.buffer[:line_end])  # doesn't include \n
                self.buffer = self.buffer[line_end + 1:]  # doesn't include \n

    def on_line(self, line):
        try:
            mpv_json = json.loads(line)
        except json.JSONDecodeError:
            logger.warning('Invalid JSON received. Skipping. ' + line, exc_info=True)
            return
        if 'event' in mpv_json:
            self.handle_event(mpv_json['event'])
        elif 'request_id' in mpv_json:
            self.handle_cmd_response(mpv_json)

    def send_command(self, elements):
        with self.lock:
            command = {'command': elements, 'request_id': self.command_counter}
            self.sent_commands[self.command_counter] = command
            self.command_counter += 1
            self.write_queue.put(str.encode(json.dumps(command) + '\n'))


class MPVPosixMon(MPVMon):
    exclude_import = os.name != 'posix'

    def __init__(self, scrobble_queue):
        super().__init__(scrobble_queue)
        self.sock = socket.socket(socket.AF_UNIX)

    def can_connect(self):
        sock = socket.socket(socket.AF_UNIX)
        errno = sock.connect_ex(self.ipc_path)
        sock.close()
        return errno == 0

    def conn_loop(self):
        self.sock = socket.socket(socket.AF_UNIX)
        self.sock.connect(self.ipc_path)
        self.is_running = True
        while self.is_running:
            r, _, e = select.select([self.sock], [], [], 0.1)
            if r == [self.sock]:
                # socket has data to read
                data = self.sock.recv(4096)
                if len(data) == 0:
                    # EOF reached
                    self.is_running = False
                    break
                self.on_data(data)
            while not self.write_queue.empty():
                # block until self.sock can be written to
                select.select([], [self.sock], [])
                try:
                    self.sock.sendall(self.write_queue.get_nowait())
                except BrokenPipeError:
                    self.is_running = False
        self.sock.close()
        logger.debug('Sock closed')


class MPVWinMon(MPVMon):
    exclude_import = os.name != 'nt'

    def __init__(self, scrobble_queue):
        super().__init__(scrobble_queue)
        self.file_handle = None

    def can_connect(self):
        return win32file.GetFileAttributes((self.ipc_path)) == \
            win32file.FILE_ATTRIBUTE_NORMAL

    def conn_loop(self):
        self.is_running = True
        self.update_vars()
        self.file_handle = win32file.CreateFile(
            self.ipc_path,
            win32file.GENERIC_READ | win32file.GENERIC_WRITE,
            0, None,
            win32file.OPEN_EXISTING,
            0, None
        )
        while self.is_running:
            try:
                while not self.write_queue.empty():
                    win32file.WriteFile(
                        self.file_handle, self.write_queue.get_nowait())
            except win32file.error:
                logger.debug('Exception while writing to pipe.', exc_info=True)
                self.is_running = False
                break
            size = win32file.GetFileSize(self.file_handle)
            if size > 0:
                while size > 0:
                    # pipe has data to read
                    _, data = win32file.ReadFile(self.file_handle, 4096)
                    self.on_data(data)
                    size = win32file.GetFileSize(self.file_handle)
            else:
                time.sleep(1)
        win32file.CloseHandle(self.file_handle)
        logger.debug('Pipe closed.')