/*
 * $Id: PlaybackNode.cc,v 1.1 2003/01/15 06:10:13 gsilber Exp $
 * PortAudio Portable Real-Time Audio Library
 * Latest Version at: http://www.portaudio.com
 * BeOS Media Kit Implementation by Joshua Haberman
 *
 * Copyright (c) 2001 Joshua Haberman <joshua@haberman.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * Any person wishing to distribute modifications to the Software is
 * requested to send the modifications to the original developer so that
 * they can be incorporated into the canonical version.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
 * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * ---
 *
 * Significant portions of this file are based on sample code from Be. The
 * Be Sample Code Licence follows:
 *
 *    Copyright 1991-1999, Be Incorporated.
 *    All rights reserved.
 *
 *    Redistribution and use in source and binary forms, with or without
 *    modification, are permitted provided that the following conditions
 *    are met:
 *
 *    1. Redistributions of source code must retain the above copyright
 *       notice, this list of conditions, and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions, and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 *    3. The name of the author may not be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 *    THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
 *    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 *    OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 *    PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 *    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 *    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 *    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 *    AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 *    TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 *    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.    
 */

#include <stdio.h>

#include <be/media/BufferGroup.h>
#include <be/media/Buffer.h>
#include <be/media/TimeSource.h>

#include "PlaybackNode.h"

#define PRINT(x) { printf x; fflush(stdout); }

#ifdef DEBUG
#define DBUG(x)  PRINT(x)
#else
#define DBUG(x)
#endif


PaPlaybackNode::PaPlaybackNode(uint32 channels, float frame_rate, uint32 frames_per_buffer,
                               PortAudioCallback* callback, void *user_data) :
        BMediaNode("PortAudio input node"),
        BBufferProducer(B_MEDIA_RAW_AUDIO),
        BMediaEventLooper(),
        mAborted(false),
        mRunning(false),
        mBufferGroup(NULL),
        mDownstreamLatency(0),
        mStartTime(0),
        mCallback(callback),
        mUserData(user_data),
        mFramesPerBuffer(frames_per_buffer)
{
    DBUG(("Constructor called.\n"));

    mPreferredFormat.type = B_MEDIA_RAW_AUDIO;
    mPreferredFormat.u.raw_audio.channel_count = channels;
    mPreferredFormat.u.raw_audio.frame_rate = frame_rate;
    mPreferredFormat.u.raw_audio.byte_order =
        (B_HOST_IS_BENDIAN) ? B_MEDIA_BIG_ENDIAN : B_MEDIA_LITTLE_ENDIAN;
    mPreferredFormat.u.raw_audio.buffer_size =
        media_raw_audio_format::wildcard.buffer_size;

    mOutput.destination = media_destination::null;
    mOutput.format = mPreferredFormat;

    /* The amount of time it takes for this node to produce a buffer when
     * asked. Essentially, it is how long the user's callback takes to run.
     * We set this to be the length of the sound data each buffer of the
     * requested size can hold. */
    //mInternalLatency = (bigtime_t)(1000000 * frames_per_buffer / frame_rate);

    /* ACK! it seems that the mixer (at least on my machine) demands that IT
        * specify the buffer size, so for now I'll just make a generic guess here */
    mInternalLatency = 1000000 / 20;
}



PaPlaybackNode::~PaPlaybackNode()
{
    DBUG(("Destructor called.\n"));
    Quit();   /* Stop the BMediaEventLooper thread */
}


/*************************
 *
 *  Local methods
 *
 */

bool PaPlaybackNode::IsRunning()
{
    return mRunning;
}


PaTimestamp PaPlaybackNode::GetStreamTime()
{
    BTimeSource *timeSource = TimeSource();
    PaTimestamp time = (timeSource->Now() - mStartTime) *
                       mPreferredFormat.u.raw_audio.frame_rate / 1000000;
    return time;
}


