import asyncio
import collections
import contextlib
import datetime
import math
import os.path
import time
from typing import Awaitable, Sequence, T, Tuple, Union

import discord
from discord.ext import commands
from dateutil.relativedelta import relativedelta
try:
	from prettytable import PrettyTable
except ImportError:
	HAVE_PRETTYTABLE = False
else:
	HAVE_PRETTYTABLE = True

def codeblock(s, *, lang=''):
	return f'```{lang}\n{s}```'

def absolute_natural_timedelta(seconds: Union[float, datetime.timedelta], *, accuracy=2):
	if isinstance(seconds, datetime.timedelta):
		seconds = seconds.total_seconds()
	seconds = round(seconds)
	if not seconds:
		return '0 seconds'

	split = split_seconds(seconds)
	words = zip(('day', 'hour', 'minute', 'second'), split)
	pluralized = [format(plural(value), word) for word, value in words if value][:accuracy]
	return natural_join(pluralized)

def natural_rate(delta: Union[float, datetime.timedelta]):
	"""return an english string representing a rate. delta is assumed to be a rate as "1 per delta"."""
	if isinstance(delta, datetime.timedelta):
		delta = delta.total_seconds()
	rate = 1 / delta
	for i, multiplier in enumerate((1, 60, 60, 24, 7)):
		rate *= multiplier
		if rate > 1:
			break
	duration = ('second', 'minute', 'hour', 'day', 'week')[i]
	return f'{rate:.2g} per {duration}'

def split_seconds(seconds: int):
	minutes, seconds = divmod(seconds, 60)
	hours, minutes = divmod(minutes, 60)
	days, hours = divmod(hours, 24)
	# no more perfectly divisible units after here (except weeks which we don't want)
	return days, hours, minutes, seconds

# plural, natural_join, and natural_timedelta use code provided by Rapptz under the MIT License
# © 2015 Rapptz
# https://github.com/Rapptz/RoboDanny/blob/6fd16002e0cbd3ed68bf5a8db10d61658b0b9d51/cogs/utils/formats.py
# https://github.com/Rapptz/RoboDanny/blob/b8c427ad97372cb47f16397ff04a6b80e2494757/cogs/utils/time.py
class plural:
	def __init__(self, value):
		self.value = value
	def __format__(self, format_spec):
		v = self.value
		singular, sep, plural = format_spec.partition('|')
		plural = plural or f'{singular}s'
		if abs(v) != 1:
			return f'{v} {plural}'
		return f'{v} {singular}'

def natural_join(seq, sep=', ', conj='and'):
	size = len(seq)
	if size == 0:
		return ''

	if size == 1:
		return seq[0]

	if size == 2:
		return f'{seq[0]} {conj} {seq[1]}'

	return sep.join(seq[:-1]) + f' {conj} {seq[-1]}'

def natural_timedelta(dt, *, source=None, accuracy=3, brief=False, ago=False):
	now = source or datetime.datetime.utcnow()
	# Microsecond free zone
	now = now.replace(microsecond=0)
	dt = dt.replace(microsecond=0)

	# This implementation uses relativedelta instead of the much more obvious
	# divmod approach with seconds because the seconds approach is not entirely
	# accurate once you go over 1 week in terms of accuracy since you have to
	# hardcode a month as 30 or 31 days.
	# A query like "11 months" can be interpreted as "!1 months and 6 days"
	if dt > now:
		delta = relativedelta(dt, now)
		suffix = ''
	else:
		delta = relativedelta(now, dt)
		suffix = ' ago' if ago else ''

	attrs = [
		('year', 'y'),
		('month', 'mo'),
		('day', 'd'),
		('hour', 'h'),
		('minute', 'm'),
		('second', 's'),
	]

	output = []
	for attr, brief_attr in attrs:
		elem = getattr(delta, attr + 's')
		if not elem:
			continue

		if attr == 'day':
			weeks = delta.weeks
			if weeks:
				elem -= weeks * 7
				if not brief:
					output.append(format(plural(weeks), 'week'))
				else:
					output.append(f'{weeks}w')

		if elem <= 0:
			continue

		if brief:
			output.append(f'{elem}{brief_attr}')
		else:
			output.append(format(plural(elem), attr))

	if accuracy is not None:
		output = output[:accuracy]

	if not output:
		return 'now'
	if not brief:
		return natural_join(output, conj='and') + suffix
	return ' '.join(output) + suffix

