/*
 * Copyright (C) 2017 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.
 */

#define LOG_TAG "NativeMIDI"

#include <poll.h>
#include <unistd.h>

#include <binder/Binder.h>
#include <utils/Errors.h>
#include <utils/Log.h>

#include "android/media/midi/BpMidiDeviceServer.h"
#include "media/MidiDeviceInfo.h"

#include "midi.h"
#include "midi_internal.h"

using android::IBinder;
using android::BBinder;
using android::OK;
using android::sp;
using android::status_t;
using android::base::unique_fd;
using android::binder::Status;
using android::media::midi::MidiDeviceInfo;

struct AMIDI_Port {
    std::atomic_int state;
    AMIDI_Device    *device;
    sp<IBinder>     binderToken;
    unique_fd       ufd;
};

#define SIZE_MIDIRECEIVEBUFFER AMIDI_BUFFER_SIZE

enum {
    MIDI_PORT_STATE_CLOSED = 0,
    MIDI_PORT_STATE_OPEN_IDLE,
    MIDI_PORT_STATE_OPEN_ACTIVE
};

enum {
    PORTTYPE_OUTPUT = 0,
    PORTTYPE_INPUT = 1
};

/* TRANSFER PACKET FORMAT (as defined in MidiPortImpl.java)
 *
 * Transfer packet format is as follows (see MidiOutputPort.mThread.run() to see decomposition):
 * |oc|md|md| ......... |md|ts|ts|ts|ts|ts|ts|ts|ts|
 *  ^ +--------------------+-----------------------+
 *  |  ^                    ^
 *  |  |                    |
 *  |  |                    + timestamp (8 bytes)
 *  |  |
 *  |  + MIDI data bytes (numBytes bytes)
 *  |
 *  + OpCode (AMIDI_OPCODE_DATA)
 *
 *  NOTE: The socket pair is configured to use SOCK_SEQPACKET mode.
 *  SOCK_SEQPACKET, for a sequenced-packet socket that is connection-oriented, preserves message
 *  boundaries, and delivers messages in the order that they were sent.
 *  So 'read()' always returns a whole message.
 */

/*
 * Device Functions
 */
status_t AMIDI_getDeviceInfo(AMIDI_Device *device, AMIDI_DeviceInfo *deviceInfoPtr) {
    MidiDeviceInfo deviceInfo;
    Status txResult = device->server->getDeviceInfo(&deviceInfo);
    if (!txResult.isOk()) {
        ALOGE("AMIDI_getDeviceInfo transaction error: %d", txResult.transactionError());
        return txResult.transactionError();
    }

    deviceInfoPtr->type = deviceInfo.getType();
    deviceInfoPtr->uid = deviceInfo.getUid();
    deviceInfoPtr->isPrivate = deviceInfo.isPrivate();
    deviceInfoPtr->inputPortCount = deviceInfo.getInputPortNames().size();
    deviceInfoPtr->outputPortCount = deviceInfo.getOutputPortNames().size();

    return OK;
}

/*
 * Port Helpers
 */
static status_t AMIDI_openPort(AMIDI_Device *device, int portNumber, int type,
        AMIDI_Port **portPtr) {
    sp<BBinder> portToken(new BBinder());
    unique_fd ufd;
    Status txResult = type == PORTTYPE_OUTPUT
            ? device->server->openOutputPort(portToken, portNumber, &ufd)
            : device->server->openInputPort(portToken, portNumber, &ufd);
    if (!txResult.isOk()) {
        ALOGE("AMIDI_openPort transaction error: %d", txResult.transactionError());
        return txResult.transactionError();
    }

    AMIDI_Port* port = new AMIDI_Port;
    port->state = MIDI_PORT_STATE_OPEN_IDLE;
    port->device = device;
    port->binderToken = portToken;
    port->ufd = std::move(ufd);

    *portPtr = port;

    return OK;
}

static status_t AMIDI_closePort(AMIDI_Port *port) {
    int portState = MIDI_PORT_STATE_OPEN_IDLE;
    while (!port->state.compare_exchange_weak(portState, MIDI_PORT_STATE_CLOSED)) {
        if (portState == MIDI_PORT_STATE_CLOSED) {
            return -EINVAL; // Already closed
        }
    }

    Status txResult = port->device->server->closePort(port->binderToken);
    if (!txResult.isOk()) {
        return txResult.transactionError();
    }

    delete port;

    return OK;
}

/*
 * Output (receiving) API
 */
status_t AMIDI_openOutputPort(AMIDI_Device *device, int portNumber,
        AMIDI_OutputPort **outputPortPtr) {
    return AMIDI_openPort(device, portNumber, PORTTYPE_OUTPUT, (AMIDI_Port**)outputPortPtr);
}

