# -*- coding: utf-8 -*-
from __future__ import print_function

import itertools as it, operator as op, functools as ft
from collections import defaultdict
from contextlib import contextmanager
import os, sys, inspect, traceback

from . import _pulsectl as c


if sys.version_info.major >= 3:
	long, unicode = int, str
	print_err = ft.partial(print, file=sys.stderr, flush=True)
	def wrapper_with_sig_info(func, wrapper, index_arg=False):
		sig = inspect.signature(func or (lambda: None))
		if index_arg:
			sig = sig.replace(parameters=[inspect.Parameter( 'index',
				inspect.Parameter.POSITIONAL_OR_KEYWORD )] + list(sig.parameters.values()))
		wrapper.__name__, wrapper.__signature__, wrapper.__doc__ = '', sig, func.__doc__
		return wrapper

else:
	range, map = xrange, it.imap
	def print_err(*args, **kws):
		kws.setdefault('file', sys.stderr)
		print(*args, **kws)
		kws['file'].flush()
	def wrapper_with_sig_info(func, wrapper, index_arg=False):
		func_args = list(inspect.getargspec(func or (lambda: None)))
		func_args[0] = list(func_args[0])
		if index_arg: func_args[0] = ['index'] + func_args[0]
		wrapper.__name__ = '...'
		wrapper.__doc__ = 'Signature: func' + inspect.formatargspec(*func_args)
		if func.__doc__: wrapper.__doc__ += '\n\n' + func.__doc__
		return wrapper


is_str = lambda v,ext=None,native=False: (
	isinstance(v, ( (unicode, bytes)
		if not native else (str,) ) + ((ext,) if ext else ())) )
is_str_native = ft.partial(is_str, native=True)
is_num = lambda v: isinstance(v, (int, float, long))
is_list = lambda v: isinstance(v, (tuple, list))
is_dict = lambda v: isinstance(v, dict)

def assert_pulse_object(obj):
	if not isinstance(obj, PulseObject):
		raise TypeError( 'Pulse<something>Info'
			' object is required instead of value: [{}] {}', type(obj), obj )

class FakeLock():
	def __enter__(self): return self
	def __exit__(self, *err): pass


@ft.total_ordering
class EnumValue(object):
	'String-based enum value, comparable to native strings.'
	__slots__ = '_t', '_value', '_c_val'
	def __init__(self, t, value, c_value=None):
		self._t, self._value, self._c_val = t, value, c_value
	def __repr__(self): return '<EnumValue {}={}>'.format(self._t, self._value)
	def __eq__(self, val):
		if isinstance(val, EnumValue): val = val._value
		return self._value == val
	def __ne__(self, val): return not (self == val)
	def __lt__(self, val):
		if isinstance(val, EnumValue): val = val._value
		return self._value < val
	def __hash__(self): return hash(self._value)

class Enum(object):

	def __init__(self, name, values_or_map):
		vals = values_or_map
		if is_str_native(vals): vals = vals.split()
		if is_list(vals): vals = zip(it.repeat(None), vals)
		if is_dict(vals): vals = vals.items()
		self._name, self._values, self._c_vals = name, dict(), dict()
		for c_val, k in vals:
			v = EnumValue(name, k, c_val)
			setattr(self, k.replace('-', '_'), v)
			self._c_vals[c_val] = self._values[k] = v

	def __getitem__(self, k, *default):
		if isinstance(k, EnumValue):
			t, k, v = k._t, k._value, k
			if t != self._name: raise KeyError(v)
		try: return getattr(self, k.replace('-', '_'), *default)
		except AttributeError: raise KeyError(k)

	def _get(self, k, default=None): return self.__getitem__(k, default)
	def __contains__(self, k): return self._get(k) is not None

	def _c_val(self, c_val, default=KeyError):
		v = self._c_vals.get(c_val)
		if v is not None: return v
		if default is not KeyError:
			return EnumValue(self._name, default, c_val)
		raise KeyError(c_val)

	def __repr__(self):
		return '<Enum {} [{}]>'.format(self._name, ' '.join(sorted(self._values.keys())))


PulseEventTypeEnum = Enum('event-type', c.PA_EVENT_TYPE_MAP)
PulseEventFacilityEnum = Enum('event-facility', c.PA_EVENT_FACILITY_MAP)
PulseEventMaskEnum = Enum('event-mask', c.PA_EVENT_MASK_MAP)

PulseStateEnum = Enum('sink/source-state', c.PA_OBJ_STATE_MAP)
PulseUpdateEnum = Enum('update-type', c.PA_UPDATE_MAP)
PulsePortAvailableEnum = Enum('available', c.PA_PORT_AVAILABLE_MAP)
PulseDirectionEnum = Enum('direction', c.PA_DIRECTION_MAP)


class PulseError(Exception): pass
class PulseOperationFailed(PulseError): pass
class PulseOperationInvalid(PulseOperationFailed): pass
class PulseIndexError(PulseError): pass

class PulseLoopStop(Exception): pass
class PulseDisconnected(Exception): pass

