from __future__ import print_function

# for localized messages
from . import _

# Plugins Config
from xml.etree.cElementTree import parse as cet_parse
from os import path as os_path
from AutoTimerConfiguration import parseConfig, buildConfig

# Tasks
import Components.Task

# GUI (Screens)
from Screens.MessageBox import MessageBox
from Tools.FuzzyDate import FuzzyTime
from Tools.Notifications import AddPopup

# Navigation (RecordTimer)
import NavigationInstance

# Timer
from ServiceReference import ServiceReference
from RecordTimer import RecordTimerEntry

# Timespan
from time import localtime, strftime, time, mktime, sleep
from datetime import timedelta, date

# EPGCache & Event
from enigma import eEPGCache, eServiceReference, eServiceCenter, iServiceInformation

# AutoTimer Component
from AutoTimerComponent import preferredAutoTimerComponent

from itertools import chain
from collections import defaultdict
from difflib import SequenceMatcher
from operator import itemgetter

from Plugins.SystemPlugins.Toolkit.SimpleThread import SimpleThread

#GML
import re

try:
	from Plugins.Extensions.SeriesPlugin.plugin import renameTimer
except ImportError as ie:
	renameTimer = None

from . import config, xrange, itervalues

XML_CONFIG = "/etc/enigma2/autotimer.xml"

NOTIFICATIONID = 'AutoTimerNotification'
CONFLICTNOTIFICATIONID = 'AutoTimerConflictEncounteredNotification'
SIMILARNOTIFICATIONID = 'AutoTimerSimilarUsedNotification'

def timeSimilarityPercent(rtimer, evtBegin, evtEnd, timer=None):
	#print("rtimer [",rtimer.begin,",",rtimer.end,"] (",rtimer.end-rtimer.begin," s) - evt [",evtBegin,",",evtEnd,"] (",evtEnd-evtBegin," s)")
	if (timer is not None) and (timer.offset is not None):
		# remove custom offset from rtimer using timer.offset as RecordTimerEntry doesn't store the offset
		# ('evtBegin' and 'evtEnd' are also without offset)
		rtimerBegin = rtimer.begin + timer.offset[0]
		rtimerEnd   = rtimer.end   - timer.offset[1]
	else:
		# remove E2 offset
		rtimerBegin = rtimer.begin + config.recording.margin_before.value * 60
		rtimerEnd   = rtimer.end   - config.recording.margin_after.value * 60
	#print("trimer [",rtimerBegin,",",rtimerEnd,"] (",rtimerEnd-rtimerBegin," s) after removing offsets")
	if (rtimerBegin <= evtBegin) and (evtEnd <= rtimerEnd):
		commonTime = evtEnd - evtBegin
	elif (evtBegin <= rtimerBegin) and (rtimerEnd <= evtEnd):
		commonTime = rtimerEnd - rtimerBegin
	elif evtBegin <= rtimerBegin <= evtEnd:
		commonTime = evtEnd - rtimerBegin
	elif rtimerBegin <= evtBegin <= rtimerEnd:
		commonTime = rtimerEnd - evtBegin
	else:
		commonTime = 0
	if evtBegin != evtEnd:
		commonTime_percent = 100*commonTime/(evtEnd - evtBegin)
	else:
		return 0
	if rtimerEnd != rtimerBegin:
		durationMatch_percent = 100*(evtEnd - evtBegin)/(rtimerEnd - rtimerBegin)
	else:
		return 0
	#print("commonTime_percent = ",commonTime_percent,", durationMatch_percent = ",durationMatch_percent)
	if durationMatch_percent < commonTime_percent:
		#avoid false match for a short event completely inside a very long rtimer's time span 
		return durationMatch_percent
	else:
		return commonTime_percent

typeMap = {
	"exact": eEPGCache.EXAKT_TITLE_SEARCH,
	"partial": eEPGCache.PARTIAL_TITLE_SEARCH,
	"start": eEPGCache.START_TITLE_SEARCH,
	"description": -99
}

caseMap = {
	"sensitive": eEPGCache.CASE_CHECK,
	"insensitive": eEPGCache.NO_CASE_CHECK
}

class AutoTimer:
	"""Read and save xml configuration, query EPGCache"""

	def __init__(self):
		# Initialize
		self.timers = []
		self.configMtime = -1
		self.uniqueTimerId = 0
		self.defaultTimer = preferredAutoTimerComponent(
			0,		# Id
			"",		# Name
			"",		# Match
			True 	# Enabled
		)

