/**
 * alsachan.cpp
 * This file is part of the YATE Project http://YATE.null.ro
 *
 * Alsa driver
 *
 * Copyright (C) 2005 Pablo Sampere
 * Derived from osschan.cpp Copyright (C) Null Team
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include <yatephone.h>

#include <sys/stat.h>
#include <sys/ioctl.h>
#include <string.h>
#include <fcntl.h>

#if defined(__linux__)
#include <linux/soundcard.h>
#elif defined (__FreeBSD__)
#include <machine/soundcard.h>
#else
#include <soundcard.h>
#endif

#define ALSA_PCM_NEW_HW_PARAMS_API
#include <alsa/asoundlib.h>

#define MIN_SWITCH_TIME 600000

using namespace TelEngine;
namespace { // anonymous

static Mutex s_mutex;

class AlsaDevice;

class AlsaSource : public ThreadedSource
{
public:
    AlsaSource(AlsaDevice* dev);
    ~AlsaSource();
    bool init();
    virtual void run();
    virtual void cleanup();
private:
    AlsaDevice* m_device;
    unsigned m_brate;
    unsigned m_total;
};

class AlsaConsumer : public DataConsumer
{
public:
    AlsaConsumer(AlsaDevice* dev);
    ~AlsaConsumer();
    bool init();
    virtual void Consume(const DataBlock &data, unsigned long tStamp);
private:
    AlsaDevice* m_device;
    unsigned m_total;
};

class AlsaDevice : public RefObject
{
public:
    AlsaDevice(const String& dev, bool init=false);
    ~AlsaDevice();
    bool timePassed(void);
	bool open();
    int write(void *buffer, int frames);
    int read(void *buffer, int frames);
    inline bool closed() const
	{ return m_closed; }
	int m_fd;
private:
    String m_dev;    
    String m_dev_in;
    String m_dev_out;
    String m_initdata;
    bool m_closed;
    snd_pcm_t *m_handle_in;
    snd_pcm_t *m_handle_out;
    u_int64_t m_lastTime;
};

class AlsaChan : public CallEndpoint
{
public:
    AlsaChan(const String& dev);
    ~AlsaChan();
    bool init();
    virtual void disconnected(bool final, const char *reason);
    void answer();
    inline void setTarget(const char* target = 0)
	{ m_target = target; }
    inline const String& getTarget() const
	{ return m_target; }

private:    
    String m_dev;
    String m_target;
};

class AlsaHandler : public MessageHandler
{
public:
    AlsaHandler(const char *name) : MessageHandler(name) { }
    virtual bool received(Message &msg);
};

class StatusHandler : public MessageHandler
{
public:
    StatusHandler() : MessageHandler("engine.status") { }
    virtual bool received(Message &msg);
};

class DropHandler : public MessageHandler
{
public:
    DropHandler(const char *name) : MessageHandler(name) { }
    virtual bool received(Message &msg);
};

class MasqHandler : public MessageHandler
{
public:
    MasqHandler(const char *name, int prio) : MessageHandler(name,prio) { }
    virtual bool received(Message &msg);
};

class AttachHandler : public MessageHandler
{
public:
    AttachHandler() : MessageHandler("chan.attach") { }
    virtual bool received(Message &msg);
};

class AlsaPlugin : public Plugin
{
public:
    AlsaPlugin();
    virtual void initialize();
    virtual bool isBusy() const;
private:
    AlsaHandler *m_handler;
};

AlsaChan *s_chan = 0;
AlsaDevice* s_dev = 0;

bool AlsaSource::init()
{
    m_brate = 16000;
    m_total = 0;
    start("AlsaSource",Thread::High);
    return true;
}

AlsaSource::AlsaSource(AlsaDevice* dev)
    : m_device(0)
{
    Debug(DebugNote,"AlsaSource::AlsaSource(%p) [%p]",dev,this);
    dev->ref();
    m_device = dev;
}

AlsaSource::~AlsaSource()
{
    Debug(DebugNote,"AlsaSource::~AlsaSource() [%p] total=%u",this,m_total);
    m_device->deref();
}

void AlsaSource::run()
{
    int r = 0;
    //u_int64_t tpos = Time::now();
    DataBlock data(0,(m_brate*20)/1000);
    do {
		if (m_device->closed()) {
			m_device->open();
			Thread::yield();
			r = 1;
			continue;
		}
		r = m_device->read(data.data(), data.length()/2);
		if (r <= 0) {
			Thread::yield();
			r = 1;
			continue;
		}
		if(((unsigned int)r)*2==data.length()) Forward(data);
		else{
			DataBlock d2(data.data(),r*2);
			Forward(d2);
		}
/*
		int64_t dly = tpos - Time::now();
		if (dly > 0) {
			Debug("AlsaSource",DebugWarn,"Sleeping for " FMT64 " usec",dly);
			Thread::usleep((unsigned long)dly);
		}
*/
			
		m_total += r;
		//tpos += (r*(u_int64_t)1000000/m_brate);
    } while (r > 0);
    Debug(DebugWarn,"AlsaSource [%p] end of data",this);
}


