from __future__ import absolute_import, division, print_function, unicode_literals

from builtins import map, str
from future.moves.urllib import request
from future.moves.urllib.parse import urlparse, urlsplit
from future.moves.urllib.error import URLError

import os, os.path, logging, gzip, errno, json
from io import open
from string import ascii_letters, digits
from typing import TYPE_CHECKING

from .file import copyfilelike, Tell
from .iter import first_not_none
from .filesystem import safe_filename

if TYPE_CHECKING:
	from typing import Callable, Optional, Tuple
	from http.cookiejar import CookieJar

logger = logging.getLogger(__name__)

# URI RFC
gen_delims = ":/?#[]@"
sub_delims = "!$&'()*+;="
reserved = gen_delims + sub_delims
unreserved = ascii_letters + digits + "-._~"
valid_uri_characters = reserved + unreserved

class ContentInvalidLength(Exception):

	def __init__(self, path, expected, received):
		Exception.__init__(self, path, expected, received)

class TimeOut(Exception):
	pass

class DownloadInterrupted(Exception):
	pass

try:
	FileExistsError
except NameError:
	class FileExistsError(IOError):
		pass

try:
	from email.utils import parsedate_to_datetime # new in 3.3

	def parsedate_to_timestamp(datestr): # correct timezone
		return parsedate_to_datetime(datestr).timestamp()

except ImportError:
	import time

	def parsedate_to_timestamp(datestr): # fixme: incorrect timezone
		# type: (str, ) -> float

		try:
			#str fixed py2.x unicode bug
			return time.mktime(time.strptime(datestr, str("%a, %d %b %Y %H:%M:%S %Z")))
		except ValueError:
			#time data 'Thu, 02 Aug 2007 06:54:23 GMT' does not match format u'%a, %d %b %Y %H:%M:%S +0000' # fixed with str()
			return time.mktime(time.strptime(datestr, str("%a, %d %b %Y %H:%M:%S +0000")))

try:
	from email.message import EmailMessage # new in 3.6

	def get_filename(headers):
		# type: (EmailMessage, ) -> str

		return headers.get_filename()

except ImportError:
	def get_filename(headers):
		# type: (Mapping, ) -> str

		try:
			content_disposition = headers["Content-Disposition"]
			return content_disposition.split("=")[1].replace("\"", "").strip()
		except (KeyError, IndexError):
			return None

def get_filename_from_url(url):
	return os.path.split(urlsplit(url).path)[1]

def get_filename_from_url_v2(url):
	return urlparse(url)[2].split("/")[-1]

class URLRequestBuilder(object):

	def __init__(self, cookiejar=None, basicauth=None):
		# type: (Optional[CookieJar], Optional[Tuple[str, str, str]]) -> None

		handlers = []

		if cookiejar:
			handlers.append(request.HTTPCookieProcessor(cookiejar))
		if basicauth:
			top_level_url, username, password = basicauth
			password_mgr = request.HTTPPasswordMgrWithDefaultRealm()
			password_mgr.add_password(None, top_level_url, username, password)
			handlers.append(request.HTTPBasicAuthHandler(password_mgr))

		if handlers:
			opener = request.build_opener(handlers)
			self.openfunc = opener.open
		else:
			self.openfunc = request.urlopen

	def getfunc(self):
		return self.openfunc

	def request(self, url, headers=None, timeout=None):
		return URLRequest(url, headers, timeout, openfunc=self.openfunc)

class FileLike(object):

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

		self.content_length = self.urlrequest._content_length()

		self.response = Tell(self.urlrequest.response)
		if self.urlrequest.headers.get("Content-Encoding", None) == "gzip":
			# if that does not work, use BytesIO
			logger.info("Using gzip")
			self.fp = gzip.GzipFile(fileobj=self.response)
		else:
			self.fp = self.response

	def close(self):
		self.fp.close()

		transferred = self.response.tell() # is this correct?
		if self.content_length is not None and self.content_length != transferred:
			raise ContentInvalidLength(None, self.content_length, transferred)

	def __enter__(self):
		return self.fp

	def __exit__(self, exc_type, exc_value, traceback):
		self.close()

