


import os
import sys
import os.path
import logging
import time
import pprint
import shutil
import traceback

import settings
import pArch
import dbPhashApi as dbApi

import scanner.fileHasher

PHASH_DISTANCE_THRESHOLD = 2

BAD_PHASHES = [
	# Phash value of '0' is commonly a result of an image where there is no content, such as a blank page.
	# There are 79 THOUSAND of these in my collection. As a result, the existence check is prohibitively slow, so
	# we just short-circuit and ignore it.
	                   0,
	-9223372036854775808,    # 0x8000000000000000
]

class ArchiveProcessorException(Exception):
	pass

class DatabaseDesynchronizedError(ArchiveProcessorException):
	pass

class InvalidArchiveContentsException(ArchiveProcessorException):
	pass

class InvalidArchivePhashContentsException(InvalidArchiveContentsException):
	pass

class InvalidArchiveMd5ContentsException(InvalidArchiveContentsException):
	pass

class ProxyDbBase(object):
	def __init__(self):
		self.db = self.getDbConnection()

	# Overridden in child classes so the unit tests can redirect
	# db access to the testing database by returning a different DB
	# connection object.
	def getDbConnection(self):  # pragma: no cover
		return dbApi.PhashDbApi()

	# def convertDbIdToPath(self, inId):
	# 	return self.db.getItem(wantCols=['fsPath', "internalPath"], dbId=inId)


class ArchChecker(ProxyDbBase):
	'''
	Class to encapsulate the required object to check if `archpath` is unique.

	'''

	hasher = scanner.fileHasher.HashThread

	def __init__(self, archPath, pathNegativeFilter=None, pathPositiveFilter=None, negativeKeywords=None):
		'''
		Params:
			pathNegativeFilter (list): default =``[]``
				List of paths to exclude from matching.
				By default, and empty list, leading to all possible paths being used.
		'''

		super().__init__()
		self.negativeMaskedPaths = pathNegativeFilter or []
		self.positiveMaskedPaths = pathPositiveFilter or []
		self.negativeKeywords    = negativeKeywords   or []
		self.archPath    = archPath
		self.arch        = pArch.PhashArchive(archPath)

		self.log = logging.getLogger("Main.Deduper")
		self.log.info("ArchChecker Instantiated on '%s'", archPath)

	# If getMatchingArchives returns something, it means we're /not/ unique,
	# because getMatchingArchives returns matching files
	def isBinaryUnique(self):
		'''
		Is the archive this class was instantiated on binary unique (e.g. there
		is a byte-for-byte complete duplicate of it) elsewhere on the filesystem,
		that still exists.

		Returns:
			Boolean: True if unique, False if not.
		'''
		ret = self.getMatchingArchives()

		if len(ret):
			return False
		return True

	def isPhashUnique(self, searchDistance=None):
		'''
		Is the archive this class was instantiated on phash unique, where the
		duplicating files elsewhere on the filesystem still exist.

		Phash-unique means there are matches for each of the files in the archive,
		including searching by phash within distance `searchDistance` for any files
		that are images.

		Returns:
			Boolean: True if unique, False if not.
		'''

		if searchDistance is None:
			searchDistance=PHASH_DISTANCE_THRESHOLD

		ret = self.getPhashMatchingArchives(searchDistance, getAllCommon=False)

		if len(ret):
			return False
		return True

	def getBestBinaryMatch(self):
		'''
		Get the filesystem path of the "best" matching archive.

		"Best" is a somewhat fuzzy concept. In this case, it's assumed to be
		the archive with the largest number of images in common.
		If two archives share the same number of matching images, the larger
		of the two matching archives is selected. If they're the same size,
		the chosen archive will be chosen arbitrarily.

		Returns:
			String: Path to archive on the local filesystem. Path is verified to
				exist at time of return.
				If the current archive contains unique files, this will return a empty string.
		'''
		ret = self.getMatchingArchives()
		return self._getBestMatchingArchive(ret)

	def getBestPhashMatch(self, distance=None):
		'''
		Get the filesystem path of the "best" matching archive.

		"Best" is a somewhat fuzzy concept. In this case, it's assumed to be
		the archive with the largest number of images in common.
		If two archives share the same number of matching images, the larger
		of the two matching archives is selected. If they're the same size,
		the chosen archive will be chosen arbitrarily.

		Identical to `getBestBinaryMatch()`, except including phash-matches
		in the search for matches.

		Returns:
			String: Path to archive on the local filesystem. Path is verified to
				exist at time of return.
		'''
		if distance is None:
			distance=PHASH_DISTANCE_THRESHOLD

		ret = self.getPhashMatchingArchives(distance, getAllCommon=False)
		return self._getBestMatchingArchive(ret)

	def getSignificantlySimilarArches(self, searchDistance=None):
		'''
		This function returns a dict of lists containing archives with files in common with
		the current archive. It only operates using phash similarity metrics (as phash searches
		are intrinsically a superset of binary match similarity metrics).

		The dict keys are the number of files in common, and the list is a number of filesystem-
		paths to the intersecting archives.

		'''
		if searchDistance is None:
			searchDistance=PHASH_DISTANCE_THRESHOLD

		common = self.getPhashMatchingArchives(getAllCommon=True, searchDistance=searchDistance)

		ret = self._processMatchesIntoRet(common)

		# Now, we truncate the return common set to every item which has >
		# the mean number of items in common
		# This is a preventative measure against things like scanlators which
		# have a credit page they put EVERYWHERE, and we therefor want to
		# disregard.

		# print("Common:")
		# pprint.pprint(common)
		# print("Ret:")
		# pprint.pprint(ret)

		keys = list(ret.keys())
		if not keys:
			return ret

		mean = (sum(keys) / len(keys))
		for key in [key for key in keys if key < mean]:
			ret.pop(key)

		# And pop all items which have only one item in common
		if 1 in ret:
			ret.pop(1)

		# Sort the return, to make it deterministic
		for item in ret.values():
			item.sort()

		return ret

	def _processMatchesIntoRet(self, matches):
		'''
		This takes a dict of items where each key is a filesystem path, and the value
		is a set of internal-paths that are matched in the archive at the key filesystem path.

		It transforms that dict into another dict where the key is the number of matches
		that a filesystem path has, and the value is a list of filesystem paths that
		had `key` matches.
		'''
		ret = {}
		for key in matches.keys():
			ret.setdefault(len(matches[key]), []).append(key)

		# Make the return ordering deterministic
		for key in ret.keys():
			ret[key].sort()
		return ret

	def _shouldSkipFile(self, fileN, fileType):
		'''
		Internal method call. Is used to filter out files that are considered
		"garbage" from evaluation in the matching search. This includes things
		like the windows "Thumbs.db" file, some of the information notes generated
		by the automated ad-removal system in MangaCMS, and `__MACOSX` resource
		fork files&directory that Crapple loves to spew all over any non-HFS
		volumes.

		Returns:
			Boolean: True if the file is garbage, False if it is not.
		'''

		thumbs_file_types = [
			# So debian wheezy is so out of date their libmagick
			# doesn't appear to have the mimetype parameter.
			'Composite Document File V2 Document, No summary info',
			'application/CDFV2-corrupt',
			'application/CDFV2',
		]