# -*- coding: utf-8 -*-
from __future__ import division
import sys
import time
import emoji
import zlib
try:
	import simplejson as json
except ImportError:
	import json
from tempfile import SpooledTemporaryFile
from .. import isDreamOS, medialist, getMemInfo
from ..e2m3u2bouquet import url_validate
from ..log import logger
from ..modules.six.moves.urllib.parse import urlparse
from .EPGConfig import openStream, Timer, bigStorage, isLocalFile, human_size
from twisted.internet import reactor, threads, ssl, _sslverify, defer
from twisted.internet.protocol import Protocol
from twisted.web.client import (Agent, BrowserLikeRedirectAgent, readBody, ResponseDone,
				ContentDecoderAgent, GzipDecoder, _GzipProtocol)
try:
	from twisted.web.client import BrowserLikePolicyForHTTPS as IgnoreHTTPSContextFactory
except ImportError:
	# for twisted < 14 backward compatibility
	from twisted.web.client import WebClientContextFactory as IgnoreHTTPSContextFactory
from twisted.web.http_headers import Headers
from twisted.python.runtime import platform
from twisted.python.failure import Failure
from collections import namedtuple
from Components.config import config


def getParser(source):
	name, cls = PARSERS.get(source.parser)
	return getattr(__import__(__name__[:__name__.rfind('.')+1] + name, fromlist=[None]), cls)

def pop_default(input_list, index=-1, default_value=None):
	try:
		return input_list.pop(index)
	except IndexError:
		return default_value

# for twisted < 16.0 backward compatibility instead of standart addTimeout
def timeout(secs):
	"""Decorator to add timeout to Deferred calls"""
	def wrap(func):
		@defer.inlineCallbacks
		def _timeout(*args, **kwargs):
			raw_d = func(*args, **kwargs)
			if not isinstance(raw_d, defer.Deferred):
				defer.returnValue(raw_d)

			timeout_d = defer.Deferred()
			times_up = reactor.callLater(secs, timeout_d.callback, None)

			try:
				raw_result, timeout_result = yield defer.DeferredList([raw_d, timeout_d],
									fireOnOneCallback=True, fireOnOneErrback=True, consumeErrors=True)
			except defer.FirstError as e:  # Only raw_d should raise an exception
				assert e.index == 0
				times_up.cancel()
				e.subFailure.raiseException()
			else:  # timeout
				if timeout_d.called:
					raw_d.cancel()
					raise Exception("downloadTimeout -> %s secs have expired" % secs)

			# no timeout
			times_up.cancel()
			defer.returnValue(raw_result)
		return _timeout
	return wrap


class _Accumulator(Protocol):
	def __init__(self, finished):
		self.finished = finished
		self.free_m = getMemInfo()['MemFree'].value * 1024 // 3
		logger.debug('%s free RAM detected' % human_size(self.free_m*3))
		self.data = SpooledTemporaryFile(max_size=self.free_m, dir=bigStorage(self.free_m, *medialist))
		self.data.seekable = lambda: True	# Fix according to https://bugs.python.org/issue26175
		self.data_size = 0

	def dataReceived(self, chunk):
		self.data.write(chunk)
		self.data_size += sys.getsizeof(chunk)

	def connectionLost(self, failure):
		if failure.check(ResponseDone):
			self.data.seek(0)
			logger.info('%s data spooled to %s' % (human_size(self.data_size), "RAM" if self.free_m > self.data_size else "tempfile"))
			self.finished.callback(openStream(self.data))
		else:
			self.finished.callback(failure)


class IgnoreHTTPS(IgnoreHTTPSContextFactory):
	def creatorForNetloc(self, hostname, port):
		options = ssl.CertificateOptions(verify=False)
		return _sslverify.ClientTLSOptions(hostname, options.getContext())


class _GzipDecoder(GzipDecoder):
	def deliverBody(self, protocol):
		self.original.deliverBody(GzipProtocol(protocol, self.original, 'gzip'))


class _DeflateDecoder(GzipDecoder):
	def deliverBody(self, protocol):
		self.original.deliverBody(GzipProtocol(protocol, self.original, 'deflate'))


class GzipProtocol(_GzipProtocol):
	def __init__(self, protocol, response, enc):
		self.original = protocol
		self._response = response
		self._zlibDecompress = {
					'gzip': zlib.decompressobj(zlib.MAX_WBITS|16),
					'deflate': zlib.decompressobj(-zlib.MAX_WBITS),
					}[enc]


class OudeisImporter(object):
	"""Wrapper to convert original patch to new one that accepts multiple services"""
	def __init__(self, epgcache):
		self.epgcache = epgcache

	# difference with old patch is that services is a list or tuple, this
	# wrapper works around it.
	def importEvents(self, services, events):
		for service in services:
			self.epgcache.importEvent(service, events)


