# holoseit 0.0004b3 by Tokigun (2004.10.15)
# very buggy, for test purposes only

import psyco
psyco.full()

import pygame, Numeric
from pygame.locals import *
import time, os, random, bisect

def merge(self, other):
	if len(self) > len(other):
		for x in xrange(len(other)):
			if other[x]: self[x] = other[x]
	else:
		for x in xrange(len(self)):
			if other[x]: self[x] = other[x]
		for x in xrange(len(self), len(other)):
			self.append(other[x])

def ffloat(str):
	num = str[:len(str)-len(str.lstrip('0123456789. '))]
	return num and float(num) or 0

def fint(str):
	num = str[:len(str)-len(str.lstrip('-0123456789 '))]
	return num and int(num) or 0

indexkey = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def indexhash(index):
	if index == None: return 0
	return indexkey.find(index[0].upper()) * 36 + indexkey.find(index[1].upper()) + 1

def get_pygamever():
	return map(fint, pygame.__version__.split('.'))

def SoundPart(sound, pos):
	return pygame.sndarray.make_sound(
		pygame.sndarary.array(sound)[int(pygame.mixer.get_init()[0] * pos):])

class BMSNote(object):
	__slots__ = [
		'sound', 'layer', 'poorimg', 'bpm', 'stop',
		'player1', 'player1inv', 'player2', 'player2inv'
	]
	
	def __init__(self):
		self.layer = []
		self.poorimg = 0
		self.bpm = 0.0
		self.stop = 0
		self.clear()
	
	def set(self, name, value):
		if isinstance(name, str):
			object.__setattr__(self, name, value)
		elif len(getattr(self, name[0])) > name[1]:
			getattr(self, name[0])[name[1]] = value
		else:
			temp = getattr(self, name[0])
			temp += [0] * (name[1] - len(getattr(self, name[0]))) + [value]
	
	def update(self, obj):
		self.sound += obj.sound
		merge(self.layer, obj.layer)
		self.poorimg = obj.poorimg
		self.bpm = obj.bpm
		self.stop = obj.stop
		merge(self.player1, obj.player1)
		merge(self.player2, obj.player2)
		merge(self.player1inv, obj.player1inv)
		merge(self.player2inv, obj.player2inv)
	
	def clone(self):
		obj = BMSNote()
		obj.sound = self.sound[:]
		obj.layer = self.layer[:]
		obj.poorimg = self.poorimg
		obj.bpm = self.bpm
		obj.stop = self.stop
		obj.player1 = self.player1[:]
		obj.player1inv = self.player1inv[:]
		obj.player2 = self.player2[:]
		obj.player2inv = self.player2inv[:]
		return obj
	
	def clear(self):
		self.sound = []
		self.player1 = [0] * 9
		self.player1inv = [0] * 9
		self.player2 = [0] * 9
		self.player2inv = [0] * 9