class PulseObject(object):

	c_struct_wrappers = dict()

	def __init__(self, struct=None, *field_data_list, **field_data_dict):
		field_data, fields = dict(), getattr(self, 'c_struct_fields', list())
		if is_str_native(fields): fields = self.c_struct_fields = fields.split()
		if field_data_list: field_data.update(zip(fields, field_data_list))
		if field_data_dict: field_data.update(field_data_dict)
		if struct is None: field_data, struct = dict(), field_data
		assert not set(field_data.keys()).difference(fields)
		if field_data: self._copy_struct_fields(field_data, fields=field_data.keys())
		self._copy_struct_fields(struct, fields=set(fields).difference(field_data.keys()))

		if struct:
			if hasattr(struct, 'proplist'):
				self.proplist, state = dict(), c.c_void_p()
				while True:
					k = c.pa.proplist_iterate(struct.proplist, c.byref(state))
					if not k: break
					self.proplist[c.force_str(k)] = c.force_str(c.pa.proplist_gets(struct.proplist, k))
			if hasattr(struct, 'volume'):
				self.volume = self._get_wrapper(PulseVolumeInfo)(struct.volume)
			if hasattr(struct, 'base_volume'):
				self.base_volume = struct.base_volume / c.PA_VOLUME_NORM
			if hasattr(struct, 'n_ports'):
				cls_port = self._get_wrapper(PulsePortInfo)
				self.port_list = list(
					cls_port(struct.ports[n].contents) for n in range(struct.n_ports) )
			if hasattr(struct, 'active_port'):
				cls_port = self._get_wrapper(PulsePortInfo)
				self.port_active = (
					None if not struct.active_port else cls_port(struct.active_port.contents) )
			if hasattr(struct, 'channel_map'):
				self.channel_count, self.channel_list = struct.channel_map.channels, list()
				self.channel_list_raw = struct.channel_map.map[:self.channel_count]
				if self.channel_count > 0:
					s = c.create_string_buffer(b'\0' * 512)
					c.pa.channel_map_snprint(s, len(s), struct.channel_map)
					self.channel_list.extend(map(c.force_str, s.value.strip().split(b',')))
			if hasattr(struct, 'state'):
				self.state = PulseStateEnum._c_val(
					struct.state, u'state.{}'.format(struct.state) )
				self.state_values = sorted(PulseStateEnum._values.values())
			if hasattr(struct, 'corked'): self.corked = bool(struct.corked)
			self._init_from_struct(struct)

	def _get_wrapper(self, cls_base):
		return self.c_struct_wrappers.get(cls_base, cls_base)

	def _copy_struct_fields(self, struct, fields=None, str_errors='strict'):
		if not fields: fields = self.c_struct_fields
		for k in fields:
			setattr(self, k, c.force_str( getattr(struct, k)
				if not is_dict(struct) else struct[k], str_errors ))

	def _init_from_struct(self, struct): pass # to parse fields in subclasses

	def _as_str(self, ext=None, fields=None, **kws):
		kws = list(it.starmap('{}={}'.format, kws.items()))
		if fields:
			if is_str_native(fields): fields = fields.split()
			kws.extend('{}={!r}'.format(k, getattr(self, k)) for k in fields)
		kws = sorted(kws)
		if ext: kws.append(str(ext))
		return ', '.join(kws)

	def __str__(self):
		return self._as_str(fields=self.c_struct_fields)

	def __repr__(self):
		return '<{} at {:x} - {}>'.format(self.__class__.__name__, id(self), str(self))


class PulsePortInfo(PulseObject):
	c_struct_fields = 'name description available priority'

	def _init_from_struct(self, struct):
		self.available = PulsePortAvailableEnum._c_val(struct.available)
		self.available_state = self.available # for compatibility with <=17.6.0

	def __eq__(self, o):
		if not isinstance(o, PulsePortInfo): raise TypeError(o)
		return self.name == o.name

	def __hash__(self): return hash(self.name)

class PulseClientInfo(PulseObject):
	c_struct_fields = 'name index driver owner_module'

class PulseServerInfo(PulseObject):
	c_struct_fields = ( 'user_name host_name'
		' server_version server_name default_sink_name default_source_name cookie' )

class PulseModuleInfo(PulseObject):
	c_struct_fields = 'index name argument n_used auto_unload'

class PulseSinkInfo(PulseObject):
	c_struct_fields = ( 'index name mute'
		' description sample_spec owner_module latency driver'
		' monitor_source monitor_source_name flags configured_latency card' )

	def __str__(self):
		return self._as_str(self.volume, fields='index name description mute')

class PulseSinkInputInfo(PulseObject):
	c_struct_fields = ( 'index name mute corked client'
		' owner_module sink sample_spec'
		' buffer_usec sink_usec resample_method driver' )

	def __str__(self):
		return self._as_str(fields='index name mute')

class PulseSourceInfo(PulseObject):
	c_struct_fields = ( 'index name mute'
		' description sample_spec owner_module latency driver monitor_of_sink'
		' monitor_of_sink_name flags configured_latency card' )

	def __str__(self):
		return self._as_str(self.volume, fields='index name description mute')

class PulseSourceOutputInfo(PulseObject):
	c_struct_fields = ( 'index name mute corked client'
		' owner_module source sample_spec'
		' buffer_usec source_usec resample_method driver' )

	def __str__(self):
		return self._as_str(fields='index name mute')

class PulseCardProfileInfo(PulseObject):
	c_struct_fields = 'name description n_sinks n_sources priority available'

class PulseCardPortInfo(PulsePortInfo):
	c_struct_fields = 'name description available priority direction latency_offset'

	def _init_from_struct(self, struct):
		super(PulseCardPortInfo, self)._init_from_struct(struct)
		self.direction = PulseDirectionEnum._c_val(struct.direction)

class PulseCardInfo(PulseObject):
	c_struct_fields = 'name index driver owner_module n_profiles'
	c_struct_wrappers = {PulsePortInfo: PulseCardPortInfo}

	def __init__(self, struct):
		super(PulseCardInfo, self).__init__(struct)
		self.profile_list = list(
			PulseCardProfileInfo(struct.profiles2[n][0]) for n in range(self.n_profiles) )
		self.profile_active = PulseCardProfileInfo(struct.active_profile2.contents)

	def __str__(self):
		return self._as_str(
			fields='name index driver n_profiles',
			profile_active='[{}]'.format(self.profile_active.name) )

