/*  This file is part of the KDE project
    Copyright (C) 2008 Matthias Kretz <kretz@kde.org>

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) version 3.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to
    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA 02110-1301, USA.

*/

#include "pcmiodevice_p.h"
#include "converter_p.h"

/*
 * On Unix there are several ways for audio I/O:
 * - OSS (/dev/dsp)
 * - ALSA (libasound) (Linux only)
 * - pulseaudio
 * - jackd
 * deprecated:
 * - ESD
 * - aRts
 */

namespace Phonon
{

void PcmIODevicePrivate::updateDevices()
{
    if (!q_func()->isOpen()) {
        return;
    }
}

static snd_pcm_format_t alsaFormatFor(const Format &f)
{
    if (f.byteOrder == QSysInfo::LittleEndian) {
        switch (f.bitRate) {
        case PcmIODevice::Signed8Bit:
            return SND_PCM_FORMAT_S8;
        case PcmIODevice::Unsigned8Bit:
            return SND_PCM_FORMAT_U8;
        case PcmIODevice::Signed16Bit:
            return SND_PCM_FORMAT_S16_LE;
        case PcmIODevice::Signed18Bit:
            return SND_PCM_FORMAT_S18_3LE;
        case PcmIODevice::Signed20Bit:
            return SND_PCM_FORMAT_S20_3LE;
        case PcmIODevice::Signed24Bit:
            return SND_PCM_FORMAT_S24_3LE;
        case PcmIODevice::Signed32Bit:
            return SND_PCM_FORMAT_S32_LE;
        case PcmIODevice::UnknownBitRate:
            break;
        }
    } else {
        switch (f.bitRate) {
        case PcmIODevice::Signed8Bit:
            return SND_PCM_FORMAT_S8;
        case PcmIODevice::Unsigned8Bit:
            return SND_PCM_FORMAT_U8;
        case PcmIODevice::Signed16Bit:
            return SND_PCM_FORMAT_S16_BE;
        case PcmIODevice::Signed18Bit:
            return SND_PCM_FORMAT_S18_3BE;
        case PcmIODevice::Signed20Bit:
            return SND_PCM_FORMAT_S20_3BE;
        case PcmIODevice::Signed24Bit:
            return SND_PCM_FORMAT_S24_3BE;
        case PcmIODevice::Signed32Bit:
            return SND_PCM_FORMAT_S32_BE;
        case PcmIODevice::UnknownBitRate:
            break;
        }
    }
    Q_ASSERT(false);
    return SND_PCM_FORMAT_S16;
}

static PcmIODevice::BitRate bitRateForAlsa(snd_pcm_format_t f)
{
    switch (f) {
    case SND_PCM_FORMAT_S32_BE:
    case SND_PCM_FORMAT_S32_LE:
        return PcmIODevice::Signed32Bit;
    case SND_PCM_FORMAT_S24_3BE:
    case SND_PCM_FORMAT_S24_3LE:
        return PcmIODevice::Signed24Bit;
    case SND_PCM_FORMAT_S20_3LE:
    case SND_PCM_FORMAT_S20_3BE:
        return PcmIODevice::Signed20Bit;
    case SND_PCM_FORMAT_S18_3LE:
    case SND_PCM_FORMAT_S18_3BE:
        return PcmIODevice::Signed18Bit;
    case SND_PCM_FORMAT_S16_BE:
    case SND_PCM_FORMAT_S16_LE:
        return PcmIODevice::Signed16Bit;
    case SND_PCM_FORMAT_S8:
        return PcmIODevice::Signed8Bit;
    case SND_PCM_FORMAT_U8:
        return PcmIODevice::Unsigned8Bit;
    default:
        Q_ASSERT(false);
        return PcmIODevice::UnknownBitRate;
    }
}

static QSysInfo::Endian byteOrderForAlsa(snd_pcm_format_t f)
{
    switch (f) {
    case SND_PCM_FORMAT_S32_BE:
    case SND_PCM_FORMAT_S24_3BE:
    case SND_PCM_FORMAT_S20_3BE:
    case SND_PCM_FORMAT_S18_3BE:
    case SND_PCM_FORMAT_S16_BE:
        return QSysInfo::BigEndian;
    case SND_PCM_FORMAT_S32_LE:
    case SND_PCM_FORMAT_S24_3LE:
    case SND_PCM_FORMAT_S20_3LE:
    case SND_PCM_FORMAT_S18_3LE:
    case SND_PCM_FORMAT_S16_LE:
        return QSysInfo::LittleEndian;
    case SND_PCM_FORMAT_S8:
    case SND_PCM_FORMAT_U8:
        return QSysInfo::ByteOrder;
    default:
        Q_ASSERT(false);
        return QSysInfo::ByteOrder;
    }
}

static const snd_pcm_format_t fallbackFormats[] = {
#if Q_BYTE_ORDER == Q_BIG_ENDIAN
    SND_PCM_FORMAT_S32_BE,
    SND_PCM_FORMAT_S32_LE,
    SND_PCM_FORMAT_S24_3BE,
    SND_PCM_FORMAT_S24_3LE,
    SND_PCM_FORMAT_S20_3BE,
    SND_PCM_FORMAT_S20_3LE,
    SND_PCM_FORMAT_S18_3BE,
    SND_PCM_FORMAT_S18_3LE,
    SND_PCM_FORMAT_S16_BE,
    SND_PCM_FORMAT_S16_LE,
#else
    SND_PCM_FORMAT_S32_LE,
    SND_PCM_FORMAT_S32_BE,
    SND_PCM_FORMAT_S24_3LE,
    SND_PCM_FORMAT_S24_3BE,
    SND_PCM_FORMAT_S20_3LE,
    SND_PCM_FORMAT_S20_3BE,
    SND_PCM_FORMAT_S18_3LE,
    SND_PCM_FORMAT_S18_3BE,
    SND_PCM_FORMAT_S16_LE,
    SND_PCM_FORMAT_S16_BE,
#endif
    SND_PCM_FORMAT_S8,
    SND_PCM_FORMAT_U8
};

static bool setFallbackFormat(AlsaPcmHwParams &params, Converter *converter)
{
    snd_pcm_format_t fallbackFormat = SND_PCM_FORMAT_UNKNOWN;
    for (unsigned int i = 0; i < sizeof(fallbackFormats)/sizeof(snd_pcm_format_t); ++i) {
        if (params.supportsFormat(fallbackFormats[i])) {
            fallbackFormat = fallbackFormats[i];
            params.setFormat(fallbackFormat);
            break;
        }
    }
    if (fallbackFormat == SND_PCM_FORMAT_UNKNOWN) {
        return false;
    }
    if (converter) {
        converter->setSourceBitRate(bitRateForAlsa(fallbackFormat));
        converter->setSourceByteOrder(byteOrderForAlsa(fallbackFormat));
    }
    return true;
}

bool PcmIODevicePrivate::setHwAndSwParams(AlsaPcm &pcm)
{
    // this part is what we always want:
    AlsaPcmHwParams params(pcm);
    try {
        params.setPeriodsInteger();
        if (params.supportsAccess(SND_PCM_ACCESS_MMAP_NONINTERLEAVED)) {
            params.setAccess(SND_PCM_ACCESS_MMAP_NONINTERLEAVED);
        } else if (params.supportsAccess(SND_PCM_ACCESS_MMAP_INTERLEAVED)) {
            params.setAccess(SND_PCM_ACCESS_MMAP_INTERLEAVED);
        } else {
            params.setAccess(SND_PCM_ACCESS_MMAP_COMPLEX);
        }
    }
    catch (AlsaError err) {
        qDebug() << err.errorString();
        return false;
    }

    QSet<Format> _formats(formats);
    snd_pcm_uframes_t bufferSize = 0;
    snd_pcm_uframes_t periodSize = 0;
    // now to the part the application decides:
    try {
        if (_formats.isEmpty()) {
            // the application wants the best quality hw format it can get
            params.setRateResample(false);
            //////////////// Bit Rate + Endian
            snd_pcm_format_t alsaFormat = SND_PCM_FORMAT_UNKNOWN;
            for (unsigned int i = 0; i < sizeof(fallbackFormats)/sizeof(snd_pcm_format_t); ++i) {
                if (params.supportsFormat(fallbackFormats[i])) {
                    alsaFormat = fallbackFormats[i];
                    params.setFormat(alsaFormat);
                    break;
                }
            }
            if (alsaFormat == SND_PCM_FORMAT_UNKNOWN) {
                return false;
            }
            //////////////// Sample Rate
            unsigned int sampleRate = 0;
            params.setRateHighest(&sampleRate, 0);
            if (sampleRate == 0) {
                return false;
            }
            //////////////// Channels
            unsigned int channels = 0;
            params.setChannelsHighest(&channels);
            if (channels == 0) {
                return false;
            }
            formatInUse = Format(sampleRate, bitRateForAlsa(alsaFormat), channels, byteOrderForAlsa(alsaFormat));
        } else if (_formats.size() == 1) {
            // the application wants exactly this one format - if necessary we convert to it
            formatInUse = *_formats.constBegin();
            //////////////// Bit Rate + Endian
            const snd_pcm_format_t alsaF = alsaFormatFor(formatInUse);
            if (params.supportsFormat(alsaF)) {
                params.setFormat(alsaF);
            } else {
                ensureConverter();
                setFallbackFormat(params, converter);
            }
            //////////////// Sample Rate
            if (params.supportsRate(formatInUse.sampleRate, 0)) {
                params.setRate(formatInUse.sampleRate, 0);
            } else {
                ensureConverter();
                unsigned int sampleRate = formatInUse.sampleRate;
                int dir = 1;
                params.setRateNear(&sampleRate, &dir);
                converter->setSourceRate(sampleRate);
            }
            //////////////// Channels
            if (params.supportsChannels(formatInUse.channels)) {
                params.setChannels(formatInUse.channels);
            } else {
                ensureConverter();
                unsigned int channels = formatInUse.channels;
                params.setChannelsNear(&channels);
                converter->setSourceChannels(channels);
            }
        } else {
            // the application wants the best format from the list provided
            // - first we try without resampling
            // - if we get no match, with resampling
            // - still no match we convert (without resampling again)
            params.setRateResample(false);
            //////////////// Bit Rate + Endian
            foreach (const Format &f, _formats) {
                if (!params.supportsFormat(alsaFormatFor(f))) {
                    _formats.remove(f);
                }
            }
            if (!_formats.isEmpty()) {
                //////////////// Sample Rate
                foreach (const Format &f, _formats) {
                    if (!params.supportsRate(f.sampleRate, 0)) {
                        _formats.remove(f);
                    }
                }
                if (!_formats.isEmpty()) {
                    //////////////// Channels
                    foreach (const Format &f, _formats) {
                        if (!params.supportsChannels(f.channels)) {
                            _formats.remove(f);
                        }
                    }
                    if (!_formats.isEmpty()) {
                        QList<Format> sortedFormats = _formats.toList();
                        qSort(sortedFormats);
                        formatInUse = sortedFormats.first();
                        const snd_pcm_format_t alsaF = alsaFormatFor(formatInUse);
                        params.setFormat(alsaF);
                        params.setRate(formatInUse.sampleRate, 0);
                        params.setChannels(formatInUse.channels);
                    }
                }
            }
            if (_formats.isEmpty()) {
                _formats = formats;
                QSet<Format> lastFormatSet = _formats;
                params.setRateResample(true);
                bool needFormatConversion = false;
                bool needRateConversion = false;
                bool needChannelsConversion = false;
                //////////////// Bit Rate + Endian
                foreach (const Format &f, _formats) {
                    if (!params.supportsFormat(alsaFormatFor(f))) {
                        _formats.remove(f);
                    }
                }
                if (_formats.isEmpty()) {
                    _formats = lastFormatSet;
                    needFormatConversion = true;
                } else {
                    lastFormatSet = _formats;
                }
                //////////////// Sample Rate
                foreach (const Format &f, _formats) {
                    if (!params.supportsRate(f.sampleRate, 0)) {
                        _formats.remove(f);
                    }
                }
                if (_formats.isEmpty()) {
                    _formats = lastFormatSet;
                    needRateConversion = true;
                } else {
                    lastFormatSet = _formats;
                }
                //////////////// Channels
                foreach (const Format &f, _formats) {
                    if (!params.supportsChannels(f.channels)) {
                        _formats.remove(f);
                    }
                }
                if (_formats.isEmpty()) {
                    _formats = lastFormatSet;
                    needChannelsConversion = true;
                } else {
                    lastFormatSet = _formats;
                }
                QList<Format> sortedFormats = _formats.toList();
                qSort(sortedFormats);
                formatInUse = sortedFormats.first();

#define CHANNELS \
    if (needChannelsConversion) { \
        unsigned int channels = 0; \
        params.setChannelsHighest(&channels); \
        if (channels == 0) { \
            return false; \
        } \
        ensureConverter(); \
        converter->setSourceChannels(channels); \
    } else { \
        params.setChannels(formatInUse.channels); \
    }

#define SAMPLE_RATE \
    if (needRateConversion) { \
        params.setRateResample(false); \
        unsigned int sampleRate = 0; \
        params.setRateHighest(&sampleRate, 0); \
        if (sampleRate == 0) { \
            return false; \
        } \
        ensureConverter(); \
        converter->setSourceRate(sampleRate); \
        CHANNELS \
    } else { \
        params.setRate(formatInUse.sampleRate, 0); \
        CHANNELS \
    }

                if (needFormatConversion) {
                    snd_pcm_format_t alsaFormat = SND_PCM_FORMAT_UNKNOWN;
                    for (unsigned int i = 0; i < sizeof(fallbackFormats)/sizeof(snd_pcm_format_t); ++i) {
                        if (params.supportsFormat(fallbackFormats[i])) {
                            alsaFormat = fallbackFormats[i];
                            params.setFormat(alsaFormat);
                            break;
                        }
                    }
                    if (alsaFormat == SND_PCM_FORMAT_UNKNOWN) {
                        return false;
                    }
                    ensureConverter();
                    converter->setSourceBitRate(bitRateForAlsa(alsaFormat));
                    converter->setSourceByteOrder(byteOrderForAlsa(alsaFormat));
                    SAMPLE_RATE
                } else {
                    params.setFormat(alsaFormatFor(formatInUse));
                    SAMPLE_RATE
                }
            }
        }
        // buffer time / latency
        latency = 0;
        try {
            params.setBufferTimeNear(&latency, 0);
            try {
                bufferSize = params.bufferSize();
                latency = params.bufferTime(0);
                unsigned int periodTime = latency / 4;
                params.setPeriodTimeNear(&periodTime, 0);
                periodSize = params.periodSize(0);
            }
            catch (AlsaError err) {
                qDebug() << err.errorString();
                return false;
            }
        }
        catch (AlsaError) {
            // trying with period time
            unsigned int periodTime = 0;
            params.setPeriodTimeNear(&periodTime, 0);
            bufferSize = params.periodSize(0) * 4;
            params.setBufferSizeNear(&bufferSize);
            bufferSize = params.bufferSize();
        }
        params.apply();
    }
    catch (AlsaError err) {
        qDebug() << err.errorString();
        return false;
    }
    if (converter) {
        converter->convertTo(formatInUse);
    }

    try {
        AlsaPcmSwParams params(pcm);

        // set threshold for transfer to when the buffer is almost full
        params.setStartThreshold((bufferSize / periodSize) * periodSize);

        // already allow transfer if we have one periodSize available
        params.setAvailableMinimum(periodSize);

        // align all transfers to 1 sample
        params.setTransferAlignment(1);

        params.apply();
    }
    catch (AlsaError err) {
        qDebug() << err.errorString();
        return false;
    }
    return true;
}

bool PcmIODevicePrivate::open(QIODevice::OpenMode openMode)
{
    try {
        if (openMode & QIODevice::ReadOnly) {
            alsaPlayback.open("default", SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
            alsaPlayback.nonblock(false);
            if (!setHwAndSwParams(alsaPlayback)) {
                return false;
            }
            //XXX fillWithSilence(alsaPlayback);
        }
        if (openMode & QIODevice::WriteOnly) {
            alsaCapture.open("default", SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
            alsaCapture.nonblock(false);
            if (!setHwAndSwParams(alsaCapture)) {
                return false;
            }
            if (openMode & QIODevice::ReadOnly) {
                alsaCapture.link(alsaPlayback);
            }
            alsaCapture.start();
        } else if (openMode & QIODevice::ReadOnly) {
            alsaPlayback.start();
        }
    } catch (AlsaError err) {
        switch (err.errorCode()) {
        case EBUSY:
            break;
        case EPERM:
            break;
        }
        qDebug() << err.errorString();
        return false;
    }
    return true;
}

void PcmIODevicePrivate::close()
{
    Q_Q(PcmIODevice);
    if ((q->openMode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) {
        alsaCapture.unlink();
    }
    alsaPlayback.close();
    alsaCapture.close();
}

qint64 PcmIODevicePrivate::readData(char *data, qint64 maxSize)
{
    return 0;
}

qint64 PcmIODevicePrivate::writeData(const char *data, qint64 maxSize)
{
    qint64 written = 0;
    try {
        const snd_pcm_sframes_t available = alsaPlayback.availableUpdate();
        qint64 size = maxSize;
        while (size > 0) {
        }
        snd_pcm_uframes_t offset, frames;
        //alsaPlayback.mmapBegin( &offset, &frames);
        //alsaPlayback.mmapCommit(offset, frames);
    }
    catch (AlsaError err) {
        qDebug() << err.errorString();
        return written;
    }
    return written;
}

} // namespace Phonon