class BMS:
	__idxstep = 100
	
	def __init__(self, filename):
		sep = filename.rfind(os.sep)
		self.basepath = filename[:sep+1]
		self.filename = filename[sep+1:]
		self.load(file(filename, "r"))
		self.make_index()
	
	def __getitem__(self, index):
		if not isinstance(index, tuple):
			return self.sequence[index]
		
		try:
			self.sequence[index[0]]
		except:
			self.sequence[index[0]] = BMSNote()
		
		if len(index) == 2:
			return getattr(self.sequence[index[0]], index[1])
		elif len(index) == 3:
			return getattr(self.sequence[index[0]], index[1])[index[2]]
		else:
			raise IndexError
	
	def __setitem__(self, index, value):
		if not isinstance(index, tuple):
			self.sequence[index] = value
			return
		
		try:
			self.sequence[index[0]]
		except:
			self.sequence[index[0]] = BMSNote()
		
		if len(index) == 2:
			self.sequence[index[0]].set(index[1], value)
		elif len(index) == 3:
			self.sequence[index[0]].set(index[1:], value)
		else:
			raise IndexError
	
	def load(self, fp):
		self.bpm = 130
		self.sndres = {}
		self.imgres = {}
		self.sequence = {}
		self.index = {}
		
		fp.seek(0)
		dice = 1
		ignored = False
		longnote = 0
		for line in fp:
			uline = line.upper()
			if uline.strip() == '#ENDIF':
				ignored = False
			elif ignored:
				pass
			elif uline.startswith('#TITLE '):
				self.title = line[7:].strip()
			elif uline.startswith('#ARTIST '):
				self.artist = line[8:].strip()
			elif uline.startswith('#BPM '):
				self.bpm = ffloat(line[5:])
			elif uline.startswith('#VOLWAV '):
				self.volume = fint(line[8:]) / 100.
			elif uline.startswith('#WAV'):
				self.sndres[indexhash(line[4:6])] = line[7:].strip()
			elif uline.startswith('#BMP'):
				self.imgres[indexhash(line[4:6])] = line[7:].strip()
			elif uline.startswith('#BGA'):
				temp = line[7:].strip().split()
				temp = [indexhash(temp[0])] + map(fint, temp[1:])
				temp[1:5] = map(lambda x:x>256 and 256 or x>0 and x or 0, temp[1:5])
				self.imgres[indexhash(line[4:6])] = \
					(temp[0], (temp[5], temp[6]), (temp[1], temp[2], temp[3]-temp[1], temp[4]-temp[2]))
			elif uline.startswith('#STAGEFILE '):
				self.imgres['title'] = line[11:].strip()
			elif uline.startswith('#BPM'):
				pass # extended bpm message is not implemented
			elif uline.startswith('#STOP'):
				pass # stop sequence message is not implemented
			elif uline.startswith('#CDDA '):
				pass # cdda is not supported
			elif uline.startswith('#BACKBMP '):
				pass # background graphic is not implemented
			elif uline.startswith('#LNTYPE '):
				longnote = fint(line[8:])
			elif uline.startswith('#LNOBJ '):
				longnote = -1
				longnoteobj = indexhash(line[7:9])
			elif uline.startswith('#MIDIFILE '):
				pass # midi is not supported
			elif uline.startswith('#RANDOM '):
				dice = random.randint(1, fint(line[8:]))
			elif uline.startswith('#IF '):
				if fint(line[4:]) == dice:
					ignored = False
				else:
					ignored = True
			elif line[0] == '#' and line[1:6].isdigit() and line[6] == ':':
				track, channel, rawdata, data = \
					int(line[1:4]), int(line[4:6]), line[7:].strip(), []
				for x in xrange(len(rawdata)/2):
					data.append(rawdata[x*2:x*2+2])
				length = float(len(data))
				
				# 01:bgm 02:shorten_measure 03:bpm 04:bga
				# 06:poorbga 07:bgalayer 08:extbpm 09:stop
				# x1-x5:1-5keys x6:scratch x7:pedal x8-x9:6-7keys
				# 1x,2x:visibleobj 3x,4x:invisibleobj 5x,6x:longnote
				
				if channel == 1:
					for x in xrange(int(length)):
						if data[x] != '00':
							self[track+x/length, 'sound'].append(indexhash(data[x]))
				elif channel == 2:
					pass # shortening note is not implemented
				elif channel == 3:
					for x in xrange(int(length)):
						if data[x] != '00':
							self[track+x/length, 'bpm'] = int(data[x], 16)
				elif channel == 4:
					for x in xrange(int(length)):
						if data[x] != '00':
							self[track+x/length, 'layer', 0] = indexhash(data[x])
				elif channel == 6:
					for x in xrange(int(length)):
						if data[x] != '00':
							self[track+x/length, 'poorimg'] = indexhash(data[x])
				elif channel == 7:
					for x in xrange(int(length)):
						if data[x] != '00':
							self[track+x/length, 'layer', 1] = indexhash(data[x])
				elif channel == 8:
					pass # extended bpm is not implemented
				elif channel == 9:
					pass # stop sequence is not implemented
				elif 11 <= channel <= 19:
					for x in xrange(int(length)):
						if data[x] != '00':
							self[track+x/length, 'player1', channel-11] = indexhash(data[x])
				elif 21 <= channel <= 29:
					for x in xrange(int(length)):
						if data[x] != '00':
							self[track+x/length, 'player2', channel-21] = indexhash(data[x])
				elif 31 <= channel <= 39:
					for x in xrange(int(length)):
						if data[x] != '00':
							self[track+x/length, 'player1inv', channel-31] = indexhash(data[x])
				elif 41 <= channel <= 49:
					for x in xrange(int(length)):
						if data[x] != '00':
							self[track+x/length, 'player2inv', channel-41] = indexhash(data[x])
