/*
 * Copyright (C) 2014 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 an
 * limitations under the License.
 */

package com.android.server.usb;

import android.annotation.NonNull;
import android.content.Context;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiDeviceServer;
import android.media.midi.MidiDeviceStatus;
import android.media.midi.MidiManager;
import android.media.midi.MidiReceiver;
import android.os.Bundle;
import android.service.usb.UsbMidiDeviceProto;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.system.StructPollfd;
import android.util.Log;

import com.android.internal.midi.MidiEventScheduler;
import com.android.internal.midi.MidiEventScheduler.MidiEvent;
import com.android.internal.util.dump.DualDumpOutputStream;

import libcore.io.IoUtils;

import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public final class UsbMidiDevice implements Closeable {
    private static final String TAG = "UsbMidiDevice";

    private final int mAlsaCard;
    private final int mAlsaDevice;
    private final int mSubdeviceCount;
    private final InputReceiverProxy[] mInputPortReceivers;

    private MidiDeviceServer mServer;

    // event schedulers for each input port of the physical device
    private MidiEventScheduler[] mEventSchedulers;

    private static final int BUFFER_SIZE = 512;

    private FileDescriptor[] mFileDescriptors;

    // for polling multiple FileDescriptors for MIDI events
    private StructPollfd[] mPollFDs;
    // streams for reading from ALSA driver
    private FileInputStream[] mInputStreams;
    // streams for writing to ALSA driver
    private FileOutputStream[] mOutputStreams;

    private final Object mLock = new Object();
    private boolean mIsOpen;

    // pipe file descriptor for signalling input thread to exit
    // only accessed from JNI code
    private int mPipeFD = -1;

    private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() {

        @Override
        public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
            MidiDeviceInfo deviceInfo = status.getDeviceInfo();
            int inputPorts = deviceInfo.getInputPortCount();
            int outputPorts = deviceInfo.getOutputPortCount();
            boolean hasOpenPorts = false;

            for (int i = 0; i < inputPorts; i++) {
                if (status.isInputPortOpen(i)) {
                    hasOpenPorts = true;
                    break;
                }
            }

            if (!hasOpenPorts) {
                for (int i = 0; i < outputPorts; i++) {
                    if (status.getOutputPortOpenCount(i) > 0) {
                        hasOpenPorts = true;
                        break;
                    }
                }
            }

            synchronized (mLock) {
                if (hasOpenPorts && !mIsOpen) {
                    openLocked();
                } else if (!hasOpenPorts && mIsOpen) {
                    closeLocked();
                }
            }
        }

        @Override
        public void onClose() {
        }
    };

    // This class acts as a proxy for our MidiEventScheduler receivers, which do not exist
    // until the device has active clients
    private final class InputReceiverProxy extends MidiReceiver {
        private MidiReceiver mReceiver;

        @Override
        public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
            MidiReceiver receiver = mReceiver;
            if (receiver != null) {
                receiver.send(msg, offset, count, timestamp);
            }
        }

        public void setReceiver(MidiReceiver receiver) {
            mReceiver = receiver;
        }

        @Override
        public void onFlush() throws IOException {
            MidiReceiver receiver = mReceiver;
            if (receiver != null) {
                receiver.flush();
            }
        }
    }

    public static UsbMidiDevice create(Context context, Bundle properties, int card, int device) {
        // FIXME - support devices with different number of input and output ports
        int subDeviceCount = nativeGetSubdeviceCount(card, device);
        if (subDeviceCount <= 0) {
            Log.e(TAG, "nativeGetSubdeviceCount failed");
            return null;
        }

        UsbMidiDevice midiDevice = new UsbMidiDevice(card, device, subDeviceCount);
        if (!midiDevice.register(context, properties)) {
            IoUtils.closeQuietly(midiDevice);
            Log.e(TAG, "createDeviceServer failed");
            return null;
        }
        return midiDevice;
    }

    private UsbMidiDevice(int card, int device, int subdeviceCount) {
        mAlsaCard = card;
        mAlsaDevice = device;
        mSubdeviceCount = subdeviceCount;

        // FIXME - support devices with different number of input and output ports
        int inputPortCount = subdeviceCount;
        mInputPortReceivers = new InputReceiverProxy[inputPortCount];
        for (int port = 0; port < inputPortCount; port++) {
            mInputPortReceivers[port] = new InputReceiverProxy();
        }
    }

    private boolean openLocked() {
        // FIXME - support devices with different number of input and output ports
        FileDescriptor[] fileDescriptors = nativeOpen(mAlsaCard, mAlsaDevice, mSubdeviceCount);
        if (fileDescriptors == null) {
            Log.e(TAG, "nativeOpen failed");
            return false;
        }

        mFileDescriptors = fileDescriptors;
        int inputStreamCount = fileDescriptors.length;
        // last file descriptor returned from nativeOpen() is only used for unblocking Os.poll()
        // in our input thread
        int outputStreamCount = fileDescriptors.length - 1;

        mPollFDs = new StructPollfd[inputStreamCount];
        mInputStreams = new FileInputStream[inputStreamCount];
        for (int i = 0; i < inputStreamCount; i++) {
            FileDescriptor fd = fileDescriptors[i];
            StructPollfd pollfd = new StructPollfd();
            pollfd.fd = fd;
            pollfd.events = (short)OsConstants.POLLIN;
            mPollFDs[i] = pollfd;
            mInputStreams[i] = new FileInputStream(fd);
        }

        mOutputStreams = new FileOutputStream[outputStreamCount];
        mEventSchedulers = new MidiEventScheduler[outputStreamCount];
        for (int i = 0; i < outputStreamCount; i++) {
            mOutputStreams[i] = new FileOutputStream(fileDescriptors[i]);

            MidiEventScheduler scheduler = new MidiEventScheduler();
            mEventSchedulers[i] = scheduler;
            mInputPortReceivers[i].setReceiver(scheduler.getReceiver());
        }

        final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers();

        // Create input thread which will read from all output ports of the physical device
        new Thread("UsbMidiDevice input thread") {
            @Override
            public void run() {
                byte[] buffer = new byte[BUFFER_SIZE];
                try {
                    while (true) {
                        // Record time of event immediately after waking.
                        long timestamp = System.nanoTime();
                        synchronized (mLock) {
                            if (!mIsOpen) break;

                            // look for a readable FileDescriptor
                            for (int index = 0; index < mPollFDs.length; index++) {
                                StructPollfd pfd = mPollFDs[index];
                                if ((pfd.revents & (OsConstants.POLLERR
                                                            | OsConstants.POLLHUP)) != 0) {
                                    break;
                                } else if ((pfd.revents & OsConstants.POLLIN) != 0) {
                                    // clear readable flag
                                    pfd.revents = 0;

                                    if (index == mInputStreams.length - 1) {
                                        // last file descriptor is used only for unblocking Os.poll()
                                        break;
                                    }

                                    int count = mInputStreams[index].read(buffer);
                                    outputReceivers[index].send(buffer, 0, count, timestamp);
                                }
                            }
                        }

                        // wait until we have a readable port or we are signalled to close
                        Os.poll(mPollFDs, -1 /* infinite timeout */);
                     }
                } catch (IOException e) {
                    Log.d(TAG, "reader thread exiting");
                } catch (ErrnoException e) {
                    Log.d(TAG, "reader thread exiting");
                }
                Log.d(TAG, "input thread exit");
            }
        }.start();

        // Create output thread for each input port of the physical device
        for (int port = 0; port < outputStreamCount; port++) {
            final MidiEventScheduler eventSchedulerF = mEventSchedulers[port];
            final FileOutputStream outputStreamF = mOutputStreams[port];
            final int portF = port;

            new Thread("UsbMidiDevice output thread " + port) {
                @Override
                public void run() {
                    while (true) {
                        MidiEvent event;
                        try {
                            event = (MidiEvent)eventSchedulerF.waitNextEvent();
                        } catch (InterruptedException e) {
                            // try again
                            continue;
                        }
                        if (event == null) {
                            break;
                        }
                        try {
                            outputStreamF.write(event.data, 0, event.count);
                        } catch (IOException e) {
                            Log.e(TAG, "write failed for port " + portF);
                        }
                        eventSchedulerF.addEventToPool(event);
                    }
                    Log.d(TAG, "output thread exit");
                }
            }.start();
        }

        mIsOpen = true;
        return true;
    }

    private boolean register(Context context, Bundle properties) {
        MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
        if (midiManager == null) {
            Log.e(TAG, "No MidiManager in UsbMidiDevice.create()");
            return false;
        }

        mServer = midiManager.createDeviceServer(mInputPortReceivers, mSubdeviceCount,
                null, null, properties, MidiDeviceInfo.TYPE_USB, mCallback);
        if (mServer == null) {
            return false;
        }

        return true;
    }

    @Override
    public void close() throws IOException {
        synchronized (mLock) {
            if (mIsOpen) {
                closeLocked();
            }
        }

        if (mServer != null) {
            IoUtils.closeQuietly(mServer);
        }
    }

    private void closeLocked() {
        for (int i = 0; i < mEventSchedulers.length; i++) {
            mInputPortReceivers[i].setReceiver(null);
            mEventSchedulers[i].close();
        }
        mEventSchedulers = null;

        for (int i = 0; i < mInputStreams.length; i++) {
            IoUtils.closeQuietly(mInputStreams[i]);
        }
        mInputStreams = null;

        for (int i = 0; i < mOutputStreams.length; i++) {
            IoUtils.closeQuietly(mOutputStreams[i]);
        }
        mOutputStreams = null;

        // nativeClose will close the file descriptors and signal the input thread to exit
        nativeClose(mFileDescriptors);
        mFileDescriptors = null;

        mIsOpen = false;
    }

    /**
     * Write a description of the device to a dump stream.
     */
    public void dump(String deviceAddr, @NonNull DualDumpOutputStream dump, @NonNull String idName,
            long id) {
        long token = dump.start(idName, id);

        dump.write("device_address", UsbMidiDeviceProto.DEVICE_ADDRESS, deviceAddr);
        dump.write("card", UsbMidiDeviceProto.CARD, mAlsaCard);
        dump.write("device", UsbMidiDeviceProto.DEVICE, mAlsaDevice);

        dump.end(token);
    }

    private static native int nativeGetSubdeviceCount(int card, int device);
    private native FileDescriptor[] nativeOpen(int card, int device, int subdeviceCount);
    private native void nativeClose(FileDescriptor[] fileDescriptors);
}