void PaPlaybackNode::SetSampleFormat(PaSampleFormat inFormat,
                                     PaSampleFormat outFormat)
{
    uint32 beOutFormat;

    switch(outFormat)
    {
    case paFloat32:
        beOutFormat = media_raw_audio_format::B_AUDIO_FLOAT;
        mOutputSampleWidth = 4;
        break;

    case paInt16:
        beOutFormat = media_raw_audio_format::B_AUDIO_SHORT;
        mOutputSampleWidth = 2;
        break;

    case paInt32:
        beOutFormat = media_raw_audio_format::B_AUDIO_INT;
        mOutputSampleWidth = 4;
        break;

    case paInt8:
        beOutFormat = media_raw_audio_format::B_AUDIO_CHAR;
        mOutputSampleWidth = 1;
        break;

    case paUInt8:
        beOutFormat = media_raw_audio_format::B_AUDIO_UCHAR;
        mOutputSampleWidth = 1;
        break;

    case paInt24:
    case paPackedInt24:
    case paCustomFormat:
        DBUG(("Unsupported output format: %x\n", outFormat));
        break;

    default:
        DBUG(("Unknown output format: %x\n", outFormat));
    }

    mPreferredFormat.u.raw_audio.format = beOutFormat;
    mFramesPerBuffer * mPreferredFormat.u.raw_audio.channel_count * mOutputSampleWidth;
}

BBuffer *PaPlaybackNode::FillNextBuffer(bigtime_t time)
{
    /* Get a buffer from the buffer group */
    BBuffer *buf = mBufferGroup->RequestBuffer(
                       mOutput.format.u.raw_audio.buffer_size, BufferDuration());
    unsigned long frames = mOutput.format.u.raw_audio.buffer_size /
                           mOutputSampleWidth / mOutput.format.u.raw_audio.channel_count;
    bigtime_t start_time;
    int ret;

    if( !buf )
    {
        DBUG(("Unable to allocate a buffer\n"));
        return NULL;
    }

    start_time = mStartTime +
                 (bigtime_t)((double)mSamplesSent /
                             (double)mOutput.format.u.raw_audio.frame_rate /
                             (double)mOutput.format.u.raw_audio.channel_count *
                             1000000.0);

    /* Now call the user callback to get the data */
    ret = mCallback(NULL,       /* Input buffer */
                    buf->Data(),      /* Output buffer */
                    frames,           /* Frames per buffer */
                    mSamplesSent / mOutput.format.u.raw_audio.channel_count, /* timestamp */
                    mUserData);

    if( ret )
        mAborted = true;

    media_header *hdr = buf->Header();

    hdr->type = B_MEDIA_RAW_AUDIO;
    hdr->size_used = mOutput.format.u.raw_audio.buffer_size;
    hdr->time_source = TimeSource()->ID();
    hdr->start_time = start_time;

    return buf;
}




/*************************
 *
 *  BMediaNode methods
 *
 */

BMediaAddOn *PaPlaybackNode::AddOn( int32 * ) const
{
    DBUG(("AddOn() called.\n"));
    return NULL;  /* we don't provide service to outside applications */
}


status_t PaPlaybackNode::HandleMessage( int32 message, const void *data,
                                        size_t size )
{
    DBUG(("HandleMessage() called.\n"));
    return B_ERROR;  /* we don't define any custom messages */
}




/*************************
 *
 *  BMediaEventLooper methods
 *
 */

void PaPlaybackNode::NodeRegistered()
{
    DBUG(("NodeRegistered() called.\n"));

    /* Start the BMediaEventLooper thread */
    SetPriority(B_REAL_TIME_PRIORITY);
    Run();

    /* set up as much information about our output as we can */
    mOutput.source.port = ControlPort();
    mOutput.source.id = 0;
    mOutput.node = Node();
    ::strcpy(mOutput.name, "PortAudio Playback");
}


