# -*- coding: UTF-8 -*-
from __future__ import print_function, unicode_literals
import io, struct, sys, binascii

OPTYPE_INVALID, OPTYPE_EXPR, OPTYPE_LABEL, OPTYPE_BYTE, OPTYPE_WORD, OPTYPE_TEXTPTR, OPTYPE_2BOR2EXPR, OPTYPE_CMD0023, OPTYPE_CMD010C, OPTYPE_CMD1004FIRST, OPTYPE_CMD1004SECOND = range(11)
commands = {
	0x0000: (), # exit
	0x0001: (OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_LABEL), # jump to function from other script?
	0x0004: (OPTYPE_EXPR, OPTYPE_EXPR), # load script arg2 to slot arg1
	0x0005: (OPTYPE_EXPR,),
	0x0006: (),
	0x0007: (OPTYPE_LABEL,), # goto
	0x0008: (OPTYPE_EXPR, OPTYPE_LABEL),
	0x000A: (OPTYPE_BYTE, OPTYPE_EXPR, OPTYPE_LABEL),
	0x000B: (OPTYPE_LABEL, OPTYPE_WORD), # call function from self
	0x000C: (OPTYPE_EXPR, OPTYPE_LABEL), # jump to function from other script
	0x000D: (OPTYPE_EXPR, OPTYPE_LABEL, OPTYPE_WORD), # call function from other script
	0x000E: (), # ret
	0x000F: (OPTYPE_LABEL, OPTYPE_EXPR),
	0x0010: (OPTYPE_BYTE, OPTYPE_EXPR, OPTYPE_LABEL), # check bit flag
	0x0011: (OPTYPE_BYTE, OPTYPE_EXPR),
	0x0012: (OPTYPE_EXPR,), # set bit flag
	0x0013: (OPTYPE_EXPR,), # clear bit flag
	0x0015: (OPTYPE_BYTE, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_LABEL),
	0x0018: (OPTYPE_EXPR, OPTYPE_EXPR),
	0x0019: (OPTYPE_EXPR, OPTYPE_EXPR),
	0x001A: (),
	0x001B: (OPTYPE_EXPR, OPTYPE_LABEL),
	0x001D: (OPTYPE_EXPR,), # nop
	0x001F: (OPTYPE_EXPR,),
	0x0020: (OPTYPE_EXPR, OPTYPE_LABEL),
	0x0021: {
		0: (OPTYPE_EXPR,),
		1: (OPTYPE_EXPR,),
		2: (OPTYPE_EXPR, OPTYPE_EXPR),
	},
	0x0022: (OPTYPE_BYTE,),
	0x0023: (OPTYPE_BYTE, OPTYPE_CMD0023),
	0x0024: (OPTYPE_BYTE,),
	0x0025: (OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR),
	0x0026: (OPTYPE_EXPR,),
	0x0029: (OPTYPE_BYTE,), # nop
	0x002A: {
		0: (),
		1: (),
		2: (),
		3: (),
		4: (),
		5: (),
		6: (),
		7: (),
		8: (),
		9: (),
		0xA: (),
		0xB: (),
		0xC: (),
		0xD: (),
		0x10: (),
		0x13: (),
		0x1E: (),
		0x1F: (),
		0x28: (),
		0x29: (),
		0x2E: (),
		0x2F: (),
		0x3C: (),
		0x3D: (),
		0x46: (),
		0x47: (),
		0x50: (),
		0x51: (),
	},
	0x002E: (OPTYPE_BYTE, OPTYPE_EXPR),
	0x002F: {
		0: (),
		1: (OPTYPE_EXPR,), # set achievement
	},
	0x0030: (OPTYPE_BYTE,),
	0x0031: (OPTYPE_EXPR,),
	0x0032: (),
	0x0033: (OPTYPE_EXPR, OPTYPE_EXPR),
	0x0034: (),
	0x0035: (OPTYPE_BYTE,),
	0x0038: (OPTYPE_BYTE, OPTYPE_EXPR),
	0x003A: {
		0: (OPTYPE_EXPR, OPTYPE_EXPR),
	},
	0x003B: (OPTYPE_BYTE,),
	0x003D: (OPTYPE_EXPR, OPTYPE_EXPR), #DebugPrint?
	0x003E: (),
	0x003F: (),
	0x0042: (OPTYPE_EXPR,),
	0x0043: {
		0: (),
		1: (),
		2: (OPTYPE_EXPR,),
		3: (OPTYPE_TEXTPTR,),
		4: (OPTYPE_TEXTPTR,),
		5: (),
		6: (),
		7: (),
		0xB: (),
		0xC: (OPTYPE_EXPR,),
		0xD: (OPTYPE_TEXTPTR,),
		0xE: (OPTYPE_TEXTPTR,),
		0xF: (),
		0x10: (),
		0x11: (),
	},
	0x0044: {
		0: (),
		1: (),
	},
	0x0046: (OPTYPE_EXPR,),
	0x004C: {
		0: (OPTYPE_EXPR,),
		1: (),
	},
	0x0050: {
		0: (OPTYPE_LABEL, OPTYPE_LABEL),
		1: (),
		2: (OPTYPE_LABEL, OPTYPE_LABEL),
		3: (OPTYPE_LABEL, OPTYPE_LABEL, OPTYPE_LABEL),
		5: (),
	},
	0x0051: {
		0: (OPTYPE_EXPR,),
		1: (),
		2: (),
		3: (),
	},
	0x0053: {
		1: (OPTYPE_EXPR, OPTYPE_LABEL),
	},
	0x005F: (), # nop
	#0x0100: (OPTYPE_BYTE, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR),
	0x0102: (OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR),
	0x0105: {
		3: (OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR),
		5: (OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR),
		6: (OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR),
	},
	0x0109: {
		0: (OPTYPE_WORD,),
	},
	0x010B: (),
	0x010C: (OPTYPE_CMD010C, OPTYPE_TEXTPTR),
	0x010D: (OPTYPE_BYTE,),
	0x010E: (OPTYPE_EXPR, OPTYPE_LABEL),
	0x010F: (OPTYPE_TEXTPTR, OPTYPE_TEXTPTR),
	0x0110: {
		0: (),
		1: (),
		2: (),
		3: (),
		4: (),
		5: (),
	},
	0x0111: {
		1: (),
		2: (),
		3: (),
		5: (OPTYPE_EXPR,),
		6: (OPTYPE_EXPR,),
		7: (OPTYPE_EXPR,),
	},
	0x0114: {
		0: (),
		1: (),
	},
	0x0121: (OPTYPE_EXPR, OPTYPE_LABEL),
	0x0122: (OPTYPE_2BOR2EXPR, OPTYPE_EXPR, OPTYPE_EXPR), # play movie?
	0x0123: {
		0: (), # wait for movie?
		2: (),
		3: (),
		0x16: (), # invalid, interpreted as nop
	},
	0x0124: (OPTYPE_EXPR,),
	0x0125: {
		0: (OPTYPE_TEXTPTR,),
	},
	0x0126: (OPTYPE_2BOR2EXPR, OPTYPE_EXPR),
	0x1000: (OPTYPE_EXPR,),
	0x1001: (OPTYPE_EXPR, OPTYPE_EXPR),
	0x1002: (OPTYPE_EXPR, OPTYPE_EXPR),
	0x1004: (OPTYPE_CMD1004FIRST, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_CMD1004SECOND, OPTYPE_EXPR),
	0x1005: (OPTYPE_BYTE, OPTYPE_EXPR, OPTYPE_EXPR),
	0x1006: (OPTYPE_EXPR, OPTYPE_EXPR),
	0x100B: (), # nop
	0x100D: {
		1: (OPTYPE_EXPR, OPTYPE_LABEL),
	},
	0x1010: (OPTYPE_EXPR,),
	0x1011: (OPTYPE_EXPR,),
	0x1013: {
		0: (),
		1: (),
		4: (),
	},
	0x1014: (OPTYPE_BYTE,),
	0x101A: {
		0: (),
		1: (),
	},
	0x101B: (OPTYPE_BYTE,), # nop
	0x101D: {
		0: (),
		1: (),
	},
	0x101F: {
		0: (),
		1: (),
	},
	0x1020: {
		0: (),
		1: (),
	},
	0x1021: {
		0: (),
		3: (),
	},
	0x1022: {
		0x00: (),
		0x02: (),
		0x03: (),
		0x05: (),
		0x0A: (OPTYPE_WORD,),
		0xFF: (),
	},
	0x1023: {
		0: (OPTYPE_BYTE,),
		1: (),
		0x14: (),
	},
	0x1024: {
		0: (OPTYPE_EXPR, OPTYPE_EXPR),
		1: (),
	},
	0x1027: {
		0: (OPTYPE_EXPR,),
	},
	0x1028: (OPTYPE_EXPR,),
	0x1029: (OPTYPE_EXPR,),
	0x102A: (OPTYPE_BYTE, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR),
	0x102D: (),
	0x102E: (OPTYPE_EXPR,),
	0x1030: {
		0xA: (),
		0xB: (OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR),
	},
	0x1033: {
		1: (),
		2: (),
		3: (),
		4: (),
	},
	0x1034: {
		0: (),
		1: (),
		2: (),
		3: (),
	},
	0x1037: { # mail processing
		# each mail has 5 flags; first byte in commands 0,1,2 identifies flag, second expr is mail id
		0: (OPTYPE_BYTE, OPTYPE_EXPR), # explicitly set mail flag
		1: (OPTYPE_BYTE, OPTYPE_EXPR), # explicitly clear mail flag
		2: (OPTYPE_BYTE, OPTYPE_EXPR, OPTYPE_LABEL), # check mail flag
		3: (OPTYPE_BYTE, OPTYPE_EXPR, OPTYPE_LABEL),
		4: (OPTYPE_LABEL, OPTYPE_LABEL, OPTYPE_LABEL, OPTYPE_LABEL, OPTYPE_LABEL, OPTYPE_LABEL),
		5: (),
		6: (),
		7: (),
		8: (),
		9: (),
		0xA: (),
		0xF: (OPTYPE_EXPR,),
		0x10: (OPTYPE_EXPR,),
		0x11: (),
		0x12: (OPTYPE_EXPR, OPTYPE_EXPR),
		0x13: (),
		0x14: (OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR), # enable outgoing call to a person(s)
		0x15: (OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR), # enable outgoing mail to a person(s)
		0x16: (),
		0x17: (),
		0x18: (),
		0x19: (),
		0x1A: (OPTYPE_EXPR,),
		0x1E: (),
	},
	0x1038: {
		1: (),
		2: (),
		3: (),
		4: (),
		5: (),
		6: (),
	},
	0x103A: (OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR, OPTYPE_EXPR),
	0x103F: (OPTYPE_BYTE,),
}


