import sys
import bz2
import gzip
from io import TextIOBase

class BaseReader(object):
	'''
	The base class for all sequential Readers handling different text file types. By default it (1) reads line-by-line, (2) skips blank lines, (3) skips lines starting with #, (4) rstrips the line.
	
	The file name '-' is treated as stdin. If a file ends in gz / bz2, it will use gzip / bz2 to open the file with utf-8 encoding

	Example usage:

	.. code-block:: python
	
		with BaseReader(filename) as br:
			lines = br.read_all()
		
		import io
		with BaseReader(io.StringIO("Line1\nLine2\n")) as br:
			lines = br.read_all()
		
	.. code-block:: python
	
		class ExampleNode(object):
			def __init__(self, value1, value2):
				self.value1 = value1
				self.value2 = value2
		
		class ExampleNodeReader(BaseReader):
			def __init__(self, filename):
				super(ExampleNodeReader, self).__init__(filename)
			def read(self):
				if self.line is None:
					return None
				words_array = self.line.split('\\t')
				value1 = words_array[0]
				value2 = words_array[1]
				self.proceed_next_line()
				return ExampleNode(value1, value2)
		
		filename = "SomeDocument.txt"
		with ExampleNodeReader(filename) as er:
			node = er.read()
			while node is not None:
				print(node.value1 + "\\t" + node.value2)
				node = er.read()
	
	
	'''
	
	__slots__ = "f", "_line"
	def __init__(self, arg):
		'''
		Either an arg or 
		'''
		if isinstance(arg, TextIOBase):
			self.f = arg 
		else:
			self.f = create_text_stream(arg, "r")
		self._metainfo_reader()
			
		
	def _proceed_next_line(self):
		'''
		Attempts to assign the next line to self._line. 
		It is recommended for any subclass to override and use this method on how to proceed to next line (e.g. Ignore comment line starting with #) 	
		'''
		while True:
			line = self.f.readline()
			if line == '':
				self._line = None
				break
			line = line.rstrip("\r\n") # Auto-stripping
			if line != '': # Auto comment removal. Blank lines are skipped by default
				self._line = line
				break
			
	
	def _metainfo_reader(self):
		'''
		Attempts to process meta info in the data file. 
		Subclasses can override this method to skip headers with other formats or process the comment / header lines (e.g. file format version)
		'''
		self._proceed_next_line()
	

	def read(self):
		'''
		Returns the next object
		'''
		line = self._line
		self._proceed_next_line()
		return line
	
	
	def __iter__(self):
		'''
		Create a generator for reading data
		'''
		obj = self.read()
		while obj is not None:
			yield obj
			obj = self.read()
			
	def close(self):
		'''
		Close the file
		'''
		if self.f is not sys.stdin:
			self.f.close()
	
	def __enter__(self): 
		return self
	
	def __exit__(self, type, value, traceback): 
		self.close()
	
	@classmethod
	def read_all(cls, read_all_func, *args, **kwargs):
		with cls(*args, **kwargs) as br:
			return read_all_func(br)

class BaseWriter(object):
	'''
	The base text class for all Writers handling different text file types. By default it overwrites the target file.
	
	The file name '-' is treated as stdout. If a file ends in gz / bz2, it will use gzip / bz2 to open the file with utf-8 encoding
	
	Example usage:

	.. code-block:: python
	
		with BaseWriter(filename) as bw:
			bw.write("This is a sentence written to the specified file.")
		
	.. code-block:: python
		
		with BaseWriter(create_file_stream(filename, mode="a")) as bw:
			bw.write("This is a sentence appended to the specified file")
		
	.. code-block:: python
	
		class ExampleNode(object):
			def __init__(self, value1, value2):
				self.value1 = value1
				self.value2 = value2
		
		class ExampleNodeWriter(BaseWriter):
			def __init__(self, filename):
				super(ExampleNodeWriter, self).__init__(filename)
			def _initialize_header(self):
				super(ExampleNodeWriter, self).write("#Value1\\tValue2\\n")
			def write(self, node):
				super(ExampleNodeWriter, self).write(node.value1 + "\\t" + node.value2 + "\\n")
		
		node = ExampleNode("1", "This is the sentence to be written in the specified file.")
		filename = "SomeDocument.txt"
		with ExampleNodeWriter(filename) as ew:
			ew.write(node)
		
	'''
	
	__slots__ = "f"
	def __init__(self, arg, mode="w"):
		if isinstance(arg, TextIOBase):
			self.f = arg
		else:
			self.f = create_text_stream(arg, mode)
		self._initialize_header()
	
	def _initialize_header(self):
		'''
		This method is automatically called during initialization. Override this method to initialize and output the headers before writing the content.
		'''
		pass
	
	def write(self, s):
		'''
		Writes the string to the file
		'''
		self.f.write(s)
	
	def close(self):
		'''
		Closes the file
		'''
		if self.f is not sys.stdout:
			self.f.close()
		
	def __enter__(self): 
		return self
	
	def __exit__(self, type, value, traceback): 
		self.close()

	@classmethod
	def write_all(cls, iterable, arg, *args, **kwargs):
		with cls(arg, *args, **kwargs) as bw:
			for i in iterable:
				bw.write(i)