#				elif 51 <= channel <= 59:
#					for x in xrange(int(length)):
#						if data[x] != '00':
#							self[track+x/length, 'player1', channel-51] = -indexhash(data[x])
#				elif 61 <= channel <= 69:
#					for x in xrange(int(length)):
#						if data[x] != '00':
#							self[track+x/length, 'player2', channel-61] = -indexhash(data[x])
		
		self.seqindex = self.sequence.keys()
		self.seqindex.sort()
		self.length = self.seqindex[-1]
	
	def make_index(self):
		current = BMSNote()
		current.bpm = self.bpm
		for pos in xrange(len(self.seqindex)):
			current.update(self.sequence[self.seqindex[pos]])
			if pos % self.__idxstep == 0:
				current.clear()
				self.index[pos] = current.clone()
		current.clear()
		self.index[self.seqindex[-1]] = current.clone()
	
	def rewind(self):
		self.position = -1
		self.stopped = False
		self.read(-1)
	
	def read(self, position):
		if position > self.length:
			self.stopped = True
			return self.index[self.seqindex[-1]]
		
		tposition = bisect.bisect(self.seqindex, position) - 1
		if tposition < 0:
			self.position = -1
			self.current = BMSNote()
			self.current.bpm = self.bpm
			return self.current
		elif 0 <= tposition - self.position < self.__idxstep:
			nposition = self.position
		else:
			nposition = tposition - tposition % self.__idxstep
			self.current = self.sequence[self.seqindex[nposition]].clone()
		self.position = tposition
		
		self.current.clear()
		while nposition < tposition:
			nposition += 1
			self.current.update(self.sequence[self.seqindex[nposition]])
		return self.current

