/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.bluetoothmidiservice;

import android.media.midi.MidiReceiver;

import com.android.internal.midi.MidiConstants;
import com.android.internal.midi.MidiFramer;

import java.io.IOException;

/**
 * This class accumulates MIDI messages to form a MIDI packet.
 */
public class BluetoothPacketEncoder extends PacketEncoder {

    private static final String TAG = "BluetoothPacketEncoder";

    private static final long MILLISECOND_NANOS = 1000000L;

    // mask for generating 13 bit timestamps
    private static final int MILLISECOND_MASK = 0x1FFF;

    private final PacketReceiver mPacketReceiver;

    // buffer for accumulating messages to write
    private final byte[] mAccumulationBuffer;
    // number of bytes currently in mAccumulationBuffer
    private int mAccumulatedBytes;
    // timestamp for first message in current packet
    private int mPacketTimestamp;
    // current running status, or zero if none
    private byte mRunningStatus;

    private boolean mWritePending;

    private final Object mLock = new Object();

    // This receives normalized data from mMidiFramer and accumulates it into a packet buffer
    private final MidiReceiver mFramedDataReceiver = new MidiReceiver() {
        @Override
        public void onSend(byte[] msg, int offset, int count, long timestamp)
                throws IOException {

            synchronized (mLock) {
                int milliTimestamp = (int)(timestamp / MILLISECOND_NANOS) & MILLISECOND_MASK;
                byte status = msg[offset];
                boolean isSysExStart = (status == MidiConstants.STATUS_SYSTEM_EXCLUSIVE);
                // Because of the MidiFramer, if it is not a status byte then it
                // must be a continuation.
                boolean isSysExContinuation = ((status & 0x80) == 0);

                int bytesNeeded;
                if (isSysExStart || isSysExContinuation) {
                    // SysEx messages can be split into multiple packets
                    bytesNeeded = 1;
                } else {
                    bytesNeeded = count;
                }

                // Status bytes must be preceded by a timestamp
                boolean needsTimestamp = (status != mRunningStatus)
                        || (milliTimestamp != mPacketTimestamp);
                if (isSysExStart) {
                    // SysEx start byte must be preceded by a timestamp
                    needsTimestamp = true;
                } else if (isSysExContinuation) {
                    // SysEx continuation packets must not have timestamp byte
                    needsTimestamp = false;
                }

                if (needsTimestamp) bytesNeeded++;  // add one for timestamp byte
                if (status == mRunningStatus) bytesNeeded--;    // subtract one for status byte

                if (mAccumulatedBytes + bytesNeeded > mAccumulationBuffer.length) {
                    // write out our data if there is no more room
                    // if necessary, block until previous packet is sent
                    flushLocked(true);
                }

                // write the header if necessary
                if (appendHeader(milliTimestamp)) {
                     needsTimestamp = !isSysExContinuation;
                }

                // write new timestamp byte if necessary
                if (needsTimestamp) {
                    // timestamp byte with bits 0 - 6 of timestamp
                    mAccumulationBuffer[mAccumulatedBytes++] =
                            (byte)(0x80 | (milliTimestamp & 0x7F));
                    mPacketTimestamp = milliTimestamp;
                }

                if (isSysExStart || isSysExContinuation) {
                    // MidiFramer will end the packet with SysEx End if there is one in the buffer
                    boolean hasSysExEnd =
                            (msg[offset + count - 1] == MidiConstants.STATUS_END_SYSEX);
                    int remaining = (hasSysExEnd ? count - 1 : count);

                    while (remaining > 0) {
                        if (mAccumulatedBytes == mAccumulationBuffer.length) {
                            // write out our data if there is no more room
                            // if necessary, block until previous packet is sent
                            flushLocked(true);
                            appendHeader(milliTimestamp);
                        }

                        int copy = mAccumulationBuffer.length - mAccumulatedBytes;
                        if (copy > remaining) copy = remaining;
                        System.arraycopy(msg, offset, mAccumulationBuffer, mAccumulatedBytes, copy);
                        mAccumulatedBytes += copy;
                        offset += copy;
                        remaining -= copy;
                    }

                    if (hasSysExEnd) {
                        // SysEx End command must be preceeded by a timestamp byte
                        if (mAccumulatedBytes + 2 > mAccumulationBuffer.length) {
                            // write out our data if there is no more room
                            // if necessary, block until previous packet is sent
                            flushLocked(true);
                            appendHeader(milliTimestamp);
                        }
                        mAccumulationBuffer[mAccumulatedBytes++] =
                                (byte)(0x80 | (milliTimestamp & 0x7F));
                        mAccumulationBuffer[mAccumulatedBytes++] = MidiConstants.STATUS_END_SYSEX;
                    }
                } else {
                    // Non-SysEx message
                    if (status != mRunningStatus) {
                        mAccumulationBuffer[mAccumulatedBytes++] = status;
                        if (MidiConstants.allowRunningStatus(status)) {
                            mRunningStatus = status;
                        } else if (MidiConstants.cancelsRunningStatus(status)) {
                            mRunningStatus = 0;
                        }
                    }

                    // now copy data bytes
                    int dataLength = count - 1;
                    System.arraycopy(msg, offset + 1, mAccumulationBuffer, mAccumulatedBytes,
                            dataLength);
                    mAccumulatedBytes += dataLength;
                }

                // write the packet if possible, but do not block
                flushLocked(false);
            }
        }
    };

    private boolean appendHeader(int milliTimestamp) {
        // write header if we are starting a new packet
        if (mAccumulatedBytes == 0) {
            // header byte with timestamp bits 7 - 12
            mAccumulationBuffer[mAccumulatedBytes++] =
                    (byte)(0x80 | ((milliTimestamp >> 7) & 0x3F));
            mPacketTimestamp = milliTimestamp;
            return true;
        } else {
            return false;
        }
    }

    // MidiFramer for normalizing incoming data
    private final MidiFramer mMidiFramer = new MidiFramer(mFramedDataReceiver);

    public BluetoothPacketEncoder(PacketReceiver packetReceiver, int maxPacketSize) {
        mPacketReceiver = packetReceiver;
        mAccumulationBuffer = new byte[maxPacketSize];
    }

    @Override
    public void onSend(byte[] msg, int offset, int count, long timestamp)
            throws IOException {
        // normalize the data by passing it through a MidiFramer first
        mMidiFramer.send(msg, offset, count, timestamp);
    }

    @Override
    public void writeComplete() {
        synchronized (mLock) {
            mWritePending = false;
            flushLocked(false);
            mLock.notify();
        }
    }

    private void flushLocked(boolean canBlock) {
        if (mWritePending && !canBlock) {
            return;
        }

        while (mWritePending && mAccumulatedBytes > 0) {
            try {
                mLock.wait();
            } catch (InterruptedException e) {
                // try again
                continue;
            }
        }

        if (mAccumulatedBytes > 0) {
            mPacketReceiver.writePacket(mAccumulationBuffer, mAccumulatedBytes);
            mAccumulatedBytes = 0;
            mPacketTimestamp = 0;
            mRunningStatus = 0;
            mWritePending = true;
        }
    }
}
