/*
	Реализация спецификаций 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.synthetic.*;
import malik.emulator.media.sound.*;

public final class SyntheticPlayer extends ContentPlayer
		implements SoundPlayerListener, VolumeControl, SoundEncoderGetter
{
	private boolean loaded;
	private boolean muted;
	private int volume;
	private int startFromIndex;
	private int loopCountSetted;
	private int loopCountRemaining;
	private long[] synthesizerMessages;
	private SoundPlayerSynthetic player;
	private SoundDecoderSynthetic decoder;
	private String contentType;

	public SyntheticPlayer(SoundDecoderSynthetic 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()
	{
		SoundPlayerSynthetic sp;
		SoundDecoderSynthetic sd;
		if(state == CLOSED)
		{
			return;
		}
		synchronized(lock)
		{
			if(state != CLOSED)
			{
				state = CLOSED;
				synthesizerMessages = 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: " +
					"не удалось получить корректный массив сообщений для синтезатора MIDI.");
			throw new MediaException("Player.realize: " +
					"не удалось получить корректный массив сообщений для синтезатора MIDI.");
		}
		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(synthesizerMessages == 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: " +
					"не удалось получить корректный массив сообщений для синтезатора MIDI.");
			throw new MediaException("Player.prefetch: " +
					"не удалось получить корректный массив сообщений для синтезатора MIDI.");
		case 3:
			deliverEvent(PlayerListener.ERROR, "Player.prefetch: " +
					"не удалось получить доступ к синтезатору MIDI.");
			throw new MediaException("Player.prefetch: " +
					"не удалось получить доступ к синтезатору MIDI.");
		}
		super.prefetch();
	}

	public void start()
			throws MediaException
	{
		int error;
		int position;
		long mediaTime;
		long[] messages;
		SoundPlayerSynthetic sp;
		error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			default:
				break;
			case CLOSED:
				error = 1;
				break;
			case UNREALIZED:
				if(realizePlayer())
				{
					error = 2;
					break;
				}
				if(synthesizerMessages == null)
				{
					break;
				}
				/* fall through */
			case REALIZED:
				if(prefetchPlayer())
				{
					error = 3;
					break;
				}
				/* fall through */
			case PREFETCHED:
				if(loopCountRemaining == 0)
				{
					loopCountRemaining = loopCountSetted;
				}
				messages = synthesizerMessages;
				sp = player;
				if(!loaded)
				{
					loaded = true;
					position = startFromIndex;
					sp.loadBlock(messages, 0, messages.length);
					if(position > 0)
					{
						sp.setCurrentBlockPosition(position);
					}
				} else
				{
					position = sp.getCurrentBlockPosition();
					sp.start();
				}
				mediaTime = 1000L * (messages[position] >>> 24);
				state = STARTED;
				deliverEvent(PlayerListener.STARTED, new Long(mediaTime));
				break;
			}
		}
		switch(error)
		{
		case 1:
			throw new IllegalStateException("Player.start: " +
					"проигрыватель закрыт.");
		case 2:
			deliverEvent(PlayerListener.ERROR, "Player.start: " +
					"не удалось получить корректный массив сообщений для синтезатора MIDI.");
			throw new MediaException("Player.start: " +
					"не удалось получить корректный массив сообщений для синтезатора MIDI.");
		case 3:
			deliverEvent(PlayerListener.ERROR, "Player.start: " +
					"не удалось получить доступ к синтезатору MIDI.");
			throw new MediaException("Player.start: " +
					"не удалось получить доступ к синтезатору MIDI.");
		}
	}

	public void stop()
			throws MediaException
	{
		int error;
		int position;
		long mediaTime;
		long[] messages;
		SoundPlayerSynthetic sp;
		error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			default:
				break;
			case CLOSED:
				error = 1;
				break;
			case STARTED:
				(sp = player).stop();
				position = sp.getCurrentBlockPosition();
				messages = synthesizerMessages;
				mediaTime = 1000L * (messages[position] >>> 24);
				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
	{
		int error;
		int positionA;
		int positionB;
		int posApprox;
		long timeApprox;
		long[] messages;
		error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			case CLOSED:
				error = 1;
				break;
			case UNREALIZED:
				error = 2;
				break;
			default:
				messages = synthesizerMessages;
				positionA = 0;
				positionB = messages.length - 1;
				do
				{
					posApprox = (positionA + positionB) >> 1;
					timeApprox = 1000L * (messages[posApprox] >>> 24);
					if(positionA == positionB)
					{
						break;
					}
					if(time < timeApprox)
					{
						if((positionB = posApprox - 1) < positionA)
						{
							positionB = positionA;
						}
					}
					else if(time > timeApprox)
					{
						if((positionA = posApprox + 1) > positionB)
						{
							positionA = positionB;
						}
					}
					else
					{
						break;
					}
				} while(true);
				time = timeApprox;
				if(!loaded)
				{
					startFromIndex = posApprox;
					break;
				}
				player.setCurrentBlockPosition(posApprox);
				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;
		long[] messages;
		error = 0;
		synchronized(lock)
		{
			switch(state)
			{
			case CLOSED:
				error = 1;
				result = 0L;
				break;
			case UNREALIZED:
				result = TIME_UNKNOWN;
				break;
			default:
				position = loaded ? player.getCurrentBlockPosition() : startFromIndex;
				messages = synthesizerMessages;
				result = 1000L * (messages[position] >>> 24);
				break;
			}
		}
		if(error == 1)
		{
			throw new IllegalStateException("Player.getMediaTime: " +
					"проигрыватель закрыт.");
		}
		return result;
	}

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

	public void loadBlock(SoundPlayer player, int blockIndex)
	{
		int position;
		long duration;
		long[] messages;
		if(blockIndex >= 0)
		{
			return;
		}
		synchronized(lock)
		{
			position = (messages = synthesizerMessages).length - 1;
			duration = 1000L * (messages[position] >>> 24);
			if(--loopCountRemaining == 0)
			{
				state = PREFETCHED;
				deliverEvent(PlayerListener.END_OF_MEDIA, new Long(duration));
			} else
			{
				player.start();
				deliverEvent(PlayerListener.END_OF_MEDIA, new Long(duration));
				deliverEvent(PlayerListener.STARTED, new Long(0L));
			}
		}
	}

	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 error;
		long[] messages;
		SoundEncoderSynthetic result;
		error = 0;
		synchronized(lock)
		{
			if(state == CLOSED)
			{
				error = 1;
				messages = null;
			}
			else if((messages = synthesizerMessages) == null)
			{
				error = 2;
			}
		}
		switch(error)
		{
		case 1:
			throw new IllegalStateException("SoundEncoderGetter.getSoundEncoder: " +
					"проигрыватель закрыт.");
		case 2:
			throw new IllegalStateException("SoundEncoderGetter.getSoundEncoder: " +
					"данные ещё не получены.");
		}
		(result = new malik.emulator.fileformats.sound.synthetic.midilib.MIDIEncoder()).
				setMessages(messages);
		return result;
	}

	protected void unrealize()
	{
		SoundDecoder sd;
		if((sd = decoder) != null)
		{
			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 position;
		long duration;
		long[] messages;
		if((messages = decoder.getMessages()) == null)
		{
			return false;
		}
		if((position = messages.length) <= 0)
		{
			return true;
		}
		position--;
		duration = 1000L * (messages[position] >>> 24);
		decoder = null;
		synthesizerMessages = messages;
		state = REALIZED;
		deliverEvent(PlayerListener.DURATION_UPDATED, new Long(duration));
		return false;
	}

	private boolean prefetchPlayer()
	{
		boolean result;
		SoundPlayerSynthetic sp;
		try
		{
			player = sp = SoundPlayerSynthetic.open();
			sp.setSoundPlayerListener(this);
			sp.setVolume(muted ? 0 : SupportedPlayerFormats.midpVolumeToDeviceVolume(volume));
			state = PREFETCHED;
			result = false;
		}
		catch(SoundPlayerException e)
		{
			e.printRealStackTrace();
			result = true;
		}
		return result;
	}
}