class PulseVolumeInfo(PulseObject):

	def __init__(self, struct_or_values=None, channels=None):
		if is_num(struct_or_values):
			assert channels is not None, 'Channel count specified if volume value is not a list.'
			self.values = [struct_or_values] * channels
		elif is_list(struct_or_values): self.values = struct_or_values
		else:
			self.values = list( (x / c.PA_VOLUME_NORM)
				for x in map(float, struct_or_values.values[:struct_or_values.channels]) )

	@property
	def value_flat(self): return (sum(self.values) / float(len(self.values))) if self.values else 0
	@value_flat.setter
	def value_flat(self, v): self.values = [v] * len(self.values)

	def to_struct(self):
		return c.PA_CVOLUME(
			len(self.values), tuple(min( c.PA_VOLUME_UI_MAX,
					int(round(v * c.PA_VOLUME_NORM)) ) for v in self.values) )

	def __str__(self):
		return self._as_str(
			channels=len(self.values), volumes='[{}]'.format(
				' '.join('{}%'.format(int(round(v*100))) for v in self.values) ) )

class PulseExtStreamRestoreInfo(PulseObject):
	c_struct_fields = 'name channel_map volume mute device'

	@classmethod
	def struct_from_value( cls, name, volume,
			channel_list=None, mute=False, device=None ):
		'Same arguments as with class instance init.'
		chan_map = c.PA_CHANNEL_MAP()
		if not channel_list: c.pa.channel_map_init_mono(chan_map)
		else:
			if not is_str(channel_list):
				channel_list = b','.join(map(c.force_bytes, channel_list))
			c.pa.channel_map_parse(chan_map, channel_list)
		if not isinstance(volume, PulseVolumeInfo):
			volume = PulseVolumeInfo(volume, chan_map.channels)
		struct = c.PA_EXT_STREAM_RESTORE_INFO(
			name=c.force_bytes(name),
			mute=int(bool(mute)), device=c.force_bytes(device),
			channel_map=chan_map, volume=volume.to_struct() )
		return struct

	def __init__( self, struct_or_name=None,
			volume=None, channel_list=None, mute=False, device=None ):
		'''If string name is passed instead of C struct, will be initialized from args/kws.
			"volume" can be either a float number
				(same level for all channels) or list (value per channel).
			"channel_list" can be a pulse channel map string (comma-separated) or list
				of channel names. Defaults to stereo map, should probably match volume channels.
			"device" - name of sink/source or None (default).'''
		if is_str(struct_or_name):
			struct_or_name = self.struct_from_value(
				struct_or_name, volume, channel_list, mute, device )
		super(PulseExtStreamRestoreInfo, self).__init__(struct_or_name)

	def to_struct(self):
		return self.struct_from_value(**dict(
			(k, getattr(self, k)) for k in 'name volume channel_list mute device'.split() ))

	def __str__(self):
		return self._as_str(self.volume, fields='name mute device')

class PulseEventInfo(PulseObject):

	def __init__(self, ev_t, facility, index):
		self.t, self.facility, self.index = ev_t, facility, index

	def __str__(self):
		return self._as_str(fields='t facility index'.split())


