# Copyright (c) 2014, 2015, 2016, 2017 Timothy Savannah under LGPL version 2.1. See LICENSE for more information.
#
# fields.compressed - Some types and objects related to compressed fields. Use in place of IRField ( in FIELDS array to activate functionality )
#


# vim: set ts=8 shiftwidth=8 softtabstop=8 noexpandtab :


import zlib
import bz2

from . import IRField, irNull

from ..compat_str import tobytes, isEmptyString


__all__ = ('COMPRESS_MODE_BZ2', 'COMPRESS_MODE_ZLIB', 'IRCompressedField')

# COMPRESS_MODE_ZLIB - Use to compress using zlib (gzip)
COMPRESS_MODE_ZLIB = 'zlib'

# All aliases for gzip compression
_COMPRESS_MODE_ALIASES_ZLIB = ('gzip', 'gz')

# COMPRESS_MODE_BZ2 - Use to compress using bz2 (bz2)
COMPRESS_MODE_BZ2 = 'bz2'

try:
	unicode
except NameError:
	unicode = str


class IRCompressedField(IRField):
	'''
		IRCompressedField - A field that automatically compresses/decompresses going to/from Redis.

		Pass this into the FIELDS array of the model to get this functionality,

		like: 
			FIELDS  = [ ..., IRCompressedField('my_compressed_field', compressMode=COMPRESS_MODE_ZLIB]
	'''

	CAN_INDEX = False

	# TODO: maybe support other compression levels. The headers change with different levels.
	#  These are for 9.
	def __init__(self, name='', compressMode=COMPRESS_MODE_ZLIB, defaultValue=irNull):
		'''
			__init__ - Create this object
		'''
		self.valueType = None
		self.defaultValue = defaultValue

		if compressMode == COMPRESS_MODE_ZLIB or compressMode in _COMPRESS_MODE_ALIASES_ZLIB:
			self.compressMode = COMPRESS_MODE_ZLIB
			self.header = b'x\xda'
		elif compressMode == COMPRESS_MODE_BZ2:
			self.compressMode = COMPRESS_MODE_BZ2
			self.header = b'BZh9'
		else:
			raise ValueError('Invalid compressMode, "%s", for field "%s". Should be one of the IndexedRedis.fields.compressed.COMPRESS_MODE_* constants.' %(str(compressMode), name))


	def getCompressMod(self):
		if self.compressMode == COMPRESS_MODE_ZLIB:
			return zlib
		if self.compressMode == COMPRESS_MODE_BZ2:
			return bz2

	def _toStorage(self, value):
		if isEmptyString(value):
			return ''

		# TODO: I don't think this next block is needed anymore..
		#   Check it out when IRCompressionTest is written
		if tobytes(value[:len(self.header)]) == self.header:
			return value


		return self.getCompressMod().compress(tobytes(value), 9)

	def _fromStorage(self, value):

		if isEmptyString(value):
			return ''

		# TODO: Check this out too, this enxt conditional probably shouldn't be here, maybe it should be an error when false..
		if issubclass(value.__class__, (bytes, str, unicode)) and tobytes(value[:len(self.header)]) == self.header:
			return self.getCompressMod().decompress(value)

		return value
	
	def _fromInput(self, value):
		return value

	def _getReprProperties(self):
		return [ 'compressMode="%s"' %(self.compressMode, ) ]

	def __new__(self, name='', compressMode=COMPRESS_MODE_ZLIB, defaultValue=irNull):
		return IRField.__new__(self, name)


# vim:set ts=8 shiftwidth=8 softtabstop=8 noexpandtab :

@@ -419,17 +419,39 @@
 tyString
+, getDefaultIREncoding
 %0A
-
 %0A%0A__all_
@@ -1128,16 +1128,199 @@
 DE_ZLIB%5D
+%0A%0A%09%09By default, after fetch the data will be encoded as %22bytes%22. If you need it to be unicode/string, use an%0A%09%09  IRFieldChain with an IRUnicodeField and an IRCompressedField together.
 %0A%09'''%0A%0A%09
@@ -2283,32 +2283,32 @@
 yString(value):%0A
-
 %09%09%09return ''%0A%0A%09%09
@@ -2297,32 +2297,485 @@
 :%0A%09%09%09return ''%0A%0A
+%0A%09%09try:%0A%09%09%09valueBytes = tobytes(value)%0A%09%09except Exception as e:%0A%09%09%09raise ValueError('Failed to convert value to bytes. If this requires a different codec than the defaultIREncoding (currently %25s), use an IRFieldChain with an IRBytesField or IRUnicodeField with the required encoding set (Depending on if you want the uncompressed value to be %22bytes%22 or %22unicode%22 type). Exception was: %3C%25s%3E %25s' %25(getDefaultIREncoding(), e.__class__.__name__, str(e)) )%0A%0A
 %09%09# TODO: I don'

Update compressed field to give a nice error message if toStorage fails to convert to bytes, on how to remedy.