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

"""
This file is part of OpenSesame.

OpenSesame is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

OpenSesame is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with OpenSesame.  If not, see <http://www.gnu.org/licenses/>.
"""

from libopensesame.py3compat import *
from openexp._sampler.sampler import Sampler
from libopensesame.exceptions import osexception
from libopensesame.oslogging import oslogger
from openexp.backend import configurable
from openexp.keyboard import Keyboard
import soundfile as sf
import numpy as np

DEFAULT_SOUND_FREQ = 48000
DEFAULT_BLOCK_SIZE = 256
NEEDS_BLOCK_SIZE = 'sounddevice', 'PTB'
# Will be intialized during init_sound()
Sound = None
PLAYING = None


class Psycho(Sampler):

	"""
	desc:
		This is a sampler backend built on top of PsychoPy. For function
		specifications and docstrings, see `openexp._sampler.sampler`.
	"""

	# The settings variable is used by the GUI to provide a list of back-end
	# settings
	settings = {
		'psycho_audiolib': {
			'name': 'Sound library',
			'description': 'Can be sounddevice, pyo, pygame, or PTB',
			'default': 'sounddevice'
		},
		'sound_freq': {
			'name': 'Sampling frequency for synth',
			'description': 'Determines the sampling rate of synthesized sounds',
			'default': DEFAULT_SOUND_FREQ
		},
		'block_size': {
			'name': 'Buffer size',
			'description': 'Low values for low latency. High values for stability.',
			'default': DEFAULT_BLOCK_SIZE
		}
	}

	def __init__(self, experiment, src, **playback_args):

		if (
			isinstance(src, np.ndarray) and
			src.dtype == np.int16 and
			src.ndim == 1
		):
			# The Synth provides the data as a 1D array with int values between
			# 0 and 32767. The signal is stereo but flattened into a single
			# trace, so we expand it here.
			self._data = np.array(src, dtype=np.float64) / 32767
			self._data = self._data.reshape((self._data.shape[0] // 2, 2))
			self._samplerate = experiment.var.get(
				'sound_freq',
				DEFAULT_SOUND_FREQ
			)
		else:
			self._data, self._samplerate = sf.read(src)
		# Create keyword arguments, which depend on the sound backend
		kwargs = {}
		if (
			experiment.var.get('psycho_audiolib', 'sounddevice')
			in NEEDS_BLOCK_SIZE
		):			
			kwargs['blockSize'] = experiment.var.get(
				'block_size',
				DEFAULT_BLOCK_SIZE
			)
		# Make sure that the data is a [samples, 2] array for stereo data
		if self._data.ndim == 1:
			self._data.shape = [len(self._data), 1]
		if self._data.shape[1] == 1:
			self._data = self._data.repeat(2, axis=1)
		self._sound = Sound(
			self._data,
			sampleRate=self._samplerate,
			**kwargs
		)
		self._keyboard = Keyboard(experiment)
		Sampler.__init__(self, experiment, src, **playback_args)

	def set_config(self, **cfg):

		Sampler.set_config(self, **cfg)
		if not cfg:
			return
		if 'volume' in cfg:
			self._set_volume(cfg['volume'])
		if 'pitch' in cfg:
			self._set_pitch(cfg['pitch'])
		if 'pan' in cfg:
			self._set_pan(cfg['pan'])
		if 'duration' in cfg:
			self._set_duration(cfg['duration'])
		if 'fade_in' in cfg:
			self._set_fade_in(cfg['fade_in'])
		self._sound.stopTime = -1  # Fixes a bug in PsychoPy 3.2.4
		self._sound.setSound(self._data)

	def _set_volume(self, volume):

		if volume == 1:
			return
		self._data *= volume

	def _set_pitch(self, pitch):

		from scipy.signal import resample
		if pitch == 1:
			return
		self._data = resample(self._data, int(self._data.shape[0] / pitch))

	def _set_pan(self, pan):

		if pan == 'left':
			right_volume = 0.
			left_volume = 1.
		elif pan == 'right':
			right_volume = 1.
			left_volume = 0.
		elif pan < 0:
			right_volume = -1. / pan
			left_volume = 1.
		elif pan > 0:
			right_volume = 1.
			left_volume = 1. / pan
		else:
			return
		self._data[:, 0] *= left_volume
		self._data[:, 1] *= right_volume

	def _ms_to_samples(self, ms):

		return int(self._samplerate * (ms / 1000))

	def _set_duration(self, duration):

		if not duration:
			return
		self._data = self._data[:self._ms_to_samples(duration), :]

	def _set_fade_in(self, fade_in):

		if fade_in is None:
			return
		ramp = np.linspace(
			0, 1, self._ms_to_samples(fade_in)
		)[:self._data.shape[0]]
		self._data[:len(ramp), 0] *= ramp
		self._data[:len(ramp), 1] *= ramp

	@configurable
	def play(self, **playback_args):

		self._sound.stop()
		self._sound.play()
		if self.block:
			self.wait()

	def stop(self):

		self._sound.stop()

	def pause(self):

		self._sound.pause()

	def resume(self):

		self._sound.play()

	def is_playing(self):

		return self._sound.status == PLAYING

	def wait(self):

		while self._sound.status == PLAYING:
			self._keyboard.flush()

	@staticmethod
	def init_sound(experiment):

		global Sound, PLAYING

		from psychopy import prefs
		prefs.hardware['audioLib'] = [
			experiment.var.get('psycho_audiolib', 'sounddevice')
		]
		from psychopy import constants
		PLAYING = constants.PLAYING
		from psychopy.sound import Sound

	@staticmethod
	def close_sound(experiment):

		pass


# Non PEP-8 alias for backwards compatibility
psycho = Psycho