ssize_t AMIDI_receive(AMIDI_OutputPort *outputPort, AMIDI_Message *messages, ssize_t maxMessages) {
    AMIDI_Port *port = (AMIDI_Port*)outputPort;
    int portState = MIDI_PORT_STATE_OPEN_IDLE;
    if (!port->state.compare_exchange_strong(portState, MIDI_PORT_STATE_OPEN_ACTIVE)) {
        // The port has been closed.
        return -EPIPE;
    }

    status_t result = OK;
    ssize_t messagesRead = 0;
    while (messagesRead < maxMessages) {
        struct pollfd checkFds[1] = { { port->ufd, POLLIN, 0 } };
        int pollResult = poll(checkFds, 1, 0);
        if (pollResult < 1) {
            result = android::INVALID_OPERATION;
            break;
        }

        AMIDI_Message *message = &messages[messagesRead];
        uint8_t readBuffer[AMIDI_PACKET_SIZE];
        memset(readBuffer, 0, sizeof(readBuffer));
        ssize_t readCount = read(port->ufd, readBuffer, sizeof(readBuffer));
        if (readCount == EINTR) {
            continue;
        }
        if (readCount < 1) {
            result = android::NOT_ENOUGH_DATA;
            break;
        }

        // set Packet Format definition at the top of this file.
        size_t dataSize = 0;
        message->opcode = readBuffer[0];
        message->timestamp = 0;
        if (message->opcode == AMIDI_OPCODE_DATA && readCount >= AMIDI_PACKET_OVERHEAD) {
            dataSize = readCount - AMIDI_PACKET_OVERHEAD;
            if (dataSize) {
                memcpy(message->buffer, readBuffer + 1, dataSize);
            }
            message->timestamp = *(uint64_t*)(readBuffer + readCount - sizeof(uint64_t));
        }
        message->len = dataSize;
        ++messagesRead;
    }

    port->state.store(MIDI_PORT_STATE_OPEN_IDLE);

    return result == OK ? messagesRead : result;
}

status_t AMIDI_closeOutputPort(AMIDI_OutputPort *outputPort) {
    return AMIDI_closePort((AMIDI_Port*)outputPort);
}

/*
 * Input (sending) API
 */
status_t AMIDI_openInputPort(AMIDI_Device *device, int portNumber, AMIDI_InputPort **inputPortPtr) {
    return AMIDI_openPort(device, portNumber, PORTTYPE_INPUT, (AMIDI_Port**)inputPortPtr);
}

status_t AMIDI_closeInputPort(AMIDI_InputPort *inputPort) {
    return AMIDI_closePort((AMIDI_Port*)inputPort);
}

ssize_t AMIDI_getMaxMessageSizeInBytes(AMIDI_InputPort */*inputPort*/) {
    return SIZE_MIDIRECEIVEBUFFER;
}

static ssize_t AMIDI_makeSendBuffer(
        uint8_t *buffer, uint8_t *data, ssize_t numBytes,uint64_t timestamp) {
    buffer[0] = AMIDI_OPCODE_DATA;
    memcpy(buffer + 1, data, numBytes);
    memcpy(buffer + 1 + numBytes, &timestamp, sizeof(timestamp));
    return numBytes + AMIDI_PACKET_OVERHEAD;
}

// Handy debugging function.
//static void AMIDI_logBuffer(uint8_t *data, size_t numBytes) {
//    for (size_t index = 0; index < numBytes; index++) {
//      ALOGI("  data @%zu [0x%X]", index, data[index]);
//    }
//}

ssize_t AMIDI_send(AMIDI_InputPort *inputPort, uint8_t *buffer, ssize_t numBytes) {
    return AMIDI_sendWithTimestamp(inputPort, buffer, numBytes, 0);
}

ssize_t AMIDI_sendWithTimestamp(AMIDI_InputPort *inputPort, uint8_t *data,
        ssize_t numBytes, int64_t timestamp) {

    if (numBytes > SIZE_MIDIRECEIVEBUFFER) {
        return android::BAD_VALUE;
    }

    // AMIDI_logBuffer(data, numBytes);

    uint8_t writeBuffer[SIZE_MIDIRECEIVEBUFFER + AMIDI_PACKET_OVERHEAD];
    ssize_t numTransferBytes = AMIDI_makeSendBuffer(writeBuffer, data, numBytes, timestamp);
    ssize_t numWritten = write(((AMIDI_Port*)inputPort)->ufd, writeBuffer, numTransferBytes);

    if (numWritten < numTransferBytes) {
        ALOGE("AMIDI_sendWithTimestamp Couldn't write MIDI data buffer. requested:%zu, written%zu",
                numTransferBytes, numWritten);
    }

    return numWritten - AMIDI_PACKET_OVERHEAD;
}

status_t AMIDI_flush(AMIDI_InputPort *inputPort) {
    uint8_t opCode = AMIDI_OPCODE_FLUSH;
    ssize_t numTransferBytes = 1;
    ssize_t numWritten = write(((AMIDI_Port*)inputPort)->ufd, &opCode, numTransferBytes);

    if (numWritten < numTransferBytes) {
        ALOGE("AMIDI_flush Couldn't write MIDI flush. requested:%zu, written%zu",
                numTransferBytes, numWritten);
        return android::INVALID_OPERATION;
    }

    return OK;
}