import re

import copy
import settings
import logging
import psycopg2
import time

import threading
import os
import os.path

import unicodedata

import pyinotify

import random
random.seed()

# --------------------------------------------------------------

# Asshole scanlators who don't put their name in "[]"
# Fuck you people. Seriously
shitScanlators = ["rhs", "rh", "mri", "rhn", "se", "rhfk", "mw-rhs"]

chapVolRe     = re.compile(r"(?:(?:ch?|v(?:ol(?:ume)?)?|(?:ep)|(?:stage)|(?:pa?r?t)|(?:chapter)|(?:story)|(?:extra)|(?:load)|(?:log)) ?\d+)", re.IGNORECASE)
trailingNumRe = re.compile(r"(\d+$)", re.IGNORECASE)


# In a lot of situations, we don't have a series name (particularly for IRC downloads, etc...)
# This function tries to clean up filenames enough that we can then match the filename into
# the name database.
# It's crude as hell, but short of a neural net or something, it's as good as it's gonna get
# for fuzzy matching strings into the database.
# Something like levenshtein string distance might be interesting, but I'd be too concerned
# about false-positive matches. Failing to no-match occationally is FAR preferable to
# failing to wrong-match, so we fail no-match.
def guessSeriesFromFilename(inStr):
	inStr = inStr.lower()
	inStr = removeBrackets(inStr)

	# if there is a "." in the last 6 chars, it's probably an extension. remove it.
	if "." in inStr[-6:]:
		inStr, dummy_ext = inStr.rsplit(".", 1)

	# Strip out scanlator name strings for scanlators who are assholes and don't bracket their group name.
	for shitScanlator in shitScanlators:
		if inStr.lower().endswith(shitScanlator.lower()):
			inStr = inStr[:len(shitScanlator)*-1]

	inStr = inStr.replace("+", " ")
	inStr = inStr.replace("_", " ")
	inStr = inStr.replace("the4koma", " ")
	inStr = inStr.replace("4koma", " ")

	inStr = stripChapVol(inStr)

	inStr = inStr.strip()
	inStr = stripTrailingNumbers(inStr)

	inStr = prepFilenameForMatching(inStr)
	return inStr

def stripChapVol(inStr):
	inStr = chapVolRe.sub(" ", inStr)
	return inStr

def stripTrailingNumbers(inStr):
	inStr = trailingNumRe.sub(" ", inStr)
	return inStr

# Execution time of ~ 0.000052889607680 second (52 microseconds)
def prepFilenameForMatching(inStr):
	# inStr = cleanUnicode(inStr)
	inStr = makeFilenameSafe(inStr)
	inStr = sanitizeString(inStr)
	return inStr.lower()

def makeFilenameSafe(inStr):

	# FUCK YOU SMART-QUOTES.
	inStr = inStr.replace("“",  " ") \
				 .replace("”",  " ")

	inStr = inStr.replace("%20", " ") \
				 .replace("<",  " ") \
				 .replace(">",  " ") \
				 .replace(":",  " ") \
				 .replace("\"", " ") \
				 .replace("/",  " ") \
				 .replace("\\", " ") \
				 .replace("|",  " ") \
				 .replace("?",  " ") \
				 .replace("*",  " ") \
				 .replace('"', " ")

	# zero-width space bullshit (goddammit unicode)
	inStr = inStr.replace("\u2009",  " ") \
				 .replace("\u200A",  " ") \
				 .replace("\u200B",  " ") \
				 .replace("\u200C",  " ") \
				 .replace("\u200D",  " ") \
				 .replace("\uFEFF",  " ")

	# Collapse all the repeated spaces down.
	while inStr.find("  ")+1:
		inStr = inStr.replace("  ", " ")


	# inStr = inStr.rstrip(".")  # Windows file names can't end in dot. For some reason.
	# Fukkit, disabling. Just run on linux.

	inStr = inStr.rstrip("! ")   # Clean up trailing exclamation points
	inStr = inStr.strip(" ")    # And can't have leading or trailing spaces

	return inStr


# I have a love-hate unicode relationship. I'd /like/ to normalize everything, but doing
# so breaks more then it fixes. Arrrrgh.
def cleanUnicode(inStr):
	return unicodedata.normalize("NFKD", inStr).encode("ascii", errors="replace").decode()


bracketStripRe = re.compile(r"(\[[\+\~\-\!\d\w &:]*\])")