class NullWriter(object):
	'''
	Creates a NULL writer, that does nothing when the user called the method write. This writer class is useful for optional output file. 
	

	Example usage:

	.. code-block:: python
	
		filename = None
		if filename is not None:
			bw = BaseWriter(filename)
		else:
			bw = NullWriter()
		bw.write("If filename is provided, this sentence will be written to the file. Otherwise, it will be written to nowhere")
		bw.close()
		
	'''
	def __init__(self):
		pass

	def write(self, s):
		pass
	
	def write_all(self, elements):
		pass
			
	def close(self):
		pass
		
	def __enter__(self): 
		return self
	
	def __exit__(self, type, value, traceback): 
		self.close()


def get_text_file_extension(filename, neglect_zip=True):
	'''
	Neglect the zip extension and return file extension 'txt', 'csv', etc.
	
	Only neglect one level of zip extension. "file.gz.bz2" will return 'gz'
	
	:param filename: The file name
	
	:returns: The file extension after decompression
	
	Example usage:
	
	.. code-block:: python
	
		get_text_file_extension("a.txt") # 'txt'
		
		get_text_file_extension("a.txt.gz") # 'txt'
		
		get_text_file_extension("a.csv.bz2") # 'csv'
		
		get_text_file_extension("a.txt.gz", neglect_zip = False) # 'gz'
		
		get_text_file_extension("example") # ''
	''' 
	supported_zip_extensions = [".gz", ".bz2"]
	if (neglect_zip):
		for supported_zip_extension in supported_zip_extensions:
			if filename.endswith(supported_zip_extension):
				filename = filename[0:filename.rfind(supported_zip_extension)]
				break
	
	index = filename.rfind(".")
	if index == -1: # No file extension found
		return ""
	else:
		return filename[index+1:] 
	

	
def create_text_stream(filename, mode):
	'''
	Create a text file stream of the file.
	
	Other than a file name, filename can also be:
	 
	(1) The filename "-" is interpreted as stdin or stdout.
	(2) The filename can be urllib.request.Request. 
	
	A decompressed stream is returned for compressed file (.gz or .bz2).
	
	All encodings are assumed utf-8    
	
	:param filename: The file name
	
	:returns: a TextIOBase representing the file stream
	
	Example usage:
	
	.. code-block:: python
		
		create_file_stream("file.txt", mode="r")
		create_file_stream(urllib.request.Request(url), mode="r")
	
	'''
	import urllib.request
	if isinstance(filename, urllib.request.Request):
		import io
		request = filename
		result = urllib.request.urlopen(request)
		remote_file_name = result.info().get_filename()
		stream = io.BytesIO(result.read())
		if remote_file_name.endswith(".gz"):
			f = gzip.open(stream, mode = mode + 't', encoding='utf-8')			
		elif remote_file_name.endswith(".bz2"):
			f = bz2.open(stream, mode = mode + 't', encoding='utf-8')
		else:
			f = io.TextIOWrapper(stream, encoding='utf-8')
	else:
		if filename == "-":
			if mode == 'r':
				f = sys.stdin
			else:
				f = sys.stdout
		else:
			if filename.endswith(".gz"):
				f = gzip.open(filename, mode = mode + 't', encoding='utf-8')			
			elif filename.endswith(".bz2"):
				f = bz2.open(filename, mode = mode + 't', encoding='utf-8')
			else:
				f = open(filename, mode)
	return f


# class BaseIReader(object):
# 	'''
# 	The base class for all readers of indexed-files. For a typical file types that allows
# 	Since there is no guaranteed on what are indexed. 
# 	Some files may have an additional associated index file.  
# 	'''
# 	def __init__(self, arg):
# 		'''
# 		'''
# 		pass