class EPGImport(object):
	"""Simple Class to import EPGData"""

	HDD_EPG_DAT = '/etc/enigma2/epg.db' if isDreamOS else '/etc/enigma2/epg.dat'

	def __init__(self, epgcache):
		self.timer = Timer()
		self.storage = None
		self.sources = []
		self.source = None
		self.onDone = None
		self.epgcache = epgcache
		self.data = None
		self.iterator = None

	def cacheStateChanged(self, state):
		"""0 = started, 1 = stopped, 2 = aborted, 3 = deferred, 4 = load_finished, 5 = save_finished"""
		if state.state == 4:
			if isDreamOS:
				logger.debug("EPGCache load finished")
			self.storage = None
			self.source = None
			if self.onDone:
				self.onDone(reboot=False, epgfile=None)

		elif state.state == 5:
			logger.debug("EPGCache save finished")
			try:
				import epg_importer
				self.storage = epg_importer.EPGClass(self.HDD_EPG_DAT)
			except Exception as err:
				logger.error('epg_importer wrapper error: %s' % err)
				self.finish()
				return
			else:
				self.nextImport()

	def saveEPGCache(self):
		if isDreamOS:
			self.cacheState_conn = self.epgcache.cacheState.connect(self.cacheStateChanged)
		logger.info("Saving the eEPGCache into %s ..." % self.HDD_EPG_DAT)
		self.epgcache.save()

	def beginImport(self):
		self.timer.counter = 0
		self.timer.start = time.time()
		self.getISO639().addErrback(lambda err: logger.warn("Failed to load ISO639 codes: %s" % err.getErrorMessage())).addCallback(self.start)

	def start(self, isodict):
		"""Starts importing using Enigma reactor"""
		global ISO639_associations
		if isinstance(isodict, dict):
			ISO639_associations = isodict
		attr = dir(self.epgcache)
		if not all(p in attr for p in ('save', 'load')) and (any('ImportEvent' in x for x in attr) if not isDreamOS else True):
			logger.error("The E2 image being used does not have all the needed eEPGCache methods")
			logger.error("EPG import is not possible")
			self.finish()
			return
		elif isDreamOS:
			logger.info("DreamOS detected ...")
			self.saveEPGCache()
			return
		elif 'importEvents' in attr:
			self.storage = self.epgcache
		elif 'importEvent' in attr:
			self.storage = OudeisImporter(self.epgcache)
		self.nextImport()

	def nextImport(self, falure=None):
		msg = "data source processing"
		if falure:
			logger.info("### '%s' %s finished %s" % (self.source.description, msg, emoji.emojize(':thumbs_up:')))
		try:
			self.source = self.sources.pop()
			self.channelFiles = self.source.channels.downloadables
		except Exception as err:
			if err.__class__.__name__ != 'IndexError':
				logger.error("EPGImport %s: %s" % (sys._getframe().f_code.co_name, err))
			self.finish()
		else:
			logger.info("### '%s' %s started %s" % (self.source.description, msg, emoji.emojize(':sign_of_the_horns:')))
			self.doDownload(pop_default(self.channelFiles)).addCallbacks(self.afterChannelDownload, self.channelDownloadFail)

	def afterChannelDownload(self, data=None):
		msg = (self.source.description, 'channels table')
		try:
			logger.info("['%s'] Processing %s ..." % msg)
			self.source.channels.parse(data)
		except Exception as err:
			self.channelDownloadFail(Failure(err))
		else:
			msg += (len(self.source.channels.items),)
			logger.info("['%s'] %s for %s id has been formed" % msg)
			self.doDownload(self.source.url).addCallbacks(self.afterDownload, self.downloadFail)
		finally:
			if data is not None:
				data.close()

	def afterDownload(self, data):
		self.data = data
		self.iterator = getParser(self.source).enumFile(data, self.source)
		msg = '### import EPG events started using'
		if platform.supportsThreads():
			logger.info("%s threads, yay! %s" % (msg, emoji.emojize(':rocket:')))
			threads.deferToThread(self.doThreadRead).addCallbacks(self.nextImport, self.downloadFail)
		else:
			logger.info("%s twisted reactor, not bad! %s" % (msg, emoji.emojize(':racing_car:')))
			reactor.addReader(self)

	def importEvents(self):
		data = next(self.iterator)
		if data is not None:
			try:
				self.storage.importEvents(*data)
				self.timer.counter += len(data[1])
			except Exception as err:
				logger.error("EPGImport ### %s exception: %s" % (sys._getframe().f_code.co_name, err))

	def doThreadRead(self):
		"""This is used on images with threading"""
		while 1:
			try:
				self.importEvents()
			except Exception as err:
				self.data.close()
				if err.__class__.__name__ != 'StopIteration':
					raise
				else:
					return True

