# -*- coding: utf-8 -*-

"""
Audio wrapper module

Can use SDL, pyglet, pyo or wx.
pyglet or SDL will be used by default if available.
pyglet can only be used if version >= 1.2.2 is available.
pyo is still buggy under Linux and has a few quirks under Windows.
wx doesn't support fading, changing volume, multiple concurrent sounds, and
only supports wav format.

Example:
sound = Sound("test.wav", loop=True)
sound.Play(fade_ms=1000)

"""

from ctypes import (CFUNCTYPE, POINTER, Structure, c_int, c_uint8, c_uint16,
					c_uint32, c_void_p)
import ctypes.util
import os
import sys
import threading
import time

if sys.platform == "win32":
	try:
		import win32api
		import pywintypes
	except ImportError:
		win32api = None

from config import pydir
from log import safe_print
from util_os import dlopen, getenvu
from util_str import safe_str, safe_unicode


_ch = {}
_initialized = False
_lib = None
_lib_version = None
_server = None
_snd = {}
_sounds = {}


def init(lib=None, samplerate=22050, channels=2, buffersize=2048, reinit=False):
	""" (Re-)Initialize sound subsystem """
	# Note on buffer size: Too high values cause crackling during fade, too low
	# values cause choppy playback of ogg files when using pyo (good value for
	# pyo is >= 2048)
	global _initialized, _lib, _lib_version, _server, pyglet, pyo, sdl, wx
	if _initialized and not reinit:
		# To re-initialize, explicitly set reinit to True
		return
	# Select the audio library we're going to use.
	# User choice or SDL > pyglet > pyo > wx
	if not lib:
		if sys.platform in ("darwin", "win32"):
			# Mac OS X, Windows
			libs = ("pyglet", "SDL", "pyo", "wx")
		else:
			# Linux
			libs = ("SDL", "pyglet", "pyo", "wx")
		for lib in libs:
			try:
				return init(lib, samplerate, channels, buffersize, reinit)
			except Exception, exception:
				pass
		raise exception
	elif lib == "pyglet":
		if not getattr(sys, "frozen", False):
			# Use included pyglet
			lib_dir = os.path.join(os.path.dirname(__file__), "lib")
			if not lib_dir in sys.path:
				sys.path.insert(0, lib_dir)
		try:
			import pyglet
			version = []
			for item in pyglet.version.split("."):
				try:
					version.append(int(item))
				except ValueError:
					version.append(item)
			if version < [1, 2, 2]:
				raise ImportError("pyglet version %s is too old" %
								  pyglet.version)
			_lib = "pyglet"
		except ImportError:
			_lib = None
		else:
			# Work around localization preventing fallback to RIFFSourceLoader
			pyglet.lib.LibraryLoader.darwin_not_found_error = ""
			pyglet.lib.LibraryLoader.linux_not_found_error = ""
			# Set audio driver preference
			pyglet.options["audio"] = ("pulse", "openal", "directsound", "silent")
			_server = pyglet.media
			_lib_version = pyglet.version
	elif lib == "pyo":
		try:
			import pyo
			_lib = "pyo"
		except ImportError:
			_lib = None
		else:
			if isinstance(_server, pyo.Server):
				_server.reinit(sr=samplerate, nchnls=channels,
							   buffersize=buffersize, duplex=0)
			else:
				_server = pyo.Server(sr=samplerate, nchnls=channels,
									 buffersize=buffersize, duplex=0,
									 winhost="asio").boot()
				_server.start()
				_lib_version = ".".join(str(v) for v in pyo.getVersion())
	elif lib == "SDL":
		SDL_INIT_AUDIO = 16
		AUDIO_S16LSB = 0x8010
		AUDIO_S16MSB = 0x9010
		if sys.byteorder == "little":
			MIX_DEFAULT_FORMAT = AUDIO_S16LSB
		else:
			MIX_DEFAULT_FORMAT = AUDIO_S16MSB
		if sys.platform == "win32":
			pth = getenvu("PATH")
			libpth = os.path.join(pydir, "lib")
			if not pth.startswith(libpth + os.pathsep):
				pth = libpth + os.pathsep + pth
				os.environ["PATH"] = safe_str(pth)
		elif sys.platform == "darwin":
			x_framework_pth = os.getenv("X_DYLD_FALLBACK_FRAMEWORK_PATH")
			if x_framework_pth:
				framework_pth = os.getenv("DYLD_FALLBACK_FRAMEWORK_PATH")
				if framework_pth:
					x_framework_pth = os.pathsep.join([x_framework_pth,
													   framework_pth])
				os.environ["DYLD_FALLBACK_FRAMEWORK_PATH"] = x_framework_pth
		for libname in ("SDL2", "SDL2_mixer", "SDL", "SDL_mixer"):
			handle = None
			if sys.platform in ("darwin", "win32"):
				libfn = ctypes.util.find_library(libname)
			if sys.platform == "win32":
				if libfn and win32api:
					# Support for unicode paths
					libfn = safe_unicode(libfn)
					try:
						handle = win32api.LoadLibrary(libfn)
					except pywintypes.error:
						pass
			elif sys.platform != "darwin":
				# Hard-code lib names for Linux
				libfn = "lib" + libname
				if libname.startswith("SDL2"):
					# SDL 2.0
					libfn += "-2.0.so.0"
				else:
					# SDL 1.2
					libfn += "-1.2.so.0"
			dll = dlopen(libfn, handle=handle)
			if dll:
				safe_print("%s:" % libname, libfn)
			if libname.endswith("_mixer"):
				if not dll:
					continue
				if not sdl:
					raise RuntimeError("SDL library not loaded")
				sdl.SDL_RWFromFile.restype = POINTER(SDL_RWops)
				_server = dll
				_server.Mix_OpenAudio.argtypes = [c_int, c_uint16, c_int, c_int]
				_server.Mix_LoadWAV_RW.argtypes = [POINTER(SDL_RWops), c_int]
				_server.Mix_LoadWAV_RW.restype = POINTER(Mix_Chunk)
				_server.Mix_PlayChannelTimed.argtypes = [c_int,
														 POINTER(Mix_Chunk),
														 c_int, c_int]
				_server.Mix_VolumeChunk.argtypes = [POINTER(Mix_Chunk), c_int]
				if _initialized:
					_server.Mix_Quit()
					sdl.SDL_Quit()
				sdl.SDL_Init(SDL_INIT_AUDIO)
				_server.Mix_OpenAudio(samplerate, MIX_DEFAULT_FORMAT, channels,
									  buffersize)
				_lib = "SDL"
				if libname.startswith("SDL2"):
					_lib_version = "2.0"
				else:
					_lib_version = "1.2"
				break
			else:
				sdl = dll
				_server = None
	elif lib == "wx":
		try:
			import wx
			_lib = "wx"
		except ImportError:
			_lib = None
		else:
			_server = wx
			_lib_version = wx.__version__
	if not _lib:
		raise RuntimeError("No audio library available")
	_initialized = True
	return _server


