/*
	Реализация спецификаций CLDC версии 1.1 (JSR-139), MIDP версии 2.1 (JSR-118)
	и других спецификаций для функционирования компактных приложений на языке
	Java (мидлетов) в среде программного обеспечения Малик Эмулятор.

	Copyright © 2016, 2019 Малик Разработчик

	Это свободная программа: вы можете перераспространять ее и/или изменять
	ее на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
	в каком она была опубликована Фондом свободного программного обеспечения;
	либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

	Эта программа распространяется в надежде, что она будет полезной,
	но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
	или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
	общественной лицензии GNU.

	Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
	вместе с этой программой. Если это не так, см.
	<https://www.gnu.org/licenses/>.
*/


package malik.emulator.midp.player;

import java.io.*;
import javax.microedition.media.*;
import javax.microedition.media.control.*;
import malik.emulator.fileformats.*;
import malik.emulator.fileformats.sound.*;
import malik.emulator.fileformats.sound.sampled.*;
import malik.emulator.media.sound.*;

public final class SampledPlayer extends ContentPlayer
		implements SoundPlayerListener, VolumeControl, SoundEncoderGetter
{
	private boolean stopped;
	private boolean loaded;
	private boolean muted;
	private double microsOnPackedSample;
	private int volume;
	private int startFromIndex;
	private int loopCountSetted;
	private int loopCountRemaining;
	private int channelsCount;
	private int samplesPerSecond;
	private long[] packedSamples;
	private SoundPlayerSampled player;
	private SoundDecoderSampled decoder;
	private String contentType;

	public SampledPlayer(SoundDecoderSampled decoder, String contentType)
	{
		this.volume = 100;
		this.loopCountSetted = 1;
		this.decoder = decoder;
		this.contentType = contentType;
	}

	public void getData(InputStream source)
			throws IOException, InvalidDataFormatException, MediaException
	{
		decoder.loadFromInputStream(source);
	}

	public void close()
	{
		SoundPlayerSampled sp;
		SoundDecoderSampled sd;
		if(state == CLOSED)
		{
			return;
		}
		synchronized(lock)
		{
			if(state != CLOSED)
			{
				state = CLOSED;
				packedSamples = null;
				if((sp = player) != null)
				{
					sp.close();
					player = sp = null;
				}
				if((sd = decoder) != null)
				{
					sd.clear();
					decoder = sd = null;
				}
				deliverEvent(PlayerListener.CLOSED, null);
			}
		}
	}

	public void realize()
			throws MediaException
	{
		int error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			default:
				break;
			case CLOSED:
				error = 1;
				break;
			case UNREALIZED:
				if(realizePlayer())
				{
					error = 2;
				}
				break;
			}
		}
		switch(error)
		{
		case 1:
			throw new IllegalStateException("Player.realize: " +
					"проигрыватель закрыт.");
		case 2:
			deliverEvent(PlayerListener.ERROR, "Player.realize: " +
					"не удалось получить корректный массив отсчётов звукового сигнала.");
			throw new MediaException("Player.realize: " +
					"не удалось получить корректный массив отсчётов звукового сигнала.");
		}
		super.realize();
	}

	public void prefetch()
			throws MediaException
	{
		int error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			default:
				break;
			case CLOSED:
				error = 1;
				break;
			case UNREALIZED:
				if(realizePlayer())
				{
					error = 2;
					break;
				}
				if(packedSamples == null)
				{
					break;
				}
				/* fall through */
			case REALIZED:
				if(prefetchPlayer())
				{
					error = 3;
				}
				break;
			}
		}
		switch(error)
		{
		case 1:
			throw new IllegalStateException("Player.prefetch: " +
					"проигрыватель закрыт.");
		case 2:
			deliverEvent(PlayerListener.ERROR, "Player.prefetch: " +
					"не удалось получить корректный массив отсчётов звукового сигнала.");
			throw new MediaException("Player.prefetch: " +
					"не удалось получить корректный массив отсчётов звукового сигнала.");
		case 3:
			deliverEvent(PlayerListener.ERROR, "Player.prefetch: " +
					"не удалось получить доступ к проигрывателю ИКМ.");
			throw new MediaException("Player.prefetch: " +
					"не удалось получить доступ к проигрывателю ИКМ.");
		}
		super.prefetch();
	}

	public void start()
			throws MediaException
	{
		int error;
		int position;
		long mediaTime;
		long[] psamples;
		SoundPlayerSampled sp;
		error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			default:
				break;
			case CLOSED:
				error = 1;
				break;
			case UNREALIZED:
				if(realizePlayer())
				{
					error = 2;
					break;
				}
				if(packedSamples == null)
				{
					break;
				}
				/* fall through */
			case REALIZED:
				if(prefetchPlayer())
				{
					error = 3;
					break;
				}
				/* fall through */
			case PREFETCHED:
				if(loopCountRemaining == 0)
				{
					loopCountRemaining = loopCountSetted;
				}
				psamples = packedSamples;
				sp = player;
				if(!loaded)
				{
					loaded = true;
					position = startFromIndex;
					sp.loadBlock(psamples, position, Math.min(psamples.length - position,
							SoundPlayerSampled.MAX_BLOCK_LENGTH));
				} else
				{
					position = startFromIndex +
							sp.getCurrentBlockIndex() * SoundPlayerSampled.MAX_BLOCK_LENGTH +
							sp.getCurrentBlockPosition();
					sp.start();
				}
				mediaTime = (long) (microsOnPackedSample * (double) position);
				state = STARTED;
				deliverEvent(PlayerListener.STARTED, new Long(mediaTime));
				break;
			}
		}
		switch(error)
		{
		case 1:
			throw new IllegalStateException("Player.prefetch: " +
					"проигрыватель закрыт.");
		case 2:
			deliverEvent(PlayerListener.ERROR, "Player.prefetch: " +
					"не удалось получить корректный массив отсчётов звукового сигнала.");
			throw new MediaException("Player.prefetch: " +
					"не удалось получить корректный массив отсчётов звукового сигнала.");
		case 3:
			deliverEvent(PlayerListener.ERROR, "Player.prefetch: " +
					"не удалось получить доступ к проигрывателю ИКМ.");
			throw new MediaException("Player.prefetch: " +
					"не удалось получить доступ к проигрывателю ИКМ.");
		}
	}

	public void stop()
			throws MediaException
	{
		int error;
		int position;
		long mediaTime;
		SoundPlayerSampled sp;
		error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			default:
				break;
			case CLOSED:
				error = 1;
				break;
			case STARTED:
				(sp = player).stop();
				position = startFromIndex +
						sp.getCurrentBlockIndex() * SoundPlayerSampled.MAX_BLOCK_LENGTH +
						sp.getCurrentBlockPosition();
				mediaTime = (long) (microsOnPackedSample * (double) position);
				state = PREFETCHED;
				deliverEvent(PlayerListener.STOPPED, new Long(mediaTime));
				break;
			}
		}
		if(error == 1)
		{
			throw new IllegalStateException("Player.stop: " +
					"проигрыватель закрыт.");
		}
	}

	public void setLoopCount(int loopCount)
	{
		int error;
		if(loopCount == 0)
		{
			throw new IllegalArgumentException("Player.setLoopCount: " +
					"количество повторов не может быть нулевым.");
		}
		error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			case CLOSED:
				error = 1;
				break;
			case STARTED:
				error = 2;
				break;
			default:
				loopCountSetted = loopCount;
				loopCountRemaining = loopCount;
				break;
			}
		}
		switch(error)
		{
		case 1:
			throw new IllegalStateException("Player.setLoopCount: " +
					"проигрыватель закрыт.");
		case 2:
			deliverEvent(PlayerListener.ERROR, "Player.setLoopCount: " +
					"невозможно задать количество повторов во время воспроизведения.");
			throw new IllegalStateException("Player.setLoopCount: " +
					"невозможно задать количество повторов во время воспроизведения.");
		}
	}

	public long setMediaTime(long time)
			throws MediaException
	{
		double microsOPS;
		int error;
		int maxpos;
		int position;
		long maxTime;
		long[] psamples;
		SoundPlayerSampled sp;
		error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			case CLOSED:
				error = 1;
				break;
			case UNREALIZED:
				error = 2;
				break;
			case REALIZED:
				maxTime = (long) ((microsOPS = microsOnPackedSample) *
						(double) (maxpos = packedSamples.length));
				time = time < 0L ? 0L : (time > maxTime ? maxTime : time);
				startFromIndex = position = (position = (int) (((double) time) / microsOPS)) < 0 ?
						0 : (position > maxpos ? maxpos : position);
				time = (long) (microsOPS * (double) position);
				break;
			case PREFETCHED:
				maxTime = (long) ((microsOPS = microsOnPackedSample) *
						(double) (maxpos = packedSamples.length));
				time = time < 0L ? 0L : (time > maxTime ? maxTime : time);
				startFromIndex = position = (position = (int) (((double) time) / microsOPS)) < 0 ?
						0 : (position > maxpos ? maxpos : position);
				time = (long) (microsOPS * (double) position);
				if(loaded)
				{
					loaded = false;
					player.reset();
				}
				break;
			case STARTED:
				maxTime = (long) ((microsOPS = microsOnPackedSample) *
						(double) (maxpos = (psamples = packedSamples).length));
				time = time < 0L ? 0L : (time > maxTime ? maxTime : time);
				startFromIndex = position = (position = (int) (((double) time) / microsOPS)) < 0 ?
						0 : (position > maxpos ? maxpos : position);
				time = (long) (microsOPS * (double) position);
				loaded = true;
				(sp = player).reset();
				sp.loadBlock(psamples, position, Math.min(psamples.length - position,
						SoundPlayerSampled.MAX_BLOCK_LENGTH));
				break;
			}
		}
		switch(error)
		{
		case 1:
			throw new IllegalStateException("Player.setMediaTime: " +
					"проигрыватель закрыт.");
		case 2:
			deliverEvent(PlayerListener.ERROR, "Player.setMediaTime: " +
					"продолжительность ещё не получена.");
			throw new IllegalStateException("Player.setMediaTime: " +
					"продолжительность ещё не получена.");
		}
		return time;
	}

	public long getMediaTime()
	{
		int error;
		int position;
		long result;
		SoundPlayerSampled sp;
		error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			case CLOSED:
				error = 1;
				result = 0L;
				break;
			case UNREALIZED:
				result = TIME_UNKNOWN;
				break;
			default:
				if(loaded)
				{
					sp = player;
					position = startFromIndex +
							sp.getCurrentBlockIndex() * SoundPlayerSampled.MAX_BLOCK_LENGTH +
							sp.getCurrentBlockPosition();
				} else
				{
					position = startFromIndex;
				}
				result = (long) (microsOnPackedSample * (double) position);
				break;
			}
		}
		if(error == 1)
		{
			throw new IllegalStateException("Player.getMediaTime: " +
					"проигрыватель закрыт.");
		}
		return result;
	}

	public long getDuration()
	{
		int error;
		int position;
		long result;
		error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			case CLOSED:
				error = 1;
				result = 0L;
				break;
			case UNREALIZED:
				result = TIME_UNKNOWN;
				break;
			default:
				position = packedSamples.length;
				result = (long) (microsOnPackedSample * (double) position);
				break;
			}
		}
		if(error == 1)
		{
			throw new IllegalStateException("Player.getMediaTime: " +
					"проигрыватель закрыт.");
		}
		return result;
	}

	public void loadBlock(SoundPlayer player, int blockIndex)
	{
		int len;
		int position;
		long duration;
		long[] psamples;
		synchronized(lock)
		{
			len = (psamples = packedSamples).length;
			if(blockIndex < 0)
			{
				position = len;
				duration = (long) (microsOnPackedSample * (double) position);
				if(--loopCountRemaining == 0)
				{
					loaded = false;
					state = PREFETCHED;
					deliverEvent(PlayerListener.END_OF_MEDIA, new Long(duration));
				} else
				{
					player.loadBlock(psamples, 0, Math.min(position,
							SoundPlayerSampled.MAX_BLOCK_LENGTH));
					deliverEvent(PlayerListener.END_OF_MEDIA, new Long(duration));
					deliverEvent(PlayerListener.STARTED, new Long(0L));
				}
			}
			else if((position = startFromIndex + blockIndex *
					SoundPlayerSampled.MAX_BLOCK_LENGTH) >= 0 && position < len)
			{
				player.loadBlock(psamples, position, Math.min(len - position,
						SoundPlayerSampled.MAX_BLOCK_LENGTH));
			}
		}
	}

	public void setMute(boolean mute)
	{
		SoundPlayer sp;
		if(this.muted != (this.muted = mute))
		{
			if((sp = player) != null)
			{
				sp.setVolume(SupportedPlayerFormats.midpVolumeToDeviceVolume(mute ? 0 : volume));
			}
			deliverEvent(PlayerListener.VOLUME_CHANGED, (VolumeControl) this);
		}
	}

	public boolean isMuted()
	{
		return muted;
	}

	public int setLevel(int volume)
	{
		SoundPlayer sp;
		if(this.volume != (this.volume = volume = (sp = player) != null ?
				SupportedPlayerFormats.deviceVolumeToMIDPVolume(sp.setVolume(
				SupportedPlayerFormats.midpVolumeToDeviceVolume(muted ? 0 : volume))) :
				SupportedPlayerFormats.midpVolumeToMIDPVolume(volume)))
		{
			deliverEvent(PlayerListener.VOLUME_CHANGED, (VolumeControl) this);
		}
		return volume;
	}

	public int getLevel()
	{
		return volume;
	}

	public SoundEncoder getSoundEncoder()
			throws InvalidDataFormatException
	{
		int i;
		int j;
		int len;
		int error;
		long psample;
		short[] samples;
		long[] psamples;
		SoundEncoderSampled result;
		error = 0;
		synchronized(lock)
		{
			if(state == CLOSED)
			{
				error = 1;
				psamples = null;
			}
			else if((psamples = packedSamples) == null)
			{
				error = 2;
			}
		}
		switch(error)
		{
		case 1:
			throw new IllegalStateException("SoundEncoderGetter.getSoundEncoder: " +
					"проигрыватель закрыт.");
		case 2:
			throw new IllegalStateException("SoundEncoderGetter.getSoundEncoder: " +
					"данные ещё не получены.");
		}
		samples = new short[(len = psamples.length) << 2];
		for(i = 0, j = 0; j < len; j++)
		{
			samples[i++] = (short) (psample = psamples[j]);
			samples[i++] = (short) (psample >> 16);
			samples[i++] = (short) (psample >> 32);
			samples[i++] = (short) (psample >> 48);
		}
		(result = new malik.emulator.fileformats.sound.sampled.wavelib.WaveEncoder()).
				setSamples(channelsCount, samplesPerSecond, samples);
		return result;
	}

	protected void unrealize()
	{
		SoundDecoder sd;
		if((sd = decoder) != null)
		{
			stopped = true;
			sd.stopLoading();
		}
		super.unrealize();
	}

	protected void unprefetch()
	{
		SoundPlayer sp;
		synchronized(lock)
		{
			if((sp = player) != null)
			{
				sp.close();
				player = null;
				loaded = false;
				startFromIndex = 0;
			}
			super.unprefetch();
		}
	}

	protected String getPlayerContentType()
	{
		return contentType;
	}

	private boolean realizePlayer()
	{
		int i;
		int j;
		int ch;
		int ss;
		int len;
		long duration;
		long samplesOC;
		short[] samples;
		long[] psamples;
		SoundDecoderSampled sd;
		stopped = false;
		if((samples = (sd = decoder).getSamples()) == null)
		{
			return false;
		}
		if((len = samples.length) < 4)
		{
			return true;
		}
		psamples = new long[len >>= 2];
		for(j = (len - 1) << 2, i = len; i-- > 0; j -= 4)
		{
			if(stopped)
			{
				return false;
			}
			psamples[i] = ((long) (samples[j] & 0xffff)) |
					(((long) (samples[j + 1] & 0xffff)) << 16) |
					(((long) (samples[j + 2] & 0xffff)) << 32) |
					(((long) (samples[j + 3] & 0xffff)) << 48);
		}
		duration = ((long) len) * 4000000L /
				(samplesOC = (long) ((ch = sd.getChannels()) * (ss = sd.getSamplesPerSecond())));
		decoder = null;
		microsOnPackedSample = 4.e+006d / ((double) samplesOC);
		channelsCount = ch;
		samplesPerSecond = ss;
		packedSamples = psamples;
		state = REALIZED;
		deliverEvent(PlayerListener.DURATION_UPDATED, new Long(duration));
		return false;
	}

	private boolean prefetchPlayer()
	{
		boolean result;
		SoundPlayerSampled sp;
		try
		{
			player = sp = SoundPlayerSampled.open(samplesPerSecond, 16, channelsCount,
					Math.min(Math.max(SoundPlayerSampled.MIN_BLOCK_LENGTH, packedSamples.length),
					SoundPlayerSampled.MAX_BLOCK_LENGTH));
			sp.setSoundPlayerListener(this);
			sp.setVolume(muted ? 0 : SupportedPlayerFormats.midpVolumeToDeviceVolume(volume));
			state = PREFETCHED;
			result = false;
		}
		catch(SoundPlayerException e)
		{
			e.printRealStackTrace();
			result = true;
		}
		return result;
	}
}
