import re
import Numeric as Num
import os.path
import xwaves_lab
import die
import fiatio
import oxi_entity
import read_words
import math


class SpeakerNotFoundError(ValueError):
	def __init__(self, x):
		ValueError.__init__(self, x)


SILENCE = '*'
HUGE = 1e30

TEST = 0

_sfp = re.compile(r'((?:[][_][0-9])|(?:[^][_]*))')

# print _sfp.findall('test[1]2')
assert _sfp.findall('test[1]2')[:3] == ['test', '[1', ']2']
assert _sfp.findall('test]1[2again')[:4] == ['test', ']1', '[2', 'again']
assert _sfp.findall('test[1')[:2] == ['test', '[1']
assert _sfp.findall(']2')[:1] == [']2']


def syn_check(data):
	"""Do some checks on the label sequence."""
	o = []
	lastt = None
	lastlbl = None
	for (i, (t, lbl)) in enumerate(data):
		if lastt and abs(t-lastt) < 0.010 and lbl==lastlbl:
			die.warn('Duplicated line: %f %s: data line %d' % (t, lbl, i+1))
			continue
		elif lastt and abs(t-lastt) < 0.010:
			raise oxi_entity.SillyLabelSequence, \
				'Inconsistent duplicated line: %f %s vs. %s: data line %d' % (t, lbl, lastlbl, i+1)
		else:
			o.append( (t, lbl) )
			lastt = t
			lastlbl = lbl
	return o


def syn_pass1(data):
	"""Split up composite labels like 'foo]1[2bar' into their component parts."""
	o = []
	for (t, lbl) in data:
		if lbl == SILENCE:
			o.append( (t, SILENCE) )
		else:
			bits = _sfp.findall(lbl)
			o.extend( [ (t, x) for x in bits if x!='' ] )
	return o


def syn_pass2(data, nspkrs):
	"""Process _N labels into [N and ]N."""
	o = []
	start_of_entity = None
	tse = None
	for (t, lbl) in data:
		if lbl == SILENCE:
			start_of_entity = len(o)
			tse = t
			o.append( (t, lbl) )
		elif lbl.startswith('_'):
			s = int(lbl[1:])-1
			if s<0 or s>nspkrs:
				raise SillySpeakerNum, 'spkr=%d t=%.3f lab=%s' % (s+1, t, lbl)
			if start_of_entity is None:
				print 'SOEd', data
				print 'SOEo', o
			assert start_of_entity is not None
			o.insert(start_of_entity, (tse, '[%d' % (s+1)))
			o.append((t, ']%d' % (s+1)))
			start_of_entity = len(o)
			tse = t
		else:
			o.append( (t, lbl) )
	return o


print syn_pass2(syn_pass1([ (1.0, ''), (1.5, 'foo') ] ), 2)
assert syn_pass2(syn_pass1([ (1.0, '*'), (1.5, 'foo_1') ] ), 2
		) == [
			(1.0, '[1'), (1.0, '*'), (1.5, 'foo'), (1.5, ']1')
			]