# Configuration
	def readXml(self):
		# Abort if no config found
		if not os_path.exists(XML_CONFIG):
			print("[AutoTimer] No configuration file present")
			return

		# Parse if mtime differs from whats saved
		mtime = os_path.getmtime(XML_CONFIG)
		if mtime == self.configMtime:
			print("[AutoTimer] No changes in configuration, won't parse")
			return

		# Save current mtime
		self.configMtime = mtime

		# Parse Config
		file = open(XML_CONFIG, 'r')
		configuration = cet_parse(file).getroot()
		file.close()

		# Empty out timers and reset Ids
		del self.timers[:]
		self.defaultTimer.clear(-1, True)

		parseConfig(
			configuration,
			self.timers,
			configuration.get("version"),
			0,
			self.defaultTimer
		)
		self.uniqueTimerId = len(self.timers)

	def getXml(self):
		return buildConfig(self.defaultTimer, self.timers, webif = True)

	def writeXml(self):
		file = open(XML_CONFIG, 'w')
		file.writelines(buildConfig(self.defaultTimer, self.timers))
		file.close()

# Manage List
	def add(self, timer):
		self.timers.append(timer)

	def getEnabledTimerList(self):
		return sorted([x for x in self.timers if x.enabled], key=lambda x: x.name)

	def getTimerList(self):
		return self.timers

	def getTupleTimerList(self):
		lst = self.timers
		return [(x,) for x in lst]

	def getSortedTupleTimerList(self):
		lst = self.timers[:]
		lst.sort()
		return [(x,) for x in lst]

	def getUniqueId(self):
		self.uniqueTimerId += 1
		return self.uniqueTimerId

	def remove(self, uniqueId):
		idx = 0
		for timer in self.timers:
			if timer.id == uniqueId:
				self.timers.pop(idx)
				return
			idx += 1

	def set(self, timer):
		idx = 0
		for stimer in self.timers:
			if stimer == timer:
				self.timers[idx] = timer
				return
			idx += 1
		self.timers.append(timer)

	#call from epgrefresh
	def parseEPGAsync(self, simulateOnly=False):
		t = SimpleThread(lambda: self.parseEPG(simulateOnly=simulateOnly))
		t.start()
		return t.deferred

	# Main function
	def parseEPG(self, autoPoll = False, simulateOnly = False, callback = None):
		self.autoPoll = autoPoll
		self.simulateOnly = simulateOnly

		self.new = 0
		self.modified = 0
		self.skipped = 0
		self.total = 0
		self.autotimers = []
		self.conflicting = []
		self.similars = []
		self.callback = callback

		# NOTE: the config option specifies "the next X days" which means today (== 1) + X
		delta = timedelta(days = config.plugins.autotimer.maxdaysinfuture.getValue() + 1)
		self.evtLimit = mktime((date.today() + delta).timetuple())
		self.checkEvtLimit = delta.days > 1
		del delta

		# Read AutoTimer configuration
		self.readXml()

		# Get E2 instances
		self.epgcache = eEPGCache.getInstance()
		self.serviceHandler = eServiceCenter.getInstance()
		self.recordHandler = NavigationInstance.instance.RecordTimer

		# Save Timer in a dict to speed things up a little
		# We include processed timers as we might search for duplicate descriptions
		# NOTE: It is also possible to use RecordTimer isInTimer(), but we won't get the timer itself on a match
		self.timerdict = defaultdict(list)
		self.populateTimerdict(self.epgcache, self.recordHandler, self.timerdict)

		# Create dict of all movies in all folders used by an autotimer to compare with recordings
		# The moviedict will be filled only if one AutoTimer is configured to avoid duplicate description for any recordings
		self.moviedict = defaultdict(list)

		# Iterate Timer
		Components.Task.job_manager.AddJob(self.createTask())

	def createTask(self):
		self.timer_count = 0
		self.completed = []
		job = Components.Task.Job(_("AutoTimer"))
		timer = None

		# Iterate Timer
		for timer in self.getEnabledTimerList():
			task = Components.Task.PythonTask(job, timer.name)
			task.work = self.JobStart
			task.weighting = 1
			self.timer_count += 1

		if timer:
			task = Components.Task.PythonTask(job, 'Show results')
			task.work = self.JobMessage
			task.weighting = 1
		
		return job

	def JobStart(self):
		for timer in self.getEnabledTimerList():
			if timer.name not in self.completed:
				self.parseTimer(timer, self.epgcache, self.serviceHandler, self.recordHandler, self.checkEvtLimit, self.evtLimit, self.autotimers, self.conflicting, self.similars, self.timerdict, self.moviedict, self.simulateOnly)
				self.new += self.result[0]
				self.modified += self.result[1]
				self.skipped += self.result[2]
				break

	def parseTimer(self, timer, epgcache, serviceHandler, recordHandler, checkEvtLimit, evtLimit, timers, conflicting, similars, timerdict, moviedict, simulateOnly=False):
		new = 0
		modified = 0
		skipped = 0

		# Precompute timer destination dir
		dest = timer.destination or config.usage.default_path.value

		# Workaround to allow search for umlauts if we know the encoding
		match = timer.match.replace('\xc2\x86', '').replace('\xc2\x87', '')
		if timer.encoding != 'UTF-8':
			try:
				match = match.decode('UTF-8').encode(timer.encoding)
			except UnicodeDecodeError:
				pass

		if timer.searchType == "description":
			epgmatches = []
			mask = (eServiceReference.isMarker | eServiceReference.isDirectory)

			casesensitive = timer.searchCase == "sensitive"
			if not casesensitive:
				match = match.lower()

			# Service filter defined
			# Search only using the specified services
			test = [(service, 0, -1, -1) for service in timer.services]

			for bouquet in timer.bouquets:
				services = serviceHandler.list(eServiceReference(bouquet))
				if not services is None:
					while True:
						service = services.getNext()
						if not service.valid(): #check end of list
							break
						if not (service.flags & mask):
							test.append( (service.toString(), 0, -1, -1 ) )

			if not test:
				# No service filter defined
				# Search within all services - could be very slow

				# Get all bouquets
				bouquetlist = []
				refstr = '1:134:1:0:0:0:0:0:0:0:FROM BOUQUET \"bouquets.tv\" ORDER BY bouquet'
				bouquetroot = eServiceReference(refstr)
				mask = eServiceReference.isDirectory
				if config.usage.multibouquet.value:
					bouquets = serviceHandler.list(bouquetroot)
					if bouquets:
						while True:
							s = bouquets.getNext()
							if not s.valid():
								break
							if s.flags & mask:
								info = serviceHandler.info(s)
								if info:
									bouquetlist.append((info.getName(s), s))
				else:
					info = serviceHandler.info(bouquetroot)
					if info:
						bouquetlist.append((info.getName(bouquetroot), bouquetroot))

				# Get all services
				mask = (eServiceReference.isMarker | eServiceReference.isDirectory)
				for name, bouquet in bouquetlist:
					if not bouquet.valid(): #check end of list
						break
					if bouquet.flags & eServiceReference.isDirectory:
						services = serviceHandler.list(bouquet)
						if not services is None:
							while True:
								service = services.getNext()
								if not service.valid(): #check end of list
									break
								if not (service.flags & mask):
									test.append( (service.toString(), 0, -1, -1 ) )

			if test:
				# Get all events
				#  eEPGCache.lookupEvent( [ format of the returned tuples, ( service, 0 = event intersects given start_time, start_time -1 for now_time), ] )
				test.insert(0, 'RITBDSE')
				allevents = epgcache.lookupEvent(test) or []

				# Filter events
				for serviceref, eit, name, begin, duration, shortdesc, extdesc in allevents:
					if match in (shortdesc if casesensitive else shortdesc.lower()) \
						or match in (extdesc if casesensitive else extdesc.lower()):
						epgmatches.append( (serviceref, eit, name, begin, duration, shortdesc, extdesc) )

		else:
			# Search EPG, default to empty list
			epgmatches = epgcache.search( ('RITBDSE', 3000, typeMap[timer.searchType], match, caseMap[timer.searchCase]) ) or []

		# Sort list of tuples by begin time 'B'
		epgmatches.sort(key=itemgetter(3))

		# Contains the the marked similar eits and the conflicting strings
		similardict = defaultdict(list)		

		# Loop over all EPG matches
		preveit = False
		for idx, ( serviceref, eit, name, begin, duration, shortdesc, extdesc ) in enumerate( epgmatches ):