class BMSPlayer:
	__version__ = "0.0004b3"
	
	def __init__(self):
		self._pygameext = (get_pygamever() >= [1, 6, 2])
		
		self.realscreen = pygame.display.set_mode((256, 288))
		self.screen = self.realscreen.subsurface(0, 16, 256, 256)
		self.topbar = self.realscreen.subsurface(0, 0, 256, 16)
		self.bottombar = self.realscreen.subsurface(0, 272, 256, 16)
		pygame.display.set_caption('holoseit %s' % self.__version__)
		self.realscreen.fill((64, 64, 64))
		self.screen.fill((0, 0, 0))
		pygame.display.flip()
		
		pygame.mixer.init()
		if self._pygameext:
			pygame.mixer.set_num_channels(64)
		self.nsamples = pygame.mixer.get_init()[0]
		self.insamples = 1. / self.nsamples
		
		pygame.font.init()
		self.font = pygame.font.Font('console.ttf', 12)
		
		self.bms = None
		self.clock = pygame.time.Clock()
		self.resource = pygame.image.load("resource.png").convert()
		
		self._double = False
		self._window = 0
		self._loop = False
		self._status = -1
		self._pstatus = -1
		self._volume = 1.0
		self._x_sound = True
		self._x_screen = True
	
	def load(self, bms):
		self.bms = bms
		pygame.display.set_caption('%s / %s - holoseit %s' % (self.bms.title, self.bms.artist, self.__version__))
		
		self.imgres = {}
		for key, value in self.bms.imgres.iteritems():
			try:
				if isinstance(value, tuple):
					self.imgres[key] = ()
				else:
					self.imgres[key] = pygame.image.load(self.bms.basepath + value).convert()
					self.imgres[key].set_colorkey((0, 0, 0), RLEACCEL)
			except:
				self.imgres[key] = None
		
		self.sndres = {}
		for key, value in self.bms.sndres.iteritems():
			try:
				self.sndres[key] = pygame.mixer.Sound(self.bms.basepath + value)
			except:
				self.sndres[key] = None
	
	def get_length(self):
		self.sndlen = {}
		if self._pygameext:
			for key, value in self.sndres.iteritems():
				self.sndlen[key] = value and value.get_length() or 0.0
		else:
			for key, value in self.sndres.iteritems():
				self.sndlen[key] = value and len(pygame.sndarray.array(value)) * self.insamples or 0.0
		
		length = 0.0
		for x in self.bms.seqindex:
			note = self.bms.sequence[x]
			pos = x * 240. / self.bms.bpm
			for y in note.sound + note.player1 + note.player1inv:
				if y and length < pos + self.sndlen[y]:
					length = pos + self.sndlen[y]
		return length
	
	def update_status(self, count):
		self.topbar.fill((64, 64, 64))
		self.topbar.blit(self.resource, (2, 2), (self._status*12, 0, 12, 12))
		self.topbar.blit(self.resource, (16, 2), (self._loop and 60 or 48, 0, 12, 12))
		surf = self.font.render("%s%d:%02d.%d / %d:%02d.%d" %
			(self._double and "[2x] " or "", count//60, int(count)%60, int(count*10)%10,
			 self.length//60, int(self.length)%60, int(self.length*10)%10),
			True, (255, 255, 255))
		self.topbar.blit(surf, (30, 2))
		surf = self.font.render(self.bms.filename, True, (255, 255, 255))
		self.topbar.blit(surf, (254 - surf.get_width(), 2))
	
	def eventhandler(self):
		while True:
			event = pygame.event.poll()
			if event.type == NOEVENT:
				return ("none",)
			#elif event.type == QUIT:
			#	return ("exit",)
			elif event.type == KEYDOWN:
				if event.key == K_ESCAPE:
					return ("exit",)
				elif event.key == K_SPACE:
					return ("pause",)
				elif event.key == K_F1:
					return ("view_help",)
				elif event.key == K_F2:
					return ("toggle_double",)
				elif event.key == K_F3:
					return ("toggle_loop",)
				elif event.key == K_F10:
					return ("toggle_info",)
				elif event.key == K_LEFT or event.key == K_RIGHT:
					return ("seek",
						(event.mod & KMOD_SHIFT and 60 or 10) *
						(event.key == K_LEFT and -1 or 1))
				elif event.key == K_HOME:
					return ("absseek", 0)
				elif event.key == K_UP or event.key == K_DOWN:
					return ("volume", event.key == K_UP and 0.1 or -0.1)
				elif event.key == K_DELETE:
					return ("toggle_sound",)
				elif event.key == K_INSERT:
					return ("toggle_screen",)
			elif event.type == KEYUP:
				if event.key == K_LEFT or event.key == K_RIGHT:
					return ("endseek",)
	
	def blit_imgres(self, id):
		if self.imgres[id] == ():
			data = self.bms.imgres[id]
			self.screen.blit(self.imgres[data[0]], data[1], data[2])
		elif self.imgres[id]:
			self.screen.blit(self.imgres[id], (0, 0))
	
	def play(self):
		measurelen = 240. / self.bms.bpm
		if self._status < 0:
			self._status = 2
		self.length = self.get_length()
		
		self.bottombar.blit(self.font.render("holoseit %s by Tokigun" % self.__version__, True, (255, 255, 255)), (2, 2))
		
		stime = time.time()
		ptime = 0
		_prevstatus = self._status
		event = ('none',)
		self.bms.rewind()
		pygame.mixer.stop()
		while time.time() - stime < self.length:
			utime = time.time()
			if not self.bms.stopped and self._status != 1:
				ntime = (utime - stime) / measurelen
				current = self.bms.read(ntime)
				if self._status != 2 or _prevstatus != 2:
					current.clear()
				self.screen.fill((0, 0, 0))
				if self._x_sound:
					for sndid in current.sound + current.player1 + current.player1inv:
						if sndid and self.sndres[sndid]:
							self.sndres[sndid].play()
				if self._x_screen:
					for imgid in current.layer:
						if imgid and self.imgres[imgid]:
							self.blit_imgres(imgid)
			
			_prevstatus = self._status
			if self._status == 1 or self._pstatus == 1:
				self.update_status(ptime)
			else:
				self.update_status(utime - stime)
			
			event = self.eventhandler()
			if event[0] == 'exit':
				return False
			elif event[0] == 'seek':
				self._pstatus = self._status
				self._status = 3
				self._status_dtime = event[1]
				pygame.mixer.stop()
				self.bms.stopped = False
			elif event[0] == 'absseek':
				pygame.mixer.stop()
				stime = utime - event[1]
			elif event[0] == 'endseek':
				self._status = self._pstatus
				self._pstatus = -1
			elif event[0] == 'pause':
				if self._status == 2:
					pygame.mixer.stop()
					ptime = utime - stime
					self._status = 1
				else:
					stime = utime - ptime
					self._status = 2
			elif event[0] == 'toggle_loop':
				self._loop = not self._loop
			elif event[0] == 'toggle_sound':
				pygame.mixer.stop()
				self._x_sound = not self._x_sound
			elif event[0] == 'toggle_screen':
				self.screen.fill((0, 0, 0))
				self._x_screen = not self._x_screen
			
			if self._status == 3:
				stime -= self._status_dtime
				if stime > utime:
					stime = utime
				if self._pstatus == 1:
					ptime = utime - stime
			
			pygame.display.flip()
			self.clock.tick(self._status==2 and 96 or 6)
		
		return True

def using():
	print "holoseit version %s by Tokigun (tokigun@gmail.com)" % BMSPlayer.__version__
	print "http://studio.tokigun.net/py/holoseit/"
	print
	print "Using: holoseit <bms file>"
	print

def main(argv):
	global player
	if len(argv) != 2:
		using()
		return
	try:
		bms = BMS(argv[1])
	except IOError:
		print "File Not Found - %s" % argv[1]
		print
		return
	
	player = BMSPlayer()
	player.load(bms)
	while player.play() and player._loop: pass

if __name__ == '__main__':
	import sys
	main(sys.argv)