def safe_init(lib=None, samplerate=22050, channels=2, buffersize=2048,
			  reinit=False):
	""" Like init(), but catch any exceptions """
	global _initialized
	try:
		return init(lib, samplerate, channels, buffersize, reinit)
	except Exception, exception:
		# So we can check if initialization failed
		_initialized = exception
		return exception


def Sound(filename, loop=False, raise_exceptions=False):
	""" Sound caching mechanism """
	if (filename, loop) in _sounds:
		# Cache hit
		return _sounds[(filename, loop)]
	else:
		try:
			sound = _Sound(filename, loop)
		except Exception, exception:
			if raise_exceptions:
				raise
			safe_print(exception)
			sound = _Sound(None, loop)
		_sounds[(filename, loop)] = sound
		return sound


class DummySound(object):

	""" Dummy sound wrapper class """

	def __init__(self, filename=None, loop=False):
		pass

	def fade(self, fade_ms, fade_in=None):
		return True

	@property
	def is_playing(self):
		return False

	def play(self, fade_ms=0):
		return True

	@property
	def play_count(self):
		return 0

	def safe_fade(self, fade_ms, fade_in=None):
		return True

	def safe_play(self, fade_ms=0):
		return True

	def safe_stop(self, fade_ms=0):
		return True

	def stop(self, fade_ms=0):
		return True

	volume = 0


