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

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

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

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

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

package malik.emulator.fileformats.sound.synthetic.midi;

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

public final class MIDIDecoder extends ControlList implements SyntheticSoundDecoder, SyntheticPlayerDecoder
{
    private static final class MetaData extends Object implements Control, MetaDataControl
    {
        public String copyright;

        public MetaData() {
        }

        public String[] getKeys() {
            return new String[] { COPYRIGHT_KEY };
        }

        public String getKeyValue(String key) {
            if(COPYRIGHT_KEY.equals(key)) return copyright;
            throw new IllegalArgumentException("MetaDataControl.getKeyValue: аргумент key имеет недопустимое значение.");
        }
    }

    private static final class Track extends ByteArrayInputStream
    {
        public int lastStatusByte;
        public long eventTimeInDelta;

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

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

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

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

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

        public String readText() {
            int len;
            char[] chars;
            byte[] bytes;
            read(bytes = new byte[len = readVolatileLengthValue()], 0, len);
            chars = new char[len];
            for(int i = len; i-- > 0; chars[i] = (char) (bytes[i] & 0xff));
            return new String(chars);
        }
    }

    public static final long SIGNATURE = 0x4d546864L; /* MThd */

    public static final String MIME_TYPE = "audio/midi";

    private static final double DEFAULT_TEMPO = 500000.d;

    private static final int MAXIMUM_TRACKS = 128;

    private static final int MTRK = 0x4d54726b; /* MTrk */

    private boolean stopped;
    private long[] messages;
    private final MetaData info;

    public MIDIDecoder() {
        MetaData info = new MetaData();
        this.controls = new Control[] { info };
        this.info = info;
    }

    public void loadFromInputStream(InputStream stream) throws IOException {
        loadFromDataStream(new ExtendedDataInputStream(stream));
    }