void PaPlaybackNode::HandleEvent( const media_timed_event *event,
                                  bigtime_t lateness, bool realTimeEvent )
{
    // DBUG(("HandleEvent() called.\n"));
    status_t err;

    switch(event->type)
    {
    case BTimedEventQueue::B_START:
        DBUG(("   Handling a B_START event\n"));
        if( RunState() != B_STARTED )
        {
            mStartTime = event->event_time + EventLatency();
            mSamplesSent = 0;
            mAborted = false;
            mRunning = true;
            media_timed_event firstEvent( mStartTime,
                                          BTimedEventQueue::B_HANDLE_BUFFER );
            EventQueue()->AddEvent( firstEvent );
        }
        break;

    case BTimedEventQueue::B_STOP:
        DBUG(("   Handling a B_STOP event\n"));
        mRunning = false;
        EventQueue()->FlushEvents( 0, BTimedEventQueue::B_ALWAYS, true,
                                   BTimedEventQueue::B_HANDLE_BUFFER );
        break;

    case BTimedEventQueue::B_HANDLE_BUFFER:
        //DBUG(("   Handling a B_HANDLE_BUFFER event\n"));

        /* make sure we're started and connected */
        if( RunState() != BMediaEventLooper::B_STARTED ||
                mOutput.destination == media_destination::null )
            break;

        BBuffer *buffer = FillNextBuffer(event->event_time);

        /* make sure we weren't aborted while this routine was running.
         * this can happen in one of two ways: either the callback returned
         * nonzero (in which case mAborted is set in FillNextBuffer() ) or
         * the client called AbortStream */
        if( mAborted )
        {
            if( buffer )
                buffer->Recycle();
            Stop(0, true);
            break;
        }

        if( buffer )
        {
            err = SendBuffer(buffer, mOutput.destination);
            if( err != B_OK )
                buffer->Recycle();
        }

        mSamplesSent += mOutput.format.u.raw_audio.buffer_size / mOutputSampleWidth;

        /* Now schedule the next buffer event, so we can send another
         * buffer when this one runs out. We calculate when it should
         * happen by calculating when the data we just sent will finish
         * playing.
         *
         * NOTE, however, that the event will actually get generated
         * earlier than we specify, to account for the latency it will
         * take to produce the buffer. It uses the latency value we
         * specified in SetEventLatency() to determine just how early
         * to generate it. */

        /* totalPerformanceTime includes the time represented by the buffer
         * we just sent */
        bigtime_t totalPerformanceTime = (bigtime_t)((double)mSamplesSent /
                                         (double)mOutput.format.u.raw_audio.channel_count /
                                         (double)mOutput.format.u.raw_audio.frame_rate * 1000000.0);

        bigtime_t nextEventTime = mStartTime + totalPerformanceTime;

        media_timed_event nextBufferEvent(nextEventTime,
                                          BTimedEventQueue::B_HANDLE_BUFFER);
        EventQueue()->AddEvent(nextBufferEvent);

        break;

    }
}




/*************************
 *
 *  BBufferProducer methods
 *
 */

status_t PaPlaybackNode::FormatSuggestionRequested( media_type type,
        int32 /*quality*/, media_format* format )
{
    /* the caller wants to know this node's preferred format and provides
     * a suggestion, asking if we support it */
    DBUG(("FormatSuggestionRequested() called.\n"));

    if(!format)
        return B_BAD_VALUE;

    *format = mPreferredFormat;

    /* we only support raw audio (a wildcard is okay too) */
    if ( type == B_MEDIA_UNKNOWN_TYPE || type == B_MEDIA_RAW_AUDIO )
        return B_OK;
    else
        return B_MEDIA_BAD_FORMAT;
}


