import logging, binascii, struct

import formats.mobi as mobi

def _flatten(*data_list):
	for item in data_list:
		if type(item) == bytes:
			yield item
		elif type(item) == int:
			yield struct.pack('!L', item)
		else:
			for subitem in _flatten(*item):
				yield subitem


_FILL_FF = b'\xFF' * 4
_FILL_FFx4 = _FILL_FF * 4
_FILL_00 = b'\0' * 4

_HEADER_LEN = 0x56
_BPAR_LEN = 0x40
_BKMK_LEN = 0x3C

# first int is 1 on Kindle and 0 on Kindle for PC *shrug*
_BKMK_BOOKMARK = b'\0\0\0\1', b'\0\0\0\0\0\0\0\1' # ?, 0, 1

# TODO first int is the page id?
_BKMK_HIGHLIGHT = _FILL_00, b'\0\xff\xff\x0f\0\0\0\4' # ?, color_mark, 4
# color_mark = 0x0f | ((highlight_color << 8) in reverse byte order)
# default highlight color in k4 is 0xFFFF00

# TODO first int is the page id?
_BKMK_NOTE = _FILL_00, b'\0\0\0\x20\0\0\0\2' # ?, 32, 2


def _build_sidecar(book, guid, last_read, annotations_list):
	# compute the size of the index block beforehand
	indexes_count = 3 * len(annotations_list) + len([ s for s in annotations_list if s.kind == 'note' ])
	if last_read:
		indexes_count += 1

	# address of BPAR block
	bpar_ptr = _HEADER_LEN + indexes_count * 8 + 2
	# address of first DATA block
	data_ptr = bpar_ptr + _BPAR_LEN

	next_id = 0x1 # first block pointer number
	# technically the pointer indexes increment as annotations are added and removed, and their history is probably maintained
	# but the Kindle works fine when they are all just consecutive numbers
	# on the Kindle, they are always incrementing
	# on the Kindle for PC, they are listed in reverse order (though still incrementing as new data blocks are added)

	last_read_ptr_index = 0
	indexes = []
	data = []
	bkmk = []

	if last_read:
		logging.debug("last_read %s", last_read)
		last_read_ptr_index = 1
		indexes = [ data_ptr, 1 ]
		next_id = 2
		data = [ last_read.state ]
		data_ptr += len(last_read.state)

	# the DATA blocks are listed in timestamp ordere
	for s in annotations_list:
		if s.kind not in ('bookmark', 'highlight', 'note'):
			logging.error("ignoring unknown kind of annotation item %s", s)
			continue

		# the first data block is the state the Kindle sent with the annotation
		logging.debug("annotation %s", s)
		first_id = next_id
		next_id += 1
		indexes += data_ptr, first_id
		data.append(s.state)
		data_ptr += len(s.state)

		# on the Kindle, this block is empty
		# on the Kindle for PC, this block has a piece of text (from the book) somewhat containing the bookmark/highlight/note
		# (I'm guessing its contents are optional)
		second_id = next_id
		next_id += 1
		indexes += data_ptr, second_id
		data.append(b'DATA\0\0\0\0')
		data_ptr += 8

		if s.kind == 'note':
			# a data block with the actual note text
			# on the Kindle, it is exactly the length of the UTF-16 encoded text
			# on the Kindle for PC, it is padded with (apparently) random bytes up to 4 bytes alignment
			third_id = next_id
			next_id += 1
			indexes += data_ptr, third_id
			text = bytes(s.text, 'UTF-16BE')
			data += b'DATA', len(text), text
			data_ptr += 8 + len(text)

		if s.kind == 'bookmark':
			b = _BKMK_BOOKMARK, _FILL_FF, second_id, first_id
		elif s.kind == 'highlight':
			b = _BKMK_HIGHLIGHT, second_id, _FILL_FF, first_id
		else: # s.kind == 'note':
			b = _BKMK_NOTE, second_id, third_id, first_id
		b = (b'BKMK', _BKMK_LEN - 8, s.begin, s.end, s.pos) + b + (_FILL_FFx4, )

		bkmk.append([ s.begin, b, next_id ])
		next_id += 1

	if bkmk:
		# for some reason, the bkmk blocks are sorted by the begin position
		# both in MBPs generated on the Kindle and ones generated by Kindle for PC
		# not sure it's necessary, but...
		bkmk.sort(key = lambda b: b[0])
		_, bkmk, bkmk_ids = zip(*bkmk)
		for bid in bkmk_ids:
			indexes += data_ptr, bid
			data_ptr += _BKMK_LEN

	# extract the header title from the MOBI file (too much trouble trying to generate it)
	title = ''
	with open(book.file_path, 'rb') as mf:
		title = mf.read(27).strip(b'\0')

	mbp_created = int(book.added_to_library) # eh
	mbp_updated = last_read.timestamp if last_read else mbp_created
	if annotations_list and annotations_list[-1].timestamp > mbp_updated:
		mbp_updated = annotations_list[-1].timestamp
	# either we have a dummy last_read and _some_ annotations
	# or a proper last_read and optional annotations
	# either way, mbp_updated SHOULD not be 0 (the value of the dummy last_read)

	header = struct.pack('! 32s 4x LL 16x 8s L 2x LL 4x', title + b'_PAR', mbp_created, mbp_updated, b'BPARMOBI', next_id, 1 + indexes_count, bpar_ptr)
	# assert len(header) == _HEADER_LEN, "header length is %s" % len(header)

	last_read_page = 0 # TODO this should be the APNX page number of the last_read position?
	time_mark = mbp_updated // 60 + 0xDE177425 # ... I don't know
	bpar = ( b'BPAR', _BPAR_LEN - 8, _FILL_FF, last_read.begin, last_read_page, last_read_ptr_index,
				_FILL_FF, b'\0\0\0\x7f', guid, time_mark, _FILL_FFx4, _FILL_00, _FILL_00 )

	items = _flatten(header, indexes, b'\0\0', bpar, data, bkmk)
	return b''.join(items)

def assemble_sidecar(book, requested_guid, annotations_list):
	if not annotations_list:
		return None

	guid = mobi.read_guid(book.file_path)
	if guid is None:
		logging.error("not a MOBI book? %s", book)
		return None

	if requested_guid:
		rguid = requested_guid
		if ':' in rguid:
			_, _, rguid = rguid.partition(':')
		try:
			rguid = binascii.unhexlify(bytes(rguid, 'ascii'))
		except:
			logging.exception("failed to parse guid from %s, expected %s", requested_guid, guid)
			return None
		if guid != rguid:
			logging.error("requested guid %s does not match book guid %s", requested_guid, guid)
			return None

	# the annotations_list's first item is an optional last_read block
	# the rest of the entries are sorted by timestamp
	last_read = None if hasattr(annotations_list[0], 'kind') else annotations_list.pop(0)

	logging.debug("filtering annotations for guid %s", guid)
	# filter out entries whose guid does not match -- the clients will ignore them anyway
	annotations_list = [ a for a in annotations_list if a.state and a.state[28:32] == guid ]
	if not last_read and not annotations_list:
		return None

	sidecar_bytes = _build_sidecar(book, guid, last_read, annotations_list)
	return ('application/x-mobipocket-sidecar', sidecar_bytes)
