
"""
    genonets_reader
    ~~~~~~~~~~~~~~~

    Parser for the genonets input file format.

    :author: Fahad Khalid
    :license: MIT, see LICENSE for more details.
"""

import csv

from genonets_exceptions import GenonetsError
from genonets_constants import ErrorCodes, SupportedAlphabet


class InReader :
	# Required input file column headers
	COL_HEADERS = ["Genotypeset", "Delta", "Genotype", "Score"]

	# Uses the given DictReader to construct a dictionary from file with,
	# key=Genotypeset, value=dict{key=sequence, value=score}
	# Returns the dictionary
	# TODO: This function can be extend to also return a reverse dictionary
	#		with key=score, value=sequence ...
	@staticmethod
	def buildDataDicts(inFilePath, tau, alphabetType) :
		# Dicts to be returned
		dataDict = {}
		deltaDict = {}
		sequences = []

		# Genotype length to be determined
		seqLength = 0

		# Get handles to the input file and a DictReader for the file
		reader, inFile = InReader.getDictReaderForFile(inFilePath)

		# Check if all the required column headers are available in the file
		if not InReader.reqHdrsArePresent(reader.fieldnames) :
			inFile.close()

			print("Error: " + 
				ErrorCodes.getErrDescription(ErrorCodes.INCONSISTENT_HEADER))

			raise GenonetsError(ErrorCodes.INCONSISTENT_HEADER)

		# For each data row in the file
		for row in reader :
			# Check for missing values in this row
			if any(row[col] in (None, "") for col in row.keys()) :
				inFile.close()

				lineNum = str(int(reader.line_num))

				print("Error: " + 
					ErrorCodes.getErrDescription(ErrorCodes.MISSING_VALUE) +
					": Line No. " + lineNum)

				raise GenonetsError(ErrorCodes.MISSING_VALUE, 
					"Line No. " + lineNum)

			# Get the fitness score
			try :
				score = float(row["Score"])
			except :
				inFile.close()

				lineNum = str(int(reader.line_num))

				print("Error: " + 
					ErrorCodes.getErrDescription(ErrorCodes.BAD_SCORE_FORMAT) +
					": Line No. " + lineNum)

				raise GenonetsError(ErrorCodes.BAD_SCORE_FORMAT,
						"Line No. " + lineNum)

			# If the score for this sequence is greater than or equal to
			# the given threshold,
			if score >= tau :
				# If the current genotypeset has not already been added
				if row["Genotypeset"] not in dataDict :
					# Initialize dict for the genotypeset
					dataDict[row["Genotypeset"]] = {}

					# Get the delta value
					try :
						delta = float(row["Delta"])
					except :
						inFile.close()

						lineNum = str(int(reader.line_num))

						print("Error: " + 
							ErrorCodes.getErrDescription(ErrorCodes.BAD_DELTA_FORMAT) +
							": Line No. " + lineNum)

						raise GenonetsError(
							ErrorCodes.BAD_DELTA_FORMAT,
							"Line No. " + lineNum)

					deltaDict[row["Genotypeset"]] = delta

				# Get the genotype sequence
				sequence = row["Genotype"]

				# If sequence length has not been initialized yet, i.e., this is
				# the first row,
				if seqLength == 0 :
					# Set length of the current genotype as the sequence length
					# for the entire dataset
					seqLength = len(sequence)

				try :
					InReader.verifyGenotype(sequence, seqLength, alphabetType, 
						str(int(reader.line_num)))
				except Exception as e :
					inFile.close()
					raise e

				# Add sequence as key and score as value to the current
				# genotypeset
				dataDict[row["Genotypeset"]][sequence] = score

				# If the sequence has not already been read for any other
				# genotypeset,
				if sequence not in sequences :
					# Add it to the list of unique sequences found in the
					# input file
					sequences.append(sequence)

		inFile.close()

		# If no genotypes were found with score >= tau,
		if not dataDict :
			print("Error: " + 
				ErrorCodes.getErrDescription(ErrorCodes.NO_USEABLE_SCORES)
				+ ": Tau=" + str(tau))

			raise GenonetsError(
					ErrorCodes.NO_USEABLE_SCORES,
					"Tau=" + str(tau))

		# Dictionary: Key=Sequence, Value=[Genotypesets]. Reverse dictionary
		# that is used in functions like evolvability.
		seqToRepDict = InReader.getSeqToRepDict(sequences, dataDict)

		return dataDict, deltaDict, seqToRepDict, seqLength

	# Builds Dictionary: Key=Sequence, Value=[Genotypesets in which this sequence
	# exists]
	@staticmethod
	def getSeqToRepDict(sequences, dataDict) :
		seqToRepDict = {}

		# For each sequence in the given list of sequences,
		for seq in sequences :
			# Initialize the list genotypesets
			seqToRepDict[seq] = []

			# For each genotypeset in the given data dictionary,
			for rep in dataDict.keys() :
				# If the sequence exists in the genotypeset,
				if seq in dataDict[rep] :
					# Append the genotypeset name to the list of values
					# for this sequence
					seqToRepDict[seq].append(rep)

		return seqToRepDict

	# If any of the required column headers are not in the file, returns
	# False. Otherwise return True.
	@staticmethod
	def reqHdrsArePresent(colNames) :
		colsInFile = [col for col in colNames]
		
		if any(header not in colsInFile for header in InReader.COL_HEADERS) :
			return False

		return True

	@staticmethod
	def verifyGenotype(genotype, seqLen, alphabetType, lineNum) :
		# Verfiy the length
		if len(genotype) != seqLen :
			print("Error: " + 
				ErrorCodes.getErrDescription(ErrorCodes.INCONSISTENT_SEQ_LEN)
				+ ": Line No. " + lineNum)

			raise GenonetsError(
					ErrorCodes.INCONSISTENT_SEQ_LEN,
					"Line No. " + lineNum)

		# Get the alphabet corresponding to the type received as
		# argument
		alphabet = SupportedAlphabet.getAlphabet(alphabetType)

		# Verify alphabet
		if any(letter not in alphabet for letter in genotype) :
			print("Error: " + 
				ErrorCodes.getErrDescription(ErrorCodes.ALPHABET_TYPE_MISMATCH)
				+ ": Line No. " + lineNum)

			raise GenonetsError(
					ErrorCodes.ALPHABET_TYPE_MISMATCH,
					"Line No. " + lineNum)


	# Open the given CSV file, and return a handle to the dict reader
	# for the file, as well as the file handle.
	@staticmethod
	def getDictReaderForFile(fileName) :
		# Open file
		try :
			dataFile = open(fileName, 'r')
		except Exception as e:
			print("Error: " + 
				ErrorCodes.getErrDescription(ErrorCodes.UNKNOWN_PARSING_ERROR))
			
			raise GenonetsError(ErrorCodes.UNKNOWN_PARSING_ERROR)

		# Read the file into a dictionary
		reader = csv.DictReader(dataFile, delimiter="\t")

		return reader, dataFile
