#include "alsaclasses.h"
#include <kdebug.h>
#include <cmath>
#include <QtCore/QtGlobal>

static inline unsigned int alignPositionInBytesToNextInteger(const unsigned int &size) { return (size + sizeof(int) - 1) / sizeof(int); }
static const unsigned int MAX_TLV_RANGE_SIZE = 256;

namespace Phonon
{
namespace Mixer
{

void AlsaTlv::parse()
{
    parseTlv(data.data(), data.size() * sizeof(int));
}

struct AlsaTlv::DBInterface
{
    virtual ~DBInterface() {}
    virtual int gain(int volume, int rangemin, int rangemax) const = 0;
};

int AlsaTlv::convertToMillibel(int volume, const AlsaElementInfo &info) const
{
    const int min = info.min();
    const int max = info.max();
    foreach (DBInterface *i, m_dbinterfaces) {
        int db = i->gain(volume, min, max);
        return db;
    }
    return 0;
}

struct AlsaTlv::DBScale : public AlsaTlv::DBInterface
{
    int min;
    int step;
    bool mute;

    DBScale() : min(-1), step(-1), mute(false) {}
    bool isValid() const { return min != -1 || step != -1 || mute != false; }

    int gain(int volume, int rangemin, int /*rangemax*/) const
    {
        if (mute && volume == rangemin) {
            return SND_CTL_TLV_DB_GAIN_MUTE;
        }
        return (volume - rangemin) * step + min;
    }
};

struct AlsaTlv::DBLinear : public AlsaTlv::DBInterface
{
    int mindB;
    int maxdB;

    DBLinear() : mindB(-1), maxdB(-1) {}
    bool isValid() const { return mindB != -1 || maxdB != -1; }

    int gain(int volume, int rangemin, int rangemax) const
    {
        if (volume <= rangemin || rangemax <= rangemin) {
            return mindB;
        }
        if (volume >= rangemax) {
            return maxdB;
        }
        double val = (double)(volume - rangemin) / (double)(rangemax - rangemin);
        if (mindB <= SND_CTL_TLV_DB_GAIN_MUTE) {
            return (int)(100.0 * 20.0 * std::log10(val)) + maxdB;
        }
        /* FIXME: precalculate and cache these values */
        double lmin = std::pow(10.0, mindB/2000.0);
        double lmax = std::pow(10.0, maxdB/2000.0);
        val = (lmax - lmin) * val + lmin;
        return (int)(100.0 * 20.0 * log10(val));
    }
};

class AlsaTlv::DBRange : public AlsaTlv::DBInterface
{
private:
    class Data;
    Data *d;

public:
    DBRange() : d(0) {}
    DBRange(int min, int max);
    ~DBRange();
    DBRange(const AlsaTlv::DBRange &rhs);
    DBRange &operator=(const AlsaTlv::DBRange &rhs);

    template<class T>
    inline void setInterface(const T &iface);
    inline void setNextRange(const AlsaTlv::DBRange &r);

    inline bool isValid() const { return d != 0; }
    int gain(int volume, int rangemin, int rangemax) const;
};

struct AlsaTlv::DBRange::Data
{
    int rangemin;
    int rangemax;
    AlsaTlv::DBInterface *interface;
    AlsaTlv::DBRange *nextRange;

    int refCount;