status_t PaPlaybackNode::FormatProposal( const media_source& output,
        media_format* format )
{
    /* This is similar to FormatSuggestionRequested(), but it is actually part
     * of the negotiation process. We're given the opportunity to specify any
     * properties that are wildcards (ie. properties that the other node doesn't
     * care one way or another about) */
    DBUG(("FormatProposal() called.\n"));

    /* Make sure this proposal really applies to our output */
    if( output != mOutput.source )
        return B_MEDIA_BAD_SOURCE;

    /* We return two things: whether we support the proposed format, and our own
     * preferred format */
    *format = mPreferredFormat;

    if( format->type == B_MEDIA_UNKNOWN_TYPE || format->type == B_MEDIA_RAW_AUDIO )
        return B_OK;
    else
        return B_MEDIA_BAD_FORMAT;
}


status_t PaPlaybackNode::FormatChangeRequested( const media_source& source,
        const media_destination& destination, media_format* io_format, int32* )
{
    /* we refuse to change formats, supporting only 1 */
    DBUG(("FormatChangeRequested() called.\n"));

    return B_ERROR;
}


status_t PaPlaybackNode::GetNextOutput( int32* cookie, media_output* out_output )
{
    /* this is where we allow other to enumerate our outputs -- the cookie is
     * an integer we can use to keep track of where we are in enumeration. */
    DBUG(("GetNextOutput() called.\n"));

    if( *cookie == 0 )
    {
        *out_output = mOutput;
        *cookie = 1;
        return B_OK;
    }

    return B_BAD_INDEX;
}


status_t PaPlaybackNode::DisposeOutputCookie( int32 cookie )
{
    DBUG(("DisposeOutputCookie() called.\n"));
    return B_OK;
}


void PaPlaybackNode::LateNoticeReceived( const media_source& what,
        bigtime_t how_much, bigtime_t performance_time )
{
    /* This function is called as notification that a buffer we sent wasn't
     * received by the time we stamped it with -- it got there late. Basically,
     * it means we underestimated our own latency, so we should increase it */
    DBUG(("LateNoticeReceived() called.\n"));

    if( what != mOutput.source )
        return;

    if( RunMode() == B_INCREASE_LATENCY )
    {
        mInternalLatency += how_much;
        SetEventLatency( mDownstreamLatency + mInternalLatency );
        DBUG(("Increasing latency to %Ld\n", mDownstreamLatency + mInternalLatency));
    }
    else
        DBUG(("I don't know what to do with this notice!"));
}


void PaPlaybackNode::EnableOutput( const media_source& what, bool enabled,
                                   int32* )
{
    DBUG(("EnableOutput() called.\n"));
    /* stub -- we don't support this yet */
}


status_t PaPlaybackNode::PrepareToConnect( const media_source& what,
        const media_destination& where, media_format* format,
        media_source* out_source, char* out_name )
{
    /* the final stage of format negotiations. here we _must_ make specific any
     * remaining wildcards */
    DBUG(("PrepareToConnect() called.\n"));

    /* make sure this really refers to our source */
    if( what != mOutput.source )
        return B_MEDIA_BAD_SOURCE;

    /* make sure we're not already connected */
    if( mOutput.destination != media_destination::null )
        return B_MEDIA_ALREADY_CONNECTED;

    if( format->type != B_MEDIA_RAW_AUDIO )
        return B_MEDIA_BAD_FORMAT;

    if( format->u.raw_audio.format != mPreferredFormat.u.raw_audio.format )
        return B_MEDIA_BAD_FORMAT;

    if( format->u.raw_audio.buffer_size ==
            media_raw_audio_format::wildcard.buffer_size )
    {
        DBUG(("We were left to decide buffer size: choosing 2048"));
        format->u.raw_audio.buffer_size = 2048;
    }
    else
        DBUG(("Using consumer specified buffer size of %lu.\n",
              format->u.raw_audio.buffer_size));

    /* Reserve the connection, return the information */
    mOutput.destination = where;
    mOutput.format      = *format;
    *out_source         = mOutput.source;
    strncpy( out_name, mOutput.name, B_MEDIA_NAME_LENGTH );

    return B_OK;
}


void PaPlaybackNode::Connect(status_t error, const media_source& source,
                             const media_destination& destination, const media_format& format, char* io_name)
{
    DBUG(("Connect() called.\n"));