class clause(oxi_entity.entity):
	def __init__(self, info, t0, tf, label, speakers, owner):
		oxi_entity.entity.__init__(self, info, t0, tf)
		self.label = label
		self.speakers = speakers
		self.owner = owner


	def exp_owner(self):
		if self.owner is None:
			return None
		return self.owner


	def str_label(self):
		if hasattr(self, 'utype'):
			if self.utype is None:
				utype = '?'
			else:
				utype = self.utype
			label = '%s(%s)' % (self.label, utype)
		else:
			label = self.label
		return label


	def str_speakers(self):
		x = ['%s:%g' % (k, v) for (k, v) in self.speakers.items() ]
		x.sort()
		return ','.join(x)


	def str_owner(self):
		if self.owner is None:
			owner = '?'
		else:
			owner = self.owner
		return owner


	def __str__(self):

		return '<clause: %.3f %s %s(%s) %.3f>' % (self.t0, self.str_label(),
						self.str_speakers(),
						self.str_owner(),
						self.tf)

	__repr__ = __str__


	def info(self):
		o = {'syntype': self.str_label(),
			'owner': self.str_owner(),
			'speakers': self.str_speakers(),
			'speaker': self.exp_owner()
			}

		# Deduce the sex from the owner, or if the owner is indeterminate,
		# and all speakers are the same sex...
		up_info = oxi_entity.entity.info(self)
		if int(up_info['nspeakers']) == 1:
			o['sex'] = up_info['sex']
		elif self.owner is not None:
			i = 0
			while 'speaker.%d'%i in up_info:
				if up_info['speaker.%d' % i] == self.owner:
					o['sex'] = up_info['sex.%d' % i]
				i += 1
			assert 'sex' in o, 'Owner: %s not found in meta-data for %s/%s' % (
						self.owner, up_data['d'], up_data['f']
						)

		else:
			fail = 0
			for i in range(2, int(up_info['nspeakers'])+1):
				if up_info['sex.%d' % i] != up_info['sex.1']:
					fail = 1
					break
			if not fail:
				o['sex'] = up_info['sex.1']
			else:
				o['sex'] = '?'
				die.warn("No known owner and cannot deduce sex: %s/%d: %s"
						% (up_info['d'], up_info['f'], up_info['phid']))

		if hasattr(self, 'utype'):
			o['utype'] = '%s:%s' % (self.label, self.utype)
		return o



def owner_from_floor(floor):
	nz = Num.greater(floor, 0.001)
	if Num.sum(nz) != 1:
		return None
	return Num.nonzero(nz)[0]


class OpenCloseProblem(oxi_entity.SillyLabelSequence):
	def __init__(self, s):
		oxi_entity.SillyLabelSequence.__init__(self, s)


class SillySpeakerNum(oxi_entity.SillyLabelSequence):
	def __init__(self, s):
		oxi_entity.SillyLabelSequence.__init__(self, s)

class NoFloorProblem(oxi_entity.SillyLabelSequence):
	def __init__(self, s):
		oxi_entity.SillyLabelSequence.__init__(self, s)


def syn_merge(synd, bgpd):
	DELTA = 0.08
	d = synd[:]
	for (t, lbl) in bgpd:
		if lbl.startswith('*'):
			d.append( (t, lbl) )
	d.sort()
	print 'SMs', synd
	print 'SMb', bgpd
	print 'SMd', d
	i = 0
	while i < len(d)-1:
		t, lab = d[i]
		if lab==SILENCE and d[i+1][1]==SILENCE and d[i+1][0]<t+DELTA:
			d[i] = ( 0.5*(t+d[i+1][0]), SILENCE )
			del d[i+1]
		else:
			i += 1
	return d
	