#GML DEBUG
#			if name == "Football League Tonight":
#				print("GML: FLT record.  begin:", begin)
#
			eserviceref = eServiceReference(serviceref)
			evt = epgcache.lookupEventId(eserviceref, eit)
			if not evt:
				print("[AutoTimer] Could not create Event!")
				continue
			# Try to determine real service (we always choose the last one)
			n = evt.getNumOfLinkageServices()
			if n > 0:
				i = evt.getLinkageService(eserviceref, n-1)
				serviceref = i.toString()

			evtBegin = begin
			evtEnd = end = begin + duration

			# If event starts in less than 60 seconds skip it
			# if begin < time() + 60:
			# 	print ("[AutoTimer] Skipping " + name + " because it starts in less than 60 seconds")
			# 	skipped += 1
			# 	continue

			# Set short description to equal extended description if it is empty.
			if not shortdesc:
				shortdesc = extdesc

			# Convert begin time
			timestamp = localtime(begin)
			# Update timer
			timer.update(begin, timestamp)

			# Check if eit is in similar matches list
			# NOTE: ignore evtLimit for similar timers as I feel this makes the feature unintuitive
			similarTimer = False
			if eit in similardict:
				similarTimer = True
				dayofweek = None # NOTE: ignore day on similar timer
			else:
				# If maximum days in future is set then check time
				if checkEvtLimit:
					if begin > evtLimit:
						continue

				dayofweek = str(timestamp.tm_wday)

			# Check timer conditions
			# NOTE: similar matches do not care about the day/time they are on, so ignore them
			if timer.checkServices(serviceref) \
				or timer.checkDuration(duration) \
				or (not similarTimer and (\
					timer.checkTimespan(timestamp) \
					or timer.checkTimeframe(begin) \
				)) or timer.checkFilter(name, shortdesc, extdesc, dayofweek):
				continue

			if timer.hasOffset():
				# Apply custom Offset
				begin, end = timer.applyOffset(begin, end)
				offsetBegin = timer.offset[0]
				offsetEnd   = timer.offset[1]
			else:
				# Apply E2 Offset
				begin -= config.recording.margin_before.value * 60
				end += config.recording.margin_after.value * 60
				offsetBegin = config.recording.margin_before.value * 60
				offsetEnd   = config.recording.margin_after.value * 60

			# Overwrite endtime if requested
			if timer.justplay and not timer.setEndtime:
				end = begin

			# Eventually change service to alternative
			if timer.overrideAlternatives:
				serviceref = timer.getAlternative(serviceref)

			# Append to timerlist and abort if simulating
			timers.append((name, begin, end, serviceref, timer.name))
			if simulateOnly:
				continue

			# Check for existing recordings in directory
			if timer.avoidDuplicateDescription == 3:
				# Reset movie Exists
				movieExists = False

				if dest and dest not in moviedict:
					self.addDirectoryToMovieDict(moviedict, dest, serviceHandler)
				for movieinfo in moviedict.get(dest, ()):
