#!/usr/bin/python3
"""
A simple alarm clock.

This script will set the BIOS alarm to a given time and,
when woke up, will fade in an audio file and will stop playing it
when a key is hit.
"""

import os
import re
import sys
import ossaudiodev
import time
import queue
import tkinter
import mdmisc
import signal

player = "/usr/bin/mpv"
sleepCommand = "sudo /etc/acpi/local/suspend.sh&"

class Error(Exception):
	pass

class CommandLineError(Error):
	pass



def setWakeupTime(gmTime):
		alarmFile = open("/sys/class/rtc/rtc0/wakealarm", "w")
		alarmFile.write("+%d"%(gmTime-time.time()))
		alarmFile.close()


def parseCmdLine():
	"""reads and checks the command line arguments.

	The function will exit if the parameters are bad.
	"""
	try:
		if len(sys.argv)<2:
			raise CommandLineError("Too few parameters")
		timeStr = sys.argv[1]
		if len(sys.argv)>2:
			audioFile = sys.argv[2]
		else:
			audioFile = "/media/muzak/wakeSound.mp3"
		if not os.path.exists(audioFile):
			raise CommandLineError("%s does not exist"%audioFile)
		mat = re.match(r"(\d\d?)[:.](\d\d)", timeStr)
		if not mat:
			raise CommandLineError("%s is not a valid time spec"%timeStr)
		wakeupHour, wakeupMinute = [int(s) for s in mat.groups()]
		year, month, day, hour, minute, _, _, _, _ = time.localtime()
		if hour>wakeupHour or (hour==wakeupHour and minute>wakeupMinute):
			year, month, day = mdmisc.getTomorrow((year, month, day))
		try:
			wakeupUnixtime = time.mktime((year, month, day, wakeupHour, 
				wakeupMinute, 0, -1, -1, -1))
		except (OverflowError, ValueError):
			raise CommandLineError("%s is not a valid time spec"%timeStr)
	except CommandLineError as msg:
		sys.stderr.write("Command line error: %s\n"%str(msg))
		sys.stderr.write("Usage: %s <wakeup time> <audio file>\n"%sys.argv[0])
		sys.exit(1)
	return wakeupUnixtime, audioFile


class VolRamper:
	"""is a class to ramp up or down the volume.

	To ramp down, set incTime to something negative.
	"""
	def __init__(self, targetDevice=ossaudiodev.SOUND_MIXER_PCM, 
			masterDevice=ossaudiodev.SOUND_MIXER_VOLUME, 
			startVol=10, maxVol=100, incTime=0.1, mixerFile="/dev/mixer",
			masterSetting=90):
		self.targetDevice = targetDevice
		self.masterDevice = masterDevice
		self.masterSetting = masterSetting
		self.startVol, self.maxVol = startVol, maxVol
		self.limits = (min(startVol, maxVol), max(startVol, maxVol))
		self.incTime = incTime
		self.mixerDev = ossaudiodev.openmixer(mixerFile)
		self.lastRamp = time.time()
		self._prepare()

	def _prepare(self):
		self.preVolumes = [self.mixerDev.get(self.masterDevice),
			self.mixerDev.get(self.targetDevice)]
		self.mixerDev.set(self.masterDevice, 
			(self.masterSetting, self.masterSetting))
		self.mixerDev.set(self.targetDevice, 
			(self.startVol, self.startVol))
	
	def cleanup(self):
		self.mixerDev.set(self.masterDevice, self.preVolumes[0])
		self.mixerDev.set(self.targetDevice, self.preVolumes[1])
		self.mixerDev.close()

	def rampVolume(self):
		rampInc = int((time.time()-self.lastRamp)/self.incTime)
		if rampInc:
			newVal = min(self.limits[1], max(self.limits[0], 
				self.mixerDev.get(self.targetDevice)[0]+rampInc))
			self.mixerDev.set(self.targetDevice, (newVal, newVal))
			self.lastRamp = time.time()


class Model:
	"""is a Model for a UI.

	This class creates two queues for communicating with the model.
	Controllers would at least use the inQueue, Views at least the
	out queue.

	Events are typically represented by tuples, the first element of
	which are the event types.

	Concrete Models will have to define methods _handle_xxx, where
	xxx is the fist element of the event to be handled.

	Concrete Models can furthermore define a _handle_timer method that
	gets called each time processEvents is called.

	Something in the program has to be arranged such that processEvents
	gets called regularly.
	"""
	def __init__(self, maxQueueSizes=10):
		self.inQueue = queue.Queue(10)
		self.outQueue = queue.Queue(10)
		self.put = self.inQueue.put
		self.get = self.outQueue.get

	def processEvents(self):
		if hasattr(self, "_handle_timer"):
			self._handle_timer()
		try:
			while 1:
				ev = self.inQueue.get_nowait()
				getattr(self, "_process_%s"%ev[0])(ev)
		except queue.Empty:
			pass

	def putOutputDiscardable(self, ev):
		try:
			self.outQueue.put(ev, False)
		except queue.Full:
			pass

	def putInputDiscardable(self, ev):
		try:
			self.inQueue.put(ev, False)
		except queue.Full:
			pass

	def getOutputEvents(self):
		try:
			while 1:
				yield self.outQueue.get_nowait()
		except queue.Empty:
			pass