def who_has_floor_syn(uttrec, nspkrs, root, loose=0):
	"""Reads a *.syn file and produces a list,
	indicating who has the floor immediately after each mark
	in the .syn file.
	Each contains start and end times, and information on who spoke.
	"""

	# bgph, bgpd = xwaves_lab.read('%s/%s.bgp' % (root, os.path.join(uttrec['d'], uttrec['f'])))
	synh, synd = xwaves_lab.read('%s.syn'
					% os.path.join(root, uttrec['d'], uttrec['f']),
						loose=1000+loose)
	synd = read_words.empty_to_star(synd)
	# synd = syn_merge(synd, bgpd)
	# print 'synd=', synd
	synd = syn_check(synd)
	synd = syn_pass1(synd)
	synd = syn_pass2(synd, nspkrs)
	# print 'synd2=', synd
	floor = (nspkrs)*[0]
	sopen = (nspkrs)*[0]
	# First, find who has the floor at the beginning of the file.
	for (t, lab) in synd:
		if lab.startswith('['):
			s = int(lab[1:]) - 1
			if s<0 or s>=nspkrs:
				raise SillySpeakerNum, 'spkr=%d t=%.3f lab=%s' % (s+1, t, lab)
			sopen[s] = 1
		elif lab.startswith(']'):
			s = int(lab[1:]) - 1
			if s<0 or s>=nspkrs:
				raise SillySpeakerNum, 'spkr=%d t=%.3f lab=%s' % (s+1, t, lab)
			if sopen[s]:
				continue
			floor[s] = 1
			sopen[s] = 1
		if not 0 in sopen:
			break	# We've seen opens for all speakers.  Done.
	if not 1 in sopen:
		floor[0] = 1	# If no brackets, assume only one speaker.

	# print 'initial floor=', floor
	# Now, we iterate over the data to compute
	# who has the floor just after each label.
	o = []
	synde = [ (None, '', Num.array(floor)) ]
	for (t, lab) in synd:
		# print '# t,lab=', t, lab
		if lab.startswith('['):
			s = int(lab[1:]) - 1
			if floor[s] != 0:
				raise OpenCloseProblem, "Double Open: spkr=%d t=%.3f lab=%s" % (s+1, t, lab)
			floor[s] = 1
		elif lab.startswith(']'):
			s = int(lab[1:]) - 1
			if floor[s] != 1:
				raise OpenCloseProblem, "Double Close: spkr=%d t=%.3f lab=%s" % (s+1, t, lab)
			floor[s] = 0
		elif lab!=SILENCE and not 1 in floor:
			raise NoFloorProblem, "No one has the floor: t=%.3f lab=%s" % (t, lab)
		o.append( (t, lab, Num.array(floor)) )
	return o



def wd_merge(wd1, wd2):
	o = []
	for (t, lab) in wd1:
		o.append( (t, 0, lab) )
	for (t, lab) in wd2:
		o.append( (t, 1, lab) )
	o.sort()
	return o



def wd_check(wd):
	MAX = 2.5
	MIN = 0.03
	t0 = None
	for (t, lab) in wd:
		assert type(t)==type(1.0), "Bad type of t(%s)=%s" % (str(t), str(type(t)))
		if not lab.startswith('*'):
			if t0 is None:
				die.warn( 'No start to word: %g %s' % (t, lab))
			elif t-t0 > MAX or t-t0<MIN:
				raise oxi_entity.SillyLabelSequence, 'Absurd word length: %g %g %s' % (t0, t-t0, lab)
		t0 = t




def who_has_floor_wd(uttrec, nspkrs, root, loose=0):
	"""Reads *.wd[12] files and produces a tuple,
	indicating who has the floor at each moment.
	"""
	BORDER = 0.300
	DT = 0.010
	nm = os.path.join(root, uttrec['d'], uttrec['f'])
	die.note('nm', nm)
	assert nspkrs == 2
	wd1 = read_words.read_with_beginnings(nm, loose+1, suffix='wd1')
	wd_check(wd1)
	if wd1:
		wt0 = wd1[0][0]
		wtf = wd1[-1][0]
	wd2 = read_words.read_with_beginnings(nm, loose+1, suffix='wd2')
	wd_check(wd2)
	if wd2:
		if wd2[0][0] < wt0:
			wt0 = wd2[0][0]
		if wd2[-1][0] > wtf:
			wtf = wd2[-1][0]
	wt0 -= BORDER
	wtf += BORDER
	n = int(math.ceil((wtf-wt0)/DT))
	d = Num.zeros((nspkrs+1,n), Num.Int)
	for (s, wd) in enumerate([wd1, wd2]):
		lastt = wt0
		lastl = '*'
		for (t, lab) in wd:
			if lab == '#':
				li = int(math.floor((lastt-wt0)/DT))
				i = int(math.ceil((t-wt0)/DT))
				d[nspkrs, li:i] = 1
			elif not lab.startswith('*'):
				li = int(math.floor((lastt-wt0)/DT))
				i = int(math.ceil((t-wt0)/DT))
				d[s, li:i] = 1
			lastt = t
			lastl = lab
	# print 'D=', d
	return (wt0, DT, d)



