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

public final class MIDIEncoder extends Object implements SyntheticSoundEncoder
{
    private static final class Track extends ByteArrayOutputStream
    {
        public Track() {
        }

        public void writeVolatileLengthValue(int intData) {
            if(intData >= 0x00000000 && intData < 0x00000080)
            {
                write(intData);
                return;
            }
            if(intData >= 0x00000080 && intData < 0x00004000)
            {
                write((intData >> 7) | 0x80);
                write(intData & 0x7f);
                return;
            }
            if(intData >= 0x00004000 && intData < 0x00200000)
            {
                write((intData >> 14) | 0x80);
                write((intData >> 7) | 0x80);
                write(intData & 0x7f);
                return;
            }
            if(intData >= 0x00200000 && intData < 0x10000000)
            {
                write((intData >> 21) | 0x80);
                write((intData >> 14) | 0x80);
                write((intData >> 7) | 0x80);
                write(intData & 0x7f);
                return;
            }
            write(0);
        }
    }

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

    private static final int PPQN  =         100;
    private static final int TEMPO = 1000 * PPQN;

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

    private long[] messages;

    public MIDIEncoder() {
    }

    public void saveToOutputStream(OutputStream stream) throws IOException {
        saveToDataStream(new ExtendedDataOutputStream(stream));
    }

    public void saveToDataStream(ExtendedDataOutputStream stream) throws IOException {
        long prevTime;
        byte[] trackData;
        long[] trackMessages = messages;
        Track track;
        if(trackMessages == null)
        {
            throw new EmptyAdapterException("MIDIEncoder.saveToOutputStream: кодер не содержит данные.");
        }
        (track = new Track()).writeVolatileLengthValue(0);
        track.write(0xff);
        track.write(0x51);
        track.write(0x03);
        track.write(TEMPO >> 16);
        track.write(TEMPO >> 8);
        track.write(TEMPO);
        prevTime = 0L;
        for(int prevStatusByte = 0, len = trackMessages.length, i = 0; i < len; i++)
        {
            int shortMsg;
            int deltaTime;
            int statusByte;
            int dataByte1;
            int dataByte2;
            long longMsg;
            long currTime = (longMsg = trackMessages[i]) >> 24;
            shortMsg = (int) longMsg & 0x00ffffff;
            deltaTime = (int) (currTime - prevTime);
            statusByte = shortMsg >> 16;
            dataByte1 = (shortMsg >> 8) & 0xff;
            dataByte2 = shortMsg & 0xff;
            if(statusByte >= 0x80 && statusByte < 0xf0)
            {
                prevTime = currTime;
                track.writeVolatileLengthValue(deltaTime);
                if(statusByte != prevStatusByte || dataByte1 >= 0x80) track.write(prevStatusByte = statusByte);
                track.write(dataByte1);
                if(statusByte < 0xc0 || statusByte >= 0xe0) track.write(dataByte2);
                continue;
            }
            if(shortMsg == 0x00ff2f00)
            {
                track.writeVolatileLengthValue(deltaTime);
                track.write(0xff);
                track.write(0x2f);
                track.write(0x00);
                break;
            }
        }
        stream.writeInt((int) SIGNATURE);
        stream.writeInt(6);
        stream.writeShort(0);
        stream.writeShort(1);
        stream.writeShort(PPQN);
        stream.writeInt(MTRK);
        stream.writeInt((trackData = track.toByteArray()).length);
        stream.write(trackData);
    }

    public void clear() {
        messages = null;
    }

    public void setMessages(long[] messages) throws InvalidDataFormatException, UnsupportedDataException {
        int len;
        long prevTime;
        if(messages == null)
        {
            throw new NullPointerException("MIDIEncoder.setMessages: аргумент messages равен нулевой ссылке.");
        }
        Array.copy(messages, 0, messages = new long[len = messages.length], 0, len);
        prevTime = 0L;
        for(int lim = len - 1, i = 0; i < len; i++)
        {
            int shortMsg;
            long longMsg;
            shortMsg = (int) (longMsg = messages[i]) & 0x00ffffff;
            if(i == lim && shortMsg != 0x00ff2f00)
            {
                throw new InvalidDataFormatException("MIDIEncoder.setMessages: неправильный формат данных.");
            }
            if((shortMsg & 0x00800000) != 0)
            {
                long currTime;
                long deltaTime;
                if((deltaTime = (currTime = longMsg >> 24) - prevTime) < 0L || deltaTime >= 0x10000000L)
                {
                    throw new UnsupportedDataException("MIDIEncoder.setMessages: неподдерживаемые данные.");
                }
                prevTime = currTime;
            }
        }
        this.messages = messages;
    }

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