class Wakeupper(Model):
	"""is the model for setting wakeup time and playing the audio file.

	You should call the processEvents method regularly.
	"""
	states = ["waiting", "playing", "stopped"]

	def __init__(self, wakeupTime, audioFile):
		Model.__init__(self, 20)
		self.childPid = None
		self.wakeupTime = wakeupTime
		self.volRamper = None
		self.audioFile = audioFile
		self.state = self.states[0]
		self._setWakeupTime()

	def _startPlaying(self):
		if self.childPid!=None:
			return
		self.childPid = os.fork()
		if self.childPid==0:
			os.execl(player, player, "-quiet", self.audioFile)
		else:
			self.volRamper = VolRamper()
			self.putOutputDiscardable(("show", "Playing %s"%self.audioFile))

	def _cleanupAfterPlay(self):
		self.childPid = None
		if self.volRamper:
			self.volRamper.cleanup()
			self.volRamper = None
		self.stopTime = time.time()
		self.putOutputDiscardable(("show", "Playing stopped"))

	def _stopPlaying(self):
		if self.childPid:
			try:
				os.kill(self.childPid, signal.SIGTERM)
				os.waitpid(self.childPid, 0)
			except os.error:
				# Child presumably died by itself
				pass
			self._cleanupAfterPlay()
		self.state = "stopped"

	def _setWakeupTime(self):
		setWakeupTime(self.wakeupTime)
		self.putOutputDiscardable(("show", "Wakeup at %s"%
			time.strftime("%H:%M", time.localtime(self.wakeupTime))))

	def _handle_timer(self):
		try:
			self.state = getattr(self, "_timer_%s"%self.state)()
		except:
			sys.stderr.write("Exception in timer handler:\n")
			traceback.print_exc()
			if self.childPid:
				self._stopPlaying()
			self.state = "stopped"
			self.outQueue.put(("quit",))
	
	def _timer_waiting(self):
		if time.time()>self.wakeupTime:
			self._startPlaying()
			return "playing"
		return "waiting"

	def _timer_playing(self):
		self.volRamper.rampVolume()
		try:
			childExited = os.waitpid(self.childPid, os.WNOHANG)
		except os.error:
			self._cleanupAfterPlay()
			return "stopped"
		return "playing"

	def _timer_stopped(self, timeToShutdown=60):
		self.outQueue.put(("show", "Song played.  %d Seconds to sleep"%(
			int(self.stopTime+timeToShutdown-time.time()))))
		if time.time()-self.stopTime>timeToShutdown:
			os.system(sleepCommand)
			self.outQueue.put(("quit",))
		return "stopped"

	def _process_stop(self, ev):
		if self.state=="playing":
			self._stopPlaying()
		self.outQueue.put(("quit",))
		return "stopped"


class WakeupUi(tkinter.Tk):
	def __init__(self, model):
		tkinter.Tk.__init__(self)
		self.title("Alarm clock")
		self.wakeupper = model
		self.bind("q", self._terminatePlayer)
		self.focus()
		self._buildUi()
		self._doPoll()
	
	def _buildUi(self):
		self.statusLabel = tkinter.Label(self)
		self.statusLabel.pack(expand=1, fill=tkinter.BOTH)

	def showMsg(self, msg):
		self.statusLabel.config(text=msg)

	def _terminatePlayer(self, ev=None):
		self.wakeupper.put(("stop",))

	def _handleEvent(self, ev):
		if ev[0]=="show":
			self.showMsg(ev[1])
		if ev[0]=="quit":
			self.quit()

	def _doPoll(self):
		try:
			self.wakeupper.processEvents()
			for ev in self.wakeupper.getOutputEvents():
				self._handleEvent(ev)
		finally:
			self.after(100, self._doPoll)
	

if __name__=="__main__":
	wakeupTime, audioFile = parseCmdLine()
	try:
		wakeupper = Wakeupper(wakeupTime, audioFile)
		ui = WakeupUi(wakeupper)
		ui.mainloop()
	finally:
		setWakeupTime(time.time()-50000)