    Data(int min, int max) : rangemin(min), rangemax(max), interface(0), nextRange(0), refCount(0) {}
    ~Data() { delete interface; delete nextRange; }
    void ref() { ++refCount; }
    void deref() { if (0 == --refCount) { delete this; } }
};

AlsaTlv::DBRange::DBRange(int min, int max)
    : d(new Data(min, max))
{
    d->ref();
}

AlsaTlv::DBRange::~DBRange()
{
    d->deref();
}

AlsaTlv::DBRange::DBRange(const AlsaTlv::DBRange &rhs)
    : AlsaTlv::DBInterface(rhs), d(rhs.d)
{
    d->ref();
}

AlsaTlv::DBRange &AlsaTlv::DBRange::operator=(const AlsaTlv::DBRange &rhs)
{
    if (d != rhs.d) {
        d->deref();
        d = rhs.d;
        d->ref();
    }
    return *this;
}

template<class T>
inline void AlsaTlv::DBRange::setInterface(const T &iface)
{
    d->interface = new T(iface);
}

inline void AlsaTlv::DBRange::setNextRange(const AlsaTlv::DBRange &r)
{
    if (r.isValid()) {
        d->nextRange = new AlsaTlv::DBRange(r);
    }
}

int AlsaTlv::DBRange::gain(int volume, int, int) const
{
    if (volume >= d->rangemin && volume <= d->rangemax) {
        Q_ASSERT(d->interface);
        return d->interface->gain(volume, d->rangemin, d->rangemax);
    }
    if (d->nextRange) {
        return d->nextRange->gain(volume, d->rangemin, d->rangemax);
    }
    return -1;
}

void AlsaTlv::parseTlv(unsigned int *tlv, unsigned int tlv_size)
{
    if (tlv_size < 2 * sizeof(int)) {
        return;
    }

    unsigned int type = tlv[0];
    unsigned int size = tlv[1]; // in bytes
    tlv_size -= 2 * sizeof(int); // in bytes
    if (size > tlv_size) {
        // data not big enough
        return;
    }

    switch (type) {
    case SND_CTL_TLVT_CONTAINER:
        size = alignPositionInBytesToNextInteger(size) * sizeof(int); // why?
        tlv += 2;
        while (size > 0) {
            parseTlv(tlv, size);
            unsigned int len = alignPositionInBytesToNextInteger(tlv[1]) + 2;
            size -= len * sizeof(int);
            tlv += len;
        }
        break;
    case SND_CTL_TLVT_DB_SCALE:
        m_dbinterfaces << new DBScale(parsedBScale(tlv + 2, size));
        break;
    case SND_CTL_TLVT_DB_LINEAR:
        m_dbinterfaces << new DBLinear(parsedBLinear(tlv + 2, size));
        break;
    case SND_CTL_TLVT_DB_RANGE:
        m_dbinterfaces << new DBRange(parsedBRange(tlv + 2, size));
        break;
    default:
        kDebug() << "unknown TLV type" << type;
    }
}

AlsaTlv::DBScale AlsaTlv::parsedBScale(unsigned int *tlv, unsigned int tlv_size)
{
    if (tlv_size < sizeof(int) * 2 || tlv_size > MAX_TLV_RANGE_SIZE) {
        return AlsaTlv::DBScale();
    }

    AlsaTlv::DBScale s;
    s.min = tlv[0];
    s.step = tlv[1] & 0x0000ffff;
    s.mute = tlv[1] & 0x00010000;
    return s;
}

AlsaTlv::DBLinear AlsaTlv::parsedBLinear(unsigned int *tlv, unsigned int tlv_size)
{
    if (tlv_size < sizeof(int) * 2 || tlv_size > MAX_TLV_RANGE_SIZE) {
        return AlsaTlv::DBLinear();
    }

    AlsaTlv::DBLinear l;
    l.mindB = tlv[0];
    l.maxdB = tlv[1];
    return l;
}

AlsaTlv::DBRange AlsaTlv::parsedBRange(unsigned int *tlv, unsigned int tlv_size)
{
    if (tlv_size < sizeof(int) * 4 || tlv_size > MAX_TLV_RANGE_SIZE) {
        return AlsaTlv::DBRange();
    }

    AlsaTlv::DBRange r(tlv[0], tlv[1]);

    const unsigned int &type = tlv[2];
    const unsigned int &size = tlv[3];
    switch (type) {
    case SND_CTL_TLVT_DB_SCALE:
        r.setInterface(parsedBScale(tlv + 4, size));
        break;
    case SND_CTL_TLVT_DB_LINEAR:
        r.setInterface(parsedBLinear(tlv + 4, size));
        break;
    case SND_CTL_TLVT_DB_RANGE:
        r.setInterface(parsedBRange(tlv + 4, size));
        break;
    default:
        kDebug() << "unknown TLV type" << type;
    }
    const unsigned int step = alignPositionInBytesToNextInteger(size) + 4;
    r.setNextRange(parsedBRange(tlv + step, tlv_size - step * sizeof(int)));
    return r;
}

} // namespace Mixer
} // namespace Phonon
// vim: sw=4 sts=4 et tw=100