#GML					if self.checkSimilarity(timer, name, movieinfo.get("name"), shortdesc, movieinfo.get("shortdesc"), extdesc, movieinfo.get("extdesc")):
					if self.checkSimilarity(timer, name, movieinfo.get("name"), shortdesc, movieinfo.get("shortdesc"), extdesc, movieinfo.get("extdesc"), start_times=(begin, movieinfo.get("begin"))):
						print("[AutoTimer] We found a matching recorded movie, skipping event:", name)
						movieExists = True
						break
				if movieExists:
					continue

			# Initialize
			newEntry = None
			oldExists = False

			# Check for double Timers
			# We first check eit and if user wants us to guess event based on time
			# we try this as backup. The allowed diff should be configurable though.
			for rtimer in timerdict.get(serviceref, ()):
				if rtimer.eit == eit or (config.plugins.autotimer.try_guessing.getValue() and timeSimilarityPercent(rtimer, evtBegin, evtEnd, timer) > 80):
					oldExists = True

					# Abort if we don't want to modify timers or timer is repeated
					if config.plugins.autotimer.refresh.value == "none" or rtimer.repeated:
						print("[AutoTimer] Won't modify existing timer because either no modification allowed or repeated timer")
						break

					if eit == preveit:
						break
					
					if (evtBegin - offsetBegin != rtimer.begin) or (evtEnd + offsetEnd != rtimer.end) or (shortdesc != rtimer.description):
						if rtimer.isAutoTimer and eit == rtimer.eit:
							print ("[AutoTimer] AutoTimer %s modified this automatically generated timer." % (timer.name))
							# rtimer.log(501, "[AutoTimer] AutoTimer %s modified this automatically generated timer." % (timer.name))
							preveit = eit
						else:
							if config.plugins.autotimer.refresh.getValue() != "all":
								print("[AutoTimer] Won't modify existing timer because it's no timer set by us")
								break
							rtimer.log(501, "[AutoTimer] Warning, AutoTimer %s messed with a timer which might not belong to it: %s ." % (timer.name, rtimer.name))
						newEntry = rtimer
						modified += 1
						self.modifyTimer(rtimer, name, shortdesc, begin, end, serviceref, eit)
						# rtimer.log(501, "[AutoTimer] AutoTimer modified timer: %s ." % (rtimer.name))
						break
					else:
						print ("[AutoTimer] Skipping timer because it has not changed.")
						skipped += 1
						break
				elif timer.avoidDuplicateDescription >= 1 and not rtimer.disabled:
#GML					if self.checkSimilarity(timer, name, rtimer.name, shortdesc, rtimer.description, extdesc, rtimer.extdesc ):
					if self.checkSimilarity(timer, name, rtimer.name, shortdesc, rtimer.description, extdesc, rtimer.extdesc, start_times = (begin, rtimer.begin)):
						print("[AutoTimer] We found a timer with similar description, skipping event")
						oldExists = True
						break

			# We found no timer we want to edit
			if newEntry is None:
				# But there is a match
				if oldExists:
					continue

				# We want to search for possible doubles
				for rtimer in chain.from_iterable( itervalues(timerdict) ):
					if not rtimer.disabled:
						if self.checkDoubleTimers(timer, name, rtimer.name, begin, rtimer.begin, end, rtimer.end ):
							oldExists = True
							# print("[AutoTimer] We found a timer with same StartTime, skipping event")
							break
						if timer.avoidDuplicateDescription >= 2:
#GML							if self.checkSimilarity(timer, name, rtimer.name, shortdesc, rtimer.description, extdesc, rtimer.extdesc ):
							if self.checkSimilarity(timer, name, rtimer.name, shortdesc, rtimer.description, extdesc, rtimer.extdesc, start_times = (begin, rtimer.begin)):
								oldExists = True
								print("[AutoTimer] We found a timer (any service) with same description, skipping event")
								break

				if oldExists:
					continue

				if timer.checkCounter(timestamp):
					print("[AutoTimer] Not adding new timer because counter is depleted.")
					continue

				newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, shortdesc, eit)
				newEntry.log(500, "[AutoTimer] Try to add new timer based on AutoTimer %s." % (timer.name))

				# Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
				# It is only temporarily, after a restart it will be lost,
				# because it won't be stored in the timer xml file
				newEntry.isAutoTimer = True

			# Apply afterEvent
			if timer.hasAfterEvent():
				afterEvent = timer.getAfterEventTimespan(localtime(end))
				if afterEvent is None:
					afterEvent = timer.getAfterEvent()
				if afterEvent is not None:
					newEntry.afterEvent = afterEvent

			newEntry.dirname = timer.destination
			newEntry.justplay = timer.justplay
			newEntry.vpsplugin_enabled = timer.vps_enabled
			newEntry.vpsplugin_overwrite = timer.vps_overwrite
			tags = timer.tags[:]
			if config.plugins.autotimer.add_autotimer_to_tags.value:
				tags.append('AutoTimer')
			if config.plugins.autotimer.add_name_to_tags.value:
				tagname = timer.name.strip()
				if tagname:
					tagname = tagname[0].upper() + tagname[1:].replace(" ", "_")
					tags.append(tagname)
			newEntry.tags = tags

			if oldExists:
				# XXX: this won't perform a sanity check, but do we actually want to do so?
				recordHandler.timeChanged(newEntry)

				if renameTimer is not None and timer.series_labeling:
					renameTimer(newEntry, name, evtBegin, evtEnd)

			else:
				conflictString = ""
				if similarTimer:
					conflictString = similardict[eit].conflictString
					newEntry.log(504, "[AutoTimer] Try to add similar Timer because of conflicts with %s." % (conflictString))

				# Try to add timer
				conflicts = recordHandler.record(newEntry)

				if conflicts:
					# Maybe use newEntry.log
					conflictString += ' / '.join(["%s (%s)" % (x.name, strftime("%Y%m%d %H%M", localtime(x.begin))) for x in conflicts])
					print("[AutoTimer] conflict with %s detected" % (conflictString))

					if config.plugins.autotimer.addsimilar_on_conflict.value:
						# We start our search right after our actual index
						# Attention we have to use a copy of the list, because we have to append the previous older matches
						lepgm = len(epgmatches)
						for i in xrange(lepgm):
							servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS = epgmatches[ (i+idx+1)%lepgm ]
#GML							if self.checkSimilarity(timer, name, nameS, shortdesc, shortdescS, extdesc, extdescS, force=True ):
							if self.checkSimilarity(timer, name, nameS, shortdesc, shortdescS, extdesc, extdescS, force=True, start_times = (begin, beginS)):
								# Check if the similar is already known
								if eitS not in similardict:
									print("[AutoTimer] Found similar Timer: " + name)

									# Store the actual and similar eit and conflictString, so it can be handled later
									newEntry.conflictString = conflictString
									similardict[eit] = newEntry
									similardict[eitS] = newEntry
									similarTimer = True
									if beginS <= evtBegin:
										# Event is before our actual epgmatch so we have to append it to the epgmatches list
										epgmatches.append((servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS))
									# If we need a second similar it will be found the next time
								else:
									similarTimer = False
									newEntry = similardict[eitS]
								break

				if conflicts is None:
					timer.decrementCounter()
					new += 1
					newEntry.extdesc = extdesc
					timerdict[serviceref].append(newEntry)

					if renameTimer is not None and timer.series_labeling:
						renameTimer(newEntry, name, evtBegin, evtEnd)

					# Similar timers are in new timers list and additionally in similar timers list
					if similarTimer:
						similars.append((name, begin, end, serviceref, timer.name))
						similardict.clear()

				# Don't care about similar timers
				elif not similarTimer:
					conflicting.append((name, begin, end, serviceref, timer.name))

					if config.plugins.autotimer.disabled_on_conflict.value:
						newEntry.log(503, "[AutoTimer] Timer disabled because of conflicts with %s." % (conflictString))
						newEntry.disabled = True
						# We might want to do the sanity check locally so we don't run it twice - but I consider this workaround a hack anyway
						conflicts = recordHandler.record(newEntry)
		self.result=(new, modified, skipped)
		self.completed.append(timer.name)
		sleep(0.5)

	def JobMessage(self):
		if self.callback is not None:
			if self.simulateOnly == True:
				self.callback(self.autotimers)
			else:
				total = (self.new+self.modified+len(self.conflicting)+self.skipped+len(self.similars))
				_result = (total, self.new, self.modified, self.autotimers, self.conflicting, self.similars, self.skipped)
				self.callback(_result)
		elif self.autoPoll:
			if self.conflicting and config.plugins.autotimer.notifconflict.value:
				AddPopup(
					_("%d conflict(s) encountered when trying to add new timers:\n%s") % (len(self.conflicting), '\n'.join([_("%s: %s at %s") % (x[4], x[0], FuzzyTime(x[2])) for x in self.conflicting])),
					MessageBox.TYPE_INFO,
					15,
					CONFLICTNOTIFICATIONID
				)
			elif self.similars and config.plugins.autotimer.notifsimilar.value:
				AddPopup(
					_("%d conflict(s) solved with similar timer(s):\n%s") % (len(self.similars), '\n'.join([_("%s: %s at %s") % (x[4], x[0], FuzzyTime(x[2])) for x in self.similars])),
					MessageBox.TYPE_INFO,
					15,
					SIMILARNOTIFICATIONID
				)
		else:
			AddPopup(
				_("Found a total of %d matching Events.\n%d Timer were added and\n%d modified,\n%d conflicts encountered,\n%d unchanged,\n%d similars added.") % ((self.new+self.modified+len(self.conflicting)+self.skipped+len(self.similars)), self.new, self.modified, len(self.conflicting), self.skipped, len(self.similars)),
				MessageBox.TYPE_INFO,
				15,
				NOTIFICATIONID
			)