def removeBrackets(inStr):
	inStr = bracketStripRe.sub(" ", inStr)
	while inStr.find("  ")+1:
		inStr = inStr.replace("  ", " ")
	return inStr

# Basically used for dir-path cleaning to prep for matching, and not much else
def sanitizeString(inStr, flatten=True):
	baseName = inStr
	if flatten:
		# Adding "-" processing.
		baseName = baseName.replace("-", " ")
		baseName = baseName.replace("!", " ")

		baseName = baseName.replace("~", "")		 # Spot fixes. We'll see if they break anything
		baseName = baseName.replace(".", "")
		baseName = baseName.replace(";", "")
		baseName = baseName.replace(":", "")
		baseName = baseName.replace("-", "")
		baseName = baseName.replace("?", "")
		baseName = baseName.replace('"', "")
		baseName = baseName.replace("'", "")

	# Bracket stripping has to be done /after/ special chars are cleaned,
	# otherwise, they can break the regex.
	baseName = removeBrackets(baseName)				#clean brackets

	# baseName = baseName.replace("'", "")
	while baseName.find("  ")+1:
		baseName = baseName.replace("  ", " ")

	# baseName = unicodedata.normalize('NFKD', baseName).encode("ascii", errors="ignore")  # This will probably break shit


	return baseName.lower().strip()

def extractRating(inStr):
	# print("ExtractRating = '%s', '%s'" % (inStr, type(inStr)))
	search = re.search(r"^(.*?)\[([~+\-!]+)\](.*?)$", inStr)
	if search:
		# print("Found rating! Prefix = {pre}, rating = {rat}, postfix = {pos}".format(pre=search.group(1), rat=search.group(2), pos=search.group(3)))
		return search.group(1), search.group(2), search.group(3)
	else:
		return inStr, "", ""

def ratingStrToInt(inStr):


	pos = inStr.count("+")
	neg = inStr.count("-")

	return pos - neg

def ratingStrToFloat(inStr):

	pos = inStr.count("+")
	neg = inStr.count("-")
	half = inStr.count("~")

	return (pos - neg) + (half * 0.5)

def extractRatingToFloat(inStr):
	dummy, rating, dummy = extractRating(inStr)
	if not rating:
		return 0
	return ratingStrToFloat(rating)



def floatToRatingStr(newRating):

	# print("Rating change call!")
	newRating, remainder = int(newRating), int((newRating%1)*2)
	if newRating > 0 and newRating <= 5:
		ratingStr = "+"*newRating
	elif newRating == 0:
		ratingStr = ""
	elif newRating < 0 and newRating > -6:
		ratingStr = "-"*abs(newRating)
	else:
		raise ValueError("Invalid rating value: %s!", newRating)
	if remainder:
		ratingStr += "~"

	return ratingStr


def isProbablyImage(fileName):
	imageExtensions = [".jpeg", ".jpg", ".gif", ".png", ".apng", ".svg", ".bmp"]
	fileName = fileName.lower()
	for ext in imageExtensions:
		if fileName.endswith(ext):
			return True

	return False

def extractChapterVol(inStr):

	# Becuase some series have numbers in their title, we need to preferrentially
	# chose numbers preceeded by known "chapter" strings when we're looking for chapter numbers
	# and only fall back to any numbers (chpRe2) if the search-by-prefix has failed.
	chpRe1 = re.compile(r"(?<!volume)(?<!vol)(?<!v)(?<!of)(?<!season) ?(?:chapter |ch|c)(?: |_|\.)?(\d+)", re.IGNORECASE)
	chpRe2 = re.compile(r"(?<!volume)(?<!vol)(?<!v)(?<!of)(?<!season) ?(?: |_)(?: |_|\.)?(\d+)", re.IGNORECASE)
	volRe = re.compile(r"(?: |_|\-)(?:volume|vol|v|season)(?: |_|\.)?(\d+)", re.IGNORECASE)

	chap = None
	for chRe in [chpRe1, chpRe2]:
		chapF = chRe.findall(inStr)
		if chapF:
			chap  = float(chapF.pop(0)) if chapF else None
		if chap != None:
			break

	volKey = volRe.findall(inStr)
	vol    = float(volKey.pop(0))  if volKey    else None

	chap   = chap if chap != None else 0.0
	vol    = vol  if vol  != None else 0.0

	return chap, vol



# ------------------------------------------------------