class timeit(contextlib.AbstractContextManager):
	def __enter__(self, _timer=time.perf_counter):
		self.t0 = _timer()
		return self

	def __exit__(self, *excinfo, _timer=time.perf_counter):
		self.t1 = _timer()
		self.elapsed = self.t1 - self.t0

class TimedReactor(contextlib.AbstractAsyncContextManager):
	"""adds a reaction to the given message if the body of the context manager takes >1s"""

	def __init__(self, message):
		self.message = message

	async def __aenter__(self):
		async def react_after_1s():
			await asyncio.sleep(1.0)
			await self.message.add_reaction('\N{black right-pointing triangle}\N{variation selector-16}')

		self.task = asyncio.get_event_loop().create_task(react_after_1s())

	async def __aexit__(self, *excinfo):
		self.task.cancel()

if HAVE_PRETTYTABLE:
	class PrettyTable(PrettyTable):
		"""an extension of PrettyTable that works with asyncpg's Records and looks better"""
		def __init__(self, rows: Sequence[Union['asyncpg.Record', collections.OrderedDict]], **options):
			defaults = dict(
				# super()'s default is ASCII | - +, which don't join seamlessly and look p bad
				vertical_char='│',
				horizontal_char='─',
				junction_char='┼')
			for option, default in defaults.items():
				options.setdefault(option, default)

			if rows:
				super().__init__(rows[0].keys(), **options)
			else:
				super().__init__()
			# PrettyTable's constructor does not set this property for some reason
			self.align = options.get('align', 'l')	# left align

			for row in rows:
				self.add_row(row)

class BotBinMisc(commands.Cog):
	"""Miscellaneous commands that don't belong in any other category"""

	def __init__(self, bot):
		self.bot = bot
		self._init_license()
		if not hasattr(bot, 'start_time'):
			bot.start_time = None

	def _init_license(self):
		try:
			filename = self.bot.config.get('copyright_license_file')
		except AttributeError:
			return

		if not filename or not os.path.isfile(filename):
			return

		with open(filename) as f:
			self.license_message = f.read()

	@commands.Cog.listener()
	async def on_ready(self):
		if getattr(self.bot, 'start_time', None) is None:
			self.bot.start_time = datetime.datetime.utcnow()

	@commands.command(aliases=['license'])
	async def copyright(self, context):
		"""Tells you about the copyright license for the bot"""
		await context.send(self.license_message)

	@commands.command(name='uptime')
	async def uptime_command(self, context):
		"""Shows you how long the bot has been online."""
		await context.send(self.uptime())

	def uptime(self, *, brief=False):
		if getattr(self.bot, 'start_time', None) is None:
			return 'Not up yet' if brief else "I'm not up yet."

		humanized = natural_timedelta(self.bot.start_time, accuracy=None, brief=brief)
		if brief:
			return humanized
		else:
			return f"I've been up for {humanized}."

	@commands.command()
	async def ping(self, context):
		"""Shows the bots latency to Discord's servers"""
		latency = round(self.bot.latency * 1000, 2)
		await context.send(f'🏓 Pong!│Average websocket latency: {latency}ms')

	@commands.command(hidden=True)
	async def pong(self, context):
		reply = await context.send('Ping')
		if context.message.created_at < reply.created_at:
			# the messages appeared in the correct order
			await reply.delete()
		# due to loose snowflake ordering, we time travelled
		# so leave the reply message alone

def setup(bot):
	cog = BotBinMisc(bot)
	bot.add_cog(cog)
	if not hasattr(cog, 'license_message'):
		bot.remove_command('copyright')