### BEGINNING "crutches" for images without twisted Threads(), like VTi, so we have to use twisted reactor

	def fileno(self):
		return -1 if self.data.closed else self.data.fileno()

	def logPrefix(self):
		return self.__class__.__name__

	def connectionLost(self, failure):
		self.data.close()
		reactor.removeReader(self)
		if failure.check(StopIteration):
			self.nextImport(True)
		else:
			self.downloadFail(failure)

	def doRead(self):
		"""Called from reactor to read data"""
		try:
			self.importEvents()
		except Exception as err:
			return err
### END "crutches"

	def channelDownloadFail(self, failure):
		msg = (self.source.description, "channels table", failure.getErrorMessage())
		logger.warn("['%s'] %s process failed: %s" % msg)
		logger.debug('%s' % failure.getTraceback())
		msg = (msg[0], "alternatives for the %s" % msg[1])
		try:
			logger.info("['%s'] Attempting %s" % msg)
			self.doDownload(self.channelFiles.pop()).addCallbacks(self.afterChannelDownload, self.channelDownloadFail)
		except IndexError:
			logger.info("['%s'] No more %s" % msg)
			self.nextImport(failure)

	def downloadFail(self, failure):
		logger.warn("['%s'] Processed EPG failed: %s" % (self.source.description, failure.getErrorMessage()))
		logger.debug('%s' % failure.getTraceback())
		msg = (self.source.description, "alternative url's for EPG")
		try:
			logger.info("['%s'] Attempting %s" % msg)
			self.doDownload(self.source.urls.pop()).addCallbacks(self.afterDownload, self.downloadFail)
		except IndexError:
			logger.info("['%s'] No more %s" % msg)
			self.nextImport(failure)

	def finish(self):
		if self.timer.counter:
			if isDreamOS:
				logger.info("Loading the %s into eEPGCache ..." % self.HDD_EPG_DAT)
				self.epgcache.load()
			else:
				self.saveEPGCache()

			self.timer.msg = 'Total imported %s events in %s' % (self.timer.counter, time.strftime("%H:%M:%S", time.gmtime(self.timer.elapsed)))
		else:
			logger.info('#### No events to import! #### %s' % emoji.emojize(':pouting_face:'))
		self.timer.__exit__()
		if not isDreamOS:
			self.cacheStateChanged(namedtuple('state', 'state')(4))

	@property
	def eventCount(self):
		return self.timer.counter

	@property
	def isImportRunning(self):
		return self.source is not None

	@timeout(int(config.plugins.e2m3u2b.download_timeout.value))
	def doDownload(self, filelink):
		if not filelink:
			return
		logger.info("['%s'] Obtaining: %s " % (self.source.description, filelink))
		if not(url_validate(filelink) or isLocalFile(filelink)):
			raise Exception("unsupported or not valid link -> %s" % filelink)

		def callback(response):
			if response.code != 200:
				# with no content deliverBody never signals connectionLost
				raise Exception("server response code received -> %s" % response.code)
			else:
				finished = defer.Deferred()
				response.deliverBody(_Accumulator(finished))
				return finished

		if isLocalFile(filelink):
			d = defer.Deferred()
			d.callback(openStream(urlparse(filelink).path))
		else:
			d = agent.request(b'GET', filelink.encode('utf-8'), headers, None)
			d.addCallback(callback)
		return d

	@timeout(6)
	@defer.inlineCallbacks
	def getISO639(self):
		try:
			import pycountry
			defer.returnValue({x.alpha_2.lower():x.alpha_3.lower() for x in pycountry.countries})
		except ImportError:
			resp = yield agent.request(b'GET', b'https://pastebin.com/raw/2wRuJh8z', headers, None)
			data = yield readBody(resp)
			defer.returnValue({x["alpha2"]:x["alpha3-b"] for x in json.loads(data)})


ISO639_associations = {}
headers = Headers({
		b'User-Agent': [b'Mozilla/5.0 (SmartHub; SMART-TV; U; Linux/SmartTV; Maple2012) AppleWebKit/534.7 (KHTML, like Gecko) SmartTV Safari/534.7'],
		b'Accept-Encoding': [b'gzip, deflate'],
		})
agent = ContentDecoderAgent(BrowserLikeRedirectAgent(Agent(reactor, connectTimeout=6, contextFactory=IgnoreHTTPS())),
				 [(b'gzip', _GzipDecoder), (b'deflate', _DeflateDecoder)])
PARSERS = {
		'xmltv': ['xmltvconverter', 'XMLTVConverter'],
		'gen_xmltv': ['xmltvconverter', 'XMLTVConverter'],
		'jtv': ['jtvconverter', 'JTVConverter'],
		'gen_jtv': ['jtvconverter', 'JTVConverter'],
	}