class URLRequest(object):

	def __init__(self, url, headers=None, timeout=None, cookiejar=None, basicauth=None, openfunc=None):
		# type: (str, Optional[dict], Optional[float], Optional[CookieJar], Optional[Tuple[str, str, str]], Optional[Callable]) -> None

		self.url = url
		self.timeout = timeout
		headers = headers or {}

		if not openfunc:
			openfunc = URLRequestBuilder(cookiejar=None, basicauth=None).getfunc()

		req = request.Request(url, data=None, headers=headers)
		self.response = openfunc(req, timeout=timeout)
		self.headers = self.response.info()

	def _close(self): # unused
		self.response.close()

	def __enter__(self):
		# type: () -> URLRequest

		return self

	def __exit__(self, exc_type, exc_value, traceback):
		self.response.close()

	def _content_length(self):
		# type: () -> Optional[int]

		try:
			content_length = self.headers["Content-Length"]
			return int(content_length)
		except (KeyError, ValueError, TypeError):
			return None

	def _last_modified(self):
		# type: () -> Optional[float]

		try:
			last_modified = self.headers["Last-Modified"]
			return parsedate_to_timestamp(last_modified)
		except (KeyError, TypeError, ValueError):
			return None

	def get_redirect_url(self):
		self.response.close()
		return self.response.geturl()

	def _load(self):
		# type: () -> bytes

		with FileLike(self) as fp:
			try:
				return fp.read()
			except URLError:
				raise TimeOut("Timed out after {}s".format(self.timeout))

	def _json(self):
		# type: () -> dict

		with FileLike(self) as fp:
			try:
				return json.load(fp)
			except URLError:
				raise TimeOut("Timed out after {}s".format(self.timeout))

	def _download(self, basepath, filename=None, fn_prio=None, overwrite=False, suffix=".partial", report=None):
		# type: (str, Optional[str], Optional[Tuple[int, int, int, int]], bool, str, Optional[Callable[[int, int], None]]) -> Tuple[Optional[int], str]

		"""PermissionError: [WinError 32] Der Prozess kann nicht auf die Datei zugreifen,
			da sie von einem anderen Prozess verwendet wird:
			'G:\\PUBLIC\\Audio\\Podcasts\\Radio Nukular\\radio_nukular_057.m4a.partial'
			-> 'G:\\PUBLIC\\Audio\\Podcasts\\Radio Nukular\\radio_nukular_057.m4a'
		"""

		logger.debug("Downloading %s to %s", self.url, basepath)

		if fn_prio is None:
			fn_prio = (0, 1, 2, 3)

		filenames = {
			0: filename,
			1: get_filename(self.headers),
			2: get_filename_from_url(self.response.geturl()), # url after redirect
			3: get_filename_from_url(self.url),
		}

		filenames = [filenames[p] for p in fn_prio]
		logger.info("Filenames: {}".format(", ".join(map(str, filenames))))

		filename = first_not_none(filenames)

		if not filename:
			raise ValueError("Please provide a filename")

		filename = safe_filename(filename)
		fullpath = os.path.join(basepath, filename)
		tmppath = fullpath + suffix

		if not overwrite and os.path.exists(fullpath):
			raise FileExistsError(errno.EEXIST, "File already exists", fullpath)

		try:
			os.makedirs(basepath) # exist_ok=True is new in 3.2
		except OSError:
			pass

		content_length = self._content_length()

		try:
			# actually download the file
			with open(tmppath, "wb") as out:
				# https://www.ietf.org/mail-archive/web/httpbisa/current/msg27484.html
				transferred = copyfilelike(self.response, out, content_length, report=report)

		except URLError:
			logger.warning("Timeout after {}s at {}: {}".format(self.timeout, self.response.geturl(), self.headers))
			raise TimeOut("Timed out after {}s".format(self.timeout))

		except ConnectionResetError as e:
			logger.warning("Connection was reset during download: %s", str(e))
			raise DownloadInterrupted("Connection was reset during download")

		except Exception as e:
			logger.exception("Downloading failed mid file. Handle.")
			raise

		if content_length and content_length != transferred:
			logger.info("{} {}".format(content_length, transferred))
			raise ContentInvalidLength(tmppath, content_length, transferred)

		last_modified = self._last_modified()
		if last_modified:
			os.utime(tmppath, (-1, last_modified))

		os.rename(tmppath, fullpath)

		return (content_length, filename)

	def load(self):
		# type: () -> bytes

		try:
			return self._load()
		finally:
			self.response.close()

	def json(self):
		# type: () -> bytes

		try:
			return self._json()
		finally:
			self.response.close()

	def download(self, basepath, filename=None, fn_prio=None, overwrite=False, suffix=".partial", report=None):
		# type: () -> Tuple[Optional[int], str]

		try:
			return self._download(basepath, filename, fn_prio, overwrite, suffix, report)
		finally:
			self.response.close()

if __name__ == "__main__":
	from argparse import ArgumentParser

	parser = ArgumentParser(description="Downloads url to file")
	parser.add_argument("url", help="url to download")
	parser.add_argument("dir", help="output directory")
	parser.add_argument("-f", "--filename", help="filename")
	args = parser.parse_args()

	URLRequest(args.url).download(args.dir, args.filename)