class SDL_RWops(Structure):
    pass


class Mix_Chunk(Structure):
	_fields_ = [("allocated", c_int),
				("abuf", POINTER(c_uint8)),
				("alen", c_uint32),
				("volume", c_uint8)]


class _Sound(object):

	""" Sound wrapper class """

	def __init__(self, filename, loop=False):
		self._filename = filename
		self._is_playing = False
		self._lib = _lib
		self._lib_version = _lib_version
		self._loop = loop
		self._play_timestamp = 0
		self._play_count = 0
		self._thread = -1
		if not _initialized:
			self._server = init()
		else:
			self._server = _server
		if _initialized and not isinstance(_initialized, Exception):
			if not self._lib and _lib:
				self._lib = _lib
				self._lib_version = _lib_version
			if not self._snd and self._filename:
				if self._lib == "pyo":
					self._snd = pyo.SfPlayer(safe_str(self._filename),
											 loop=self._loop)
				elif self._lib == "pyglet":
					snd = pyglet.media.load(self._filename, streaming=False)
					self._ch = pyglet.media.Player()
					self._snd = snd
				elif self._lib == "SDL":
					rw = sdl.SDL_RWFromFile(safe_str(self._filename, "UTF-8"),
											"rb")
					self._snd = self._server.Mix_LoadWAV_RW(rw, 1)
				elif self._lib == "wx":
					self._snd = wx.Sound(self._filename)

	def _get_ch(self):
		return _ch.get((self._filename, self._loop))

	def _set_ch(self, ch):
		_ch[(self._filename, self._loop)] = ch

	_ch = property(_get_ch, _set_ch)

	def _fade(self, fade_ms, fade_in, thread):
		volume = self.volume
		if fade_ms and ((fade_in and volume < 1) or (not fade_in and volume)):
			count = 200
			for i in xrange(count + 1):
				if fade_in:
					self.volume = volume + i / float(count) * (1.0 - volume)
				else:
					self.volume = volume - i / float(count) * volume
				time.sleep(fade_ms / 1000.0 / count)
				if self._thread is not thread:
					# If we are no longer the current thread, return immediately
					return
		if not self.volume:
			self.stop()

	def _get_volume(self):
		volume = 1.0
		if self._snd:
			if self._lib == "pyo":
				volume = self._snd.mul
			elif self._lib == "pyglet":
				volume = self._ch.volume
			elif self._lib == "SDL":
				volume = (float(self._server.Mix_VolumeChunk(self._snd, -1)) /
						  128)
		return volume

	def _set_volume(self, volume):
		if self._snd and self._lib != "wx":
			if self._lib == "pyo":
				self._snd.mul = volume
			elif self._lib == "pyglet":
				self._ch.volume = volume
			elif self._lib == "SDL":
				self._server.Mix_VolumeChunk(self._snd,
											 int(round(volume * 128)))
			return True
		return False

	def _get_snd(self):
		return _snd.get((self._filename, self._loop))

	def _set_snd(self, snd):
		_snd[(self._filename, self._loop)] = snd

	_snd = property(_get_snd, _set_snd)

	def fade(self, fade_ms, fade_in=None):
		"""
		Fade in/out.
		
		If fade_in is None, fade in/out depending on current volume.
		
		"""
		if fade_in is None:
			fade_in = not self.volume
		if fade_in and not self.is_playing:
			return self.play(fade_ms=fade_ms)
		elif self._snd and self._lib != "wx":
			self._thread += 1
			threading.Thread(target=self._fade, name="AudioFading-%d[%sms]" %
													 (self._thread, fade_ms),
							 args=(fade_ms, fade_in, self._thread)).start()
			return True
		return False

	@property
	def is_playing(self):
		if self._lib == "pyo":
			return bool(self._snd and self._snd.isOutputting())
		elif self._lib == "pyglet":
			return bool(self._ch and self._ch.playing and self._ch.source and
						(self._loop or time.time() - self._play_timestamp <
						 self._ch.source.duration))
		elif self._lib == "SDL":
			return bool(self._ch is not None and
						self._server.Mix_Playing(self._ch))
		return self._is_playing

	def play(self, fade_ms=0, stop_already_playing=True):
		if self._snd:
			volume = self.volume
			if stop_already_playing:
				self.stop()
			if self._lib == "pyglet":
				# Can't reuse the player, won't replay the sound under Mac OS X
				# and Linux even when seeking to start position which allows
				# replaying the sound under Windows.
				if stop_already_playing:
					self._ch.delete()
				self._ch = pyglet.media.Player()
				if self._lib_version >= "1.4.0":
					self._ch.loop = self._loop
				self.volume = volume
			if not self.is_playing and fade_ms and volume == 1:
				self.volume = 0
			self._play_timestamp = time.time()
			if self._lib == "pyo":
				self._snd.out()
			elif self._lib == "pyglet":
				if self._loop and self._lib_version < "1.4.0":
					snd = pyglet.media.SourceGroup(self._snd.audio_format,
												   self._snd.video_format)
					snd.loop = True
					snd.queue(self._snd)
				else:
					snd = self._snd
				self._ch.queue(snd)
				self._ch.play()
			elif self._lib == "SDL":
				self._ch = self._server.Mix_PlayChannelTimed(-1, self._snd,
															 -1 if self._loop else 0,
															 -1)
			elif self._lib == "wx" and self._snd.IsOk():
				flags = wx.SOUND_ASYNC
				if self._loop:
					flags |= wx.SOUND_LOOP
					# The best we can do is have the correct state reflected
					# for looping sounds only
					self._is_playing = True
				# wx.Sound.Play is supposed to return True on success.
				# When I tested this, it always returned False, but still
				# played the sound.
				self._snd.Play(flags)
			if self._lib:
				self._play_count += 1
			if fade_ms and self._lib != "wx":
				self.fade(fade_ms, True)
			return True
		return False

	@property
	def play_count(self):
		return self._play_count

	def safe_fade(self, fade_ms, fade_in=None):
		""" Like fade(), but catch any exceptions """
		if not _initialized:
			safe_init()
		try:
			return self.fade(fade_ms, fade_in)
		except Exception, exception:
			return exception

	def safe_play(self, fade_ms=0):
		""" Like play(), but catch any exceptions """
		if not _initialized:
			safe_init()
		try:
			return self.play(fade_ms)
		except Exception, exception:
			return exception

	def safe_stop(self, fade_ms=0):
		""" Like stop(), but catch any exceptions """
		try:
			return self.stop(fade_ms)
		except Exception, exception:
			return exception

	def stop(self, fade_ms=0):
		if self._snd and self.is_playing:
			if self._lib == "wx":
				self._snd.Stop()
				self._is_playing = False
			elif fade_ms:
				self.fade(fade_ms, False)
			else:
				if self._lib == "pyglet":
					self._ch.pause()
				elif self._lib == "SDL":
					self._server.Mix_HaltChannel(self._ch)
				else:
					self._snd.stop()
			return True
		else:
			return False

	volume = property(_get_volume, _set_volume)


if __name__ == "__main__":
	import wx
	from config import get_data_path
	sound = Sound(get_data_path("theme/engine_hum_loop.wav"), True)
	app = wx.App(0)
	frame = wx.Frame(None, -1, "Test")
	frame.Bind(wx.EVT_CLOSE, lambda event: (sound.stop(1000) and
											_lib != "wx" and time.sleep(1),
											event.Skip()))
	panel = wx.Panel(frame)
	panel.Sizer = wx.BoxSizer()
	button = wx.Button(panel, -1, "Play")
	button.Bind(wx.EVT_BUTTON, lambda event: not sound.is_playing and
											 sound.play(3000))
	panel.Sizer.Add(button, 1)
	button = wx.Button(panel, -1, "Stop")
	button.Bind(wx.EVT_BUTTON, lambda event: sound.is_playing and
											 sound.stop(3000))
	panel.Sizer.Add(button, 1)
	panel.Sizer.SetSizeHints(frame)
	frame.Show()
	app.MainLoop()