# proxy that makes a DB look like a dict
# Opens a dynamically specifiable database, though the database must be one of a predefined set.
class MtNamesMapWrapper(object):

	log = logging.getLogger("Main.NSLookup")

	modes = {
		"buId->fsName" : {"cols" : ["buId", "fsSafeName"], "table" : 'munamelist',  'failOnMissing' : False},
		"buId->name"   : {"cols" : ["buId", "name"],       "table" : 'munamelist',  'failOnMissing' : False},
		"fsName->buId" : {"cols" : ["fsSafeName", "buId"], "table" : 'munamelist',  'failOnMissing' : False},
		"buId->buName" : {"cols" : ["buId", "buName"],     "table" : 'mangaseries', 'failOnMissing' : False},
		"buName->buId" : {"cols" : ["buName", "buId"],     "table" : 'mangaseries', 'failOnMissing' : False}
	}

	loaded = False
	# special class members that are picked up by the maintenance service, and used to trigger periodic updates from the DB
	# TL;DR magical runtime-introspection bullshit. Basically, if there is an
	# object defined in this file's namespace, with the `NEEDS_REFRESHING` attribute, the houskeeping task
	# will call {object}.refresh() every REFRESH_INTERVAL seconds.
	NEEDS_REFRESHING = True
	REFRESH_INTERVAL = 60*2.5

	# define conn to shut up pylinter
	conn = None
	def __init__(self, mode):


		self.updateLock = threading.Lock()


		self.log.info("Loading NSLookup")

		if not mode in self.modes:
			raise ValueError("Specified mapping mode not valid")
		self.modeKey = mode
		self.mode = self.modes[mode]
		self.openDB()

		self.lastUpdate = 0
		self.lutItems = {}

		self.queryStr = 'SELECT %s FROM %s WHERE %s=%%s;' % (self.mode["cols"][1], self.mode["table"], self.mode["cols"][0])
		self.allQueryStr = 'SELECT %s, %s FROM %s;' % (self.mode["cols"][0], self.mode["cols"][1], self.mode["table"])
		self.log.info("Mode %s, Query %s", mode, self.queryStr)
		self.log.info("Mode %s, IteratorQuery %s",  mode, self.allQueryStr)


	def stop(self):
		self.log.info("Unoading NSLookup")
		self.closeDB()

	def refresh(self):
		self.log.info("Refresh call! for %s mapping cache.", self.modeKey)
		tmp = {}
		for key, buId in self.iteritems():
			key = key.lower()

			if not key in tmp:
				tmp[key] = set([buId])
			else:
				tmp[key].add(buId)

		self.log.info("Refresh call complete. Have %s keys", len(tmp))
		self.lutItems = tmp

	def openDB(self):
		self.log.info( "NSLookup Opening DB...")
		try:
			self.conn = psycopg2.connect(dbname=settings.DATABASE_DB_NAME, user=settings.DATABASE_USER,password=settings.DATABASE_PASS)
		except psycopg2.OperationalError:
			self.conn = psycopg2.connect(host=settings.DATABASE_IP, dbname=settings.DATABASE_DB_NAME, user=settings.DATABASE_USER,password=settings.DATABASE_PASS)

		# self.conn.autocommit = True
		self.log.info("opened")

		with self.conn.cursor() as cur:
			cur.execute('''SELECT tablename FROM pg_catalog.pg_tables WHERE tablename='%s';''' % self.mode["table"])
			rets = cur.fetchall()

		self.conn.commit()
		if rets:
			rets = rets[0]
		if not self.mode["table"] in rets:   # If the DB doesn't exist, set it up.
			self.log.warning("DB Not setup for %s.", self.mode["table"])
			if self.mode['failOnMissing']:
				raise ValueError
			else:
				# We can't preload the dict, since it doesn't exist, so disable preloading.
				pass

	def closeDB(self):
		self.log.info( "Closing DB...")
		self.conn.close()
		self.log.info( "done")

	def iteritems(self):


		with self.conn.cursor() as cur:
			cur.execute(self.allQueryStr)
			rets = cur.fetchall()

		self.conn.commit()

		for fsSafeName, buId in rets:
			yield fsSafeName, buId


	def __getitem__(self, key):
		if not self.loaded:
			self.loaded = True
			self.refresh()

		# if we have a key filtering function, run the key through it
		if "keyfunc" in self.mode:
			key = self.mode["keyfunc"](key)

		# db is all CITEXT, so we emulate that by calling lower on ALL THE THINGS
		key = key.lower()

		if not key in self.lutItems:
			return []

		# Have to do list comprehension so we don't return the item by reference,
		# which can lead to it getting clobbered.
		return [item for item in self.lutItems[key]]

	def __contains__(self, key):

		if key in self.lutItems[key]:
			return True
		return False