void AlsaSource::cleanup()
{
    Debug(DebugNote,"AlsaSource [%p] cleanup, total=%u",this,m_total);
}

bool AlsaConsumer::init()
{
    m_total = 0;
    return true;
}

AlsaConsumer::AlsaConsumer(AlsaDevice* dev)
    : m_device(0)
{
    Debug(DebugNote,"AlsaConsumer::AlsaConsumer(%p) [%p]",dev,this);
    dev->ref();
    m_device = dev;
}

AlsaConsumer::~AlsaConsumer()
{
    Debug(DebugNote,"AlsaConsumer::~AlsaConsumer() [%p] total=%u",this,m_total);
    m_device->deref();
}

void AlsaConsumer::Consume(const DataBlock &data, unsigned long tStamp)
{
    if (m_device->closed() || data.null())
	return;
    m_device->write(data.data(),data.length()/2);
    m_total += data.length();
}

AlsaChan::AlsaChan(const String& dev)
    : CallEndpoint("alsa"),
      m_dev(dev)
{
    Debug(DebugNote,"AlsaChan::AlsaChan dev [%s] [%p]",dev.c_str(),this);
    s_chan = this;
}

AlsaChan::~AlsaChan()
{
    Debug(DebugNote,"AlsaChan::~AlsaChan() [%p]",this);
    setTarget();
    setSource();
    setConsumer();
    s_chan = 0;
}

bool AlsaChan::init()
{
    if (s_dev)
	return false;
    AlsaDevice* dev = new AlsaDevice(m_dev,true);
    if (dev->closed()) {
	dev->deref();
	return false;
    }
    AlsaSource* source = new AlsaSource(dev);
    dev->deref();
    if (!source->init()) {
	source->deref();
	return false;		
    }
    setSource(source);
    source->deref();
    AlsaConsumer* cons = new AlsaConsumer(dev);
    if (!cons->init()) {
	cons->deref();
	setSource();
	return false;		
    }	
    setConsumer(cons);
    cons->deref();
    return true;
}

AlsaDevice::AlsaDevice(const String& dev,bool init)
    : m_dev(dev), m_dev_in(dev), m_dev_out(dev), m_closed(true),
      m_handle_in(0), m_handle_out(0)
{
    Debug(DebugNote,"AlsaDevice::AlsaDevice('%s') [%p]",m_dev.c_str(),this);
    if (!s_dev)
	s_dev = this;
    int p = dev.find('/');
    if (p>0) {
        m_dev_in = dev.substr(0,p);
        int q = dev.substr(p+1).find('/');
        m_dev_out = dev.substr(p+1,q);
        if (m_dev_out.null()) m_dev_out = m_dev_in;
        if(q>0) m_initdata = dev.substr(p+2+q);
    }
	if (init) open();
};