class Pulse(object):

	_ctx = None

	def __init__(self, client_name=None, server=None, connect=True, threading_lock=False):
		'''Connects to specified pulse server by default.
			Specifying "connect=False" here prevents that, but be sure to call connect() later.
			"connect=False" can also be used here to
				have control over options passed to connect() method.
			"threading_lock" option (either bool or lock instance) can be used to wrap
				non-threadsafe eventloop polling (can only be done from one thread at a time)
				into a mutex lock, and should only be needed if same-instance methods
				will/should/might be called from different threads at the same time.'''
		self.name = client_name or 'pulsectl'
		self.server, self.connected = server, None
		self._ret = self._ctx = self._loop = self._api = None
		self._actions, self._action_ids = dict(),\
			it.chain.from_iterable(map(range, it.repeat(2**30)))
		self.init()
		if threading_lock:
			if threading_lock is True:
				import threading
				threading_lock = threading.Lock()
			self._loop_lock = threading_lock
		if connect:
			try: self.connect(autospawn=True)
			except PulseError:
				self.close()
				raise

	def init(self):
		self._pa_state_cb = c.PA_STATE_CB_T(self._pulse_state_cb)
		self._pa_subscribe_cb = c.PA_SUBSCRIBE_CB_T(self._pulse_subscribe_cb)

		self._loop, self._loop_lock = c.pa.mainloop_new(), FakeLock()
		self._loop_running = self._loop_closed = False
		self._api = c.pa.mainloop_get_api(self._loop)
		self._ret = c.pa.return_value()

		self._ctx_init()
		self.event_types = sorted(PulseEventTypeEnum._values.values())
		self.event_facilities = sorted(PulseEventFacilityEnum._values.values())
		self.event_masks = sorted(PulseEventMaskEnum._values.values())
		self.event_callback = None

		chan_names = dict()
		for n in range(256):
			name = c.pa.channel_position_to_string(n)
			if name is None: break
			chan_names[n] = name
		self.channel_list_enum = Enum('channel_pos', chan_names)

	def _ctx_init(self):
		if self._ctx:
			with self._loop_lock:
				self.disconnect()
				c.pa.context_unref(self._ctx)
		self._ctx = c.pa.context_new(self._api, self.name)
		c.pa.context_set_state_callback(self._ctx, self._pa_state_cb, None)
		c.pa.context_set_subscribe_callback(self._ctx, self._pa_subscribe_cb, None)

	def connect(self, autospawn=False, wait=False, timeout=None):
		'''Connect to pulseaudio server.
			"autospawn" option will start new pulse daemon, if necessary.
			Specifying "wait" option will make function block until pulseaudio server appears.
			"timeout" (in seconds) will raise PulseError if connection not established within it.'''
		if self._loop_closed:
			raise PulseError('Eventloop object was already'
				' destroyed and cannot be reused from this instance.')
		if self.connected is not None: self._ctx_init()
		flags, self.connected = 0, None
		if not autospawn: flags |= c.PA_CONTEXT_NOAUTOSPAWN
		if wait: flags |= c.PA_CONTEXT_NOFAIL
		try: c.pa.context_connect(self._ctx, self.server, flags, None)
		except c.pa.CallError: self.connected = False
		if not timeout: # simplier process
			while self.connected is None: self._pulse_iterate()
		else:
			self._loop_stop, delta, ts_deadline = True, 1, c.mono_time() + timeout
			while self.connected is None:
				delta = ts_deadline - c.mono_time()
				self._pulse_poll(delta)
				if delta <= 0: break
			self._loop_stop = False
			if not self.connected:
				c.pa.context_disconnect(self._ctx)
				while self.connected is not False: self._pulse_iterate()
				raise PulseError('Timed-out connecting to pulseaudio server [{:,.1f}s]'.format(timeout))
		if self.connected is False: raise PulseError('Failed to connect to pulseaudio server')

	def disconnect(self):
		if not self._ctx or not self.connected: return
		c.pa.context_disconnect(self._ctx)

	def close(self):
		if not self._loop: return
		if self._loop_running: # called from another thread
			self._loop_closed = True
			c.pa.mainloop_quit(self._loop, 0)
			return # presumably will be closed in a thread that's running it
		with self._loop_lock:
			try:
				self.disconnect()
				c.pa.context_unref(self._ctx)
				c.pa.mainloop_free(self._loop)
			finally: self._ctx = self._loop = None

	def __enter__(self): return self
	def __exit__(self, err_t, err, err_tb): self.close()


	def _pulse_state_cb(self, ctx, userdata):
		state = c.pa.context_get_state(ctx)
		if state >= c.PA_CONTEXT_READY:
			if state == c.PA_CONTEXT_READY: self.connected = True
			elif state in [c.PA_CONTEXT_FAILED, c.PA_CONTEXT_TERMINATED]:
				self.connected, self._loop_stop = False, True

	def _pulse_subscribe_cb(self, ctx, ev, idx, userdata):
		if not self.event_callback: return
		n = ev & c.PA_SUBSCRIPTION_EVENT_FACILITY_MASK
		ev_fac = PulseEventFacilityEnum._c_val(n, 'ev.facility.{}'.format(n))
		n = ev & c.PA_SUBSCRIPTION_EVENT_TYPE_MASK
		ev_t = PulseEventTypeEnum._c_val(n, 'ev.type.{}'.format(n))
		try: self.event_callback(PulseEventInfo(ev_t, ev_fac, idx))
		except PulseLoopStop: self._loop_stop = True

	def _pulse_poll_cb(self, func, func_err, ufds, nfds, timeout, userdata):
		fd_list = list(ufds[n] for n in range(nfds))
		try: nfds = func(fd_list, timeout / 1000.0)
		except Exception as err:
			func_err(*sys.exc_info())
			return -1
		return nfds

	@contextmanager
	def _pulse_loop(self):
		with self._loop_lock:
			if not self._loop: return
			if self._loop_running:
				raise PulseError(
					'Running blocking pulse operations from pulse eventloop callbacks'
						' or other threads while loop is running is not supported by this python module.'
					' Supporting this would require threads or proper asyncio/twisted-like async code.'
					' Workaround can be to stop the loop'
						' (raise PulseLoopStop in callback or event_loop_stop() from another thread),'
						' doing whatever pulse calls synchronously and then resuming event_listen() loop.' )
			self._loop_running, self._loop_stop = True, False
			try: yield self._loop
			finally:
				self._loop_running = False
				if self._loop_closed: self.close() # to free() after stopping it

	def _pulse_run(self):
		with self._pulse_loop() as loop: c.pa.mainloop_run(loop, self._ret)

	def _pulse_iterate(self, block=True):
		with self._pulse_loop() as loop: c.pa.mainloop_iterate(loop, int(block), self._ret)

	@contextmanager
	def _pulse_op_cb(self, raw=False):
		act_id = next(self._action_ids)
		self._actions[act_id] = None
		try:
			cb = lambda s=True,k=act_id: self._actions.update({k: bool(s)})
			if not raw: cb = c.PA_CONTEXT_SUCCESS_CB_T(lambda ctx,s,d,cb=cb: cb(s))
			yield cb
			while self.connected and self._actions[act_id] is None: self._pulse_iterate()
			if not self._actions[act_id]: raise PulseOperationFailed(act_id)
		finally: self._actions.pop(act_id, None)

	def _pulse_poll(self, timeout=None):
		'''timeout should be in seconds (float),
			0 for non-blocking poll and None (default) for no timeout.'''
		with self._pulse_loop() as loop:
			ts = c.mono_time()
			ts_deadline = timeout and (ts + timeout)
			while True:
				delay = max(0, int((ts_deadline - ts) * 1000000)) if ts_deadline else -1
				c.pa.mainloop_prepare(loop, delay) # delay in us
				c.pa.mainloop_poll(loop)
				if self._loop_closed: break # interrupted by close() or such
				c.pa.mainloop_dispatch(loop)
				if self._loop_stop: break
				ts = c.mono_time()
				if ts_deadline and ts >= ts_deadline: break


	def _pulse_info_cb(self, info_cls, data_list, done_cb, ctx, info, eof, userdata):
		# No idea where callbacks with "userdata != NULL" come from,
		#  but "info" pointer in them is always invalid, so they are discarded here.
		# Looks like some kind of mixup or corruption in libpulse memory?
		# See also: https://github.com/mk-fg/python-pulse-control/issues/35
		if userdata is not None: return
		# Empty result list and conn issues are checked elsewhere.
		# Errors here are non-descriptive (errno), so should not be useful anyway.
		# if eof < 0: done_cb(s=False)
		if eof: done_cb()
		else: data_list.append(info_cls(info[0]))

	def _pulse_get_list(cb_t, pulse_func, info_cls, singleton=False, index_arg=True):
		def _wrapper_method(self, index=None):
			data = list()
			with self._pulse_op_cb(raw=True) as cb:
				cb = cb_t(
					ft.partial(self._pulse_info_cb, info_cls, data, cb) if not singleton else
					lambda ctx, info, userdata, cb=cb: data.append(info_cls(info[0])) or cb() )
				pa_op = pulse_func( self._ctx,
					*([index, cb, None] if index is not None else [cb, None]) )
			c.pa.operation_unref(pa_op)
			data = data or list()
			if index is not None or singleton:
				if not data: raise PulseIndexError(index)
				data, = data
			return data
		return wrapper_with_sig_info( None, _wrapper_method,
			not (pulse_func.__name__.endswith('_list') or singleton or not index_arg) )

	get_sink_by_name = _pulse_get_list(
		c.PA_SINK_INFO_CB_T,
		c.pa.context_get_sink_info_by_name, PulseSinkInfo )
	get_source_by_name = _pulse_get_list(
		c.PA_SOURCE_INFO_CB_T,
		c.pa.context_get_source_info_by_name, PulseSourceInfo )
	get_card_by_name = _pulse_get_list(
		c.PA_CARD_INFO_CB_T,
		c.pa.context_get_card_info_by_name, PulseCardInfo )

	sink_input_list = _pulse_get_list(
		c.PA_SINK_INPUT_INFO_CB_T,
		c.pa.context_get_sink_input_info_list, PulseSinkInputInfo )
	sink_input_info = _pulse_get_list(
		c.PA_SINK_INPUT_INFO_CB_T,
		c.pa.context_get_sink_input_info, PulseSinkInputInfo )
	source_output_list = _pulse_get_list(
		c.PA_SOURCE_OUTPUT_INFO_CB_T,
		c.pa.context_get_source_output_info_list, PulseSourceOutputInfo )
	source_output_info = _pulse_get_list(
		c.PA_SOURCE_OUTPUT_INFO_CB_T,
		c.pa.context_get_source_output_info, PulseSourceOutputInfo )

	sink_list = _pulse_get_list(
		c.PA_SINK_INFO_CB_T, c.pa.context_get_sink_info_list, PulseSinkInfo )
	sink_info = _pulse_get_list(
		c.PA_SINK_INFO_CB_T, c.pa.context_get_sink_info_by_index, PulseSinkInfo )
	source_list = _pulse_get_list(
		c.PA_SOURCE_INFO_CB_T, c.pa.context_get_source_info_list, PulseSourceInfo )
	source_info = _pulse_get_list(
		c.PA_SOURCE_INFO_CB_T, c.pa.context_get_source_info_by_index, PulseSourceInfo )
	card_list = _pulse_get_list(
		c.PA_CARD_INFO_CB_T, c.pa.context_get_card_info_list, PulseCardInfo )
	card_info = _pulse_get_list(
		c.PA_CARD_INFO_CB_T, c.pa.context_get_card_info_by_index, PulseCardInfo )
	client_list = _pulse_get_list(
		c.PA_CLIENT_INFO_CB_T, c.pa.context_get_client_info_list, PulseClientInfo )
	client_info = _pulse_get_list(
		c.PA_CLIENT_INFO_CB_T, c.pa.context_get_client_info, PulseClientInfo )
	server_info = _pulse_get_list(
		c.PA_SERVER_INFO_CB_T, c.pa.context_get_server_info, PulseServerInfo, singleton=True )
	module_info = _pulse_get_list(
		c.PA_MODULE_INFO_CB_T, c.pa.context_get_module_info, PulseModuleInfo )
	module_list = _pulse_get_list(
		c.PA_MODULE_INFO_CB_T, c.pa.context_get_module_info_list, PulseModuleInfo )


	def _pulse_method_call(pulse_op, func=None, index_arg=True):
		'''Creates following synchronous wrapper for async pa_operation callable:
			wrapper(index, ...) -> pulse_op(index, [*]args_func(...))
			index_arg=False: wrapper(...) -> pulse_op([*]args_func(...))'''
		def _wrapper(self, *args, **kws):
			if index_arg:
				if 'index' in kws: index = kws.pop('index')
				else: index, args = args[0], args[1:]
			pulse_args = func(*args, **kws) if func else list()
			if not is_list(pulse_args): pulse_args = [pulse_args]
			if index_arg: pulse_args = [index] + list(pulse_args)
			with self._pulse_op_cb() as cb:
				try: pulse_op(self._ctx, *(list(pulse_args) + [cb, None]))
				except c.ArgumentError as err: raise TypeError(err.args)
				except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
		return wrapper_with_sig_info(func, _wrapper, index_arg)

	card_profile_set_by_index = _pulse_method_call(
		c.pa.context_set_card_profile_by_index, lambda profile_name: profile_name )

	sink_default_set = _pulse_method_call(
		c.pa.context_set_default_sink, index_arg=False,
		func=lambda sink: sink.name if isinstance(sink, PulseSinkInfo) else sink )
	source_default_set = _pulse_method_call(
		c.pa.context_set_default_source, index_arg=False,
		func=lambda source: source.name if isinstance(source, PulseSourceInfo) else source )

	sink_input_mute = _pulse_method_call(
		c.pa.context_set_sink_input_mute, lambda mute=True: mute )
	sink_input_move = _pulse_method_call(
		c.pa.context_move_sink_input_by_index, lambda sink_index: sink_index )
	sink_mute = _pulse_method_call(
		c.pa.context_set_sink_mute_by_index, lambda mute=True: mute )
	sink_input_volume_set = _pulse_method_call(
		c.pa.context_set_sink_input_volume, lambda vol: vol.to_struct() )
	sink_volume_set = _pulse_method_call(
		c.pa.context_set_sink_volume_by_index, lambda vol: vol.to_struct() )
	sink_suspend = _pulse_method_call(
		c.pa.context_suspend_sink_by_index, lambda suspend=True: suspend )
	sink_port_set = _pulse_method_call(
		c.pa.context_set_sink_port_by_index,
		lambda port: port.name if isinstance(port, PulsePortInfo) else port )

	source_output_mute = _pulse_method_call(
		c.pa.context_set_source_output_mute, lambda mute=True: mute )
	source_output_move = _pulse_method_call(
		c.pa.context_move_source_output_by_index, lambda sink_index: sink_index )
	source_mute = _pulse_method_call(
		c.pa.context_set_source_mute_by_index, lambda mute=True: mute )
	source_output_volume_set = _pulse_method_call(
		c.pa.context_set_source_output_volume, lambda vol: vol.to_struct() )
	source_volume_set = _pulse_method_call(
		c.pa.context_set_source_volume_by_index, lambda vol: vol.to_struct() )
	source_suspend = _pulse_method_call(
		c.pa.context_suspend_source_by_index, lambda suspend=True: suspend )
	source_port_set = _pulse_method_call(
		c.pa.context_set_source_port_by_index,
		lambda port: port.name if isinstance(port, PulsePortInfo) else port )


	def module_load(self, name, args=''):
		if is_list(args): args = ' '.join(args)
		name, args = map(c.force_bytes, [name, args])
		data = list()
		with self._pulse_op_cb(raw=True) as cb:
			cb = c.PA_CONTEXT_INDEX_CB_T(
				lambda ctx, index, userdata, cb=cb: data.append(index) or cb() )
			try: c.pa.context_load_module(self._ctx, name, args, cb, None)
			except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
		index, = data
		if index == c.PA_INVALID:
			raise PulseError('Failed to load module: {} {}'.format(name, args))
		return index

	module_unload = _pulse_method_call(c.pa.context_unload_module, None)


	def stream_restore_test(self):
		'Returns module-stream-restore version int (e.g. 1) or None if it is unavailable.'
		data = list()
		with self._pulse_op_cb(raw=True) as cb:
			cb = c.PA_EXT_STREAM_RESTORE_TEST_CB_T(
				lambda ctx, version, userdata, cb=cb: data.append(version) or cb() )
			try: c.pa.ext_stream_restore_test(self._ctx, cb, None)
			except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
		version, = data
		return version if version != c.PA_INVALID else None

	stream_restore_read = _pulse_get_list(
		c.PA_EXT_STREAM_RESTORE_READ_CB_T,
		c.pa.ext_stream_restore_read, PulseExtStreamRestoreInfo, index_arg=False )
	stream_restore_list = stream_restore_read # for consistency with other *_list methods

	@ft.partial(_pulse_method_call, c.pa.ext_stream_restore_write, index_arg=False)
	def stream_restore_write( obj_name_or_list,
			mode='merge', apply_immediately=False, **obj_kws ):
		'''Update module-stream-restore db entry for specified name.
			Can be passed PulseExtStreamRestoreInfo object or list of them as argument,
				or name string there and object init keywords (e.g. volume, mute, channel_list, etc).
			"mode" is PulseUpdateEnum value of
				'merge' (default), 'replace' or 'set' (replaces ALL entries!!!).'''
		mode = PulseUpdateEnum[mode]._c_val
		if is_str(obj_name_or_list):
			obj_name_or_list = PulseExtStreamRestoreInfo(obj_name_or_list, **obj_kws)
		if isinstance(obj_name_or_list, PulseExtStreamRestoreInfo):
			obj_name_or_list = [obj_name_or_list]
		# obj_array is an array of structs, laid out contiguously in memory, not pointers
		obj_array = (c.PA_EXT_STREAM_RESTORE_INFO * len(obj_name_or_list))()
		for n, obj in enumerate(obj_name_or_list):
			obj_struct, dst_struct = obj.to_struct(), obj_array[n]
			for k,t in obj_struct._fields_: setattr(dst_struct, k, getattr(obj_struct, k))
		return mode, obj_array, len(obj_array), int(bool(apply_immediately))

	@ft.partial(_pulse_method_call, c.pa.ext_stream_restore_delete, index_arg=False)
	def stream_restore_delete(obj_name_or_list):
		'''Can be passed string name,
			PulseExtStreamRestoreInfo object or a list of any of these.'''
		if is_str(obj_name_or_list, PulseExtStreamRestoreInfo):
			obj_name_or_list = [obj_name_or_list]
		name_list = list((obj.name if isinstance( obj,
			PulseExtStreamRestoreInfo ) else obj) for obj in obj_name_or_list)
		name_struct = (c.c_char_p * len(name_list))()
		name_struct[:] = list(map(c.force_bytes, name_list))
		return [name_struct]


	def default_set(self, obj):
		'Set passed sink or source to be used as default one by pulseaudio server.'
		assert_pulse_object(obj)
		method = {
			PulseSinkInfo: self.sink_default_set,
			PulseSourceInfo: self.source_default_set }.get(type(obj))
		if not method: raise NotImplementedError(type(obj))
		method(obj)

	def sink_default_get(self):
		'Wrapper around server_info() to return sink for default_sink_name there.'
		return self.get_sink_by_name(self.server_info().default_sink_name)
	def source_default_get(self):
		'Wrapper around server_info() to return source for default_source_name there.'
		return self.get_source_by_name(self.server_info().default_source_name)

	def mute(self, obj, mute=True):
		assert_pulse_object(obj)
		method = {
			PulseSinkInfo: self.sink_mute,
			PulseSinkInputInfo: self.sink_input_mute,
			PulseSourceInfo: self.source_mute,
			PulseSourceOutputInfo: self.source_output_mute }.get(type(obj))
		if not method: raise NotImplementedError(type(obj))
		method(obj.index, mute)
		obj.mute = mute

	def port_set(self, obj, port):
		assert_pulse_object(obj)
		method = {
			PulseSinkInfo: self.sink_port_set,
			PulseSourceInfo: self.source_port_set }.get(type(obj))
		if not method: raise NotImplementedError(type(obj))
		method(obj.index, port)
		obj.port_active = port

	def card_profile_set(self, card, profile):
		assert_pulse_object(card)
		if is_str(profile):
			profile_dict = dict((p.name, p) for p in card.profile_list)
			if profile not in profile_dict:
				raise PulseIndexError( 'Card does not have'
					' profile with specified name: {!r}'.format(profile) )
			profile = profile_dict[profile]
		self.card_profile_set_by_index(card.index, profile.name)
		card.profile_active = profile

	def volume_set(self, obj, vol):
		assert_pulse_object(obj)
		method = {
			PulseSinkInfo: self.sink_volume_set,
			PulseSinkInputInfo: self.sink_input_volume_set,
			PulseSourceInfo: self.source_volume_set,
			PulseSourceOutputInfo: self.source_output_volume_set }.get(type(obj))
		if not method: raise NotImplementedError(type(obj))
		method(obj.index, vol)
		obj.volume = vol

	def volume_set_all_chans(self, obj, vol):
		assert_pulse_object(obj)
		obj.volume.value_flat = vol
		self.volume_set(obj, obj.volume)

	def volume_change_all_chans(self, obj, inc):
		assert_pulse_object(obj)
		obj.volume.values = [max(0, v + inc) for v in obj.volume.values]
		self.volume_set(obj, obj.volume)

	def volume_get_all_chans(self, obj):
		# Purpose of this func can be a bit confusing, being here next to set/change ones
		'''Get "flat" volume float value for info-object as a mean of all channel values.
			Note that this DOES NOT query any kind of updated values from libpulse,
				and simply returns value(s) stored in passed object, i.e. same ones for same object.'''
		assert_pulse_object(obj)
		return obj.volume.value_flat


	def event_mask_set(self, *masks):
		mask = 0
		for m in masks: mask |= PulseEventMaskEnum[m]._c_val
		with self._pulse_op_cb() as cb:
			c.pa.context_subscribe(self._ctx, mask, cb, None)

	def event_callback_set(self, func):
		'''Call event_listen() to start receiving these,
				and be sure to raise PulseLoopStop in a callback to stop the loop.
			Callback should accept single argument - PulseEventInfo instance.
			Passing None will disable the thing.'''
		self.event_callback = func

	def event_listen(self, timeout=None, raise_on_disconnect=True):
		'''Does not return until PulseLoopStop
				gets raised in event callback or timeout passes.
			timeout should be in seconds (float),
				0 for non-blocking poll and None (default) for no timeout.
			raise_on_disconnect causes PulseDisconnected exceptions by default.
			Do not run any pulse operations from these callbacks.'''
		assert self.event_callback
		try: self._pulse_poll(timeout)
		except c.pa.CallError: pass # e.g. from mainloop_dispatch() on disconnect
		if raise_on_disconnect and not self.connected: raise PulseDisconnected()

	def event_listen_stop(self):
		'''Stop event_listen() loop from e.g. another thread.
			Does nothing if libpulse poll is not running yet, so might be racey with
				event_listen() - be sure to call it in a loop until event_listen returns or something.'''
		self._loop_stop = True
		c.pa.mainloop_wakeup(self._loop)


	def set_poll_func(self, func, func_err_handler=None):
		'''Can be used to integrate pulse client into existing eventloop.
			Function will be passed a list of pollfd structs and timeout value (seconds, float),
				which it is responsible to use and modify (set poll flags) accordingly,
				returning int value >= 0 with number of fds that had any new events within timeout.
			func_err_handler defaults to traceback.print_exception(),
				and will be called on any exceptions from callback (to e.g. log these),
				returning poll error code (-1) to libpulse after that.'''
		if not func_err_handler: func_err_handler = traceback.print_exception
		self._pa_poll_cb = c.PA_POLL_FUNC_T(ft.partial(self._pulse_poll_cb, func, func_err_handler))
		c.pa.mainloop_set_poll_func(self._loop, self._pa_poll_cb, None)


	def get_peak_sample(self, source, timeout, stream_idx=None):
		'''Returns peak (max) value in 0-1.0 range for samples in source/stream within timespan.
			"source" can be either int index of pulseaudio source
				(i.e. source.index), its name (source.name), or None to use default source.
			Resulting value is what pulseaudio returns as
				PA_SAMPLE_FLOAT32NE float after "timeout" seconds.
			If specified source does not exist, 0 should be returned after timeout.
			This can be used to detect if there's any sound
				on the microphone or any sound played through a sink via its monitor_source index,
				or same for any specific stream connected to these (if "stream_idx" is passed).
			Sample stream masquerades as
				application.id=org.PulseAudio.pavucontrol to avoid being listed in various mixer apps.
			Example - get peak for specific sink input "si" for 0.8 seconds:
				pulse.get_peak_sample(pulse.sink_info(si.sink).monitor_source, 0.8, si.index)'''
		samples, proplist = [0], c.pa.proplist_from_string('application.id=org.PulseAudio.pavucontrol')
		ss = c.PA_SAMPLE_SPEC(format=c.PA_SAMPLE_FLOAT32NE, rate=25, channels=1)
		s = c.pa.stream_new_with_proplist(self._ctx, 'peak detect', c.byref(ss), None, proplist)
		c.pa.proplist_free(proplist)

		@c.PA_STREAM_REQUEST_CB_T
		def read_cb(s, bs, userdata):
			buff, bs = c.c_void_p(), c.c_int(bs)
			c.pa.stream_peek(s, buff, c.byref(bs))
			try:
				if not buff or bs.value < 4: return
				# This assumes that native byte order for floats is BE, same as pavucontrol
				samples[0] = max(samples[0], c.cast(buff, c.POINTER(c.c_float))[0])
			finally:
				# stream_drop() flushes buffered data (incl. buff=NULL "hole" data)
				# stream.h: "should not be called if the buffer is empty"
				if bs.value: c.pa.stream_drop(s)

		if stream_idx is not None: c.pa.stream_set_monitor_stream(s, stream_idx)
		c.pa.stream_set_read_callback(s, read_cb, None)
		if source is not None: source = unicode(source).encode('utf-8')
		try:
			c.pa.stream_connect_record( s, source,
				c.PA_BUFFER_ATTR(fragsize=4, maxlength=2**32-1),
				c.PA_STREAM_DONT_MOVE | c.PA_STREAM_PEAK_DETECT |
					c.PA_STREAM_ADJUST_LATENCY | c.PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND )
		except c.pa.CallError:
			c.pa.stream_unref(s)
			raise

		try: self._pulse_poll(timeout)
		finally:
			try: c.pa.stream_disconnect(s)
			except c.pa.CallError: pass # stream was removed

		c.pa.stream_unref(s)
		return min(1.0, samples[0])

	def play_sample(self, name, sink=None, volume=1.0, proplist_str=None):
		'''Play specified sound sample,
				with an optional sink object/name/index, volume and proplist string parameters.
			Sample must be stored on the server in advance, see e.g. "pacmd list-samples".
			See also libcanberra for an easy XDG theme sample loading, storage and playback API.'''
		if isinstance(sink, PulseSinkInfo): sink = sink.index
		sink = str(sink) if sink is not None else None
		proplist = c.pa.proplist_from_string(proplist_str) if proplist_str else None
		volume = int(round(volume*c.PA_VOLUME_NORM))
		with self._pulse_op_cb() as cb:
			try:
				if not proplist:
					c.pa.context_play_sample(self._ctx, name, sink, volume, cb, None)
				else:
					c.pa.context_play_sample_with_proplist(
						self._ctx, name, sink, volume, proplist, cb, None )
			except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])


