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

import java.io.*;
import malik.emulator.fileformats.*;
import malik.emulator.fileformats.sound.*;
import malik.emulator.fileformats.sound.synthetic.*;

public final class MIDIDecoder extends Object
		implements InputAdapter, SoundDecoder, DataHolder, DataDecoder, SoundDecoderSynthetic
{
	private static final class Track extends ByteArrayInputStream
	{
		public int lastStatusByte;
		public long eventTimeInDelta;

		public Track(byte[] stream)
		{
			super(stream);
		}

		public void seek(int delta)
		{
			pos += delta;
		}

		public void endOfTrack()
		{
			eventTimeInDelta = Long.MAX_VALUE;
		}

		public void readDeltaTime()
		{
			eventTimeInDelta += (long) readVolatileLengthValue();
		}

		public int readVolatileLengthValue()
		{
			int result = 0;
			int position = pos;
			byte[] stream = buf;
			for(int len = stream != null ? stream.length : 0, i = 4;
					position >= 0 && position < len && i-- > 0; )
			{
				int b;
				result = result << 7 | (b = stream[position++] & 0xff) & 0x7f;
				if((b & 0x80) == 0) break;
			}
			pos = position;
			return result;
		}
	}

	public static final long MIDI_SIGNATURE = 0x4d54686400000006L;
	private static final int MTRK_SIGNATURE = 0x4d54726b; /* MTrk */
	private static final int DEFAULT_TEMPO = 500000;
	private static final int MAX_TRACKS = 32;


	private boolean stopped;
	private int ppqn;
	private long[] messages;
	private Track[] tracks;

	public MIDIDecoder()
	{
	}

	public void loadFromInputStream(InputStream stream)
			throws IOException, InvalidDataFormatException
	{
		loadFromDataStream(new DataInputStream(stream));
	}

	public void loadFromDataStream(DataInputStream stream)
			throws IOException, InvalidDataFormatException
	{
		int i;
		int len;
		int ppqn;
		int ntracks;
		byte[] trackBytes;
		Track[] tracks;
		clear();
		stream.readUnsignedShort();
		if((ntracks = stream.readUnsignedShort()) > MAX_TRACKS)
		{
			throw new InvalidDataFormatException("Декодирование MIDI-файлов: " +
					"количество треков не должно превышать " + MAX_TRACKS + ".");
		}
		if(ntracks <= 0)
		{
			throw new InvalidDataFormatException("Декодирование MIDI-файлов: " +
					"количество треков не должно быть нулевым.");
		}
		if((ppqn = stream.readShort()) <= 0)
		{
			throw new InvalidDataFormatException("Декодирование MIDI-файлов: " +
					"время в формате SMPTE и нулевое PPQN не поддерживаются.");
		}
		tracks = new Track[ntracks];
		for(i = 0; i < ntracks; i++)
		{
			int type = stream.readInt();
			if((len = stream.readInt()) <= 0)
			{
				throw new InvalidDataFormatException("Декодирование MIDI-файлов: " +
						"длина блока данных может быть только положительной.");
			}
			if(type != MTRK_SIGNATURE)
			{
				stream.skipBytes(len);
				continue;
			}
			stream.readFully(trackBytes = new byte[len]);
			tracks[i] = new Track(trackBytes);
		}
		this.stopped = false;
		this.ppqn = ppqn;
		this.messages = null;
		this.tracks = tracks;
	}

	public void stopLoading()
	{
		stopped = true;
	}

	public void clear()
	{
		messages = null;
		tracks = null;
	}

	public boolean isEmpty()
	{
		return messages == null && tracks == null;
	}

	public long[] getMessages()
	{
		long[] result;
		if((result = this.messages) == null)
		{
			boolean stop;
			int len;
			Track[] tracks;
			if((tracks = this.tracks) == null)
			{
				return null;
			}
			result = new long[0x0f];
			stop = false;
			len = 1; /* первое (по индексу [0]) сообщение – ориентир начала песни */
			label0:
			try
			{
				int ntracks = tracks.length;
				int ntracksEnds = 0;
				long currentTimeInDelta = 0L;
				double currentTimeInMillis = 0d;
				double ppqn = (double) this.ppqn;
				double tempo = (double) DEFAULT_TEMPO;
				/* подсчёт времени начал треков */
				for(int i = ntracks; i-- > 0; )
				{
					if(stopped)
					{
						stop = true;
						break label0;
					}
					tracks[i].readDeltaTime();
				}
				label1: for(; ; )
				{
					int statusByte;
					int dataByte1;
					int dataByte2;
					int dataByte3;
					long dtimeInDelta;
					double dtimeInMillis;
					Track currentTrack;
					if(stopped)
					{
						stop = true;
						break label0;
					}
					/* выбираем трек с наименьшим дельта-временем наступления события */
					currentTrack = tracks[0];
					{
						long minDeltaTime = currentTrack.eventTimeInDelta;
						for(int i = 1; i < ntracks; i++)
						{
							long curDeltaTime;
							Track curTrack;
							if(stopped)
							{
								stop = true;
								break label0;
							}
							if((curDeltaTime = (curTrack = tracks[i]).eventTimeInDelta) < minDeltaTime)
							{
								currentTrack = curTrack;
								minDeltaTime = curDeltaTime;
							}
						}
					}
					/* обработка очередного события в выбранном треке */
					if((statusByte = currentTrack.read()) < 0) break label0;
					if((statusByte & 0x80) == 0)
					{
						statusByte = currentTrack.lastStatusByte;
						currentTrack.seek(-1);
					}
					else if(statusByte != 0xff)
					{
						currentTrack.lastStatusByte = statusByte;
					}
					label2:
					{
						if(statusByte >= 0x80 && statusByte < 0xc0 || statusByte >= 0xe0 && statusByte < 0xf0)
						{
							dataByte1 = currentTrack.read();
							dataByte2 = currentTrack.read();
							break label2;
						}
						else if(statusByte >= 0xc0 && statusByte < 0xe0)
						{
							dataByte1 = currentTrack.read();
							dataByte2 = 0;
							break label2;
						}
						else if(statusByte == 0xf0 || statusByte == 0xf7)
						{
							currentTrack.seek(currentTrack.readVolatileLengthValue());
							currentTrack.readDeltaTime();
							continue label1;
						}
						else if(statusByte == 0xf1 || statusByte == 0xf3)
						{
							currentTrack.seek(1);
							currentTrack.readDeltaTime();
							continue label1;
						}
						else if(statusByte == 0xf2)
						{
							currentTrack.seek(2);
							currentTrack.readDeltaTime();
							continue label1;
						}
						else if(statusByte == 0xff)
						{
							switch(currentTrack.read())
							{
							default:
								currentTrack.seek(currentTrack.readVolatileLengthValue());
								currentTrack.readDeltaTime();
								continue label1;
							case -1:
								break label0;
							case 0x2f:
								if(currentTrack.read() != 0x00) break label0;
								break;
							case 0x51:
								if((statusByte = currentTrack.readVolatileLengthValue()) < 3) break label0;
								dataByte1 = currentTrack.read();
								dataByte2 = currentTrack.read();
								dataByte3 = currentTrack.read();
								if((dataByte1 | dataByte2 | dataByte3) < 0) break label0;
								tempo = dataByte1 << 16 | dataByte2 << 8 | dataByte3;
								currentTrack.seek(statusByte - 3);
								currentTrack.readDeltaTime();
								continue label1;
							}
						}
						else
						{
							currentTrack.readDeltaTime();
							continue label1;
						}
						/* обработка окончания трека */
						if(++ntracksEnds < ntracks)
						{
							currentTrack.endOfTrack();
							continue label1;
						}
						dtimeInDelta = currentTrack.eventTimeInDelta - currentTimeInDelta;
						dtimeInMillis = 0.001d * (double) dtimeInDelta * tempo / ppqn;
						currentTimeInDelta += dtimeInDelta;
						currentTimeInMillis += dtimeInMillis;
						if(len == result.length)
						{
							Array.copy(result, 0, result = new long[(len << 1) + 1], 0, len);
						}
						result[len++] = (long) currentTimeInMillis << 24 | 0xff2f00L;
						break label0;
					}
					/* обработка голосового сообщения */
					dtimeInDelta = currentTrack.eventTimeInDelta - currentTimeInDelta;
					dtimeInMillis = 0.001d * (double) dtimeInDelta * tempo / ppqn;
					currentTimeInDelta += dtimeInDelta;
					currentTimeInMillis += dtimeInMillis;
					if(len == result.length)
					{
						Array.copy(result, 0, result = new long[(len << 1) + 1], 0, len);
					}
					result[len++] = (long) currentTimeInMillis << 24 | statusByte << 16 | dataByte1 << 8 | dataByte2;
					currentTrack.readDeltaTime();
				}
			}
			catch(RuntimeException e)
			{
				e.printRealStackTrace();
			}
			if(stop)
			{
				return null;
			}
			if(len < result.length)
			{
				Array.copy(result, 0, result = new long[len], 0, len);
			}
			this.messages = result;
			this.tracks = null;
		}
		return result;
	}
}
