# inclided from https://github.com/suclearnub/python-discord-rpc

# References:
# * https://github.com/devsnek/discord-rpc/tree/master/src/transports/IPC.js
# * https://github.com/devsnek/discord-rpc/tree/master/example/main.js
# * https://github.com/discordapp/discord-rpc/tree/master/documentation/hard-mode.md
# * https://github.com/discordapp/discord-rpc/tree/master/src
# * https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields

from abc import ABCMeta, abstractmethod
import json
import logging
import os
import socket
import sys
import struct
import uuid


OP_HANDSHAKE = 0
OP_FRAME = 1
OP_CLOSE = 2
OP_PING = 3
OP_PONG = 4

logger = logging.getLogger(__name__)


class DiscordIpcError(Exception):
	pass


class DiscordIpcClient(metaclass=ABCMeta):

	"""Work with an open Discord instance via its JSON IPC for its rich presence API.

	In a blocking way.
	Classmethod `for_platform`
	will resolve to one of WinDiscordIpcClient or UnixDiscordIpcClient,
	depending on the current platform.
	Supports context handler protocol.
	"""

	def __init__(self, client_id):
		self.client_id = client_id
		self._connect()
		self._do_handshake()
		logger.info("connected via ID %s", client_id)

	@classmethod
	def for_platform(cls, client_id, platform=sys.platform):
		if platform == 'win32':
			return WinDiscordIpcClient(client_id)
		else:
			return UnixDiscordIpcClient(client_id)

	@abstractmethod
	def _connect(self):
		pass

	def _do_handshake(self):
		ret_op, ret_data = self.send_recv({'v': 1, 'client_id': self.client_id}, op=OP_HANDSHAKE)
		# {'cmd': 'DISPATCH', 'data': {'v': 1, 'config': {...}}, 'evt': 'READY', 'nonce': None}
		if ret_op == OP_FRAME and ret_data['cmd'] == 'DISPATCH' and ret_data['evt'] == 'READY':
			return
		else:
			if ret_op == OP_CLOSE:
				self.close()
			raise RuntimeError(ret_data)

	@abstractmethod
	def _write(self, date: bytes):
		pass

	@abstractmethod
	def _recv(self, size: int) -> bytes:
		pass

	def _recv_header(self) -> (int, int):
		header = self._recv_exactly(8)
		return struct.unpack("<II", header)

	def _recv_exactly(self, size) -> bytes:
		buf = b""
		size_remaining = size
		while size_remaining:
			chunk = self._recv(size_remaining)
			buf += chunk
			size_remaining -= len(chunk)
		return buf

	def close(self):
		logger.warning("closing connection")
		try:
			self.send({}, op=OP_CLOSE)
		finally:
			self._close()

	@abstractmethod
	def _close(self):
		pass

	def __enter__(self):
		return self

	def __exit__(self, *_):
		self.close()

	def send_recv(self, data, op=OP_FRAME):
		self.send(data, op)
		return self.recv()

	def send(self, data, op=OP_FRAME):
		logger.debug("sending %s", data)
		data_str = json.dumps(data, separators=(',', ':'))
		data_bytes = data_str.encode('utf-8')
		header = struct.pack("<II", op, len(data_bytes))
		self._write(header)
		self._write(data_bytes)

	def recv(self) -> (int, "JSON"):
		"""Receives a packet from discord.

		Returns op code and payload.
		"""
		op, length = self._recv_header()
		payload = self._recv_exactly(length)
		data = json.loads(payload.decode('utf-8'))
		logger.debug("received %s", data)
		return op, data

	def set_activity(self, act):
		# act
		data = {
			'cmd': 'SET_ACTIVITY',
			'args': {'pid': os.getpid(),
					 'activity': act},
			'nonce': str(uuid.uuid4())
		}
		self.send(data)


class WinDiscordIpcClient(DiscordIpcClient):

	_pipe_pattern = R'\\?\pipe\discord-ipc-{}'

	def _connect(self):
		for i in range(10):
			path = self._pipe_pattern.format(i)
			try:
				self._f = open(path, "w+b")
			except OSError as e:
				logger.error("failed to open {!r}: {}".format(path, e))
			else:
				break
		else:
			return DiscordIpcError("Failed to connect to Discord pipe")

		self.path = path

	def _write(self, data: bytes):
		self._f.write(data)
		self._f.flush()

	def _recv(self, size: int) -> bytes:
		return self._f.read(size)

	def _close(self):
		self._f.close()


class UnixDiscordIpcClient(DiscordIpcClient):

	def _connect(self):
		self._sock = socket.socket(socket.AF_UNIX)
		pipe_pattern = self._get_pipe_pattern()

		for i in range(10):
			path = pipe_pattern.format(i)
			if not os.path.exists(path):
				continue
			try:
				self._sock.connect(path)
			except OSError as e:
				logger.error("failed to open {!r}: {}".format(path, e))
			else:
				break
		else:
			return DiscordIpcError("Failed to connect to Discord pipe")

	@staticmethod
	def _get_pipe_pattern():
		env_keys = ('XDG_RUNTIME_DIR', 'TMPDIR', 'TMP', 'TEMP')
		for env_key in env_keys:
			dir_path = os.environ.get(env_key)
			if dir_path:
				break
		else:
			dir_path = '/tmp'
		return os.path.join(dir_path, 'discord-ipc-{}')

	def _write(self, data: bytes):
		self._sock.sendall(data)

	def _recv(self, size: int) -> bytes:
		return self._sock.recv(size)

	def _close(self):
		self._sock.close()