def connect_to_cli(server=None, as_file=True, socket_timeout=1.0, attempts=5, retry_delay=0.3):
	'''Returns connected CLI interface socket (as file object, unless as_file=False),
			where one can send same commands (as lines) as to "pacmd" tool
			or pulseaudio startup files (e.g. "default.pa").
		"server" option can be specified to use non-standard unix socket path
			(when passed absolute path string) or remote tcp socket,
			when passed remote host address (to use default port) or (host, port) tuple.
		Be sure to adjust "socket_timeout" option for tcp sockets over laggy internet.
		Returned file object has line-buffered output,
			so there should be no need to use flush() after every command.
		Be sure to read from the socket line-by-line until
			"### EOF" or timeout for commands that have output (e.g. "dump\\n").
		If default server socket is used (i.e. not specified),
			server pid will be signaled to load module-cli between connection attempts.
		Completely separate protocol from the regular API, as wrapped by libpulse.
		PulseError is raised on any failure.'''
	import socket, errno, signal, time
	s, n = None, attempts if attempts > 0 else None
	try:
		pid_path, sock_af, sock_t = None, socket.AF_UNIX, socket.SOCK_STREAM
		if not server: server, pid_path = map(c.pa.runtime_path, ['cli', 'pid'])
		else:
			if not is_list(server):
				server = c.force_str(server)
				if not server.startswith('/'): server = server, 4712 # default port
			if is_list(server):
				try:
					addrinfo = socket.getaddrinfo(
						server[0], server[1], 0, sock_t, socket.IPPROTO_TCP )
					if not addrinfo: raise socket.gaierror('No addrinfo for socket: {}'.format(server))
				except (socket.gaierror, socket.error) as err:
					raise PulseError( 'Failed to resolve socket parameters'
						' (address, family) via getaddrinfo: {!r} - {} {}'.format(server, type(err), err) )
				sock_af, sock_t, _, _, server = addrinfo[0]

		s = socket.socket(sock_af, sock_t)
		s.settimeout(socket_timeout)
		while True:
			ts = c.mono_time()
			try: s.connect(server)
			except socket.error as err:
				if err.errno not in [errno.ECONNREFUSED, errno.ENOENT, errno.ECONNABORTED]: raise
			else: break
			if n:
				n -= 1
				if n <= 0: raise PulseError('Number of connection attempts ({}) exceeded'.format(attempts))
			if pid_path:
				with open(pid_path) as src: os.kill(int(src.read().strip()), signal.SIGUSR2)
			time.sleep(max(0, retry_delay - (c.mono_time() - ts)))

		if as_file: res = s.makefile('rw', 1)
		else: res, s = s, None # to avoid closing this socket
		return res

	except Exception as err: # CallError, socket.error, IOError (pidfile), OSError (os.kill)
		raise PulseError( 'Failed to connect to pulse'
			' cli socket {!r}: {} {}'.format(server, type(err), err) )

	finally:
		if s: s.close()