def get_speaker_map(root, uttrec):
	if int(uttrec['nspeakers']) == 1:
		return [ uttrec['speaker'] ]

	assert int(uttrec['nspeakers']) == 2
	maph, mapd, mapc = fiatio.read(open('%s/speaker_table.fiat' % root, 'r'))
	o = None
	lt = None
	for t in mapd:
		s1l, s1s, s1n = uttrec['speaker.1'].split(':')
		s2l, s2s, s2n = uttrec['speaker.2'].split(':')
		if s1s == s2s:
			pair = '%s%d_%d' % (s1s, int(s1n), int(s2n))
		else:
			pair = '%s%d_%s%d' % (s1s, int(s1n), s2s, int(s2n))
		if(t['location'] == uttrec['location'] and t['pair'] == pair
				and t['task']==uttrec['task']):
			# if o is not None:
				# print 't', t
				# print 'uttrec', uttrec
				# print 'lt', lt
			assert o == None, "Ambiguous speaker mapping: %s %s" % (
					t['location'], t['pair'])
			o = [ t['speaker.1'], t['speaker.2'] ]
			lt = t
	if o is not None:
		return o
	raise SpeakerNotFoundError, str(uttrec)



def map_speakers(a, map):
	o = {}
	for (i, tmp) in enumerate(a):
		o[map[i]] = tmp
	return o


class floorregions(oxi_entity.entity_list):
	Nspkrs = 2
	DELTA = 0.1
	T0 = 0
	TF = 1
	OWNER = 2
	HUGE = 1e30

	def __init__(self, uttrec):
		oxi_entity.entity_list.__init__(self)
		if int(uttrec['nspeakers']) == 1:
			self.append( (-self.HUGE, self.HUGE, uttrec['speaker']) )
			return


	def region_index(self, t):
		"""Return an index that specifies a region
		covering time t, or None if none do.
		"""
		for (i, (t0, tf, owner)) in enumerate(self.contents):
			if t<=tf and t>=t0:
				return i
		return None


	def region_floor_owner(self, t0, tf):
		"""Does a single speaker own the floor for the entire region
		from t0 to tf?
		"""
		owner = None
		for (i, (tt0, ttf, towner)) in enumerate(self.contents):
			if tt0 > tf:
				die.info('All done with %.3f %.3f: saw %.3f %.3f %s' %
							(t0, tf, tt0, ttf, towner))
				return owner

			# conflict
			if owner is not None and owner!=towner:
				die.info('Ownership conflict: %s and %s' % (str(owner), str(towner)))
				return None

			if owner is None and t0<=ttf:
				die.info('Found owner for %.3f,%.3f : region %..3f,%.3f %s' %
							(t0, tf, tt0, ttf, towner))
				owner = towner

			if ttf>=tf:
				die.info('All done (2) with %.3f %.3f: saw %.3f %.3f %s' %
							(t0, tf, tt0, ttf, towner))
				return owner
		return owner



	def merge_and_map(self, synde, map):
		"""synde is a list of regions describing who has the floor.
		"""
		self.contents = synde[:]
		# We merge nearby regions:
		i = 0
		while i < len(self.contents)-1:
			t0, tf, floor = self.contents[i]
			if ( floor==self.contents[i+1][2]
					and self.contents[i+1][0] < tf+self.DELTA
					):
				self.contents[i] = (t0, self.contents[i+1][1], floor)
				del self.contents[i+1]
			else:
				i += 1

		# Now, we map speakers:
		for i in range(len(self.contents)):
			t0, tf, floor = self.contents[i]
			print '#F', floor, map
			if Num.sum(floor) == 0:
				owner = None
			elif Num.sum(floor) == 1:
				owner = map[Num.nonzero(floor)[0]]
			else:
				owner = 'MULTIPLE'
			self.contents[i] = (t0, tf, owner)

	def __repr__(self):
		o = [ '<FR' ]
		for r in self.contents:
			o.append('%.3f:%.3f=%s' % r)
		o.append( '>' )
		return ','.join(o)

	__str__ = __repr__