# Supporting functions

	def populateTimerdict(self, epgcache, recordHandler, timerdict):
		for timer in chain(recordHandler.timer_list, recordHandler.processed_timers):
			if timer and timer.service_ref:
				if timer.eit is not None:
					event = epgcache.lookupEventId(timer.service_ref.ref, timer.eit)
					extdesc = event and event.getExtendedDescription() or ''
					timer.extdesc = extdesc
				elif not hasattr(timer, 'extdesc'):
					timer.extdesc = ''
				timerdict[str(timer.service_ref)].append(timer)

	def modifyTimer(self, timer, name, shortdesc, begin, end, serviceref, eit):
		# Don't update the name, it will overwrite the name of the SeriesPlugin
		#timer.name = name
		timer.description = shortdesc
		timer.begin = int(begin)
		timer.end = int(end)
		timer.service_ref = ServiceReference(serviceref)
		timer.eit = eit

	def addDirectoryToMovieDict(self, moviedict, dest, serviceHandler):
		movielist = serviceHandler.list(eServiceReference("2:0:1:0:0:0:0:0:0:0:" + dest))
		if movielist is None:
			print("[AutoTimer] listing of movies in " + dest + " failed")
		else:
			append = moviedict[dest].append
			while 1:
				movieref = movielist.getNext()
				if not movieref.valid():
					break
				if movieref.flags & eServiceReference.mustDescent:
					continue
				info = serviceHandler.info(movieref)
				if info is None:
					continue
				event = info.getEvent(movieref)
				if event is None:
					continue
				append({
					"begin": event.getBeginTime(),  #GML
					"name": info.getName(movieref),
					"shortdesc": info.getInfoString(movieref, iServiceInformation.sDescription),
					"extdesc": event.getExtendedDescription() or '' # XXX: does event.getExtendedDescription() actually return None on no description or an empty string?
				})

#GML - trimming options are handled by an option and filters.
#
	flags_re = r"\[[^[]*?\]"
	def re_from_filters(self, filters, include_flags):
		import re
#
		filt_re = []
		for filt in filters:
			m = re.search(r"\+(\d+?)\s*$", filt)
			if m:
				num = int((m.group(0))[1:])
				strip = len(m.group(0))
				act_re = filt[:-strip]
			else:
				num = 0
				act_re = filt
			act_re = re.escape(act_re)
			if include_flags:
				act_re = self.flags_re + r"\s*" + act_re
			if num > 0:
				act_re += r".{,%d}" % num
			filt_re.append(act_re)
		return filt_re
		
	def descr_trim(self, timer, text):

# self.trimFilters values from AutoTimerEditor.py:
#   ("0", _("No")),
#   ("1", _("Flags only")),
#   ("2", _("Filters only")),
#   ("3", _("Flags and filters")),
#
# We wish to create a regex after parsing the user-supplied strings.
#
		regex = []

		if timer.trimFilters in (1, 3):  # Trim flags
			regex.append(self.flags_re)
		if timer.trimFilters in (2, 3):  # Trim filters
			if timer.include[5]:
				regex.extend(self.re_from_filters(timer.include[5], True))
			if timer.exclude[5]:
				regex.extend(self.re_from_filters(timer.exclude[5], False))

		all_rex = "(" + "|".join(regex) + ")\s*$"
		sub_made = True
		while sub_made:
			(text, sub_made) = re.subn(all_rex, '', text)
		return text