def skip_expr(content, pos):
	while True:
		type_ = content[pos]
		pos += 1
		if type_ == 0:
			return pos
		if type_ & 0x80: # immediate constant
			pos += {0:0, 0x20:1, 0x40:2, 0x60:4}[type_ & 0x60] # additional bits to (type_ & 0x1F)
		pos += 1 # priority for operations; ignored byte for constants


characters = None
characters_map = None
def load_characters_data(filename, enable_cyrillic=False):
	global characters
	global characters_map
	with io.open(filename, 'rb') as f:
		characters_data = f.read()
	characters_data = [characters_data[2*i:2*i+2] for i in range(len(characters_data) // 2)]
	characters = {i:s.decode('utf-16le') for i,s in enumerate(characters_data) if s != b'\0\0'}
	overrides = {
		0x110:'ｷﾀ',
		0x1D04:'ｶｴ', 0x1D05:'ﾚ ', 0x1D06:'ﾊﾊ', 0x1D07:'ｱｯ', 0x1D08:'ｰ ',
		0x1D09:'ﾏﾀ', 0x1D0A:'ﾞｰ', 0x1D0B:'ﾁﾝ', 0x1D0C:'ｵﾜ', 0x1D0D:'ﾀ ',
		0x1D85:'А́', 0x1D86:'Е́', 0x1D87:'И́', 0x1D88:'О́', 0x1D89:'У́', 0x1D8A:'Ы́', 0x1D8B:'Э́', 0x1D8C:'Ю́', 0x1D8D:'Я́',
		0x1D8E:'а́', 0x1D8F:'е́', 0x1D90:'и́', 0x1D91:'о́', 0x1D92:'у́', 0x1D93:'ы́', 0x1D94:'э́', 0x1D95:'ю́', 0x1D96:'я́',
	}
	for x,y in overrides.items():
		characters[x] = y
	characters_map = {s:i for i,s in characters.items()}
	# Emoticons in some mails use cyrillic 'Д'/'д'.
	# We want to keep an important property encode(decode(text)) == text.
	# For cyrillic letters, we don't want to rely on emoticons
	# that happened to use cyrillic by coincidence,
	# we have a range with the full cyrillic alphabet.
	# (Well, the second requirement isn't that important,
	# but I'm the one writing this code, and I like it this way.)
	# (We could even choose another glyph that would be better suitable for emoticons
	# rather than to the regular text. Although who cares?)
	# So, we modify the mapping for emoticons glyphs to be able to map them back
	# by adding a (somewhat random) zero-width combining modifier.
	# 'Я' (the last letter in the alphabet) comes not from emoticons
	# but from CERN notes about project Z, but let's give it the same treatment.
	for code, char in ((0x175, 'Д'), (0x1D02, 'д'), (0x1CD2, 'Я')):
		if enable_cyrillic:
			characters[code] = char + '̠'
			characters_map[characters[code]] = code
		else:
			if char in characters_map:
				del characters[characters_map[char]]
				del characters_map[char]
			characters[code] = char
			characters_map[char] = code
	#characters_map['«'] = characters_map['“'] # russian opening quote vs standard opening quote
	#characters_map['»'] = characters_map['”'] # russian closing quote vs standard closing quote
	#characters_map['́'] = characters_map['´'] # zero-width combining acute accent -> acute accent


ansi = '!#$%&()*+,.0123456789:;=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]abcdefghijklmnopqrstuvwxyz'
wide = '！＃＄％＆（）＊＋，．０１２３４５６７８９：；＝＞？＠ＡＢＣＤＥＦＧＨＩＪＫＬＭＮＯＰＱＲＳＴＵＶＷＸＹＺ［］ａｂｃｄｅｆｇｈｉｊｋｌｍｎｏｐｑｒｓｔｕｖｗｘｙｚ'
def setup_for_main_text(allow_fullwidth_input):
	for a, w in zip(ansi, wide):
		characters[characters_map[w]] = a
		del characters[characters_map[a]]
		characters_map[a] = characters_map[w]
		if not allow_fullwidth_input:
			del characters_map[w]


class ScxData(object):
	def __init__(self, filename):
		with io.open(filename, 'rb') as f:
			self.content = bytearray(f.read())
		magic, self.strings_table, self.savepoints_table, self.eip = struct.unpack_from('<4sIII', self.content, 0)
		if magic != b'SC3\0':
			raise Exception("Invalid magic")
		if (self.savepoints_table - self.strings_table) % 4:
			raise Exception("Invalid strings table size")
		self.labels = struct.unpack_from('<' + str((self.eip - 0x10) // 4) + 'I', self.content, 0x10)
		if self.strings_table == self.savepoints_table:
			self.savepoints = ()
		else:
			first_text = struct.unpack_from('<I', self.content, self.strings_table)[0]
			self.savepoints = struct.unpack_from('<' + str((first_text - self.savepoints_table) // 4) + 'I', self.content, self.savepoints_table)

	def num_strings(self):
		return (self.savepoints_table - self.strings_table) // 4

	def parse_text(self, ptr):
		result = []
		ptr = struct.unpack_from('<I', self.content, self.strings_table + ptr * 4)[0]
		while self.content[ptr] != 0xFF:
			if self.content[ptr] & 0x80:
				code = ((self.content[ptr] - 0x80) << 8) | self.content[ptr + 1]
				ptr += 2
				translated_char = characters.get(code)
				if translated_char is not None:
					if not result or type(result[-1]) != type(""):
						result.append("")
					result[-1] += translated_char
				else:
					result.append(code)
			elif self.content[ptr] == 4:
				# parse_expr assuming simple integer
				ptr += 1
				code = None
				if (self.content[ptr] & 0xE0) == 0x80:
					code = self.content[ptr] & 0x1F
					if code >= 0x10:
						code -= 0x20
					ptr += 1
				elif (self.content[ptr] & 0xE0) == 0xA0:
					code = ((self.content[ptr] & 0x1F) << 8) | self.content[ptr+1]
					if code >= 0x1000:
						code -= 0x2000
					ptr += 2
				elif (self.content[ptr] & 0xE0) == 0xC0:
					code = ((self.content[ptr] & 0x1F) << 16) | self.content[ptr+1] | (self.content[ptr+2] << 8)
					if code >= 0x100000:
						code -= 0x200000
					ptr += 3
				elif (self.content[ptr] & 0xE0) == 0xE0:
					code = struct.unpack_from('<i', self.content, ptr+1)[0]
					ptr += 5
				else:
					raise Exception('text parse error')
				# self.content[ptr] is ignored? Always zero except exactly one case in SG05_17.SCX of Japanese version
				if self.content[ptr+1] != 0:
					raise Exception('text parse error')
				ptr += 2
				result.append((-1, code))
			elif self.content[ptr] in (12,17,18):
				result.append((-2, self.content[ptr], struct.unpack_from('>h', self.content, ptr+1)[0]))
				ptr += 3
			elif self.content[ptr] == 21:
				next_ptr = skip_expr(self.content, ptr + 1)
				result.append((-2, 21, self.content[ptr+1:next_ptr]))
				ptr = next_ptr
			else:
				result.append((-2, self.content[ptr]))
				ptr += 1
		return result


class OutputBuilder(object):
	def __init__(self, new_text_index):
		self.labels = []
		self.savepoints = []
		self.text_ptrs = []
		self.code = bytearray()
		self.texts = bytearray()
		self.pending_texts = []
		self.pending_text_index = new_text_index

	def add_label(self, offset):
		self.labels.append(offset)

	def add_savepoint(self, offset):
		self.savepoints.append(offset)

	def add_code(self, data):
		self.code += data

	def get_code_pos(self, pos_base):
		return len(self.code) + pos_base

	def serialize_text(self, text):
		self.text_ptrs.append(len(self.texts))
		global characters_map
		for item in text:
			if type(item) == type(""):
				pos = 0
				while pos + 1 < len(item):
					code = characters_map.get(item[pos:pos+2])
					if code is not None:
						pos += 2
					else:
						code = characters_map.get(item[pos])
						if code is None:
							logging.error("Cannot encode character: " + item[pos])
							raise Exception("character not in characters table")
						pos += 1
					self.texts.append(0x80 + (code >> 8))
					self.texts.append(code & 0xFF)
				if pos < len(item):
					code = characters_map[item[pos]]
					self.texts.append(0x80 + (code >> 8))
					self.texts.append(code & 0xFF)
			elif type(item) == tuple:
				if item[0] == -1:
					self.texts.append(4)
					if item[1] < 0:
						raise Exception("negative number in serialize_text")
					elif item[1] < 0x10:
						self.texts.append(0x80 + item[1])
					elif item[1] < 0x1000:
						self.texts.append(0xA0 + (item[1] >> 8))
						self.texts.append(item[1] & 0xFF)
					elif item[1] < 0x100000:
						self.texts.append(0xC0 + (item[1] >> 16))
						self.texts.append(item[1] & 0xFF)
						self.texts.append((item[1] >> 8) & 0xFF)
					else:
						self.texts.append(0xE0)
						self.texts.append(item[1] & 0xFF)
						self.texts.append((item[1] >> 8) & 0xFF)
						self.texts.append((item[1] >> 16) & 0xFF)
						self.texts.append(item[1] >> 24)
					self.texts.append(0)
					self.texts.append(0)
				elif item[0] == -2:
					self.texts.append(item[1])
					if item[1] in (12,17,18):
						self.texts += struct.pack('>h', item[2])
					if item[1] == 21:
						self.texts += item[2]
				else:
					raise Exception("internal error in serialize_text")
			else:
				self.texts.append(0x80 + (item >> 8))
				self.texts.append(item & 0xFF)
		self.texts.append(0xFF)
		return len(self.text_ptrs) - 1

	def save(self, f):
		if self.pending_text_index is not None and self.pending_text_index != len(self.text_ptrs):
			raise Exception("text count inconsistence: %d != %d" % (self.pending_text_index, len(self.text_ptrs)))
		for text in self.pending_texts:
			self.serialize_text(text)
		while len(self.code) % 4:
			self.code.append(0)
		code_offset = len(self.labels) * 4 + 0x10
		strings_offset = code_offset + len(self.code)
		savepoints_offset = strings_offset + 4 * len(self.text_ptrs)
		texts_offset = savepoints_offset + 4 * len(self.savepoints)
		f.write(struct.pack('<4sIII', b'SC3\0', strings_offset, savepoints_offset, code_offset))
		f.write(struct.pack('<' + str(len(self.labels)) + 'I', *self.labels))
		f.write(self.code)
		f.write(struct.pack('<' + str(len(self.text_ptrs)) + 'I', *[x + texts_offset for x in self.text_ptrs]))
		f.write(struct.pack('<' + str(len(self.savepoints)) + 'I', *self.savepoints))
		f.write(self.texts)


def text_to_string(text):
	key = ''
	pos = 0
	while pos < len(text):
		if type(text[pos]) == tuple:
			if text[pos][0] == -2:
				if text[pos][1] == 0:
					key += '[br]'
					pos += 1
				elif text[pos][1] == 1:
					if type(text[pos+1]) == type("") and text[pos+2] == (-2, 2):
						key += '[s:'
						key += text[pos+1].replace('’', '\'')
						key += ']'
						pos += 3
					else:
						raise Exception("unmatched speaker tag")
				elif text[pos][1] == 3:
					key += '[output]'
					pos += 1
				elif text[pos][1] == 8:
					key += '[reset][output]'
					pos += 1
				elif text[pos][1] == 15:
					key += '[center]'
					pos += 1
				elif text[pos][1] == 17 and text[pos][2] == 228 and text[pos+1] == (-2, 15):
					move_type = 1
					key += '[center_screen]'
					pos += 2
				elif text[pos][1] == 17:
					key += '[y:%d]' % text[pos][2]
					pos += 1
				elif text[pos][1] == 18:
					key += '[x:%d]' % text[pos][2]
					pos += 1
				elif text[pos][1] == 21:
					key += '[call:' + binascii.hexlify(text[pos][2]).decode('ascii') + ']'
					pos += 1
				elif text[pos][1] == 12:
					key += '[fontsize:%d]' % text[pos][2]
					pos += 1
				elif text[pos][1] == 9:
					key += '[link]'
					pos += 1
				elif text[pos][1] == 10:
					key += '[ruby]'
					pos += 1
				elif text[pos][1] == 11:
					key += '[/link]'
					pos += 1
				elif text[pos][1] == 14:
					key += '[tag14]'
					pos += 1
				else:
					raise Exception("unknown tag: {}".format(text[pos]))
			elif text[pos][0] == -1:
				key += '[color:%d]' % text[pos][1]
				pos += 1
			else:
				raise Exception("text error: unknown tuple: {}".format(text[pos]))
		elif type(text[pos]) == type(""):
			#key += text[pos].replace('“', '"').replace('”', '"').replace('’', '\'').replace('％', '%').replace('／', '/').replace('＿', '_').replace('‘', '\'').replace('[', '\\[').replace(']', '\\]')
			key += text[pos].replace('[', '\\[').replace(']', '\\]')
			pos += 1
		else:
			raise Exception("unknown character code: %d" % text[pos])
	return key


def find_unescaped_bracket(key, bracket, startpos):
	pos = startpos
	while True:
		pos = key.find(bracket, pos)
		if pos == -1:
			return -1
		num_slashes = 0
		while pos > startpos + num_slashes and key[pos - num_slashes - 1] == '\\':
			num_slashes += 1
		if num_slashes % 2 == 0:
			return pos
		pos += 1


def find_unescaped_bracket_r(key, bracket, endpos):
	pos = endpos
	while True:
		pos = key.rfind(bracket, 0, pos)
		if pos == -1:
			return -1
		num_slashes = 0
		while pos > num_slashes and key[pos - num_slashes - 1] == '\\':
			num_slashes += 1
		if num_slashes % 2 == 0:
			return pos
		pos -= 1

def find_speech_markers(line):
	pos = 0
	while pos < len(line) and line[pos] == '[':
		pos = find_unescaped_bracket(line, ']', pos)
		if pos == -1:
			break
		pos += 1
	if pos != -1 and pos < len(line) and line[pos] == '"':
		pos2 = len(line) - 1
		while True:
			tag_pos = find_unescaped_bracket_r(line, ']', pos2 + 1)
			if tag_pos != pos2:
				break
			pos2 = find_unescaped_bracket_r(line, '[', pos2)
			if pos2 == -1:
				break
			pos2 -= 1
		if pos2 != -1 and line[pos2] == '"':
			return pos, pos2
	return -1, -1

def string_to_text(key):
	text = []
	pos = 0
	while True:
		tag_pos = find_unescaped_bracket(key, '[', pos)
		if tag_pos == -1:
			break
		if pos < tag_pos:
			text.append(key[pos:tag_pos].replace('\\[', '[').replace('\\]', ']'))
		tag_pos2 = find_unescaped_bracket(key, ']', tag_pos)
		if tag_pos2 == -1:
			raise Exception("unclosed tag: " + key[tag_pos:])
		tag_arg_pos = key.find(':', tag_pos)
		if tag_arg_pos != -1 and tag_arg_pos < tag_pos2:
			tag = key[tag_pos+1:tag_arg_pos]
			tag_arg = key[tag_arg_pos+1:tag_pos2].replace('\\[', '[').replace('\\]', ']')
		else:
			tag = key[tag_pos+1:tag_pos2]
			tag_arg = None
		pos = tag_pos2 + 1
		if tag == 's':
			if tag_arg is None:
				raise Exception("[s:speaker] tag requires one string arg")
			text.append((-2, 1))
			text.append(tag_arg)
			text.append((-2, 2))
		elif tag == 'br':
			if tag_arg is not None:
				raise Exception("[br] tag has no arg")
			text.append((-2, 0))
		elif tag == 'center_screen':
			if tag_arg is not None:
				raise Exception("[center_screen] tag has no arg")
			text.append((-2, 17, 228))
			text.append((-2, 15))
		elif tag == 'reset':
			if tag_arg is not None:
				raise Exception("[reset] tag has no arg")
			if not key[pos:].startswith('[output]'):
				raise Exception("[reset] tag must be followed by [output]")
			pos += len('[output]')
			text.append((-2, 8))
		elif tag == 'output':
			if tag_arg is not None:
				raise Exception("[output] tag has no arg")
			text.append((-2, 3))
		elif tag == 'center':
			if tag_arg is not None:
				raise Exception("[center] tag has no arg")
			text.append((-2, 15))
		elif tag == 'y' or tag == 'x' or tag == 'fontsize':
			try:
				x = int(tag_arg)
			except Exception:
				raise Exception("[" + tag + ":value] tag requires one integer argument")
			text.append((-2, {'y':17, 'x':18, 'fontsize':12}[tag], x))
		elif tag == 'call':
			text.append((-2, 21, binascii.unhexlify(tag_arg)))
		elif tag == 'link':
			if tag_arg is not None:
				raise Exception("[link] tag has no arg")
			text.append((-2, 9))
		elif tag == 'ruby':
			if tag_arg is not None:
				raise Exception("[ruby] tag has no arg")
			text.append((-2, 10))
		elif tag == '/link':
			if tag_arg is not None:
				raise Exception("[/link] tag has no arg")
			text.append((-2, 11))
		elif tag == 'tag14':
			if tag_arg is not None:
				raise Exception("[tag14] tag has no arg")
			text.append((-2, 11))
		elif tag == 'color':
			try:
				color = int(tag_arg)
			except Exception:
				raise Exception("[color:c] tag requires one integer argument")
			text.append((-1, color))
		else:
			raise Exception("unknown tag: " + tag)
	if pos < len(key):
		text.append(key[pos:].replace('\\[', '[').replace('\\]', ']'))
	return text