class floorregions_syn(floorregions):
	Nspkrs = 2
	def __init__(self, uttrec, Valid, loose=0, root=None):
		"""Produces a list of time regions in a dialog where a single unique
		speaker had the floor.
		"""
		floorregions.__init__(self, uttrec)
		if int(uttrec['nspeakers']) == 1:
			self.append( (-self.HUGE, self.HUGE, uttrec['speaker']) )
			return

		if root is None:
			root = '.'

		if TEST==0 and not Valid.syn(uttrec):
			raise Valid.InvalidUtterance, 'syn: %s/%s' % (uttrec['d'], uttrec['f'])

		map = get_speaker_map(root, uttrec)

		nspkrs = min(self.Nspkrs, int(uttrec['nspeakers']))
		synde = who_has_floor_syn(uttrec, nspkrs, root,  loose)

		lastfloor = None
		startt = None
		tmp = []
		for (t, lab, floor) in synde:
			# print 'ITLF', i, t, lab, floor
			if(lastfloor is not None and floor != lastfloor
					and Num.sum(lastfloor)>0):
				self.append( (startt, t, lastfloor) )
			lastfloor = floor
			startt = t

		self.merge_and_map(tmp, map)



class floorregions_wd:
	Nspkrs = 2

	def __init__(self, uttrec, Valid, loose=0, root=None):
		"""Produces a list of time regions in a dialog where a single unique
		speaker had the floor.
		"""
		self.uttrec = uttrec
		if int(self.uttrec['nspeakers']) == 1:
			return	# The answer will be easy to compute later...

		if root is None:
			root = '.'

		if TEST==0 and not Valid.wd1(uttrec):
			raise Valid.InvalidUtterance, 'wd1: %s/%s' % (uttrec['d'], uttrec['f'])
		if TEST==0 and not Valid.wd2(uttrec):
			raise Valid.InvalidUtterance, 'wd2: %s/%s' % (uttrec['d'], uttrec['f'])

		self.map = get_speaker_map(root, uttrec)

		self.nspkrs = min(self.Nspkrs, int(uttrec['nspeakers']))

		self.t0, self.dt, self.own = who_has_floor_wd(uttrec, self.nspkrs, root, loose)


	def region_floor_owner(self, t0, tf):
		"""Does a single speaker own the floor for the entire region
		from t0 to tf?
		"""
		if int(self.uttrec['nspeakers']) == 1:
			return self.uttrec['speaker']

		it0 = max(0, int(math.floor((t0-self.t0)/self.dt)))
		itf = min(self.own.shape[1], int(math.ceil((tf-self.t0)/self.dt)))
		ows = Num.sum(self.own[:,it0:itf], axis=1)
		assert ows.shape == (self.nspkrs+1,)
		if Num.sum( Num.greater(ows, 0) ) != 1:
			die.warn('Ownership: %s' % str(ows))
			return None
		fnz = Num.nonzero(ows)[0]
		if fnz == self.nspkrs:
			die.warn('Noisy')
			return None	# Noise
		return self.map[ fnz ]