#GML

# Function to process include/exclude filters to get an on/off value for
# each day.
# NOTE: that we turn on all of the includes, then turn off all of the
# excludes.
# The "record days/eves" only apply to tgBase setting (fset=4), and will
# not affect record day handling (fset=3), so the self call is only
# one-level recursion.
#
	def map_a_week(self, timer, fset, eves=0):

		if not timer.include[fset]:
			days_on = [1]*7     # Set all 7 days on
		else:
			days_on = [0]*7     # Start with all off
			for d in timer.include[fset][:]:
				if d == "weekday":
					days_on[0:5] = [1]*5
				elif d == "weekend":
					days_on[5:7] = [1]*2
				elif d == "record days":
					days_on = map (lambda x,y: x|y, days_on, self.map_a_week(timer, 3))
				elif d == "record eves":
					days_on = map (lambda x,y: x|y, days_on, self.map_a_week(timer, 3, eves=1))
				else:
					days_on[int(d)] = 1

		if timer.exclude[fset]:
			for x in timer.exclude[fset][:]:
				if d == "weekday":
					days_on[0:5] = [0]*5
				elif x == "weekend":
					days_on[5:7] = [0]*2
				elif d == "record days":
					days_on = map (lambda x,y: x&(not y), days_on, self.map_a_week(timer, 3))
				elif d == "record eves":
					days_on = map (lambda x,y: x&(not y), days_on, self.map_a_week(timer, 3, eves=1))
				else:
					days_on[int(d)] = 0

# If we were asked for the "eves" then we need to rotate things back a day
#
		if eves > 0:
			days_on = days_on + days_on[0:1]    # Copy first to last
			del days_on[0:1]                    # Delete first
		return days_on


# A function to determine how many days to go back to get to a previous
# recordable day. A timegroup cannot start on a day when the tgBase
# is filtered out. It is allowed to be on a day when a recording
# wouldn't be done, to allow a recording starting at 00:15 to use a 
# tgBase time of 23:30 on the preceding day (hence the "eves" option).
#
# It's a function just to separate out the logic - it is only called
# once. 
#
	def get_days_back(self, timer, dow):
# If we haven't set a "filter" to specify specific days, then 
# we allow all days...
#
		if not timer.include[4] and not timer.exclude[4]:
			return 1
# We have some filtering...
# ...and we may have to do the "same" stuff twice. So it's in
# a function

		days_on = self.map_a_week(timer, 4)
#		print ("GML: [tgBase] for", timer.name, "days on:", days_on)

# We don't want to loop forever, so check we have a day on (although we
# shouldn't ever be checking a potential time if we don't, but...)
# Return something in such a case
#
		if 1 not in days_on:
			return 1

# All we want now is to determine the days back for the specific day we
# have.
# Rather then testing whether we need to loop round to the other end of
# the array, we double its length and just make a straight run.
# We know we'll hit a 1 to terminate the loop, as we've check above that
# there is one.
#
		days_on_2wk = (days_on + days_on)
		change = 0
		check_day = dow + 7  # Move into second half
		while True:
			change += 1 
			check_day -= 1
			if days_on_2wk[check_day]:
				break
		return change

#GML - end added function

	def checkSimilarity(self, timer, name1, name2, shortdesc1, shortdesc2, extdesc1, extdesc2, force=False, start_times=None):
		foundTitle = False
		foundShort = False
		retValue = False
		if name1 and name2:
#GML
#			foundTitle = ( 0.8 < SequenceMatcher(lambda x: x == " ",name1, name2).ratio() )
			foundTitle = ( timer.titleSimilarityScore <= 100*SequenceMatcher(lambda x: x == " ",name1, name2).ratio() )
#GML
		# NOTE: only check extended & short if title is a partial match
		if foundTitle:
			if timer.searchForDuplicateDescription > 0 or force:
				if shortdesc1 and shortdesc2:
#GML
#					# If the similarity percent is higher then 0.7 it is a very close match
#					foundShort = ( 0.7 < SequenceMatcher(lambda x: x == " ",shortdesc1, shortdesc2).ratio() )
# If the similarity percent is higher then what we want it is a very close match
#
# shortdesc1 and shortdesc2 are copies of the originals, so we can alter
# them here...HOWEVER - THEY APPEAR TO BE 200 chars MAX(!?!?!)
#
					if timer.trimFilters:
						shortdesc1 = self.descr_trim(timer, shortdesc1)
						shortdesc2 = self.descr_trim(timer, shortdesc2)
					foundShort = ( timer.shortDescSimilarityScore <= 100*SequenceMatcher(lambda x: x == " ",shortdesc1, shortdesc2).ratio() )