class EventHandler(pyinotify.ProcessEvent):
	def __init__(self, paths):
		super(EventHandler, self).__init__()
		self.paths = {}
		for path in paths:
			self.paths[path] = False
		self.updateLock = threading.Lock()

	def process_default(self, event):
		self.updateLock.acquire()
		# print("Dir monitor detected change!", event)
		for path in self.paths.keys():
			if event.path.startswith(path):
				self.paths[path] |= True
				# print("Changed base-path = ", path)
		# print("Event path?", event.path)
		self.updateLock.release()

	def setPathDirty(self, path):
		print("Setting path '{path}' as dirty".format(path=path))
		self.updateLock.acquire()
		self.paths[path] = True
		self.updateLock.release()

	def getClearChangedStatus(self, path):

		self.updateLock.acquire()
		ret = self.paths[path]
		self.paths[path] = False
		self.updateLock.release()

		return ret


MONITORED_FS_EVENTS = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY | pyinotify.IN_MOVED_FROM | \
						pyinotify.IN_MOVED_TO | pyinotify.IN_MOVE_SELF | pyinotify.IN_MODIFY | pyinotify.IN_ATTRIB

# Caching proxy that makes a directories look like a dict.
# Does folder-name mangling to provide case-insensitivity, and provide some
# robusness to minor name variations.
class DirNameProxy(object):

	# Make it a borg class (all instances share state)
	_shared_state = {}

	log = logging.getLogger("Main.DirLookup")

	# test-mode is when the unittesting system pre-loads the dir-dict with known values,
	# so we don't have to start the dir observers (sloooow).
	# Therefore, in test-mode, we don't check if the observers exist.
	testMode = False


	def __init__(self, paths):
		self.__dict__ = self._shared_state

		self.notifierRunning = False
		self.updateLock = threading.Lock()


		self.paths = paths

		self.lastCheck = 0
		self.maxRate = 5
		self._dirDicts = {}


		# for watch in self.


	# special class members that are picked up by the maintenance service, and used to trigger periodic updates from the DB
	# Basically, if there is an object defined in this file's namespace, with the `NEEDS_REFRESHING` attribute, the houskeeping task
	# will call {object}.refresh() every REFRESH_INTERVAL seconds.
	# TL;DR magical runtime-introspection bullshit.
	NEEDS_REFRESHING = True
	REFRESH_INTERVAL = 60


	# define a few things to shut up pylinter
	wm       = None
	eventH   = None
	notifier = None
	def refresh(self):
		self.log.info("Refresh call! for dirMonitor system.")
		self.checkUpdate()
		self.log.info("DirMonitor system refreshed.")

	def observersActive(self):
		return self.notifierRunning

	def startDirObservers(self, useObservers=True):
		# Observers do not need to be started for simple use, particularly
		# for quick-scripts where the filesystem is not expected to change significantly.
		# Pass useObservers=False to avoid the significant delay
		# in allocating directory observers.


		self.notifierRunning = True
		# Used to check that the directories have been loaded.
		# Should probably be broken up into `notifierRunning` and `dirsLoaded` flags.

		if useObservers:
			if not "wm" in self.__dict__:
				self.wm = pyinotify.WatchManager(exclude_filter=lambda p: os.path.isfile(p))
				self.eventH = EventHandler([item["dir"] for item in self.paths.values()])
				self.notifier = pyinotify.ThreadedNotifier(self.wm, self.eventH)

			self.log.info("Setting up filesystem observers")
			for key in self.paths.keys():
				if not "observer" in self.paths[key]:
					self.log.info("Instantiating observer for path %s", self.paths[key]["dir"])

					self.paths[key]["observer"] = self.wm.add_watch(self.paths[key]["dir"], MONITORED_FS_EVENTS, rec=True)


				else:
					self.log.info("WARNING = DirNameProxy Instantiated multiple times!")

			self.notifier.start()
			self.log.info("Filesystem observers initialized")
			self.log.info("Loading DirLookup")
		else:
			self.eventH = EventHandler([item["dir"] for item in self.paths.values()])

		self.checkUpdate(force=True)
		baseDictKeys = list(self._dirDicts.keys())
		baseDictKeys.sort()

	def stop(self):
		# Only stop once (should prevent on-exit errors)
		if self.notifierRunning:
			self.log.info("Unoading DirLookup")
			self.notifier.stop()
			self.notifierRunning = False

	def getDirDict(self, dlPath):

		self.log.info( "Loading Output Dirs for path '%s'...", dlPath)
		if not os.path.exists(dlPath):
			raise ValueError("Download path %s does not exist?" % dlPath)
		targetContents = os.listdir(dlPath)
		targetContents.sort()
		#self.log.info( "targetContents", targetContents)
		targets = {}
		for dirPath in targetContents:
			fullPath = os.path.join(dlPath, dirPath)
			if os.path.isdir(fullPath):
				baseName = getCanonicalMangaUpdatesName(dirPath)
				baseName = prepFilenameForMatching(baseName)

				if baseName in targets:
					print("ERROR - Have muliple entries for directory!")
					print("Current dir = '%s'" % fullPath)
					print("Other   dir = '%s'" % targets[baseName])
					# raise ValueError("Have muliple entries for directory!")

				targets[baseName] = fullPath

			# print("Linking '%s' to '%s'" % (fullPath, baseName))
		self.log.info( "Done")


		return targets

	def manuallyLoadDirDict(self, dirItems):
		tmp = {}
		self.testMode = True
		for name in dirItems:

			baseName = getCanonicalMangaUpdatesName(name)
			baseName = prepFilenameForMatching(baseName)
			tmp[baseName] = name

		self._dirDicts[0] = tmp


	def checkUpdate(self, force=False, skipTime=False):

		updateTime = time.time()
		if not updateTime > (self.lastCheck + self.maxRate) and (not force) and (not skipTime):
			print("DirDicts not stale!")
			return
		self.updateLock.acquire()

		self.lastCheck = updateTime

		keys = list(self.paths.keys())
		keys.sort()
		# print("Keys = ", keys)
		# print("DirNameLookup checking for changes (force=%s)!" % force)

		for key in keys:
			# Only query the filesystem at most once per *n* seconds.
			if updateTime > self.paths[key]["lastScan"] + self.paths[key]["interval"] or force or skipTime:
				changed = self.eventH.getClearChangedStatus( self.paths[key]["dir"])

				if changed or force:
					self.log.info("DirLookupTool updating %s, path=%s!", key, self.paths[key]["dir"])
					self.log.info("DirLookupTool updating from Directory")
					self._dirDicts[key] = self.getDirDict(self.paths[key]["dir"])
					self.paths[key]["lastScan"] = updateTime

		self.updateLock.release()

	# Force the update of the directory containing the passed path dirPath
	# Useful for when programmatic changes are made, such as creating a directory, and
	# you want to force that change to be recognized in the dir proxy immediately.
	# This is needed because the change-watching mechanism doesn't always seem
	# to properly catch folder creation or manipulation.
	# It works great for file changes.
	def forceUpdateContainingPath(self, dirPath):

		self.updateLock.acquire()

		keys = list(self.paths.keys())
		keys.sort()
		for key in keys:
			if self.paths[key]["dir"] in dirPath:
				self.log.info("DirLookupTool updating %s, path=%s!", key, self.paths[key]["dir"])
				self.log.info("DirLookupTool updating from Directory")
				self._dirDicts[key] = self.getDirDict(self.paths[key]["dir"])
				self.paths[key]["lastScan"] = time.time()

		self.updateLock.release()

	def changeRating(self, mangaName, newRating):
		item = self[mangaName]
		if not item['fqPath']:
			raise ValueError("Invalid item")

		print("Item", item)
		print("Path", self.paths[item['sourceDict']]['dir'])
		oldPath = item['fqPath']
		self.changeRatingPath(oldPath, newRating)

	def _checkLookupNewDir(self, fromPath):
		for key in settings.ratingsSort["fromkey"]:
			if fromPath.startswith(settings.mangaFolders[key]["dir"]):
				fromBase = settings.mangaFolders[key]["dir"]
				toBase   = settings.mangaFolders[settings.ratingsSort["tokey"]]["dir"]
				print("Replacing base '%s with base '%s" % (fromBase, toBase))
				return fromPath.replace(fromBase, toBase)

		# If we don't have a directory we want to replace, we just return the string as passed
		return fromPath

	def changeRatingPath(self, oldPath, newRating):

		tmpPath = oldPath
		if hasattr(settings, "ratingsSort"):
			if newRating >= settings.ratingsSort["thresh"]:
					tmpPath = self._checkLookupNewDir(oldPath)

		prefix, dummy_rating, postfix = extractRating(tmpPath)

		if newRating == 0:
			return

		ratingStr = floatToRatingStr(newRating)

		if len(ratingStr):
			ratingStr = " [{rat}] ".format(rat=ratingStr)

		newPath = "{pre}{rat}{pos}".format(pre=prefix, rat=ratingStr, pos=postfix)
		newPath = newPath.rstrip(" ").lstrip(" ")

		# print("Oldpath = ", oldPath)
		# print("Newpath = ", newPath)
		if oldPath != newPath:
			if os.path.exists(newPath):
				raise ValueError("New path exists already!")
			else:
				os.rename(oldPath, newPath)
				if self.notifierRunning:
					self.eventH.setPathDirty(os.path.split(oldPath)[0])
					print("Calling checkUpdate")
					self.checkUpdate(skipTime=True)
					print("checkUpdate Complete")


	def filterPreppedNameThroughDB(self, name):
		if not self.notifierRunning and self.testMode == False:
			self.log.warning("Directory observers not started! No directory contents will have been loaded!")
		name = getCanonicalMangaUpdatesName(name)
		name = prepFilenameForMatching(name)
		return name

	def getPathByKey(self, key):
		return self.paths[key]

	def getDirDicts(self):
		return self._dirDicts

	def getRawDirDict(self, key):
		return self._dirDicts[key]

	def getFromSpecificDict(self, dictKey, itemKey):
		filteredKey = self.filterPreppedNameThroughDB(itemKey)
		if not filteredKey:
			return {"fqPath" : None, "item": None, "inKey" : None, "dirKey": filteredKey, "rating": None, "sourceDict": None}

		# print("ItemKey", itemKey, filteredKey)
		# print("Key = ", dictKey, filteredKey,  filteredKey in self._dirDicts[dictKey])
		if filteredKey in self._dirDicts[dictKey]:
			tmp = self._dirDicts[dictKey][filteredKey]
			return self._processItemIntoRet(tmp, itemKey, filteredKey, dictKey)

		return {"fqPath" : None, "item": None, "inKey" : None, "dirKey": filteredKey, "rating": None, "sourceDict": None}


	def whichDictContainsKey(self, itemKey):
		baseDictKeys = list(self._dirDicts.keys())
		baseDictKeys.sort()
		for dirDictKey in baseDictKeys:
			if itemKey in self._dirDicts[dirDictKey]:
				return dirDictKey
		return False

	def iteritems(self):
		# self.checkUpdate()

		baseDictKeys = list(self._dirDicts.keys())
		baseDictKeys.sort()
		for dirDictKey in baseDictKeys:
			keys = list(self._dirDicts[dirDictKey].keys())  # I want the items sorted by name, so we have to sort the list of keys, and then iterate over that.
			keys.sort()

			for key in keys:
				item = self[key]

				# Inject the key we're iterating from, so we can see if we're fetching an item from a different/the wrong dict
				# when doing the actual lookup
				item['iterKey'] = dirDictKey
				yield key, item

	def _processItemIntoRet(self, item, origKey, filteredKey, dirDictKey):
		dummy_basePath, dirName = os.path.split(item)
		dummy_prefix, rating, dummy_postfix = extractRating(dirName)
		ret = {"fqPath" : item, "item": dirName, "inKey" : origKey, "dirKey": filteredKey, "rating": rating, "sourceDict": dirDictKey}
		return ret

	def getTotalItems(self):
		items = 0
		for item in self._dirDicts.values():
			items += len(item)
		return items

	def random(self):
		items = self.getTotalItems()

		# Special-case for no items, return nothing.
		if items == 0:
			return {"fqPath" : None, "item": None, "inKey" : None, "dirKey": 'None', "rating": None, "sourceDict": None}
		index = random.randint(0, items-1)
		# print("Getting random item with indice", index)
		return self.getByIndex(index)

	def getByIndex(self, index):
		if index < 0 or index >= self.getTotalItems():
			raise ValueError("Index value exceeds allowable range - %s" % index)
		for dummy_key, itemSet in self._dirDicts.items():
			if index >= len(itemSet):
				index -= len(itemSet)
				continue
			else:
				item = itemSet[list(itemSet.keys())[index]]
				dummy_basePath, dirName = os.path.split(item)
				# print("Selected item with dirPath: ", item)
				filteredKey = prepFilenameForMatching(dirName)
				return self[filteredKey]
		raise ValueError("Exceeded valid range?")

	def __getitem__(self, key):
		# self.checkUpdate()
		if len(key.strip()) == 0:
			return {"fqPath" : None, "item": None, "inKey" : None, "dirKey": key, "rating": None, "sourceDict": None}

		filteredKey = self.filterPreppedNameThroughDB(key)
		if not filteredKey:
			return {"fqPath" : None, "item": None, "inKey" : None, "dirKey": filteredKey, "rating": None, "sourceDict": None}

		baseDictKeys = list(self._dirDicts.keys())
		baseDictKeys.sort()


		for dirDictKey in baseDictKeys:
			if filteredKey in self._dirDicts[dirDictKey]:
				tmp = self._dirDicts[dirDictKey][filteredKey]
				return self._processItemIntoRet(tmp, key, filteredKey, dirDictKey)

		return {"fqPath" : None, "item": None, "inKey" : key, "dirKey": filteredKey, "rating": None}

	def __contains__(self, key):
		# self.checkUpdate()

		key = self.filterPreppedNameThroughDB(key)

		if not key:
			return {"fqPath" : None, "item": None, "inKey" : None, "dirKey": key, "rating": None, "sourceDict": None}

		baseDictKeys = list(self._dirDicts.keys())
		baseDictKeys.sort()
		for dirDictKey in baseDictKeys:

			# Limit scanned items to < 100
			if dirDictKey > 99:
				continue

			if key in self._dirDicts[dirDictKey]:
				return key in self._dirDicts[dirDictKey]

		return False

	def __len__(self):
		ret = 0
		for dirDictKey in self._dirDicts.keys():
			ret += len(self._dirDicts[dirDictKey])
		return ret