    public void loadFromDataStream(ExtendedDataInputStream stream) throws IOException {
        double ppqn;
        double tempo;
        double currentTimeInMillis;
        int len;
        int ippqn;
        int ntracks;
        int ntracksEnds;
        long currentTimeInDelta;
        long[] messages;
        Track[] tracks;
        String copyright;
        checkStopped();
        if(stream.readInt() != 6)
        {
            throw new InvalidDataFormatException("MIDIDecoder.loadFromInputStream: неправильный формат данных.");
        }
        stream.readUnsignedShort();
        if((ntracks = stream.readUnsignedShort()) > MAXIMUM_TRACKS)
        {
            throw new UnsupportedDataException("MIDIDecoder.loadFromInputStream: неподдерживаемые данные.");
        }
        if(ntracks <= 0)
        {
            throw new InvalidDataFormatException("MIDIDecoder.loadFromInputStream: неправильный формат данных.");
        }
        if((ippqn = stream.readShort()) <= 0)
        {
            throw new UnsupportedDataException("MIDIDecoder.loadFromInputStream: неподдерживаемые данные.");
        }
        tracks = new Track[ntracks];
        for(int i = 0; i < ntracks; i++)
        {
            int chunkName;
            int chunkSize;
            byte[] trackData;
            checkStopped();
            chunkName = stream.readInt();
            chunkSize = stream.readInt();
            if(chunkName != MTRK)
            {
                stream.skip((long) chunkSize & 0x00000000ffffffffL);
                continue;
            }
            if(chunkSize < 0)
            {
                throw new UnsupportedDataException("MIDIDecoder.loadFromInputStream: неподдерживаемые данные.");
            }
            stream.readFully(trackData = new byte[chunkSize]);
            tracks[i] = new Track(trackData);
        }
        checkStopped();
        ppqn = 1000.d * (double) ippqn;
        tempo = DEFAULT_TEMPO;
        currentTimeInMillis = 0.d;
        len = 1;
        ntracksEnds = 0;
        currentTimeInDelta = 0L;
        messages = new long[0x0f];
        copyright = null;

        /* подсчёт времени начал траков */
        for(int i = ntracks; i-- > 0; )
        {
            checkStopped();
            tracks[i].readTimeInDelta();
        }
        label0: for(; ; )
        {
            double offsetTimeInMillis;
            int statusByte;
            int dataByte1;
            int dataByte2;
            int dataByte3;
            long minTimeInDelta;
            long offsetTimeInDelta;
            Track currentTrack;
            checkStopped();

            /* выбор трака с наименьшим дельта-временем наступления события */
            minTimeInDelta = (currentTrack = tracks[0]).eventTimeInDelta;
            for(int i = 1; i < ntracks; i++)
            {
                long thisTimeInDelta;
                Track thisTrack;
                checkStopped();
                if((thisTimeInDelta = (thisTrack = tracks[i]).eventTimeInDelta) < minTimeInDelta)
                {
                    currentTrack = thisTrack;
                    minTimeInDelta = thisTimeInDelta;
                }
            }

            /* обработка очередного события в выбранном траке */
            if((statusByte = currentTrack.read()) < 0) break label0;
            if((statusByte & 0x80) == 0)
            {
                statusByte = currentTrack.lastStatusByte;
                currentTrack.seek(-1);
            }
            else if(statusByte != 0xff)
            {
                currentTrack.lastStatusByte = statusByte;
            }
            label1:
            {
                if(statusByte >= 0x80 && statusByte < 0xc0 || statusByte >= 0xe0 && statusByte < 0xf0)
                {
                    dataByte1 = currentTrack.read();
                    dataByte2 = currentTrack.read();
                    break label1;
                }
                if(statusByte >= 0xc0 && statusByte < 0xe0)
                {
                    dataByte1 = currentTrack.read();
                    dataByte2 = 0;
                    break label1;
                }
                if(statusByte == 0xf0 || statusByte == 0xf7)
                {
                    currentTrack.seek(currentTrack.readVolatileLengthValue());
                    currentTrack.readTimeInDelta();
                    continue label0;
                }
                if(statusByte == 0xf1 || statusByte == 0xf3)
                {
                    currentTrack.seek(1);
                    currentTrack.readTimeInDelta();
                    continue label0;
                }
                if(statusByte == 0xf2)
                {
                    currentTrack.seek(2);
                    currentTrack.readTimeInDelta();
                    continue label0;
                }
                if(statusByte != 0xff)
                {
                    currentTrack.readTimeInDelta();
                    continue label0;
                }
                switch(currentTrack.read())
                {
                default:
                    currentTrack.seek(currentTrack.readVolatileLengthValue());
                    currentTrack.readTimeInDelta();
                    continue label0;
                case -1:
                    break label0;
                case 0x02:
                    copyright = currentTrack.readText();
                    currentTrack.readTimeInDelta();
                    continue label0;
                case 0x2f:
                    if(currentTrack.read() == 0x00) break;
                    break label0;
                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 = (double) (dataByte1 << 16 | dataByte2 << 8 | dataByte3);
                    currentTrack.seek(statusByte - 3);
                    currentTrack.readTimeInDelta();
                    continue label0;
                }

                /* обработка окончания трака */
                if(++ntracksEnds < ntracks)
                {
                    currentTrack.endOfTrack();
                    continue label0;
                }
                offsetTimeInDelta = currentTrack.eventTimeInDelta - currentTimeInDelta;
                offsetTimeInMillis = (double) offsetTimeInDelta * tempo / ppqn;
                currentTimeInDelta += offsetTimeInDelta;
                currentTimeInMillis += offsetTimeInMillis;
                if(len == messages.length) Array.copy(messages, 0, messages = new long[(len << 1) + 1], 0, len);
                messages[len++] = (long) currentTimeInMillis << 24 | 0xff2f00L;
                break label0;
            }

            /* обработка голосового сообщения */
            offsetTimeInDelta = currentTrack.eventTimeInDelta - currentTimeInDelta;
            offsetTimeInMillis = (double) offsetTimeInDelta * tempo / ppqn;
            currentTimeInDelta += offsetTimeInDelta;
            currentTimeInMillis += offsetTimeInMillis;
            if(len == messages.length) Array.copy(messages, 0, messages = new long[(len << 1) + 1], 0, len);
            messages[len++] = (long) currentTimeInMillis << 24 | (long) (statusByte << 16 | dataByte1 << 8 | dataByte2);
            currentTrack.readTimeInDelta();
        }
        if(len < messages.length) Array.copy(messages, 0, messages = new long[len], 0, len);
        this.messages = messages;
        this.info.copyright = copyright;
    }

    public void stopDecoding() {
        stopped = true;
    }

    public void clear() {
        stopped = false;
        messages = null;
        info.copyright = null;
    }

    public boolean isEmpty() {
        return messages == null && info.copyright == null;
    }

    public long[] getMessages() {
        int len;
        long[] result;
        if((result = messages) == null) return null;
        Array.copy(result, 0, result = new long[len = result.length], 0, len);
        return result;
    }

    public String getContentType() {
        return MIME_TYPE;
    }

    private void checkStopped() throws InterruptedIOException {
        if(stopped)
        {
            stopped = false;
            throw new InterruptedIOException("MIDIDecoder.loadFromInputStream: процесс декодирования был прерван.");
        }
    }
}