class synobjects(oxi_entity.entity_list):
	Nspkrs = 2

	def __init__(self, uttrec, loose=0, Valid=None, root=None):
		"""Reads a *.syn file and produces a list of syntactic entities.
		Each contains start and end times, information on who spoke (self.speaker),
		and what the syntactic label was.
		"""
		if root is None:
			root = '.'

		if TEST==0 and not Valid.syn(uttrec):
			raise Valid.InvalidUtterance, 'syn: %s/%s' % (uttrec['d'], uttrec['f'])

		oxi_entity.entity_list.__init__(self)

		map = get_speaker_map(root, uttrec)

		nspkrs = min(self.Nspkrs, int(uttrec['nspeakers']))
		synde = who_has_floor_syn(uttrec, nspkrs, root,  loose)
		for (i, (t, lab, floor)) in enumerate(synde):
			# print 'TLF', t, lab, floor
			if lab.startswith('['):
				continue
			if lab.startswith(']'):
				continue
			if lab.startswith('_'):
				continue
			if lab == SILENCE:
				continue
			if i == 0:
				if t is not None:
					die.warn("Clause end mark has no beginning: %d@%.3f (%s)." % (i, t, lab))
				continue

			# OK.  We have the end of a syntactic entity.
			# First, we see if it has an associated close tag.
			if( i+1<len(synde) and abs(t-synde[i+1][0])<0.001
					and synde[i+1][1].startswith(']')
				):
				owner = int(synde[i+1][1][1:]) - 1
			else:
				owner = owner_from_floor(floor)

			if owner is None:
				die.warn("Uncertain owner of syntax %d@%.3f (%s)." %
						(i, t, lab))

			# Next, we find the beginning.
			j = i-1
			while j >= 0:
				jt, jlab, jfloor = synde[j]
				if jlab == SILENCE:
					break	# There is a silence.
				elif owner is not None and jlab.startswith('[') \
						and int(jlab[1:])==owner:
					break	# The owner's turn began.
				elif not jlab.startswith('[') and not jlab.startswith(']'):
					break	# There is a previous entity.
				if not Num.sometrue( jfloor * floor):
					break	# No one has the floor.
				j -= 1
			if jt is None:
				continue

			# Go through and see how much time each person had the floor:
			stot = Num.zeros((nspkrs,), Num.Float)
			tlast = jt
			for k in range(j+1, i+1):
				kt, klab, kfloor = synde[k]
				stot += kfloor * (kt-tlast)
				tlast = kt

			assert t > jt, 't>jt fails: %s > %s i=%d' % (str(t), str(jt), i)
			print '##owner=', owner, map
			self.append( clause(uttrec, jt, t, lab, map_speakers(stot/(t-jt), map), map[owner]) )



class synuobjects(synobjects):
	DELTA = 0.010
	def __init__(self, uttrec, loose=0, Valid=None):
		"""This just adds an attribute '.utype' to each synobjects entity.
		If there is no match, it sets .utype to None.
		"""

		if TEST==0 and not Valid.utype(uttrec):
			raise Valid.InvalidUtterance, 'utype'
		synobjects.__init__(self, uttrec, loose)

		uh, ud = xwaves_lab.read('%s.utype' % os.path.join(uttrec['d'], uttrec['f']), loose)

		# Go through and associate labels from ud with matching
		# objects from the .syn file.
		ud.sort()
		i = 0
		for o in self.contents:
			while i < len(ud):
				t, lab = ud[i]
				if lab != '' and abs(o.tf - t) < self.DELTA:
					o.utype = lab
					i += 1
					break
				elif t > o.tf:
					o.utype = None
					break
				elif lab != '':
					die.warn("Label %d@%.3f(%s) in utype file without a matching clause in .syn file."
							% (i, t, lab))
				i += 1
			if not hasattr(o, 'utype'):
				o.utype = None



def test():
	info = {'d': 'test', 'f': 'c-m2-f1_3', 'nspeakers': 2,
		'speaker.1': 'cambridge:f:1',
		'speaker.2': 'cambridge:f:3',
		'location': 'cambridge'}
	global TEST
	TEST = 1
	x = synuobjects(info)
	for t in x:
		print t



if __name__ == '__main__':
	import sys
	import load_mod
	import xwaves_errs
	Valid = load_mod.load_named_module('/home/mint/gpk/oxigen/oxigen_valid')
	if len(sys.argv) == 1:
		test()
	else:
		h, d, c = fiatio.read(open('DB.fiat', 'r'))
		for i in d:
			if i['f'] != 'j-m2-f1_5':
				continue
			print 'FOUND', i['f']

			if not Valid.data(i):
				continue
			if not Valid.tns(i):
				continue

			print '#FILE', i['d'], i['f']
			try:
				x = floorregions_wd(i, Valid=Valid)
				for t in x:
					print t
			except Valid.InvalidUtterance, x:
				print '# ', x
			except xwaves_errs.NoSuchFileError, x:
				print '# ', x
			except SpeakerNotFoundError, x:
				print '# SpeakerNotFound', x
			except oxi_entity.SillyLabelSequence, x:
				print '# SillyLabelSequence', x
