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

# Copyright (c) 2010, Stefan Parviainen <pafcu@iki.fi>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

"""
pymplb (PYthonMPLayerBingings) is a library that can be used to play media using an external MPlayer process.
The library runs the MPlayer binary in slave mode as a subprocess and then sends slave-mode commands to the process.
Commands are mapped to class methods and properties to class properties (by default prefixed with 'p_').
Commands are discovered at runtime and thus these bindings should automatically also support any new commands added to MPlayer in the future.
An example:
>>> import pymplb
>>> player = pymplb.MPlayer()
>>> player.loadfile('test.ogv')
>>> player.p_filename
'test.ogv'
"""

from functools import partial
import subprocess
import atexit

class PlayerNotFoundException(Exception):
	"""Exception which is raised when the external mplayer binary is not found."""
	def __init__(self, player_path):
		Exception.__init__(self, 'Player not found at %s'%player_path)

def make_mplayer_class(mplayer_bin='mplayer', method_prefix='', property_prefix='p_'):
	"""
	Construct a MPlayer class which user mplayer_bin as the player binary and prepends the given prefixes to property and method names.
	Prefixes are needed because some properties and methods have the same name.
	You only need to construct a new class if the default values are not suitable (i.e. mplayer is not in your path, or some new commands have been introduced that conflict with the default prefixes.
	"""

	# Yes, I'm aware it's a bit messy to have a function in a function in a class in a function
	# Decrease your indentation and bear with me here
	class _MPlayer(object): #pylint: disable-msg=R0903
		"""
		This is the main class used to play audio and video files by launching mplayer as a subprocess in slave mode.
		Slave mode methods can be called directly (e.g. x.loadfile("somefile)") while properties are prefixed to avoid
		name conflicts between methods and properties (e.g. x.p_looping = False).
		Available methods and properties are determined at runtime when the class is instantiated. All methods and properties are
		type-safe and properties respect minimum and maximum values given by mplayer.
		"""
		_arg_types = {'Flag':type(False), 'String':type(''), 'Integer':type(0), 'Float':type(0.0), 'Position':type(0.0), 'Time':type(0.0)} # Mapping from mplayer -> Python types
		_player_methods = {} # Need to keep track of methods because they must be modified after they have been added

		def __init__(self, env=None, mplayer_args_d=None, **mplayer_args):
			if mplayer_args_d: # Make pylint happy by not passing {} as an argument
				mplayer_args.update(mplayer_args_d)
			cmd_args = [mplayer_bin, '-slave', '-quiet', '-idle', '-msglevel', 'all=-1:global=4']
			for (name, value) in mplayer_args.items():
				cmd_args.append('-'+name)
				if value != None and value != True:
					cmd_args.append(str(value))

			self.__player = _MPlayer._run_player(cmd_args, env=env)

			# Partially apply methods to use the newly created player
			for (name, func) in self._player_methods.items():
				setattr(self, name, partial(func, self.__player))

			atexit.register(self.close) # Make sure subprocess is killed

		def close(self):
			"""Method that kills the MPlayer subprocess"""
			try:
				self.__player.terminate()
			except:
				pass

		@staticmethod
		def _run_player(args, env=None):
			"""Helper function that runs MPlayer with the given arguments"""
			try:
				player = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
			except OSError as err:
				if err.errno == 2:
					raise PlayerNotFoundException(args[0])
				else:
					raise
			return player

		@classmethod
		def _add_methods(cls, mplayer_bin):
			"""Discover which commands MPlayer understands and add them as class methods"""
			def cmd(name, argtypes, obligatory, player, *args, **kwargs):
				"""Function which sends the given command to the MPlayer process"""
				if len(args) < obligatory:
					raise TypeError('TypeError: %s() takes at least %d arguments (%d given)'%(name, obligatory, len(args)))
				if len(args) > len(argtypes):
					raise TypeError('TypeError: %s() takes at most %d arguments (%d given)'%(name, len(argtypes), len(args)))
				for i in range(len(args)):
					if type(args[i]) != argtypes[i]:
						raise TypeError('Argument %d of %s() has type %s, should be %s'%(i, name, type(args[i]).__name__, argtypes[i].__name__))

				pausing = kwargs.get('pausing', '')
				if pausing != '':
					pausing = pausing + ' '

				mplayer_command = '%s%s %s\n' % (pausing, name, ' '.join((str(x) for x in args)))
				player.stdin.write(mplayer_command.encode('utf-8'))
				player.stdin.flush()

				# Read return value of commands that give one
				# Hopefully this is smart enough ...
				if name.startswith('get_'):
					while True:
						line = player.stdout.readline().decode('utf-8')
						if line == '': # no more lines
							return None

						if not line[:3] == 'ANS':
							continue

						retval = line.split('=', 2)[1].rstrip()
						if retval == 'PROPERTY_UNAVAILABLE':
							return None

						return retval

			player = cls._run_player([mplayer_bin, '-input', 'cmdlist'])

			# Add each command found
			for line in player.stdout:
				line = str(line.decode('utf-8'))
				parts = line.strip().split()
				name = parts[0]
				args = parts[1:]
				if len(parts) > 1:
					obligatory = len([x for x in args if x[0] != '[']) # Number of obligatory args
					try:
						argtypes = [cls._arg_types[y] for y in [x.strip('[]') for x in args]]
					except KeyError:  # Unknown argument type
						continue # Some garbage on the output (version?)

				method = partial(cmd, name, argtypes, obligatory)
				if not args:
					method.__doc__ = 'Method taking no arguments'
				elif len(args) == 1:
					method.__doc__ = 'Method taking argument of type %s' % args[0]
				else:
					method.__doc__ = 'Method taking arguments of types %s' % ' '.join(args)

				cls._player_methods[cls._method_prefix+name] = method
				setattr(cls, cls._method_prefix+name, method)

		@classmethod
		def _add_properties(cls, mplayer_bin):
			"""Discover which properties MPlayer understands and add them as class properties"""
			def get_prop(name, prop_type, islist, self):
				"""Function which calls the get_property method to get the property value and does some type checking"""
				# self argument is needed to be property at the end because of partial
				retval = getattr(self, cls._method_prefix+'get_property')(name)
				if islist and retval == '(null)':
					return []
				if retval != None:
					if prop_type != type(False):
						if islist:
							retval = [prop_type(x) for x in retval.split(',')]
						else:
							retval = prop_type(retval)
					else:
						if islist:
							retval = [x == 'yes' for x in retval.split(',')]
						else:
							retval = (retval == 'yes')
				return retval

			# Function for getting and setting properties
			def set_prop(name, prop_type, islist, prop_min, prop_max, self, value):
				"""Function which calls the set_property method to set the property value and does some type checking"""
				if islist:
					for elem in value:
						if type(elem) != prop_type:
							raise TypeError('TypeError: Element %s has wrong type %s, not %s'%(elem, type(elem).__name__, prop_type))
						if prop_min != None and elem < prop_min:
							raise ValueError('ValueError: Element %s must be at least %s'%(elem, prop_min))
						if prop_max != None and elem > prop_max:
							raise ValueError('ValueError: Element %s must be at most %s'%(elem, prop_max))
					value = ','.join([str(elem) for elem in value])
				else:
					if type(value) != prop_type:
						raise TypeError('TypeError: %s has type %s, not %s'%(name, prop_type.__name__, type(value).__name__))
					if prop_min != None and value < prop_min:
						raise ValueError('ValueError: %s must be at least %s (>%s)'%(name, prop_min, value))
					if prop_max != None and value > prop_max:
						raise ValueError('ValueError: %s must be at most %s (<%s)'%(name, prop_max, value))

				getattr(self, cls._method_prefix+'set_property')(name, str(value))

			player = cls._run_player([mplayer_bin, '-list-properties'])
			# Add each property found
			for line in player.stdout:
				parts = line.strip().decode('utf-8').split()
				if not (len(parts) == 4 or (len(parts) == 5 and parts[2] == 'list')):
					continue
				name = parts[0]
				try:
					prop_type = cls._arg_types[parts[1]]
				except KeyError:
					continue

				if parts[2] == 'list': # Actually a list
					prop_min = parts[3]
					prop_max = parts[4]
					islist = True
				else:
					prop_min = parts[2]
					prop_max = parts[3]
					islist = False

				if prop_min == 'No':
					prop_min = None
				else:
					prop_min = prop_type(prop_min)

				if prop_max == 'No':
					prop_max = None
				else:
					prop_max = prop_type(prop_max)


				getter = partial(get_prop, name, prop_type, islist)
				setter = partial(set_prop, name, prop_type, islist, prop_min, prop_max)
				setattr(cls, cls._property_prefix+name, property(getter, setter, doc='Property of type %s in range [%s, %s].'%(prop_type.__name__, prop_min, prop_max)))
	# end of _MPlayer

	_MPlayer._method_prefix = method_prefix
	_MPlayer._property_prefix = property_prefix

	_MPlayer._add_methods(mplayer_bin)
	_MPlayer._add_properties(mplayer_bin)
	return _MPlayer

try:
	MPlayer = make_mplayer_class() # pylint: disable-msg=C0103
except PlayerNotFoundException as e:
	print("Fatal:", e)

if __name__ == "__main__":
	import doctest
	doctest.testmod(optionflags=doctest.ELLIPSIS)