## If we have the series name in the synonym database, look it up there, and use the ID
## to fetch the proper name from the MangaUpdates database
def getCanonicalMangaUpdatesName(sourceSeriesName):

	mId = getMangaUpdatesId(sourceSeriesName)
	canon = getCanonNameByMuId(mId)
	if canon:
		return canon
	return sourceSeriesName

muIdRegex = re.compile(r'\[MuId (\d+)\]')

## If we have the series name in the synonym database, look it up there, and use the ID
## to fetch the proper name from the MangaUpdates database
def getMangaUpdatesId(sourceSeriesName):

	# Allow the Id Override tag in the dirname to hard-code the Id.
	idS = muIdRegex.search(sourceSeriesName)
	if idS:
		return idS.group(1)

	fsName = prepFilenameForMatching(sourceSeriesName)
	if not fsName:
		return False

	mId = buIdLookup[fsName]
	if mId and len(mId) == 1:
		return mId.pop()
	return False


def getCanonNameByMuId(muId):

	if muId:
		correctSeriesName = idLookup[muId]
		if correctSeriesName and len(correctSeriesName) == 1:
			return correctSeriesName.pop()
	return None

def getAllMangaUpdatesIds(sourceSeriesName):

	fsName = prepFilenameForMatching(sourceSeriesName)
	if not fsName:
		return False

	mId = buIdLookup[fsName]
	return mId




## If we have the series name in the synonym database, look it up there, and use the ID
## to fetch the proper name from the MangaUpdates database
def haveCanonicalMangaUpdatesName(sourceSeriesName):

	mId = getMangaUpdatesId(sourceSeriesName)

	if mId:
		return True
	# mId = buIdFromName[sourceSeriesName]
	# if mId and len(mId) == 1:
	# 	return True
	return False


buIdLookup       = MtNamesMapWrapper("fsName->buId")
buSynonymsLookup = MtNamesMapWrapper("buId->name")
idLookup         = MtNamesMapWrapper("buId->buName")
buIdFromName     = MtNamesMapWrapper("buName->buId")

dirNameProxy     = DirNameProxy(settings.mangaFolders)




def testNameTools():
	import unittest


	class TestSequenceFunctions(unittest.TestCase):

		def setUp(self):
			dirNameProxy.startDirObservers()

		def test_name_001(self):
			self.assertTrue("Danshi Koukousei no Nichijou" in dirNameProxy)




	unittest.main()