#					import traceback
#					traceback.print_stack()
#					print ("GML: titleSimilarityScore", timer.titleSimilarityScore, type(timer.titleSimilarityScore))
#					print ("GML: shortDescSimilarityScore", timer.shortDescSimilarityScore, type(timer.shortDescSimilarityScore))
#					print ("GML: longDescSimilarityScore", timer.longDescSimilarityScore, type(timer.longDescSimilarityScore))
#					print ("GML: shortdesc1:", shortdesc1)
#					print ("GML: shortdesc2:", shortdesc2)
#					print ("GML: foundShort:", foundShort)
#
# NOTE: that the timer.searchForDuplicateDescription == 3 can NEVER SUCCEED as
#       its maximum value is 2.  3 seems to be an older test.
#       Perhaps this test should be against 2 instead??
#GML
					if foundShort:
						if timer.searchForDuplicateDescription == 3:
							if extdesc1 and extdesc2:
								# Some channels indicate replays in the extended descriptions
								# If the similarity percent is higher then 0.7 it is a very close match
								retValue = ( 0.7 < SequenceMatcher(lambda x: x == " ",extdesc1, extdesc2).ratio() )
						else:
							retValue = True
			else:
				retValue = True

#GML - If we get here and retValue is True, then we might wish to switch
#      it back to False if it is in a different timegroup.
#
		if not retValue or not start_times:
			return retValue

		if not timer.useTimegroup:
			return retValue
                extent = int(timer.timegroupExtent)
		if extent <= 0:     # Sanitiy check
			return retValue

		base = timer.timegroupBase
		(earlier, later) = sorted(start_times)

		if (earlier == later):  # ?? - must be in same group
			return True

#		print ("GML: [tgBase] days are:", timer.include[4][:] if timer.include[4] else "<<undefined>>")
#
# Need to get the "first preceding" base time.
# If we are *after OR at* tgBase hh:mm today we need to check whether
# today is a valid tgBase date - which we do by checking whether we
# would be asked to go back a day tomorrow! Otherwise we find the day(s)
# back from today. 
#
		edate = [x for x in localtime(earlier)]
#		print ("GML: [edate]: Starts as", edate[6])
		if (edate[3:5] >= base[0:2]):
# Add enough hours to ensure we end up in tomoorow, but not too many as
# we don't want to end up in the day after that.
# Add more than 24 and 12 to allow for possible DLST changes.
#
			if edate[3] < 12:   # In morning
				earlier += 28*3600
			else:               # In afternoon
				earlier += 16*3600
			edate = [x for x in localtime(earlier)]
#			print ("GML: [edate]: adjusted to", edate[6])

# Now we need to get the preceding day (from edate) ***on which a recording
# would be done***.
# So find out how many days we need to go back,
# Go back that no. of days (using a mid-day offset to avoid DLST issues)
# Then put the real base hh:mm into place
#
		backup_s = 86400*self.get_days_back(timer, edate[6])
#		print ("GML: [edate]: will back up", backup_s)
		edate[3:5] = (12,0)[0:2]
		tgBase = [x for x in localtime(mktime(edate) - backup_s)]
		tgBase[3:5] = base[0:2]
#		print ("GML: [tgBase] for", timer.name, "moved to day:", tgBase[6], "at", tgBase[3:5])

		base_epoch = mktime(tgBase)
#
# Now we need to advance extent hours from tgBase.
# Again DLST rears its head.
# Someone setting 15 hours from 17:00 means 17:00-08:00. They don't
# expect it to be 07:00 on one day and 09:00 on another.  Even if it is
# only two days a year, we might as well make the logic appear human.
#
# First we figure out how many days ahead the extent will take us from
# the base. We do this just by considering the hour-offset in the base
# (as extent is hours only).
# (NOTE: 00:00 is the next day - we don't have to worry about any mm!!)
# Then we get the date structure for that day.
# Lastly we fill in the expected hh:mm
#
		total_hours = tgBase[3] + extent
		days_to_advance = total_hours/24
		end_hh = total_hours % 24

		enddate = [x for x in localtime(base_epoch)]	# A starting point
		enddate[3] = 12 				# Switch to ~midday
		end_epoch = mktime(enddate)
		end_epoch += days_to_advance*86400		# Now on correct day
		enddate = [x for x in localtime(end_epoch)]	# A closer point
		enddate[3] = end_hh				# Fix up hours
		end_epoch = mktime(enddate)			# Final endpoint

		retValue = base_epoch < later < end_epoch   # base_epoch == later can't happen
#		if retValue:
#			print ("GML: [Autotimer]", name1, "timegroup MATCHES for", start_times)
#		else:
#			print ("GML: [Autotimer]", name1, "timegroup DIFFERS for", start_times)
		return retValue

	def checkDoubleTimers(self, timer, name1, name2, starttime1, starttime2, endtime1, endtime2):
		foundTitle = name1 == name2
		foundstart = starttime1 == starttime2
		foundend = endtime1 == endtime2
		return foundTitle and foundstart and foundend