bool AlsaDevice::open()
{
    int err;
    snd_pcm_hw_params_t *hw_params = NULL;
    unsigned int rate_in=8000;
    unsigned int rate_out=8000;
    int direction=0;
    snd_pcm_uframes_t period_size_in = 20 * 4;
    snd_pcm_uframes_t buffer_size_in= period_size_in * 16;
    snd_pcm_uframes_t period_size_out = 20 * 4;
    snd_pcm_uframes_t buffer_size_out= period_size_out * 16;
    snd_pcm_sw_params_t *swparams = NULL;
	Debug(DebugNote, "Opening ALSA input device %s",m_dev_in.c_str());
	Lock lock(s_mutex);
    if ((err = snd_pcm_open (&m_handle_in, m_dev_in.c_str(), SND_PCM_STREAM_CAPTURE, 0)) < 0) {
	Debug(DebugWarn, "cannot open audio device %s (%s)", m_dev.c_str(),snd_strerror (err));
	return false;
    }
    if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) {
	Debug(DebugWarn, "cannot allocate hardware parameter structure (%s)", snd_strerror (err));
	return false;
    }
    if ((err = snd_pcm_hw_params_any (m_handle_in, hw_params)) < 0) Debug(DebugWarn, "cannot initialize hardware parameter structure (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params_set_access (m_handle_in, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) Debug(DebugWarn, "cannot set access type (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params_set_format (m_handle_in, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) Debug(DebugWarn, "cannot set sample format (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params_set_rate_near (m_handle_in, hw_params, &rate_in, &direction)) < 0) Debug(DebugWarn, "cannot set sample rate (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params_set_channels (m_handle_in, hw_params, 1)) < 0) Debug(DebugWarn, "cannot set channel count (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params (m_handle_in, hw_params)) < 0) Debug(DebugWarn, "cannot set parameters (%s)", snd_strerror (err));
    snd_pcm_hw_params_free (hw_params);

    Debug(DebugNote, "Opening ALSA output device %s",m_dev_out.c_str());
    if ((err = snd_pcm_open (&m_handle_out, m_dev_out.c_str(), SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
	Debug(DebugWarn, "cannot open audio device %s (%s)", m_dev.c_str(), snd_strerror (err));
	return false;
    }

    if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) {
	Debug(DebugWarn, "cannot allocate hardware parameter structure (%s)", snd_strerror (err));
	return false;
    }
    if ((err = snd_pcm_hw_params_any (m_handle_out, hw_params)) < 0) Debug(DebugWarn, "cannot initialize hardware parameter structure (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params_set_access (m_handle_out, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) Debug(DebugWarn, "cannot set access type (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params_set_format (m_handle_out, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) Debug(DebugWarn, "cannot set sample format (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params_set_rate_near (m_handle_out, hw_params, &rate_out, 0)) < 0) Debug(DebugWarn, "cannot set sample rate (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params_set_channels (m_handle_out, hw_params, 1)) < 0) Debug(DebugWarn, "cannot set channel count (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params_set_period_size_near(m_handle_out, hw_params, &period_size_out, &direction)) < 0) Debug(DebugWarn, "cannot set period size (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params_set_buffer_size_near(m_handle_out, hw_params, &buffer_size_out)) < 0) Debug(DebugWarn, "cannot set buffer size (%s)", snd_strerror (err));
    if ((err = snd_pcm_hw_params (m_handle_out, hw_params)) < 0) Debug(DebugWarn, "cannot set parameters (%s)", snd_strerror (err));
    snd_pcm_hw_params_free (hw_params);

    snd_pcm_uframes_t val;

    snd_pcm_sw_params_alloca(&swparams);
    snd_pcm_sw_params_current(m_handle_out, swparams);

    if ((err = snd_pcm_sw_params_get_start_threshold( swparams, &val)) < 0) Debug(DebugWarn, "cannot get start threshold: (%s)", snd_strerror (err));

    if ((err = snd_pcm_sw_params_get_stop_threshold( swparams, &val)) < 0) Debug(DebugWarn, "cannot get stop threshold: (%s)", snd_strerror (err));

    if ((err = snd_pcm_sw_params_get_boundary( swparams, &val)) < 0) Debug(DebugWarn, "cannot get boundary: (%s)", snd_strerror (err));

    if ((err = snd_pcm_sw_params_set_silence_threshold(m_handle_out, swparams, 0)) < 0) Debug(DebugWarn, "cannot set silence threshold: (%s)", snd_strerror (err));

    if ((err = snd_pcm_sw_params_set_silence_size(m_handle_out, swparams, 0)) < 0) Debug(DebugWarn, "cannot set silence size: (%s)", snd_strerror (err));

    if ((err = snd_pcm_sw_params(m_handle_out, swparams)) < 0) Debug(DebugWarn, "cannot set sw param: (%s)", snd_strerror (err));

    Debug(DebugNote, "Alsa(%s/%s) %u/%u %u/%u %u/%u", m_dev_in.c_str(),m_dev_out.c_str(),rate_in,rate_out,(unsigned int)period_size_in,(unsigned int)period_size_out,(unsigned int)buffer_size_in,(unsigned int)buffer_size_out);
    m_closed = false;
    m_lastTime = Time::now() + MIN_SWITCH_TIME;
    return true;
}


AlsaDevice::~AlsaDevice()
{
    Debug(DebugNote,"AlsaDevice::~AlsaDevice [%p]",this);
    m_closed = true;
    if (m_handle_in) {
	snd_pcm_drop (m_handle_in);
	snd_pcm_close (m_handle_in);
	m_handle_in = 0;
    }
    if (m_handle_out) {
	snd_pcm_drop (m_handle_out);
	snd_pcm_close (m_handle_out);
	m_handle_out = 0;
    }
    if (s_dev == this)
	s_dev = 0;
}

int AlsaDevice::read(void *buffer, int frames)
{
    if (closed() || !m_handle_in) return 0;
    int rc = ::snd_pcm_readi(m_handle_in, buffer, frames);
    if (rc <= 0) {
        int err = rc;
        if (err == -EPIPE) {    /* under-run */
            Debug(DebugWarn, "ALSA read underrun: %s", snd_strerror(err));
            err = snd_pcm_prepare(m_handle_in);
            if (err < 0)
                    Debug(DebugWarn, "ALSA read can't recover from underrun, prepare failed: %s", snd_strerror(err));
            return 0;
        } else if (err == -ESTRPIPE) {
                while ((err = snd_pcm_resume(m_handle_in)) == -EAGAIN)
                        sleep(1);       /* wait until the suspend flag is released */
                if (err < 0) {
                        err = snd_pcm_prepare(m_handle_in);
                        if (err < 0)
                                Debug(DebugWarn, "ALSA read can't recover from suspend, prepare failed: %s", snd_strerror(err));
                }
                return 0;
        }
        return err;
    }
    return rc;
}

int AlsaDevice::write(void *buffer, int frames)
{
    if (closed() || !m_handle_out) return 0;
    int rc = snd_pcm_writei(m_handle_out, buffer, frames);
    if (rc == -EPIPE) {
		Debug(DebugWarn, "ALSA write underrun occurred");
		snd_pcm_prepare(m_handle_out);
		Debug(DebugWarn, "ALSA write underrun fix frame 1");
		rc = snd_pcm_writei(m_handle_out, buffer, frames);
    	if (rc == -EPIPE) snd_pcm_prepare(m_handle_out);
		Debug(DebugWarn, "ALSA write underrun fix frame 2"); //to catch-up missed time
		rc = snd_pcm_writei(m_handle_out, buffer, frames);
    	if (rc == -EPIPE) snd_pcm_prepare(m_handle_out);
    } else if (rc < 0) {
      Debug(DebugWarn,"ALSA error from writei: %s",::snd_strerror(rc));
    }  else if (rc != (int)frames) {
      Debug(DebugWarn,"ALSA short write, writei wrote %d frames", rc);
    }
	return rc;
}

bool AlsaDevice::timePassed(void)
{
    return Time::now() > m_lastTime;
}

void AlsaChan::disconnected(bool final, const char *reason)
{
    Debugger debug("AlsaChan::disconnected()"," '%s' [%p]",reason,this);
    setTarget();
}

void AlsaChan::answer()
{
    Message* m = new Message("call.answered");
    m->addParam("module","alsa");
    String tmp("alsa/");
    tmp += m_dev;
    m->addParam("id",tmp);
    if (m_target)
	m->addParam("targetid",m_target);
    Engine::enqueue(m);
}

bool AlsaHandler::received(Message &msg)
{
    String dest(msg.getValue("callto"));
    if (dest.null())
	return false;
    Regexp r("^alsa/\\(.*\\)$");
    if (!dest.matches(r))
	return false;
    if (s_chan) {
	msg.setParam("error","busy");
	return false;
    }
    AlsaChan *chan = new AlsaChan(dest.matchString(1).c_str());
    if (!chan->init())
    {
	chan->destruct();
	return false;
    }
    CallEndpoint* ch = static_cast<CallEndpoint*>(msg.userData());
    Debug(DebugInfo,"We are routing to device '%s'",dest.matchString(1).c_str());
    if (ch && chan->connect(ch,msg.getValue("reason"))) {
	chan->setTarget(msg.getValue("id"));
	msg.setParam("peerid",dest);
	msg.setParam("targetid",dest);
	chan->answer();
	chan->deref();
    }
    else {
        const char *direct = msg.getValue("direct");
	if (direct)
	{
	    Message m("call.execute");
	    m.addParam("module","alsa");
	    m.addParam("id",dest);
	    m.addParam("caller",dest);
	    m.addParam("callto",direct);
	    m.userData(chan);
	    if (Engine::dispatch(m)) {
		chan->setTarget(m.getValue("targetid"));
		msg.addParam("targetid",chan->getTarget());
		chan->deref();
		return true;
	    }
	    Debug(DebugInfo,"Alsa outgoing call not accepted!");
	    chan->destruct();
	    return false;
	}	
	const char *targ = msg.getValue("target");
	if (!targ) {
	    Debug(DebugWarn,"Alsa outgoing call with no target!");
	    chan->destruct();
	    return false;
	}
	Message m("call.route");
	m.addParam("module","alsa");
	m.addParam("id",dest);
	m.addParam("caller",dest);
	m.addParam("called",targ);
	if (Engine::dispatch(m)) {
	    m = "call.execute";
	    m.addParam("callto",m.retValue());
	    m.retValue() = 0;
	    m.userData(chan);
	    if (Engine::dispatch(m)) {
		chan->setTarget(m.getValue("targetid"));
		msg.addParam("targetid",chan->getTarget());
		chan->deref();
		return true;
	    }
	    Debug(DebugInfo,"Alsa outgoing call not accepted!");
	}
	else
	    Debug(DebugWarn,"Alsa outgoing call but no route!");
	chan->destruct();
	return false;
    }

    return true;
}

bool StatusHandler::received(Message &msg)
{
    const char *sel = msg.getValue("module");
    if (sel && ::strcmp(sel,"alsa"))
	return false;
    msg.retValue() << "name=alsa,type=misc;alsachan=" << (s_chan != 0 ) << "\r\n";
    return false;
}

bool DropHandler::received(Message &msg)
{
    String id(msg.getValue("id"));
    if (id.null() || id.startsWith("alsa/")) {
	if (s_chan) {
	    Debug("AlsaDropper",DebugInfo,"Dropping call");
	    s_chan->disconnect();
	}
	return !id.null();
    }
    return false;
}

bool MasqHandler::received(Message &msg)
{
    String id(msg.getValue("id"));
    if (msg.getParam("message") && id.startsWith("alsa/")) {
	msg = msg.getValue("message");
	msg.clearParam("message");
	if (s_chan) {
	    msg.addParam("targetid",s_chan->getTarget());
	    msg.userData(s_chan);
	}
    }
    return false;
}

bool AttachHandler::received(Message &msg)
{
    if (s_dev)
	return false;
    int more = 2;
    String src(msg.getValue("source"));
    if (src.null())
	more--;
    else {
	if (!src.startSkip("alsa/",false))
	    src = "";
    }

    String cons(msg.getValue("consumer"));
    if (cons.null())
	more--;
    else {
	if (!cons.startSkip("alsa/",false))
	    cons = "";
    }

    if (src.null() && cons.null())
	return false;

    if (src && cons && (src != cons)) {
	Debug(DebugWarn,"Alsa asked to attach source '%s' and consumer '%s'",src.c_str(),cons.c_str());
	return false;
    }

    DataEndpoint *dd = static_cast<DataEndpoint*>(msg.userObject("DataEndpoint"));
    if (!dd) {
	CallEndpoint *ch = static_cast<CallEndpoint*>(msg.userObject("CallEndpoint"));
	if (ch)
	    dd = ch->setEndpoint();
    }
    if (!dd) {
	Debug(DebugWarn,"Alsa attach request with no control or data channel!");
	return false;
    }
    
    AlsaDevice* dev = new AlsaDevice(src ? src : cons,true);
    if (dev->closed()) {
	dev->deref();
	return false;
    }

    if (src) {
	AlsaSource* s = new AlsaSource(dev);
	if (s->init())
	    dd->setSource(s);
	s->deref();
    }

    if (cons) {
	AlsaConsumer* c = new AlsaConsumer(dev);
	if (c->init())
	    dd->setConsumer(c);
	c->deref();
    }

    dev->deref();

    // Stop dispatching if we handled all requested
    return !more;
}

AlsaPlugin::AlsaPlugin()
    : m_handler(0)
{
    Debug(DebugAll,"Loaded module ChanALSA");
}

void AlsaPlugin::initialize()
{
    Debug(DebugAll,"Initializing module ChanALSA");
    if (!m_handler) {
	m_handler = new AlsaHandler("call.execute");
	Engine::install(new DropHandler("call.drop"));
	Engine::install(new MasqHandler("chan.masquerade",10));
	Engine::install(m_handler);
	Engine::install(new StatusHandler);
	Engine::install(new AttachHandler);
    }
}

bool AlsaPlugin::isBusy() const
{
    return (s_dev != 0);
}

INIT_PLUGIN(AlsaPlugin);

}; // anonymous namespace

/* vi: set ts=8 sw=4 sts=4 noet: */
